掘金社区

4年100万赚10倍,神奇公式的掘金量化回测思考与探讨(附源代码)Pinned highlighted

厚朴 发表在策略分享 2021-11-22 10:54:11

策略分享
363
5
0

价值投资是巴菲特一生坚持的投资理念。

近期网上流传着一个简单的神奇公式,这个神奇公式正是根据巴老先生的价投理念简化而来,出自于华尔街明星投资人格林布拉特之手。

最初格林布拉特只是想,能不能用量化的手段复制巴菲特的价投理念,在经过了大量的研究和实测之后,他发明了这个公式,并把它写进了《股市稳赚》一书,公之于众。

在美国,根据神奇公式制作的投资策略,1988年至2004年这17年间,平均年化收益率达到30.8%,而同期标普500的平均年化收益率12.4%。

在中国,根据神奇公式官网显示,从2005年1月到2016年4月,它的回测收益为864.93%,平均年化收益为22.28%。

那么该公式是否真的如宣传的那样神奇呢?我们普通散户也能用得上吗?今天就来一探究竟。

神奇公式的核心思想

简单来说,这个公式是秉承着“物美价廉”的原则来选择股票,也就是“好公司+便宜价格”。

怎么来衡量这个好与便宜,格林布拉特发明了两个指标:ROC与EY。

ROC(资本收益率)来衡量公司好与差;EY(股票收益率)来衡量股票当前的价格是贵还是便宜。

ROC越大就表明该公司能在同样的时间里用同样的资金赚取越多的利润,公司赚钱效率越高,当然就越是好公司;
另一方面,EY越大,表明该公司在盈利差不多的情况下,市值比别的公司更小,也就是股票价格相对更便宜。

神奇公式就是根据ROC和EY的排名来选择优质低价的股票。

一、神奇公式的计算方法

先分别计算每只股票的ROC(资本收益率)与EY(股票收益率),再按照ROC和EY对股票池中的股票进行排名,最后将两个排名相加,按照两者排名之和再次排名,得到排名前三十的股票。

ROC与EY的具体计算方式如下:

  • ROC = 息税前利润 / (净营运资本 + 固定资产)

  • EY = 息税前利润 / 企业价值

EBIT(息税前利润)= 税前利润 + 利息费用
净营运资本 = 应收账款 + 其他应收款 + 预付账款 + 存货 - 无息流动负债 + 长期股权投资 + 投资性房地产
无息流动负债 = 应付账款 + 预收款项 + 应付职工薪酬 + 应交税费 + 其他应付款 + 预提费用 + 递延收益流动负债 + 其他流动负债
企业价值 = 总市值 + 其他权益工具 + 带息负债 + 少数股东权益
带息债务 = 负债合计 - 无息流动负债 - 无息非流动负债
无息非流动负债 = 非流动负债合计 - 长期借款 - 应付债券

二、神奇公式掘金源代码

公式说起来简单,但是真的要收集数据计算一遍还是很复杂的,好在我们有计算机帮忙整理数据和计算,这里用掘金量化平台的python语言编写程序,假定掘金量化平台获取到的相关财务数据都是准确可信的,根据神奇公式的计算方法,编写策略代码,源代码参见文末。

三、神奇公式在A股的回测效果

0_1637548294168_图片1.png
从2018年1月1日至2021年11月20日,累计收益率为85.74%,年化收益率22.05%,基本与神奇公式官网公布的年化收益率22.28%相符,但是我们做量化并不满足于这个成绩,有没有更好的参数组合,或者更合理的买卖策略呢?

四、策略参数寻优

雪球上有人说取前80只股票效果最好,那么我们就取前80只来看看,不仅取前80只的,我们还可以从10只到100只,分成10段,每段都看看效果,同时我们把我们的资金利用率从原来的0.8,改成0.8、0.9和1,3个档位,分别看看哪种组合效果最好。参数寻优代码如下:
0_1637558488761_11.png
(如果需要这部分源代码,可以私聊我,可以把完整的代码都发给你)
回测结果如下:
0_1637548502796_图片2.png
这个回测总共有30种组合,我开启12个线程同时开跑,也足足花了我5个多小时才跑完这么多回测,程序跑完后会生成一个结果文档,30种组合的累计盈利率和年化收益等数据一目了然:
0_1637548518499_图片3.png
结果让我大跌眼镜,买入前80只排名最大的效果并不比前30只的好,相反,股票池数量越多,反而收益越低,而其中效果最好的一组,赫然是ratio=1且num=10时,也就是说全仓买入排名前10的股票,然后根据策略发出的信号来进行调仓,收益是最大的,累计收益率达到了168.31%,年化收益率则达到了42.29%,当然在这种情况下,回撤也是最大的,达到了28%。

由此,我想到了一种最极端的情况,如果我把ratio=1且num=1,也就是说全仓买入排名最大的那一只股票,结果会怎样呢?想到这,我开始兴奋了,这个回测结果也确实让我瞠目结舌,见下图:
0_1637548549857_图片4.png
从2018年1月1日至2021年11月20日,累计收益率为969.12%,年化收益率249.28%,回撤则是达到了最大的56.21%。

五、策略进一步改进

至此,我对这个研究结果已经比较满意了,相当于4年翻了近10倍,不过,我还想再挖掘一下,看看这个指标的潜力到底有多大。这一次,我们采用上一次找到的最优参数组合,让ratio和num都等于1,同时把买卖策略改一下,每个月计算一次ROC排名,选出排名在前100名的,然后对这100只股票每个星期计算一次EY的排名,选取EY最大的那只,并把每个月调一次仓改成每星期调一次仓。

为什么要这样改呢,因为像资产负债表这样的财务数据是每个季度才会有变化的,本来没有必要每个月都计算的,但是掘金量化平台目前不支持按季度执行任务,只能每个月计算一次了,另外一方面,与EY相关的公司市值这个数据是跟股票当前价格相关的,因此这个数值每天都在变化,所以,我们在买卖策略里改成每个星期计算一次EY的排名,同时调仓周期随着改成每星期一次,这样似乎更为合理一些,代码如下:

0_1637558579362_22.png
(如果需要这部分源代码,可以私聊我,可以把完整的代码都发给你)
来看看效果如何

0_1637548770587_图片5.png
经过改进的神奇公式策略,回测结果更加炸裂,累计收益率达到了惊人的4893.97%,年化收益率则达到1258.84%。

但是这个结果可能有很大的运气成分在里面,因为从交易明细里可以看到,今年的收益之所以如此高,完全是因为抓住了一两只大牛股,热景生物和智光电气,当然,也可以认为这种运气本来就包括在选股策略里,迟早会降临。但是即便不算热景生物,累计收益率仍然在10倍以上。
0_1637548850798_图片6.png

总结,从以上的研究,至少可以得出一个结论,全仓买入一只股票,比分散投资买入多只股票收益要高,这完全颠覆了我之前“鸡蛋不要放在一篮子里”的认知,当然前提是要买到真正的“又好又便宜”的股票,而这个“又好又便宜”并不止一种衡量标准,每个人观点不一样,衡量标准也会不一样,神奇公式只是其中的一种。
不过高收益的同时也要承担高风险,尤其是价值投资本身就是反人性的,当亏损达到50%甚至更高时,你还能坚持信念一直按策略执行吗?

最后,在量化学习的路上,我相信很多人都和我一样,非常希望遇到志通道合的同行者,一起学习和提高,写本文的目的在于,分享一种验证神奇公式的技术方法,其中还有很多细节值得商榷和探讨,希望感兴趣的在此留言,一起探讨改进方法。

评论: 5
  • 请发一份最后一种回测的原码给我,谢谢!

    2021-11-24 10:16:30
  • Hi,能否把week交易的代码分享给我呢。。。

    2021-11-24 10:46:38
  • @勇敢的琦仔 你有收到代码吗?同求

    2021-11-24 10:57:40
  • @勇敢的琦仔
    # coding=utf-8

    from __future__ import print_function, absolute_import, unicode_literals
    
    from gm.api import *
    
    import numpy as np
    
    import pandas as pd
    
    
    
    """
    
    神奇公式策略回测
    
    本策略第月触发一次,按照神奇公式的原始定义,分别计算出所有股票的资本收益率和股票收益率的排名,对排名值相加,再取前30名的股票等权重买入
    
    每个月重新计算排名,先卖出不在前30名的股票,再对新的前30名股票池进行等权重调仓,如此循环。
    
    回测时间为:2018-01-01 08:00:00 到 2021-11-20 16:00:00 
    
    """
    
    
    
    def init(context):
    
        context.roc_symbols = None    # 存储ROC计算数据表
    
        context.ratio = 1    # 设置买入股票资金比例
    
        context.num = 1       # 设置股票池的数量
    
        #通过get_instruments获取所有的上市股票代码,这里需要包含所有的停牌股和st股
    
        context.all_stock = get_instruments(exchanges='SHSE, SZSE', sec_types=SEC_TYPE_STOCK, skip_suspended=False,
    
                                       skip_st=False, fields='symbol, delisted_date', df=True)
    
    
    
        # 每月的第一个交易日的09:40:00执行策略algo
    
        schedule(schedule_func=algo, date_rule='1m', time_rule='9:40:00')
    
        # 保证先运行ROC计算
    
        if context.roc_symbols == None:
    
            algo(context)
    
        #每周的第一个交易日的09:40:00执行策略algo_week
    
        schedule(schedule_func=algo_week, date_rule='1w', time_rule='9:40:00')
    
    
    
    def algo(context):
    
        # 获取当前时间
    
        today = context.now.strftime("%Y-%m-%d %H:%M:%S")
    
        last_day = get_previous_trading_date(exchange='SHSE', date=today)
    
        print('当前时间:'+today)
    
        # 剔除当前退市股和B股
    
        stocks = context.all_stock[(context.all_stock['delisted_date'] > today) & (context.all_stock['symbol'].str[5] != '9')
    
                               & (context.all_stock['symbol'].str[5] != '2')]['symbol'].to_list()
    
    
    
        # 剔除当前停牌股和ST股
    
        stocks = get_history_instruments(stocks, fields='symbol,sec_level,is_suspended',
    
                                        start_date=last_day, end_date=last_day, df=True)
    
        stocks = stocks[(stocks['sec_level'] == 1) & (stocks['is_suspended'] == 0)]['symbol'].to_list()
    
    
    
        # 获取息税前利润
    
        finance_data = get_fundamentals_n(table='deriv_finance_indicator', symbols=stocks, count=1,
    
                                    end_date=today, fields='EBIT',df=True)
    
    
    
        # 获取资产负债表其他数据
    
        balance_data = get_fundamentals_n(table='balance_sheet', symbols=stocks,count=1,end_date=today,
    
                                fields='ACCORECE,OTHERRECE,PREP,INVE,EQUIINVE,INVEPROP,ACCOPAYA,ADVAPAYM,COPEWORKERSAL,TAXESPAYA,OTHERPAY,ACCREXPE,DEFEREVE,OTHERCURRELIABI,FIXEDASSENETW,OTHEQUIN,MINYSHARRIGH,TOTLIAB,LONGBORR,BDSPAYA,TOTALNONCLIAB', df=True)
    
    
    
        # 合并衍生数据表和资产负债表以及交易行情衍生表
    
        fundamental = pd.merge(finance_data, balance_data, on='symbol',how='inner')
    
    
    
        # 计算资本收益率ROC:ROC = 息税前利润 EBIT/ (净运营资本 + 固定资产 FIXEDASSENETW)
    
        # 净运营资本 = 应收账款 ACCORECE + 其他应收款 OTHERRECE + 预付账款 PREP + 存货 INVE - 无息流动负债 + 长期股权投资 EQUIINVE+ 投资性房地产 INVEPROP
    
        # 无息流动负债 = 应付账款 ACCOPAYA + 预收款项 ADVAPAYM + 应付职工薪酬 COPEWORKERSAL + 应交税费 TAXESPAYA + 其他应付款 OTHERPAY + 预提费用 ACCREXPE+ 递延收益.流动负债 DEFEREVE + 其他流动负债 OTHERCURRELIABI
    
        fundamental['ROC'] = fundamental['EBIT']/(fundamental['ACCORECE']+
    
                                                    fundamental['OTHERRECE']+
    
                                                    fundamental['PREP']+
    
                                                    fundamental['INVE']+
    
                                                    fundamental['EQUIINVE']+
    
                                                    fundamental['INVEPROP']-
    
                                                    fundamental['ACCOPAYA']-
    
                                                    fundamental['ADVAPAYM']-
    
                                                    fundamental['COPEWORKERSAL']-
    
                                                    fundamental['TAXESPAYA']-
    
                                                    fundamental['OTHERPAY']-
    
                                                    fundamental['ACCREXPE']-
    
                                                    fundamental['DEFEREVE']-
    
                                                    fundamental['OTHERCURRELIABI']+
    
                                                    fundamental['FIXEDASSENETW'])
    
        # ROC按大小排序,取最大的前100名,作为初选池
    
        fundamental['ROC_rank']= fundamental['ROC'].rank()
    
        fundamental.sort_values(by='ROC',ascending=False, inplace=True)
    
        context.roc_symbols = fundamental.reset_index(drop=True).loc[:100]
    
    
    
    def algo_week(context): 
    
        # 获取当前时间
    
        today = context.now.strftime("%Y-%m-%d %H:%M:%S")
    
        print('当前时间:'+today)
    
        # 获取总市值
    
        indicator_data = get_fundamentals_n(table='trading_derivative_indicator', symbols=context.roc_symbols['symbol'].to_list(),
    
                                            count=1,end_date=today, fields='TOTMKTCAP',df=True)
    
        
    
        # 拼合context.roc_symbols和indicator_data
    
        fundamental = pd.merge(context.roc_symbols, indicator_data, on='symbol',how='inner')
    
    
    
        # 计算股票收益率EY:EY = 息税前利润 EBIT/ 企业价值
    
        # 企业价值 = 总市值 TOTMKTCAP + 其他权益工具 OTHEQUIN + 带息债务 + 少数股东权益 MINYSHARRIGH
    
        # 带息债务 = 负债合计 TOTLIAB - 无息流动负债 - 无息非流动负债
    
        # 无息非流动负债 = 非流动负债合计 TOTALNONCLIAB - 长期借款 LONGBORR- 应付债券 BDSPAYA
    
        fundamental['EY'] = fundamental['EBIT']/(fundamental['TOTMKTCAP']+
    
                                                    fundamental['OTHEQUIN']+
    
                                                    fundamental['MINYSHARRIGH']+
    
                                                    fundamental['TOTLIAB']+
    
                                                    fundamental['LONGBORR']+
    
                                                    fundamental['BDSPAYA']-
    
                                                    fundamental['TOTALNONCLIAB']-
    
                                                    fundamental['ACCOPAYA']-
    
                                                    fundamental['ADVAPAYM']-
    
                                                    fundamental['COPEWORKERSAL']-
    
                                                    fundamental['TAXESPAYA']-
    
                                                    fundamental['OTHERPAY']-
    
                                                    fundamental['ACCREXPE']-
    
                                                    fundamental['DEFEREVE']-
    
                                                    fundamental['OTHERCURRELIABI'])
    
    
    
        # EY排序,取值最大的1只股票进行买入
    
        fundamental.sort_values(by='EY',ascending=False, inplace=True)
    
        symbols = fundamental.reset_index(drop=True).loc[:context.num - 1, 'symbol'].to_list()
    
    
    
        # 获取当前所有仓位
    
        positions = context.account().positions()
    
    
    
        # 平不在股票池的仓位
    
        for position in positions:
    
            symbol = position['symbol']
    
            if symbol not in symbols:
    
                order_target_percent(symbol=symbol, percent=0, order_type=OrderType_Market,
    
                                    position_side=PositionSide_Long)
    
                print('市价单平不在股票池的仓位', symbol)
    
    
    
        # 计算每只股票买入比例
    
        if len(symbols) > 0:
    
            # 将股票池中的股票持仓调整至percent
    
            percent = context.ratio / len(symbols)
    
            for symbol in symbols:
    
                order_target_percent(symbol=symbol, percent=percent, order_type=OrderType_Market,
    
                                position_side=PositionSide_Long)
    
                print(symbol, '以市价单调整至权重', percent)
    
    
    
    # 查看最终的回测结果
    
    def on_backtest_finished(context, indicator):
    
        print(indicator)
    
    
    
    
    
    if __name__ == '__main__':
    
        '''
    
            strategy_id策略ID, 由系统生成
    
            filename文件名, 请与本文件名保持一致
    
            mode运行模式, 实时模式:MODE_LIVE回测模式:MODE_BACKTEST
    
            token绑定计算机的ID, 可在系统设置-密钥管理中生成
    
            backtest_start_time回测开始时间
    
            backtest_end_time回测结束时间
    
            backtest_adjust股票复权方式, 不复权:ADJUST_NONE前复权:ADJUST_PREV后复权:ADJUST_POST
    
            backtest_initial_cash回测初始资金
    
            backtest_commission_ratio回测佣金比例
    
            backtest_slippage_ratio回测滑点比例
    
            '''
    
        run(strategy_id='XXXXXXX',
    
            filename='main.py',
    
            mode=MODE_BACKTEST,
    
            token='XXXXXXXXX',
    
            backtest_start_time='2018-01-01 08:00:00',
    
            backtest_end_time='2021-11-20 16:00:00',
    
            backtest_adjust=ADJUST_PREV,
    
            backtest_initial_cash=100000,
    
            backtest_commission_ratio=0.0015,
    
            backtest_slippage_ratio=0.0001)
    2021-11-24 20:43:35
  • @厚朴 棒棒的,楼主威武

    2021-11-25 13:44:40

Looks like your connection to 掘金量化社区 - 量化交易者的交流社区 was lost, please wait while we try to reconnect.