如何编辑回测模块——以羊驼策略复现为例

策略分享
新手专区
标签: #<Tag:0x00007fcf6d782190> #<Tag:0x00007fcf6d781fb0>

(sszy) #1

新手学习第三弹:编辑回测模块+羊驼策略复现
回测是量化交易中非常重要的一个模块,理解bigquant上的回测机制才能编写出个性化的交易策略。
本文以羊驼策略复现为例,分享回测模块编辑的学习过程。

什么是羊驼策略

上个世纪80年代末,美国《旧金山纪事报》曾做过大猩猩选股实验,让大猩猩向写有股票代码的纸板投飞镖,投中一个代码就意味着选中一只股票,用此方法让大猩猩挑选出5只股票。然后,用大猩猩挑选的股票组合与《华尔街日报》8位知名分析师精心计算分析挑选的5只股票相比较,在持有一段时间之后,大猩猩随机抽取购买的股票票面价值竟然超过了操盘手挑选的股票。

羊驼策略就是受了大猩猩选股实验的启发:所有初始持仓完全由羊驼选出。每次调仓时,卖掉持有的股票中收益率差的,买入羊驼选中的新股票。

羊驼策略背后的经济学规律:趋势跟随

美国学者在1993年发现,买入过去表现比较好的股票,卖出过去表现比较差的股票,在3-12个月里有高于市场的超额收益!这就是所谓的动量理论,其本质是“趋势跟随”,认为价格的运动存在惯性,而且强者在一定时段内,会更容易保持强势。

羊驼策略就是用相反的动量原理来选择股票,通过主动不断地淘汰差股票,留下好股票,从而达到筛选股票的目的。

经典的羊驼策略

  • 原始羊驼策略:起始时随机买入n只股票,然后每天卖掉持有的股票中收益率最低的n只,再随机买入剩下的股票池中的n只。
  • 每天按照收益率从大到小对股票池中的所有股票进行排序,起始时买入n只股票,然后每天卖掉收益率最低的m只股票,再买入股票池中收益率最高的m只。
  • 每天按照收益率从大到小对股票池中的所有股票进行排序,起始时买入n只股票,然后每天在整个股票池中选出收益率前n,如果这些股票已持有,则继续持有,如果未持有则买入,并卖掉收益率不是排在前n的股票。

本文选择复现第三种羊驼策略:

  • 买入10只过去10日收益率最高的股票。
  • 每周调仓,同样选出10日收益率前十的股票,如果这些股票已持有,则继续持有,如果未持有则买入,并卖掉收益率不是排在前10的股票
  • 调仓时注意风险控制,所以设定选取收益率不超过50%的股票

复现羊驼策略

学习过程中参考了帖子“BigQuant回测模块详解”文档相关内容。

回测模块介绍

回测模块主要有四个函数

  • 主函数: 通常编写策略的每日交易逻辑
  • 数据准备函数: 通常计算技术指标或每日买卖列表
  • 初始化函数:通常会设置手续费、滑点和全局变量
  • 盘前处理函数: 通常编写订单管理逻辑

编辑时直接右击回测/模拟模块,在属性中编辑。如果想进一步了解各个函数的功能,可以新建一个空白可视化策略,拖入一个回测/模拟模块调至“python代码”模式查看。

羊驼策略的回测模块编辑

由于策略设定是每周调仓,而主函数handle_data进行每日交易,所以不能用默认的主函数。
%E4%B8%BB%E5%87%BD%E6%95%B0

需要在初始化函数中设置周期执行调度函数和rebalance调仓函数。

如果每根Bar不一定都要运行一下主函数(handle_data),因此你的策略可以回测的更快。比如按月调仓的多因子选股策略,只需要在特定的Bar上运行handle_data主函数,为此,平台提供了schedule_function这一周期执行调度函数。
schedule_function(func, date_rule, time_rule)

此时,设置参数如下:

context.schedule_function(func=rebalance, date_rule=date_rules.week_start()) 

表示每周第一个交易日按照rebalance函数的逻辑调仓。

所以,重点在于,如果策略的交易的调仓频率不是以“日”为单位,那么就应该在初始化函数里面设置周期执行调度函数并编写新的调仓函数

最后,如果没有用到某个函数,就pass掉。

羊驼策略的实现

  • 拖入证券代码列表模块,并设置回测起止时间,以及股票池范围。
  • 拖入Trade回测模块并连接证券代码模块,并设置成交率、买入卖出点、初始资金等参数

完整的策略和源代码如下:

克隆策略

    {"Description":"实验创建于2019/3/25","Summary":"","Graph":{"EdgesInternal":[{"DestinationInputPortId":"-118:instruments","SourceOutputPortId":"-845:data"}],"ModuleNodes":[{"Id":"-118","ModuleId":"BigQuantSpace.trade.trade-v4","ModuleParameters":[{"Name":"start_date","Value":"","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"end_date","Value":"","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"handle_data","Value":"# 回测引擎:每日数据处理函数,每天执行一次\ndef bigquant_run(context, data):\n pass","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"prepare","Value":"# 回测引擎:准备数据,只执行一次\ndef bigquant_run(context):\n pass","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"initialize","Value":"# 回测引擎:初始化函数,只执行一次\ndef bigquant_run(context):\n # 设置交易手续费和滑点\n context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))\n # 设置回测开始和结束日期\n start_date = context.start_date\n end_date = context.end_date\n fields = ['close']\n df = DataSource('bar1d_CN_STOCK_A').read(start_date = start_date, end_date = end_date, fields = fields)\n # 计算收益率\n def cal_ret(data):\n data['ret'] = data['close'] / data['close'].shift(10) - 1\n data = data.dropna()\n return data \n ret_df = df.groupby('instrument').apply(cal_ret)\n # 获取每次买卖股票列表\n df_sort = ret_df.groupby('date').apply(lambda df:df[(df['ret']<0.5)].sort_values('ret',ascending=False)[:10])\n context.daily_buy_stock = df_sort \n \n \n # 判断是否买入,每周执行一次\n def rebalance(context, data):\n # 当前的日期\n date = data.current_dt.strftime('%Y-%m-%d')\n # 根据日期获取调仓需要买入的股票的列表并设置容错\n #(计算10日收益率时删除了前几天的数据)\n try:\n stock_to_buy = list(context.daily_buy_stock.ix[date].instrument)\n except:\n return\n \n # 通过positions对象,使用列表生成式的方法获取目前持仓的股票列表\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 # 卖出\n for stock in stock_to_sell:\n # 如果该股票停牌,则没法成交。因此需要用can_trade方法检查下该股票的状态,如果返回真值,则可以正常下单\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 # 买入\n for stock in stock_to_buy:\n if data.can_trade(context.symbol(stock)):\n # 下单使得某只股票的持仓权重达到weight,因为weight大于0,因此是等权重买入\n context.order_target_percent(context.symbol(stock), weight)\n\n # 调仓规则(每周的第一天调仓)\n context.schedule_function(func=rebalance, date_rule=date_rules.week_start()) ","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":"close","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":"-118"},{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"options_data","NodeId":"-118"},{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"history_ds","NodeId":"-118"},{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"benchmark_ds","NodeId":"-118"},{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"trading_calendar","NodeId":"-118"}],"OutputPortsInternal":[{"Name":"raw_perf","NodeId":"-118","OutputType":null}],"UsePreviousResults":false,"moduleIdForCode":1,"IsPartOfPartialRun":null,"Comment":"","CommentCollapsed":true},{"Id":"-845","ModuleId":"BigQuantSpace.instruments.instruments-v2","ModuleParameters":[{"Name":"start_date","Value":"2016-01-01","ValueType":"Literal","LinkedGlobalParameter":null},{"Name":"end_date","Value":"2017-01-01","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":0,"ValueType":"Literal","LinkedGlobalParameter":null}],"InputPortsInternal":[{"DataSourceId":null,"TrainedModelId":null,"TransformModuleId":null,"Name":"rolling_conf","NodeId":"-845"}],"OutputPortsInternal":[{"Name":"data","NodeId":"-845","OutputType":null}],"UsePreviousResults":true,"moduleIdForCode":2,"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='-118' Position='195,270,200,200'/><NodePosition Node='-845' Position='259,121,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":false}
    In [10]:
    # 本代码由可视化策略环境自动生成 2019年3月25日 18:33
    # 本代码单元只能在可视化模式下编辑。您也可以拷贝代码,粘贴到新建的代码单元或者策略,然后修改。
    
    
    # 回测引擎:每日数据处理函数,每天执行一次
    def m1_handle_data_bigquant_run(context, data):
        pass
    # 回测引擎:准备数据,只执行一次
    def m1_prepare_bigquant_run(context):
        pass
    # 回测引擎:初始化函数,只执行一次
    def m1_initialize_bigquant_run(context):
        # 设置交易手续费和滑点
        context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
        # 设置回测开始和结束日期
        start_date = context.start_date
        end_date = context.end_date
        fields = ['close']
        df = DataSource('bar1d_CN_STOCK_A').read(start_date = start_date, end_date = end_date, fields = fields)
        # 计算收益率
        def cal_ret(data):
            data['ret'] = data['close'] / data['close'].shift(10) - 1
            data = data.dropna()
            return data 
        ret_df = df.groupby('instrument').apply(cal_ret)
        # 获取每次买卖股票列表
        df_sort = ret_df.groupby('date').apply(lambda df:df[(df['ret']<0.5)].sort_values('ret',ascending=False)[:10])
        context.daily_buy_stock = df_sort     
        
        
        # 判断是否买入,每周执行一次
        def rebalance(context, data):
            # 当前的日期
            date = data.current_dt.strftime('%Y-%m-%d')
            # 根据日期获取调仓需要买入的股票的列表并设置容错
            #(计算10日收益率时删除了前几天的数据)
            try:
                stock_to_buy = list(context.daily_buy_stock.ix[date].instrument)
            except:
                return
            
            # 通过positions对象,使用列表生成式的方法获取目前持仓的股票列表
            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:
                # 如果该股票停牌,则没法成交。因此需要用can_trade方法检查下该股票的状态,如果返回真值,则可以正常下单
                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 stock in stock_to_buy:
                if data.can_trade(context.symbol(stock)):
                    # 下单使得某只股票的持仓权重达到weight,因为weight大于0,因此是等权重买入
                    context.order_target_percent(context.symbol(stock), weight)
    
        # 调仓规则(每周的第一天调仓)
        context.schedule_function(func=rebalance, date_rule=date_rules.week_start()) 
    # 回测引擎:每个单位时间开始前调用一次,即每日开盘前调用一次。
    def m1_before_trading_start_bigquant_run(context, data):
        pass
    
    
    m2 = M.instruments.v2(
        start_date='2016-01-01',
        end_date='2017-01-01',
        market='CN_STOCK_A',
        instrument_list='',
        max_count=0
    )
    
    m1 = M.trade.v4(
        instruments=m2.data,
        start_date='',
        end_date='',
        handle_data=m1_handle_data_bigquant_run,
        prepare=m1_prepare_bigquant_run,
        initialize=m1_initialize_bigquant_run,
        before_trading_start=m1_before_trading_start_bigquant_run,
        volume_limit=0.025,
        order_price_field_buy='open',
        order_price_field_sell='close',
        capital_base=1000000,
        auto_cancel_non_tradable_orders=True,
        data_frequency='daily',
        price_type='后复权',
        product_type='股票',
        plot_charts=True,
        backtest_only=False,
        benchmark=''
    )
    
    • 收益率6.01%
    • 年化收益率6.21%
    • 基准收益率-11.28%
    • 阿尔法0.28
    • 贝塔1.12
    • 夏普比率0.3
    • 胜率0.47
    • 盈亏比1.16
    • 收益波动率46.96%
    • 信息比率0.04
    • 最大回撤25.18%

    (szchance) #2

    您好,正在学习策略,可以分享您的源码学习吗,谢谢!


    (iQuant) #3

    策略源码可以直接点击克隆获取哈。