0%

screen(股票筛选器)之二 -- 回测,没有全面调查就没有伤害

本文描述了在对screen(股票筛选器)进行回测的过程中所碰到的各种问题以及对应的解决方法。最后,通过对portfolio中个股权重进行优化,获得了一个相当不错的回测结果。回测以股灾顶峰(2015-06-12)为开始时点一直到2020-09-25,并以比较保守的参数设置,期间获得了近45%的总收益或年化7.5%的收益。

这段时间真的是做回测做到吐了。回测真的是一件超重体力活啊!自己刚从一个坑爬出来紧接着又跌入下一个坑,不断往复。。。痛不欲生!如果有人告诉你一个量化策略并说能盈利,但是没有回测记录那是在耍流氓。如果他给你看回测记录而没有告诉你回测条件那也是在耍流氓。

上篇一个基于价值的screen(筛选器) -- 大概是最简单的量化投资介绍了一个基于价值的Screen,并且用最原始的方式做了回测,包括:

  • 在计算aggregate z-score的时候使用等权重
  • 在构建portfolio时简单地使用aggregate z-score为top n的股票
  • rebalance时的操作为先全部卖出,再全部买入

回测的结果显示能明显跑赢基准,当时心理还是沾沾自喜,要知道这可是一把成功,没有overfitting这回事啊!想接下来再做一些优化,最主要想对aggregate z-score的权重进行优化。但转念觉得自己还没有评估过该策略在不同情况下的表现,比如:

  • 调整rebalance的时间点,如前移或后移
  • 增加或减少rebalance 的次数

第一感觉是这些调整对回测结果只会产生微弱的影响。但是噩梦就此开始。。。

问题暴露

说干就干,撸起袖子开始改代码(尽显码农本色)。

改变调仓(rebalance)周期

上篇screen中每年做3次rebalance,分别对应三个财报季:

  • 1季报:4月30日以后的第一个交易日。
  • 2季报(中报):8月31日以后的第一个交易日。
  • 3季报: 10月31日以后的第一个交易日。

由以上描述可以看出,从三季报到次年一季报之间有6个月的时间,这就导致了rebalance之间的interval不一致,因此考虑在1月31日以后的第一个交易日新增一次rebalance(年报:每年1月1日——4月30日,因此同1季报的时间重叠,因此等年报出来一季报也基本发布了,那么年报的信息相对就有些过期了,所以,原本3次调仓其实是忽略了年报的信息)。那么rebalance每年4次,interval基本上在3个月左右。

可是结果有点出乎意料,5年的回测成绩不理想,如下图所示:

而如果换作半年一次调仓,调仓时点放在一季报和三季报发布的时间段,那么5年的回测收益接近原来的收益。如下图所示:

增加一次调仓的结果真的让我大跌眼镜!隐约感觉策略可能出问题了。继续测试其他场景。。。

改变调仓时点

如果改变调仓的时间点,则回测收益也出现了明显的下滑。首先,测试将调仓时间后移2周,每年两次调仓,即调仓的时间点为:

  • 1季报:5月15日以后的第一个交易日。
  • 3季报: 11月15日以后的第一个交易日。

测试结果如下图所示:

如果说将调仓时间后移使得相关财报信息被市场消化,导致收益下降是一种解释。但是,明显跑输沪深300就感觉很难解释了,是因为市场风格发生了变化吗?

那么如果将rebalance时点比原来提前一周呢?,即调仓的时间点为:

  • 1季报:4月23日以后的第一个交易日。
  • 3季报: 10月24日以后的第一个交易日。

测试结果如下所示:

微弱跑输沪深300。可能是因为没有获取到全部财报的更新数据?

根据以上描述,可以发现该Screen对rebalance的时点和次数都是非常敏感。这同我的预期形成了鲜明的反差,严重伤害了我的感情!这样的策略不可能应用到实战中,于是开始着手调查。。。

陷阱

由于整个回测是自动化完成的,要找到问题并不容易,输出日志也许是为数不多的方法。

陷阱之一 — 一定是策略出问题了

对于调整rebalance时间点对回测结果产生重大影响这个问题,我第一反应是很可能策略很有可能在rebalance时间前后会给出差异巨大的输出,这样就可能导致回测结果出现严重的不一致。该如何排查这一点呢?只能通过日志输出来进行人工比较。但是,结果显示,策略的输出并无明显差别,如下所示。下表是策略原来预设的调仓时间点:2017-04-30以后的第一交易日所产生的输出:

label z_score_sum symbol industry_name
000502.XSHE 8.890258 绿景控股 房地产业
000004.XSHE 8.681055 国农科技 软件和信息技术服务业
600338.XSHG 8.068400 西藏珠峰 有色金属矿采选业
002110.XSHE 7.868558 三钢闽光 黑色金属冶炼和压延加工业
000036.XSHE 7.327801 华联控股 房地产业
600052.XSHG 6.932945 浙江广厦 广播、电视、电影和影视录音制作业
600519.XSHG 6.889754 贵州茅台 酒、饮料和精制茶制造业
002120.XSHE 6.812944 韵达股份 邮政业
002508.XSHE 6.806315 老板电器 电气机械和器材制造业
000011.XSHE 6.669217 深物业A 房地产业
002372.XSHE 6.640031 伟星新材 橡胶和塑料制品业
002032.XSHE 6.371474 苏泊尔 金属制品业
002352.XSHE 6.352925 顺丰控股 邮政业
600681.XSHG 6.345977 百川能源 燃气生产和供应业
002558.XSHE 6.327748 巨人网络 互联网和相关服务
600641.XSHG 6.267760 万业企业 房地产业
002035.XSHE 6.266626 华帝股份 电气机械和器材制造业
002468.XSHE 6.229671 申通快递 邮政业
600477.XSHG 6.075676 杭萧钢构 金属制品业
002466.XSHE 6.044695 天齐锂业 有色金属冶炼和压延加工业
600887.XSHG 5.964935 伊利股份 食品制造业
002597.XSHE 5.962833 金禾实业 化学原料及化学制品制造业
600167.XSHG 5.836436 联美控股 电力、热力生产和供应业
002677.XSHE 5.706263 浙江美大 电气机械和器材制造业
600779.XSHG 5.639503 水井坊 酒、饮料和精制茶制造业
600746.XSHG 5.600572 江苏索普 化学原料及化学制品制造业
002647.XSHE 5.583226 仁东控股 其他金融业
300268.XSHE 5.565489 佳沃股份 农副食品加工业
300136.XSHE 5.494172 信维通信 计算机、通信和其他电子设备制造业
600688.XSHG 5.400361 上海石化 石油加工、炼焦和核燃料加工业
002233.XSHE 5.293639 塔牌集团 非金属矿物制品业
002136.XSHE 5.285238 安纳达 化学原料及化学制品制造业
002460.XSHE 5.247013 赣锋锂业 有色金属冶炼和压延加工业
603288.XSHG 5.194811 海天味业 食品制造业
600132.XSHG 5.166631 重庆啤酒 酒、饮料和精制茶制造业
002043.XSHE 5.161986 兔宝宝 木材加工和木、竹、藤、棕、草制品业
600507.XSHG 5.078334 方大特钢 黑色金属冶炼和压延加工业
002555.XSHE 4.955648 三七互娱 互联网和相关服务
002636.XSHE 4.927382 金安国纪 计算机、通信和其他电子设备制造业
002133.XSHE 4.854489 广宇集团 房地产业
300176.XSHE 4.773486 派生科技 汽车制造业
600309.XSHG 4.762293 万华化学 化学原料及化学制品制造业
002713.XSHE 4.732748 东易日盛 建筑装饰和其他建筑业
000568.XSHE 4.716226 泸州老窖 酒、饮料和精制茶制造业
000910.XSHE 4.704433 大亚圣象 木材加工和木、竹、藤、棕、草制品业
002507.XSHE 4.697732 涪陵榨菜 食品制造业
600211.XSHG 4.618739 西藏药业 医药制造业
002572.XSHE 4.540608 索菲亚 家具制造业
000930.XSHE 4.525929 中粮科技 化学原料及化学制品制造业
000813.XSHE 4.461262 德展健康 医药制造业

而下表为推后的调仓时间点:2017-05-16这个交易日所产生的输出,比策略原来预设的调仓时间点晚了2周:

label z_score_sum symbol industry_name
000502.XSHE 8.778548 绿景控股 房地产业
000004.XSHE 8.597785 国农科技 软件和信息技术服务业
002110.XSHE 7.754317 三钢闽光 黑色金属冶炼和压延加工业
002120.XSHE 7.576107 韵达股份 邮政业
600519.XSHG 7.571283 贵州茅台 酒、饮料和精制茶制造业
600338.XSHG 7.553469 西藏珠峰 有色金属矿采选业
000036.XSHE 7.347328 华联控股 房地产业
002508.XSHE 7.269823 老板电器 电气机械和器材制造业
000011.XSHE 7.180023 深物业A 房地产业
002352.XSHE 7.168590 顺丰控股 邮政业
002035.XSHE 7.091673 华帝股份 电气机械和器材制造业
002032.XSHE 7.063430 苏泊尔 金属制品业
002372.XSHE 7.029911 伟星新材 橡胶和塑料制品业
600052.XSHG 6.886197 浙江广厦 广播、电视、电影和影视录音制作业
002468.XSHE 6.820134 申通快递 邮政业
600477.XSHG 6.657167 杭萧钢构 金属制品业
002597.XSHE 6.645728 金禾实业 化学原料及化学制品制造业
600887.XSHG 6.407632 伊利股份 食品制造业
002466.XSHE 6.316711 天齐锂业 有色金属冶炼和压延加工业
300136.XSHE 6.279457 信维通信 计算机、通信和其他电子设备制造业
600779.XSHG 6.244783 水井坊 酒、饮料和精制茶制造业
600641.XSHG 6.199704 万业企业 房地产业
002677.XSHE 6.016379 浙江美大 电气机械和器材制造业
002460.XSHE 5.974417 赣锋锂业 有色金属冶炼和压延加工业
603288.XSHG 5.783055 海天味业 食品制造业
000568.XSHE 5.741317 泸州老窖 酒、饮料和精制茶制造业
002636.XSHE 5.690895 金安国纪 计算机、通信和其他电子设备制造业
600681.XSHG 5.650337 百川能源 燃气生产和供应业
600746.XSHG 5.632605 江苏索普 化学原料及化学制品制造业
600132.XSHG 5.505306 重庆啤酒 酒、饮料和精制茶制造业
600688.XSHG 5.485803 上海石化 石油加工、炼焦和核燃料加工业
300268.XSHE 5.423325 佳沃股份 农副食品加工业
002507.XSHE 5.385897 涪陵榨菜 食品制造业
000858.XSHE 5.205664 五粮液 酒、饮料和精制茶制造业
002555.XSHE 5.198571 三七互娱 互联网和相关服务
000910.XSHE 5.184445 大亚圣象 木材加工和木、竹、藤、棕、草制品业
600507.XSHG 5.169067 方大特钢 黑色金属冶炼和压延加工业
600309.XSHG 5.164688 万华化学 化学原料及化学制品制造业
002572.XSHE 5.162698 索菲亚 家具制造业
600167.XSHG 5.118928 联美控股 电力、热力生产和供应业
002136.XSHE 5.080895 安纳达 化学原料及化学制品制造业
002043.XSHE 5.013130 兔宝宝 木材加工和木、竹、藤、棕、草制品业
000049.XSHE 5.000309 德赛电池 电气机械和器材制造业
002558.XSHE 4.966376 巨人网络 互联网和相关服务
002304.XSHE 4.951366 洋河股份 酒、饮料和精制茶制造业
002415.XSHE 4.854819 海康威视 计算机、通信和其他电子设备制造业
002647.XSHE 4.782066 仁东控股 其他金融业
300176.XSHE 4.772607 派生科技 汽车制造业
002713.XSHE 4.767137 东易日盛 建筑装饰和其他建筑业
600211.XSHG 4.707598 西藏药业 医药制造业

由以上两表对比可见,策略输出的一致性还是相当高的,除了排名发生了些许变化(因为股价改变了),总体来看排名前50的成分股票变化很少(有贵州茅台哦!!!)。那么就排除了策略算法上的问题。到底哪里出问题了?感觉没啥方向,已经感觉大伤元气了。。。

陷阱之二 — 停牌

既然策略本身没有问题,那很有可能在执行策略的过程中出问题了!于是将排查的重点放到持仓上。果然很快就发现问题了,通过日志可以发现每次rebalance的时候,总是有一些股票卖不出去,同时会有一些股票买不进来。再通过日志发现因为股票停牌了!对于普通投资者来说本来的持仓的股票个数也不会很大,因此很少会碰到股票停牌。但是,对于策略来说,我的portfolio由50只股票组成。几乎每次rebalance时总是会碰到有股票停牌。尤其是在2015年6月份极端行情的那段时间,出现了大面积的股票停牌,这对策略的结果产生了很大的冲击。因此就会出现改变rebalance的时间点,portfolio的持仓会有很大的区别。例如对于通过策略入围的50只股票,在t天有10只股票停牌,而在t+n天可只有3只股票停牌了。那么在t日买入和在t+n日买入portfolio的构成会有很大的区别。由于停牌,导致了我的portfolio在不同时点做rebalance会有不同只数的股票构成,而且股票只数不足50只。

发现问题以后,又是狂敲一通代码修改策略的回测过程,在rebalance当天:

  • 如果有股票因停牌无法卖出的,在买入时还是以portfolio的股票到达50只为止。例如当日有10只股票因停牌无法卖出的,那么只能买入40只股票。
  • 由于停牌无法卖出的股票在后续交易日执行卖出操作,有可能后续交易日继续停牌,或跌停无法卖出的,在下一交易日继续执行卖出,直至全部卖出为止。
  • 在卖出的同时补入对应数量入围股票。

一番测试,修改bug以后觉得没啥问题了,结果一跑回测,发现portfolio里面股票的数量还是不对。。。继续排查。。。

陷阱之三 — 涨跌停

顺藤摸瓜,发现策略执行过程中没有考虑涨跌停的股票。例如在rebalance当天,需卖出的股票如果碰到跌停,则是无法卖出的,那么只能在后续交易日卖出。后续交易日也可能继续跌停,那我的策略就是一直执行卖出操作,直至卖出为止。同停牌相似,后续卖出以后也必须买入相同只数的股票。例如后续某交易日补卖出3只股票,则必须同时买入3只股票。也就是说,portfolio的构成始终维持在50只股票。

如果碰到涨停则在rebalance当天无法买入。那么这里的策略同卖出有所不同。买入如果碰到涨停则换一只入围股票,直到portfolio达到50只,也就是说,rebalance当天肯定会使portfolio的持股数量达到50只。

继续修改代码,回测结果如下图所示:

上图的rebalance时间点为预设时间点后移14天,总收益为-1.88%。而预设rebalance的最终收益是-1.96%,两者对比从最终收益角度看差别不大。看上去问题是解决了,但是这收益和基准(沪深300全收益指数)没啥区别,而且很长时间里面还落后基准。难道我设计的策略就只能取得市场收益吗?

我对取得市场收益没有什么偏见,恰恰相反,我觉得能取得市场收益也很不错了。但是,我设计这个策略的初衷肯定是想跑赢市场,那为何没有跑赢呢?继续排查。。。

陷阱之四 — 个股权重

在解决上门几个问题时,已经做了无数次的回测了,每次都会发生同一个现象,那就是:从2017年开始策略就会落后基准,然后同基准的差距越拉越大。初看感觉就是策略上出问题了。但是,检查持仓也没有发现什么。难道是回测框架(RQAlpha)的问题?

为了验证我的想法,我又设计了一个测试方案。我想,既然我的基准是沪深300全收益指数,那么我干脆再写一个策略。每次调仓从沪深300里面随机抽取50只股票,也是等权重。那么,跑出来的收益应该同沪深300指数差不多吧。于是继续一通敲击键盘。执行回测,出结果,见下图:

What?!同样从2017年开始分叉了。我再将佣金调成了0,结果也一样。。。不会真的是回测框架的bug吧?!当时也真的是两眼一黑,觉得要另找回测框架了,心中真的是一万匹草泥马奔腾啊!

转念一想,我每次调仓用的是等权重,而沪深300是流通市值权重。那我是不是该找沪深300等权重指数作为基准来回测一下呢?想想觉得有一定道理,反正死马当活马,再跑一边,出结果:

跑了几遍,基本能验证我的想法。其实用沪深300指数和沪深300等权重指数直接比较一下,就能发现端倪:

等权重指数严重跑输市值指数。说明市值大的股票要比市值小的股票表现更出色。

Ok,长舒一口气,看来权重决定成败了。那么,接下去就不是排查问题了,目标很明确,对股票权重进行优化。(overfitting?!)

优化权重

有了明确的目标,接下来就好办了,不用等权重,换用其他的权重做一下测试。继续猛敲一通键盘(看来应该去买一个手感好一点的键盘了)。

使用流通市值权重

首先想到的就是以市值做为权重。那么在入围的50只股票里面,大盘股的权重大而小盘股的权重小。我直接使用“000985.XSHG,中证全指” 里面的权重为基础,对50只股票按比例重新分配权重(即50只股票的权重之和等于1)。举例来说,假如我的portfolio里有2只股票000001.XSHE和000004.XSHE,在2015-06-12的中证全指里:

  • 000001.XSHE的权重为0.0040
  • 000004.XSHE的权重为0.0001

那么经过换算,这两只股票的权重分别为:

  • 000001.XSHE的权重为0.97561
  • 000004.XSHE的权重为0.02439

使用流通市值权重进行回测的结果是:

Holly Cow… 71.5%的收益而基准收益为-3%!!!我整个人为之一振啊,这岂止是咸鱼翻身啊。。。(放飞自我了10多分钟以后)我想还是该仔细看一下结果,这确实有点事后诸葛亮的味道,毕竟我看到了等权重指数和市值指数的差异以后再来改我的策略。这有点overfitting的味道了。

以2019-05-06 rebalance后的持仓前5位为例:

label position_value name weight
600519.XSHG 271800.0 贵州茅台 0.361408
000858.XSHE 128968.0 五粮液 0.146703
600585.XSHG 50063.0 海螺水泥 0.059027
601888.XSHG 45606.0 中国中免 0.055920
603288.XSHG 44250.0 海天味业 0.053504

可见光茅台一家就占了31%的仓位。这样的仓位从投资组合角度来说风险暴露程度太高了,茅台一家的涨跌直接影响了整个portfolio。而另一个问题是排在第二大仓位的股票是五粮液,而茅台和五粮液同属一个行业,因此,这样的持仓行业集中度也太高了。可以说这是overfitting了。

流通市值++

那就继续对股票权重进行改进。我想到了一个方案:我设置一个单只股票持仓的阈值,portfolio里的股票持仓不能超过这个阈值。比如我设置20%作为阈值,在根现有权重排序,最大的排在最前。那么,上面持仓中,茅台就超过了,因此茅台的持仓变为20%,而多余的部分按比例分配给其余几家公司。如果排在第二家的权重也超了,那么继续这个过程。

举例来说,我有如下的仓位:

label w
001 0.50
002 0.40
005 0.04
003 0.03
004 0.03

我的阈值为20%,那么经过处理得到如下的权重:

label w
001 0.200
002 0.160
003 0.192
004 0.192
005 0.256

通过这样处理就好比regularization能将那些出现极端仓位的情况化解掉,防止overfitting。

那么对于我个人而言,20%是个股的极端仓位了,因此我将阈值设置成20%并回测,结果如下:

近60%的收益,同之前71.5%的收益比有所下降。但这样的权重设置在实际应用中会更加可靠。再来比较一下2019-05-06 rebalance后持仓比例的变化

label position_value name 原weight 新weight
600519.XSHG 271800.0 贵州茅台 0.361408 0.2
000858.XSHE 128968.0 五粮液 0.146703 0.16
600585.XSHG 50063.0 海螺水泥 0.059027 0.0768
601888.XSHG 45606.0 中国中免 0.055920 0.072758
603288.XSHG 44250.0 海天味业 0.053504 0.069614

由上表可见,不光茅台的权重设置成了0.2,茅台+五粮液的权重也明显下降了。个人感觉这个权重分配的算法是比较适合真实应用。由于阈值可以灵活设置,因此可以设置得更加保守,比如15%或10%。下图显示了10%为阈值的回测结果(真的是回测做到吐啊。。。),依旧能获得45.37%的回报。

策略回测结果

最后展示一下策略生成持仓结果,我的设置如下:

  • 起始时间2015-06-12
  • 终止时间2020-09-25
  • 持仓阈值10%
  • 选股范围:中证全指
  • 每年rebalance2次,分别为4月30日以及10月31日以后的第一个交易日

以下展示每年10月31日后第一个交易日rebalance完成后的持仓(top 5):

2015

label position_value name weight
600276.XSHG 51510.0 恒瑞医药 0.088675
600271.XSHG 44728.0 航天信息 0.077000
600066.XSHG 40090.0 宇通客车 0.069016
300017.XSHE 36174.0 网宿科技 0.062274
600649.XSHG 28740.0 城投控股 0.049476

2016

label position_value name weight
600519.XSHG 63336.0 贵州茅台 0.104413
600887.XSHG 57824.0 伊利股份 0.095326
600276.XSHG 51579.0 恒瑞医药 0.085031
300072.XSHE 30821.0 三聚环保 0.050810
000568.XSHE 28320.0 泸州老窖 0.046687

2017

label position_value name weight
600887.XSHG 77004.0 伊利股份 0.093416
600276.XSHG 68510.0 恒瑞医药 0.083112
600519.XSHG 62301.0 贵州茅台 0.075579
002304.XSHE 56600.0 洋河股份 0.068663
600309.XSHG 44820.0 万华化学 0.054373

2018

label position_value name weight
600585.XSHG 70560.0 海螺水泥 0.103942
601888.XSHG 63516.0 中国中免 0.093566
603288.XSHG 58680.0 海天味业 0.086442
600309.XSHG 52110.0 万华化学 0.076764
600516.XSHG 29400.0 方大炭素 0.043309

2019

label position_value name weight
000858.XSHE 93590.0 五粮液 0.099662
002475.XSHE 91868.0 立讯精密 0.097829
603288.XSHG 78603.0 海天味业 0.083703
600585.XSHG 73899.0 海螺水泥 0.078694
300015.XSHE 54522.0 爱尔眼科 0.058060

最后

最后聊点感想,如果要写一些言之有物的东西真心是需要做大量的工作。这篇文章(其实也包括我写的其他内容)背后是大量的代码和测试。对于那些每天更新的自媒体,我不敢说100%但十之八九是没啥信息量。高质量的文章如果能做到每周更新那真的非常不易。

本篇把回测过程中碰到的陷阱给逐个解决了,而且还搞定了个股权重问题,下一篇《screen(筛选器)之三》打算着手优化aggregate z-score的权重问题,也就是每个factor该如何分配权重。敬请期待!

欢迎关注我的其它发布渠道