格雷厄姆的选股因子测试及改进

156 阅读3分钟

格雷厄姆(1894-1976)是价值投资的开创者,其弟子在后面几十年很多都通过价值投资成为投资大师,巴菲特是其中之一。其代表作1934年出版的《证券分析》、1949年出版的《聪明的投资者》都被奉为价值投资的经典必读。

格雷厄姆的根本思想是:股票应该作为企业的一部分。一名投资者应当通过对财务报表的详尽分析得到关于这只证券价值的一个保守估计。如果证券的市场价格能够使其在拥有足够折价以至于大体可以提供一个安全边际的价位上,那么就可以购买这一证券。这就是“价值投资”。

image.png

格雷厄姆最初提出的 投资方法是“流动资产净值分析法”,大致意思是要购买价格低于营运资本或者净流动资产的股票,这种方法在早期的30多年中,取得了超过20%的年均收益,但是后来这种方法因为要求太过严格,这样的买入机会变得屈指可数,其可行性也大打折扣。

后来他又提出一种基于构建组合的方法,他相信这种方法从长期看可以获得两倍于道琼斯工业指数的平均收益,或者说约等于一年15%的收益。组合构建的逻辑描述如下:

image.png

这个策略从1976-2011年的表现如下

image.png

image.png

今天我们尝试将格雷厄姆的策略思路用到A股上,看看效果如何。

测试周期从2014年到2022年, 选股范围是中证500成分股,对标的指数就是中证500指数,每20个交易日调整一次持仓。

原始逻辑

用到的因子是市盈率(pe_ttm)和债务股权比(int_to_talcap),选取市盈率小于10且债务股权比小于50%的成分股作为组合,等权配置。

股票卖出逻辑是:股票持有的收益率超过50%或者持有的天数超过240天。

逻辑实现片段如下:

idx_weight = self.data_obj.factor_arr_dict['index_weight'][k] > 0 # 定位当天的指数成分股

idx_close = np.where((self.hold_days[k] >= 240) | (self.hold_rtn[k] >= 0.5) | (idx_weight == False))[0]

if np.mod(k,20) != 0:

    pos = copy.deepcopy(self.pre_pos)

    pos[idx_close] = 0

    return pos

else:

    pos = np.zeros(len(self.symbol_list))

  


if k > 240:

    pos_idx = np.where((idx_weight == True) & (self.blank_days[k] >= 240))[0]

else:

    pos_idx = np.where((idx_weight == True))[0]

if len(pos_idx) == 0:

    return pos

  


pe_ttm = self.data_obj.factor_arr_dict['pe_ttm'][k, pos_idx]              #市盈率

int_to_talcap = self.data_obj.factor_arr_dict['int_to_talcap'][k, pos_idx] #债务股权比

idx_trdae = np.where((pe_ttm < 10) & (int_to_talcap < 50))[0]              #筛选满足条件的股票

  


target_idx = list(set(pos_idx[idx_trdae]).union(set(np.where(pos>0)[0])) )

if len(target_idx) > 0:

    pos[target_idx] = 1.0/len(target_idx)

下图是回测10年得到的净值曲线,持股数量和持仓比例图。

image.png

可以看到大部分时间策略是持续跑输基准的,从持仓股票数量看,绝大部分时间里持股数都很少,个位数甚至是0,也就是能满足这个绝对值要求的股票太少了,持股数太少很容易受到个别股票极端行情的影响,得不到具有统计效应的信息。

image.png

改进逻辑:

针对原始逻辑存在的问题,我们将逻辑改进一个版本如下:

20天决策一次,在成分股内分别对市盈率和债务股权比这2个因子进行排序,将两个因子的排序序号相加,取相加后的排序序号最小的前10%股票进行等权持仓。

逻辑实现片段如下:

# 准备决策数据并处理异常值

pe_ttm = self.data_obj.factor_arr_dict['pe_ttm'][k, idx]

int_to_talcap = self.data_obj.factor_arr_dict['int_to_talcap'][k, idx]

pe_ttm[np.isnan(pe_ttm)] = 999

pe_ttm[pe_ttm < 0] = 999

int_to_talcap[np.isnan(int_to_talcap)] = 999

int_to_talcap[int_to_talcap < 0] = 999

hold_num = int(len(pe_ttm) * 0.1)

# hold_num = 20

ridx1 = np.argsort(pe_ttm)

index1 = np.zeros(len(pe_ttm))

index1[ridx1] = range(len(pe_ttm))

  
ridx2 = np.argsort(int_to_talcap)

index2 = np.zeros(len(pe_ttm))

index2[ridx2] = range(len(pe_ttm))

index = index1 + index2

idx_trdae = np.argsort(index)[:hold_num]

target_idx = pos_idx[idx_trdae]


if len(target_idx) > 0:

    pos[target_idx] = 1.0/len(target_idx)

回测得到的结果如下,从超额收益看,还是有明显改善的,尤其在2019年以前比较稳定的跑赢基准,在2019年之后超额表现就不太理想了。

image.png

image.png

从持仓数量看,一直稳定在50只左右,上下波动是因为有时因为涨跌停或停牌导致买不进或者卖不出的情况。

image.png 下表是改进后的策略的具体绩效指标。

image.png