[量化学堂-策略开发]大师系列之价值投资选股策略

价值投资
大师系列
绩效
回测指标
精选
标签: #<Tag:0x00007fcf6b350bf0> #<Tag:0x00007fcf6b350ab0> #<Tag:0x00007fcf6b350970> #<Tag:0x00007fcf6b350830> #<Tag:0x00007fcf6b3506f0>

(iQuant) #1

国外证券市场比较悠久,曾出现过本杰明·格雷厄姆、彼得·林奇、詹姆斯·奥肖内西、查尔斯·布兰德斯等多位投资大师,这些投资大师有一个共同点,他们在证券市场上保持了常年的稳定持续盈利,他们的投资法则及选股标准在一些著作中有详细的描述。值得欣慰的是,申万宏源证券研究所发布了<申万宏源-申万大师系列价值投资篇>系列第一季共20篇研究报告。学习这些报告主要有两个目的:

一是我们自身想去认真的学习经典,复制这些策略本身就是自我学习过程,我们深信向这些被市场证明长期优秀,被后世尊为经典的投资大师学习,必然值得,必有所得;

二是复制和验证大师策略的过程, 会自然的驱使我们更多的从投资逻辑和投资思维上思考收益之源,而不再是不停的数据挖掘和数理分析。大师系列的尝试,于我们是一个求道,而非求术的旅程。

本贴主要是帮助用户怎样开发大师系列的策略,让大家更了解我们的平台,同时帮助大家在我们的平台上开发更丰富的策略。因此我们介绍一种简单的价值投资法来选取股票,规则如下:

策略逻辑:当股票处于价值洼地时,具备投资价值

策略内容:每月月初买入市盈率小于15倍、市净率小于1.5倍的30只股票,持有至下个月月初再调仓

资金管理:等权重买入

风险控制:无单只股票仓位上限控制、无止盈止损

我们测试了15年到19年4月这长达约4年半的时间,发现策略盈利比较稳健,收益曲线如下:

整体来看,该策略是正收益系统策略,长期坚持该策略收益是不错的,除了18年量化小年没有盈利,其他年份都是盈利的,即使跨越了股灾和熔断期间,最大回撤也是在17.5%以内,风险可控。

是不是发现我们平台很方便开发策略?之前朋友问我,为什么Python运行速度不是最快但会成为量化的主流语言。其实对于量化研究人员来说,虽然速度是一方面考虑,但更多的是为了验证策略思想,Python语言的优势就是在此,有一个思想就可以很快的将思想验证,然而C++虽然速度快,但要验证一个简单的思想却要编写大量的代码。好比为什么飞机速度快,但市里面上班开汽车就足够了(不考虑其他因素),因为汽车足够灵活。所以,还在犹豫选择什么语言从事量化投资的小伙伴们,Python就是你比较好的选择。本文到此就要结束了,策略的完整代码分享在文末,小伙伴们赶紧 克隆策略吧!

克隆策略

    {"Description":"实验创建于2019/4/24","Summary":"","Graph":{"EdgesInternal":[{"DestinationInputPortId":"-64:input_ds","SourceOutputPortId":"-6:data"},{"DestinationInputPortId":"-6:instruments","SourceOutputPortId":"-21:data"},{"DestinationInputPortId":"-43:instruments","SourceOutputPortId":"-21:data"},{"DestinationInputPortId":"-43:options_data","SourceOutputPortId":"-30:data"},{"DestinationInputPortId":"-6:features","SourceOutputPortId":"-35:data"},{"DestinationInputPortId":"-30:input_data","SourceOutputPortId":"-64:sorted_data"}],"ModuleNodes":[{"Id":"-6","ModuleId":"BigQuantSpace.general_feature_extractor.general_feature_extractor-v7","ModuleParameters":[{"Name":"start_date","Value":"","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"end_date","Value":"","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"before_start_days","Value":90,"ValueType":"Literal","LinkedGlobalParameter":null}],"InputPortsInternal":[{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"instruments","NodeId":"-6"},{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"features","NodeId":"-6"}],"OutputPortsInternal":[{"Name":"data","NodeId":"-6","OutputType":null}],"UsePreviousResults":true,"moduleIdForCode":1,"IsPartOfPartialRun":null,"Comment":"","CommentCollapsed":true},{"Id":"-21","ModuleId":"BigQuantSpace.instruments.instruments-v2","ModuleParameters":[{"Name":"start_date","Value":"2015-01-01","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"end_date","Value":"2019-04-23","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"market","Value":"CN_STOCK_A","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"instrument_list","Value":"","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"max_count","Value":"","ValueType":"Literal","LinkedGlobalParameter":null}],"InputPortsInternal":[{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"rolling_conf","NodeId":"-21"}],"OutputPortsInternal":[{"Name":"data","NodeId":"-21","OutputType":null}],"UsePreviousResults":true,"moduleIdForCode":3,"IsPartOfPartialRun":null,"Comment":"","CommentCollapsed":true},{"Id":"-30","ModuleId":"BigQuantSpace.filter.filter-v3","ModuleParameters":[{"Name":"expr","Value":"pb_lf_0 < 1.5 & pe_ttm_0 < 15 & amount_0 > 0 & pb_lf_0 > 0 & pe_ttm_0 > 0","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"output_left_data","Value":"False","ValueType":"Literal","LinkedGlobalParameter":null}],"InputPortsInternal":[{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"input_data","NodeId":"-30"}],"OutputPortsInternal":[{"Name":"data","NodeId":"-30","OutputType":null},{"Name":"left_data","NodeId":"-30","OutputType":null}],"UsePreviousResults":true,"moduleIdForCode":4,"IsPartOfPartialRun":null,"Comment":"","CommentCollapsed":true},{"Id":"-35","ModuleId":"BigQuantSpace.input_features.input_features-v1","ModuleParameters":[{"Name":"features","Value":"pb_lf_0\npe_ttm_0\namount_0","ValueType":"Literal","LinkedGlobalParameter":null}],"InputPortsInternal":[{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"features_ds","NodeId":"-35"}],"OutputPortsInternal":[{"Name":"data","NodeId":"-35","OutputType":null}],"UsePreviousResults":true,"moduleIdForCode":5,"IsPartOfPartialRun":null,"Comment":"","CommentCollapsed":true},{"Id":"-43","ModuleId":"BigQuantSpace.trade.trade-v4","ModuleParameters":[{"Name":"start_date","Value":"","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"end_date","Value":"","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"initialize","Value":"# 回测引擎:初始化函数,只执行一次\ndef bigquant_run(context):\n \n # 加载股票指标数据,数据继承自m4模块\n context.indicator_data = context.options['data'].read_df().set_index('date')\n print('indicator_data:', context.indicator_data.head()) \n # 设置交易费用,买入是万三,卖出是千分之1.3,如果不足5元按5元算\n context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))\n # 设置股票数量\n context.stock_num = 30\n \n # 调仓天数,22个交易日大概就是一个月。可以理解为一个月换仓一次\n context.rebalance_days = 22\n \n # 如果策略运行中,需要将数据进行保存,可以借用extension这个对象,类型为dict\n # 比如当前运行的k线的索引,比如个股持仓天数、买入均价\n if 'index' not in context.extension:\n context.extension['index'] = 0\n \n","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"handle_data","Value":"# 回测引擎:每日数据处理函数,每天执行一次\ndef bigquant_run(context, data):\n \n # 按每个K线递增\n context.extension['index'] += 1\n \n # 每隔22个交易日进行换仓\n if context.extension['index'] % context.rebalance_days != 0:\n return \n \n # 日期\n date = data.current_dt.strftime('%Y-%m-%d')\n \n print('debug:', date)\n # 买入股票列表\n stock_to_buy = context.indicator_data.ix[date]['instrument'][:context.stock_num]\n \n # 目前持仓列表 \n stock_hold_now = [equity.symbol for equity in context.portfolio.positions]\n # 继续持有股票列表\n no_need_to_sell = [i for i in stock_hold_now if i in stock_to_buy]\n # 卖出股票列表 \n stock_to_sell = [i for i in stock_hold_now if i not in no_need_to_sell]\n # 执行卖出\n for stock in stock_to_sell:\n if data.can_trade(context.symbol(stock)):\n context.order_target_percent(context.symbol(stock), 0)\n \n # 如果当天没有买入就返回\n if len(stock_to_buy) == 0:\n return\n \n # 等权重\n weight = 1 / len(stock_to_buy)\n # 执行买入\n for cp in stock_to_buy:\n if data.can_trade(context.symbol(cp)):\n context.order_target_percent(context.symbol(cp), weight)","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"prepare","Value":"# 回测引擎:准备数据,只执行一次\ndef bigquant_run(context):\n pass\n","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"before_trading_start","Value":"# 回测引擎:每个单位时间开始前调用一次,即每日开盘前调用一次。\ndef bigquant_run(context, data):\n pass\n","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"volume_limit","Value":0.025,"ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"order_price_field_buy","Value":"open","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"order_price_field_sell","Value":"open","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"capital_base","Value":1000000,"ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"auto_cancel_non_tradable_orders","Value":"True","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"data_frequency","Value":"daily","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"price_type","Value":"后复权","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"product_type","Value":"股票","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"plot_charts","Value":"True","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"backtest_only","Value":"False","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"benchmark","Value":"","ValueType":"Literal","LinkedGlobalParameter":null}],"InputPortsInternal":[{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"instruments","NodeId":"-43"},{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"options_data","NodeId":"-43"},{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"history_ds","NodeId":"-43"},{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"benchmark_ds","NodeId":"-43"},{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"trading_calendar","NodeId":"-43"}],"OutputPortsInternal":[{"Name":"raw_perf","NodeId":"-43","OutputType":null}],"UsePreviousResults":false,"moduleIdForCode":2,"IsPartOfPartialRun":null,"Comment":"","CommentCollapsed":true},{"Id":"-64","ModuleId":"BigQuantSpace.sort.sort-v4","ModuleParameters":[{"Name":"sort_by","Value":"pe_ttm_0,pb_lf_0","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"group_by","Value":"date","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"keep_columns","Value":"--","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"ascending","Value":"True","ValueType":"Literal","LinkedGlobalParameter":null}],"InputPortsInternal":[{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"input_ds","NodeId":"-64"},{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"sort_by_ds","NodeId":"-64"}],"OutputPortsInternal":[{"Name":"sorted_data","NodeId":"-64","OutputType":null}],"UsePreviousResults":true,"moduleIdForCode":6,"IsPartOfPartialRun":null,"Comment":"按多个因子进行排序","CommentCollapsed":true}],"SerializedClientData":"<?xml version='1.0' encoding='utf-16'?><DataV1 xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'><Meta /><NodePositions><NodePosition Node='-6' Position='259,164,200,200'/><NodePosition Node='-21' Position='13,72,200,200'/><NodePosition Node='-30' Position='237.15786743164062,302.4209899902344,200,200'/><NodePosition Node='-35' Position='367,55,200,200'/><NodePosition Node='-43' Position='160,385,200,200'/><NodePosition Node='-64' Position='235,230,200,200'/></NodePositions><NodeGroups /></DataV1>"},"IsDraft":true,"ParentExperimentId":null,"WebService":{"IsWebServiceExperiment":false,"Inputs":[],"Outputs":[],"Parameters":[{"Name":"交易日期","Value":"","ParameterDefinition":{"Name":"交易日期","FriendlyName":"交易日期","DefaultValue":"","ParameterType":"String","HasDefaultValue":true,"IsOptional":true,"ParameterRules":[],"HasRules":false,"MarkupType":0,"CredentialDescriptor":null}}],"WebServiceGroupId":null,"SerializedClientData":"<?xml version='1.0' encoding='utf-16'?><DataV1 xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'><Meta /><NodePositions></NodePositions><NodeGroups /></DataV1>"},"DisableNodesUpdate":false,"Category":"user","Tags":[],"IsPartialRun":true}
    In [ ]:
    # 本代码由可视化策略环境自动生成 2020年4月30日 16:41
    # 本代码单元只能在可视化模式下编辑。您也可以拷贝代码,粘贴到新建的代码单元或者策略,然后修改。
    
    
    # 回测引擎:初始化函数,只执行一次
    def m2_initialize_bigquant_run(context):
        
        # 加载股票指标数据,数据继承自m4模块
        context.indicator_data = context.options['data'].read_df().set_index('date')
        print('indicator_data:', context.indicator_data.head()) 
        # 设置交易费用,买入是万三,卖出是千分之1.3,如果不足5元按5元算
        context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
         # 设置股票数量
        context.stock_num = 30
        
        # 调仓天数,22个交易日大概就是一个月。可以理解为一个月换仓一次
        context.rebalance_days = 22
        
        # 如果策略运行中,需要将数据进行保存,可以借用extension这个对象,类型为dict
        # 比如当前运行的k线的索引,比如个股持仓天数、买入均价
        if 'index' not in context.extension:
            context.extension['index'] = 0
            
    
    # 回测引擎:每日数据处理函数,每天执行一次
    def m2_handle_data_bigquant_run(context, data):
        
        # 按每个K线递增
        context.extension['index']  += 1
        
        # 每隔22个交易日进行换仓
        if context.extension['index'] % context.rebalance_days != 0:
            return 
            
        # 日期
        date = data.current_dt.strftime('%Y-%m-%d')
        
        print('debug:', date)
        # 买入股票列表
        stock_to_buy = context.indicator_data.ix[date]['instrument'][:context.stock_num]
        
        # 目前持仓列表    
        stock_hold_now = [equity.symbol for equity in context.portfolio.positions]
        # 继续持有股票列表
        no_need_to_sell = [i for i in stock_hold_now  if i in stock_to_buy]
        # 卖出股票列表 
        stock_to_sell = [i for i in stock_hold_now if i not in no_need_to_sell]
        # 执行卖出
        for stock in stock_to_sell:
            if data.can_trade(context.symbol(stock)):
                context.order_target_percent(context.symbol(stock), 0)
                
        # 如果当天没有买入就返回
        if len(stock_to_buy) == 0:
            return
        
        # 等权重
        weight = 1 / len(stock_to_buy)
        # 执行买入
        for  cp in stock_to_buy:
            if data.can_trade(context.symbol(cp)):
                context.order_target_percent(context.symbol(cp), weight)
    # 回测引擎:准备数据,只执行一次
    def m2_prepare_bigquant_run(context):
        pass
    
    # 回测引擎:每个单位时间开始前调用一次,即每日开盘前调用一次。
    def m2_before_trading_start_bigquant_run(context, data):
        pass
    
    
    m3 = M.instruments.v2(
        start_date='2015-01-01',
        end_date='2019-04-23',
        market='CN_STOCK_A',
        instrument_list=''
    )
    
    m5 = M.input_features.v1(
        features="""pb_lf_0
    pe_ttm_0
    amount_0"""
    )
    
    m1 = M.general_feature_extractor.v7(
        instruments=m3.data,
        features=m5.data,
        start_date='',
        end_date='',
        before_start_days=90
    )
    
    m6 = M.sort.v4(
        input_ds=m1.data,
        sort_by='pe_ttm_0,pb_lf_0',
        group_by='date',
        keep_columns='--',
        ascending=True
    )
    
    m4 = M.filter.v3(
        input_data=m6.sorted_data,
        expr='pb_lf_0 < 1.5 & pe_ttm_0 < 15 & amount_0 > 0 & pb_lf_0 > 0 & pe_ttm_0 > 0',
        output_left_data=False
    )
    
    m2 = M.trade.v4(
        instruments=m3.data,
        options_data=m4.data,
        start_date='',
        end_date='',
        initialize=m2_initialize_bigquant_run,
        handle_data=m2_handle_data_bigquant_run,
        prepare=m2_prepare_bigquant_run,
        before_trading_start=m2_before_trading_start_bigquant_run,
        volume_limit=0.025,
        order_price_field_buy='open',
        order_price_field_sell='open',
        capital_base=1000000,
        auto_cancel_non_tradable_orders=True,
        data_frequency='daily',
        price_type='后复权',
        product_type='股票',
        plot_charts=True,
        backtest_only=False,
        benchmark=''
    )
    
    indicator_data:                 amount_0  instrument   pb_lf_0  pe_ttm_0
    date                                                    
    2014-10-08  1.398725e+09  600000.SHA  0.840770  4.153471
    2014-10-08  1.115449e+09  601166.SHA  0.893689  4.368097
    2014-10-08  5.460992e+08  601668.SHA  0.822129  4.405653
    2014-10-08  3.438451e+08  600015.SHA  0.831473  4.506142
    2014-10-08  1.714654e+08  601988.SHA  0.778156  4.529399
    
    debug: 2015-02-03
    
    代码策略-点击查看

    第一步:获取数据, 整理换仓时的买入股票列表

    BigQuant平台具有丰富的金融数据,包括行情数据和财报数据,并且具有便捷、简单的API调用接口。

    def prepare(context):
        start_date = context.start_date # 开始日期
        end_date = context.end_date # 结束日期
        context.instruments = D.instruments(context.start_date, context.end_date, market='CN_STOCK_A')
        # 获取市盈率、市净率、成交额数据。history_data是我们平台获取数据的一个重要API。fields参数为列表形式,传入的列表即为我们想要获取的数据。
        history_data = D.history_data(instruments, context.start_date, context.end_date, ['pb_lf', 'pe_ttm','amount'])
        context.daily_buy_stock = history_data.groupby('date').apply(seek_symbol)  #  按交易日groupby,获取每个交易日选出的股票列表
        
    def seek_symbol(df):
        selected = df[(df['pb_lf'] < 1.5)
            & (df['pe_ttm'] < 15) 
            & (df['amount'] > 0) 
            & (df['pb_lf'] > 0)
            & (df['pe_ttm'] > 0)]
                                        
        # 按pe_ttm和pb_lf 升序排列
        selected = selected.sort_values(['pe_ttm', 'pb_lf'])
        return list(selected.instrument)[:30] # 记得转化成list
    

    第二步:回测主体函数
    我们平台策略回测有丰富的文档介绍,请参考:帮助文档

    def initialize(context):
        # 设置交易费用,买入是万三,卖出是千分之1.3,如果不足5元按5元算
        context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
        # 设置换仓规则,即每个月月初换仓,持有至下个月,再换仓
        context.schedule_function(rebalance, date_rule=date_rules.month_start(days_offset=0)) 
       
    def handle_data(context,data):
        pass
    
    # 换仓
    def rebalance(context, data):
        # 日期
        date = data.current_dt.strftime('%Y-%m-%d')
        
        # 买入股票列表
        stock_to_buy = context.daily_buy_stock.ix[date]
        # 目前持仓列表    
        stock_hold_now = [equity.symbol for equity in context.portfolio.positions]
        # 继续持有股票列表
        no_need_to_sell = [i for i in stock_hold_now  if i in stock_to_buy]
        # 卖出股票列表 
        stock_to_sell = [i for i in stock_hold_now if i not in no_need_to_sell]
        # 执行卖出
        for stock in stock_to_sell:
            if data.can_trade(context.symbol(stock)):
                context.order_target_percent(context.symbol(stock), 0)
                
        # 如果当天没有买入就返回
        if len(stock_to_buy) == 0:
            return
        
        # 等权重
        weight = 1 / len(stock_to_buy)
        # 执行买入
        for  cp in stock_to_buy:
            if data.can_trade(context.symbol(cp)):
                context.order_target_percent(context.symbol(cp), weight)
    
    

    第三步:回测接口

    # 使用该回测接口,需要传入多个策略参数
    m = M.trade.v3( 
        instruments=None,
        start_date='2013-01-01', 
        end_date='2018-02-02',
        prepare=prepare,
        # 必须传入initialize,只在第一天运行
        initialize=initialize,
        # 必须传入handle_data,每个交易日都会运行
        handle_data=handle_data,
        # 买入以开盘价成交
        order_price_field_buy='open',
        # 卖出也以开盘价成交
        order_price_field_sell='open',
        # 策略本金
        capital_base=1000000,
        # 比较基准:沪深300
        benchmark='000300.INDX',
    ) 
    

    已经生成了策略,如何利用策略推荐股票操作
    向导式策略生成器新建AI策略
    请教:如何选出高市值的标的
    (DANNIKMAO) #2

    非常奇怪,年化20%多,回撤35%,信息比率居然能达到8.39,确定算法正确??


    (小Q) #3

    经过全面和详细的检查,发现回测结果指标确实有一点问题,已经完全debug了。谢谢您的 宝贵意见!


    (iQuant) #4

    @DANNIKMAO 我们进行了指标的检验,发现修复后指标计算是正确的。
    指标计算参考《回测结果指标详解》
    检验过程如下:

    克隆策略

    1.获取数据

    In [1]:
    start_date = '2013-02-01' # 开始日期
    end_date = '2017-05-07' # 结束日期
    instruments = D.instruments()
    # 获取市盈率、市净率、成交额数据
    history_data = D.history_data(instruments, start_date=start_date,
                   end_date= end_date, fields=[ 'pb_lf', 'pe_ttm','amount'])
    

    2.整理换仓时买入股票列表

    In [2]:
    # 该函数的目的是通过history_data这个大的原始数据,获取每日满足价值投资股票列表
    def seek_symbol(df):
        selected = df[(df['pb_lf'] < 1.5)
            & (df['pe_ttm'] < 15) 
            & (df['amount'] > 0) 
            & (df['pb_lf'] > 0)
            & (df['pe_ttm'] > 0 ) ]
                                        
        # 按pe_ttm和pb_lf 升序排列
        selected = selected.sort_values(['pe_ttm','pb_lf'])
        return list(selected.instrument)[:30] # 记得转化成list
    
    daily_buy_stock = history_data.groupby('date').apply(seek_symbol)
    

    3. 回测主体

    In [3]:
    def initialize(context):
        # 设置交易费用,买入是万三,卖出是千分之1.3,如果不足5元按5元算
        context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
        # 设置换仓规则,即每个月月初换仓,持有至下个月,再换仓
        context.schedule_function(rebalance, date_rule=date_rules.month_start(days_offset=0)) 
        # 上面schedule_function函数的这句代码,其实可以写到一行,分两行是为了便于展示
       
    def handle_data(context,data):
        pass
    
    # 换仓
    def rebalance(context, data):
        # 日期
        date = data.current_dt.strftime('%Y-%m-%d')
       
        # 买入股票列表
        stock_to_buy = daily_buy_stock.ix[date]
        # 目前持仓列表    
        stock_hold_now = [equity.symbol for equity 
                                   in context.portfolio.positions]
        # 继续持有股票列表
        no_need_to_sell = [i for i in stock_hold_now 
                                   if i in stock_to_buy]
        # 卖出股票列表 
        stock_to_sell = [i for i in stock_hold_now if
                               i not in no_need_to_sell]
        # 执行卖出
        for stock in stock_to_sell:
            if data.can_trade(context.symbol(stock)):
                context.order_target_percent(context.symbol(stock), 0)
        
        if len(stock_to_buy) == 0:
            return
        # 等权重
        weight = 1 / len(stock_to_buy)
        # 执行买入
        for  cp in stock_to_buy:
            if data.can_trade(context.symbol(cp)):
                context.order_target_percent(context.symbol(cp), weight)
    

    4.回测接口

    In [4]:
    # 使用第四版的回测接口,需要传入多个策略参数
    m=M.backtest.v5( 
        instruments=instruments,
        start_date=start_date, 
        end_date=end_date,
        # 必须传入initialize,只在第一天运行
        initialize=initialize,
        # 必须传入handle_data,每个交易日都会运行
        handle_data=handle_data,
        # 买入以开盘价成交
        order_price_field_buy='open',
        # 卖出也以开盘价成交
        order_price_field_sell='open',
        # 策略本金
        capital_base=1000000,
        # 比较基准:沪深300
        benchmark='000300.INDX',
    )
    
    [2017-05-09 17:49:18.984825] INFO: bigquant: backtest.v5 start ..
    [2017-05-09 17:52:30.058895] INFO: Performance: Simulated 1032 trading days out of 1032.
    [2017-05-09 17:52:30.061169] INFO: Performance: first open: 2013-02-01 14:30:00+00:00
    [2017-05-09 17:52:30.062580] INFO: Performance: last close: 2017-05-05 19:00:00+00:00
    
    • 收益率134.04%
    • 年化收益率23.08%
    • 基准收益率25.89%
    • 阿尔法0.17
    • 贝塔0.92
    • 夏普比率0.7
    • 收益波动率26.85%
    • 信息比率1.42
    • 最大回撤35.61%
    [2017-05-09 17:52:38.484237] INFO: bigquant: backtest.v5 end [199.499353s].
    
    In [ ]:
    ## 
    
    In [14]:
    beta = results['beta'].tail(1)[0] # 系统自动计算的beta
    print('beta:' ,beta)
    risk_free = results['treasury_period_return'].tail(1)[0] # 无风险收益率
    cum_algo_return = results['algorithm_period_return'].tail(1)[0] # 总收益
    print('总收益:',cum_algo_return)
    cum_benchmark_return = results['benchmark_period_return'].tail(1)[0] # 基准总收益
    print('基准总收益:',cum_benchmark_return)
    num_trading_days = results['trading_days'].tail(1)[0] # 天数
    annualized_algo_return = (cum_algo_return + 1.0) ** (252.0 / num_trading_days) - 1.0
    print('策略年化收益:', annualized_algo_return)
    annualized_benchmark_return = (cum_benchmark_return + 1.0) **  (252.0 / num_trading_days) - 1.0
    alpha_correct =annualized_algo_return - (risk_free + beta * (annualized_benchmark_return - risk_free) )  
    print('alpha: ', alpha_correct)
    
    beta: 0.92017861797
    总收益: 1.34038272453
    基准总收益: 0.258910141263
    策略年化收益: 0.230763772066
    alpha:  0.174153251028
    

    sharp计算

    In [11]:
     
    sharp_ratio = ((cum_algo_return +1)** (252.0 / num_trading_days) -1 - risk_free) / algo_volatility  
    print('sharp_ratio: ', sharp_ratio)
    
    sharp_ratio:  0.701069020707
    

    信息比率计算

    In [12]:
    diff = algo_daily_return - benchmark_daily_return
    std = np.std(diff)
    annualized_std = std*(252**0.5)
    ir = (annualized_algo_return - annualized_benchmark_return) / annualized_std
    print('信息比率' ,ir)
    
    手动计算的信息比率 1.42944640716
    

    (eqsxin) #5

    时间拉长就看到了这个策略的力量!


    (chenjianjia) #6

    《回测结果指标详解》这个连接打不开


    (chenjianjia) #7

    两个克隆版本,一个有prepare函数,一个没有,有什么区别吗?回测或者实盘模拟需不需要prepare函数?


    (小Q) #8

    实盘模拟需要prepare函数的。

    没有prepare的版本是最开始的版本,只能用户回测。最近更新的版本有prepare函数,不仅能用来回测还能用来实盘模拟。

    建议使用带prepare的版本。


    (小Q) #9

    验证时间:2018年2月8日 20:35 ,可以打开该篇文章哈。


    (jam) #10

    还是打不开


    (upndown) #11

    我用了这种方式计算各个回测指标:

    import empyrical
    # 统计策略指标
    def get_stats(results):
        return_ratio  = empyrical.cum_returns_final(results.returns)
        annual_return_ratio  = empyrical.annual_return(results.returns)
        sharp_ratio = empyrical.sharpe_ratio(results.returns,0.035/252)
        return_volatility = empyrical.annual_volatility(results.returns)
        max_drawdown  = empyrical.max_drawdown(results.returns)
        benchmark_returns = (results.benchmark_period_return+1)/(results.benchmark_period_return+1).shift(1)-1
        alpha, beta =empyrical.alpha_beta_aligned(results.returns, benchmark_returns)
        
        return {
          'return_ratio': return_ratio,
          'annual_return_ratio': annual_return_ratio,
          'beta': beta,
          'alpha': alpha,
          'sharp_ratio': sharp_ratio,
          'return_volatility': return_volatility,
          'max_drawdown': max_drawdown,
        }
    get_stats(m.raw_perf.read_df())