理查马文价值导向选股法则(年化 34%,附代码)

引言

理查和马文是华尔街著名的财务分析专家,二人合作研究愈 30 年,在业界有着很高的声望,现在均任罗根资产管理公司投资经理。他们二人都是格雷厄姆和巴菲特的追随者,奉行价值投资的理念,本篇将要介绍的理查 & 马文价值导向选股法则正是两人价值投资理念的集中体现。

策略思路

理查和马文是价值投资的拥护者,在投资组合选股中,尤其重视分红率高且稳定的公司。二人于 1980 年代开始合作研究关系,曾先后于核心财务公司(CoreStates Financial)、伯恩财务集团合作进行投资方面的研究。之后二人正式开始以价值投资方式进行投资组合管理,其投资业绩始终保持在同类资产管理公司的前列。其策略主要从五个角度对股票提出要求:

一、股票具备一定的市值规模;

二、股票具有一定的分红收益;

三、公司的财务状况要求健康;

四、股票具备合理的估值;

五、公司有充足的现金流。

从这五个角度出发,我们给出选股的六个指标:

1. 市值
2. 市盈率
3. 股息收益率
4. 股息率相对全市场水平
5. 资产负债率
6. 每股现金流净额

这六个指标共同构建了选股准则,行程了理查马文价值导向选股法则。这五条准则非常清晰明确,均为可直接量化实现的准则。每条准则具有明确的量化描述和判断条件,除个别部分需对中国市场调整外,我们的回测将严格按照这六条准则复制,不存在需主观判断而不能量化的准则。

策略细节

归纳理查马文价值投资策略初始版本,可以大致总结策略衡量的五个方面:

(1)用总市值来要求目标公司具备一定的公司规模;

(2)用股息收益率来衡量股票的分红水平,保证价值投资者能够获得一定的分红收益。

(3)用资产负债率来衡量公司的偿债能力和财务结构,保证买入的投资标的具备足够强大的资产负债表;

(4)用市盈率来衡量股票的估值,要求买入的投资标的足够“便宜”,具有合理的估值;

(5)用每股现金流量保证目标公司具有较为充足的现金水平。

其价值选股标准也主要围绕着这三个方面,具体为:

1)罗伯·瑞克超额现金流选股法则的通用版本:

a) 总市值大于等于市场平均值
b) 股息率大于等于市场平均值的 1.5 倍
c) 最近一期资产负债率小于市场均值
d) 最近一年市盈率小于市场平均值
e) 最近一年每股现金流量为正值且大于市场平均值
f) 姑息率最高的前 10 家

策略实现

为了比较长期价值投资的效果,我们进行了 3 次回测,回测时长分别为 1 年,5 年,10 年。

(1)理查马文价值导向选股法则

投资标的:A 股
调仓周期:60 天
回测时间:2016.01.01~2017.01.01
回测时长:1 年

收益曲线

收益归因

业绩分析

从回测结果看,该价值选股策略取得了 10% 以上的超额收益。表现强于大盘。该策略有一定的选股效果。

(2)理查马文价值导向选股法则

投资标的:A 股
调仓周期:60 天
回测时间:2014.01.01~2017.01.01
回测时长:3 年

收益曲线

收益归因

业绩分析

当回测时间延长到三年,我们发现该策略的成果更加优异。年化收益率达到了 36.7%。观察收益曲线,可以发现该策略在大盘下跌时依然取得了一段不错的收益。这表明该策略对价值股的选取一定程度上减轻了熊市带来的影响。

(3)理查马文价值导向选股法则

投资标的:A 股
调仓周期:60 天
回测时间:2007.01.01~2017.01.01
回测时长:10 年

收益曲线

收益归因

业绩分析

在 10 年期的回测中,该策略表现比较一般,远逊与其在 3 年期回测中的表现。这可能是因为 A 股特有的小市值因素造成的。由于壳资源的价值和其他原因,A 股市场长期以来一直偏向小市值风格,而这一风格在近几年发生了变化,单纯的小市值难以取得过人的成绩。而我们的策略更偏向于寻找市值较大的公司,也因此该策略在小市值有效的十年期表现不佳,而在一年、三年回测时间段有所表现。

小结

理查 & 马文价值导向选股法则是理查和马文价值投资理念的集中体现。其从五个角度对投资标的进行要求,分别是足够的公司规模,合理的估值,健康的财务状况,一定的分红水平和相对充足的现金流。围绕这五个角度,提出了 6 个具体的选股准则,用总市值,要求目标公司具备足够的公司规模;市盈率衡量估值水平,要求股票的价格尽量“便宜”;用资产负债率限制公司的财务杠杆,要求企业具备健康的财务状况;用股息率来衡量收益水平,要求满足一定的分红收益;用每股现金流衡量公司的现金流水平,要求目标公司具备一定的现金流量。

从我们的回测结果来看,理查 & 马文价值导向选股法则表现并不稳定,在中国市场还不具备很强的实用性,非常值得我们沿着这六条准则所体现的思路作进一步的优化和探索。

但需要说明的是,目前我们所测试的选股法则并不能算成熟的策略,仍有不足。 其一、策略相对基准的 相对强弱波动较大,仅在 2008 年到 2010 年、2013 年后体现出明显的相对优势, 而在其他 5 年间没有体现出稳定的相对优势;其二、策略持股个数波动略大,易造成 交易执行的问题。当股票个数过少时,隐藏的风险和波动性加大。

我们再次提出四个简单的优化建议: 一是吸收理查 & 马文价值导向选股法则中的投资思维,在原有基础上扩展筛选股的考查纬度、或者调整具体的考核指标,进一步优化考察指标的参数;二是对选股数量和行业分布进行调整,利用分散化投资和行业中性的优势来避免策略的大幅波动,减少策略中明显不足的最大回撤指标;三适当对持股数量进行挑战,利用分散化投资的优势来平稳策略的表现。

关于回测平台、历史数据,可以在这里下载:http://www.yunkuanke.com/#/introduce
这个平台的数据都是经过清洗的,而且策略不会外露,有保密系统,还是比较安全的。
想学习更多量化策略,也可以来这里看看:http://www.yunkuanke.com/#/lzClass
想学习更多关于量化投资策略相关内容的,可以关注微信:量化投资与金融科技(微信号:QuantumFintech)
微信二维码:

Code

# -*- coding:utf-8 -*-

**from **CloudQuant **import **SDKCoreEngine  # 导入量子金服SDK
**from **CloudQuant **import **AssetType
**from **CloudQuant **import **QuoteCycle
**import **numpy **as **np
**import **pandas **as **pd

np.seterr(invalid='ignore')

config = {
'username': 'username',
'password': 'password',
'rootpath': 'c:/cStrategy',  # 客户端所在路径
'assetType': AssetType.Stock,
'initCapitalStock': 100000000,
'startDate': 20140101,
'endDate': 20170101,
'cycle': QuoteCycle.D,
'strategyName': '理查马文',
'feeRate': 0.001,
'feeLimit': 5,
'stampTaxRate': 0.001,
'dealByVolume': True
}

HOLDING_PERIOD=60

**def ****initial**(sdk):
sdk.setGlobal('c',0)

**def ****initPerDay**(sdk):
**pass****
****
****def ****strategy**(sdk):
count=sdk.getGlobal('c')
**if **count==0:
    TCAP=sdk.getFactorData('LZ_CN_STKA_VAL_A_TCAP')[-1] #总市值
    PE = sdk.getFactorData('LZ_CN_STKA_VAL_PE')[-1] #  PE
    PC = sdk.getFactorData('LZ_CN_STKA_VAL_PC')[-1]  #  市现率
    D2A=sdk.getFactorData('LZ_CN_STKA_FIN_IND_DEBTTOASTS')[-1] #资产负债率
    PPD=sdk.getFactorData('LZ_CN_STKA_VAL_PPD')[-1] # 股价/每股派息
    DR=1/PPD #股息率
    ST = sdk.getFactorData('LZ_CN_STKA_SLCIND_ST_FLAG')[-1]  # ST
    STOP = sdk.getFactorData('LZ_CN_STKA_SLCIND_STOP_FLAG')[-1]  # 停牌
    stock_list = sdk.getStockList()
    stock_list = np.array(stock_list)

    condition_TCAP=TCAP>=np.nanmean(TCAP)
    condition_DR=DR>=np.nanmean(DR)*1.5
    condition_D2A = D2A <= np.nanmean(D2A)
    condition_PE = PE<= np.nanmean(PE)
    condition_PC = PC <= np.nanmean(PC)
    condition_ST = ST == 0
    condition_STOP = STOP == 0
    condition = condition_TCAP*condition_DR* condition_PE * condition_PC * condition_D2A * condition_ST * condition_STOP

    DRs=DR[condition]
    stock_list_s=stock_list[condition]
    DRs=pd.Series(DRs,index=stock_list_s)
    DRs=DRs.sort_values(ascending=False)
    stock_pool=DRs.index[:10]
    transferPosition(sdk, stock_pool)
count+=1
**if **count==HOLDING_PERIOD:
    count=0
sdk.setGlobal('c',count)

**def ****transferPosition**(sdk,stock_pool):
position = sdk.getPositions()
position_dict = dict([i.code, i.optPosition] **for **i **in **position)
stock_to_buy = set(stock_pool) - set(position_dict.keys())
stock_to_sell = set(position_dict.keys()) - set(stock_pool)
quotes = sdk.getQuotes(list(stock_to_buy | stock_to_sell))
**if **stock_to_sell:
    sell_orders = []
    **for **stock **in **stock_to_sell:
        **if **stock **in **quotes.keys():
            price = quotes[stock].current
            volume = position_dict[stock]
            order = [stock, price, volume, -1]
            sell_orders.append(order)
    **if **sell_orders:
        sdk.makeOrders(sell_orders)
        sdk.sdklog("------------------------------------------")
        sdk.sdklog(sdk.getNowDate(),"DATE")
        sdk.sdklog(sell_orders,"SELL")
**if **stock_to_buy:
    available_cash = sdk.getAccountInfo().availableCash
    available_cash_one_stock = available_cash / len(stock_to_buy)
    buy_orders = []
    **for **stock **in **stock_to_buy:
        **if **stock **in **quotes.keys():
            price = quotes[stock].open
volume = int(available_cash_one_stock / (price * 100)) * 100
            **if **volume > 0:
                order = [stock, price, volume, 1]
                buy_orders.append(order)
    **if **buy_orders:
        sdk.makeOrders(buy_orders)
        sdk.sdklog("------------------------------------------")
        sdk.sdklog(sdk.getNowDate(),"DATE")
        sdk.sdklog(buy_orders,'BUY')

**def ****run_all**():
config['initial'] = initial
config['strategy'] = strategy
config['preparePerDay'] = initPerDay
# 启动SDK
SDKCoreEngine(**config).run()

**if **__name__ == '__main__':
run_all()