克隆策略

交割单回测功能

版本v2.0

以"交割单模板.csv"为例,展示了成交单分析功能,该数据是直接从模板策略的回测详细数据中的交易详情下载处理后得到。

1、成交单csv数据读取

输入:交割单模板.csv 指定:

  • 成交日期列的列名,本例的csv文件中日期列表头为"日期",修改定义为"date"
  • 证券代码列名,本例的csv文件中证券代码列表头为"股票代码",修改定义为"instrument"
  • 买卖标志列名,本例的csv文件中买卖标志列表头为"买/卖",修改定义为"side",且当中的“买入”“卖出”也修改为"BUY" & "SELL"
  • 成交价格列名,本例的csv文件中成交价格列表头为"成交价",修改定义为"trade_price"
  • 成交数量列名,本例的csv文件中成交数量列表头为"数量",修改定义为"trade_volum"
  • 成交数量列名,本例的csv文件中成交成本列表头为"总成本",修改定义为"total_cost"

此模块会将每日的交易记录读取至DataFrame,并将信号时间前移1天来实现模拟当日成交,输出股票代码列表、起止时间和前移1日后的交易信号数据(包括日期、代码、买卖方向、成交价格和数量)。

2、回测

  • 在初始化函数中,通过设置固定滑点模型实现按照指定价格交易,设置手续费
  • 在主函数中,首先获取当日的交易信号数据trade_data,然后针对每行交易信号数据执行买卖动作 这里由于要更好的去模拟成交时的情况,所以在设置交易逻辑的时候我们分为了买卖两种情况:
    1. 当该笔交易标签为BUY的时候,设置交易逻辑以该笔交易总成本执行该笔交易操作 context.order_value(context.symbol(trade_data['instrument'].iloc[i]), cash)
    2. 当该笔交易标签为SELL的时候,设置交易逻辑以目标volume为0执行该笔交易操作 context.order_target(context.symbol(trade_data['instrument'].iloc[i]), 0) 在设计交易逻辑的时候,没有指定限价单去进行交易复现,是因为某些股票会存在股价复权的情况,所以单纯指定限价单会存在部分交易无法全部成功的情况
  • 同时设置成交率限制为0,以保证所有下单能够成交。

3、下单检查

  • 我们通过回测结果中的交易详情可以看到每笔单子的下单时间、股票代码、买卖方向、成交数量、成交价和佣金。可以检查这些记录与成交单记录的csv文件的一致性。

4、 结果分析

  • 通过回测结果可以看到绩效曲线、成交记录和策略的阿尔法、夏普比率等统计指标;
  • 从画布左侧模块列表的“共享模块”列表中拖入 “最近N日绩效评估” 模块。运行后可以查看相关的阶段绩效等信息。

    {"description":"实验创建于8/17/2021","graph":{"edges":[{"to_node_id":"-58:backtest_ds","from_node_id":"-36:raw_perf"},{"to_node_id":"-36:instruments","from_node_id":"-50:data_1"},{"to_node_id":"-36:options_data","from_node_id":"-50:data_2"}],"nodes":[{"node_id":"-36","module_id":"BigQuantSpace.trade.trade-v4","parameters":[{"name":"start_date","value":"","type":"Literal","bound_global_parameter":null},{"name":"end_date","value":"","type":"Literal","bound_global_parameter":null},{"name":"initialize","value":"# 回测引擎:初始化函数,只执行一次\ndef bigquant_run(context):\n context.show_debug_info = False\n context.trade_data = context.options['data'].read_df()\n context.set_long_only()\n from zipline.finance.slippage import SlippageModel\n class FixedPriceSlippage(SlippageModel):\n def process_order(self, data, order, bar_volume=0, trigger_check_price=0):\n if order.limit is None:\n price_field = self._price_field_buy if order.amount > 0 else self._price_field_sell\n price = data.current(order.asset, price_field)\n return (price, order.amount)\n else:\n price = order.limit\n# print(\"*********\", order.asset, price, order.amount, \"********\")\n return (price, order.amount)\n fix_slippage = FixedPriceSlippage()\n fix_slippage = FixedPriceSlippage(price_field_buy='open', price_field_sell='close')\n context.set_slippage(us_equities=fix_slippage)\n context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))\n# context.set_commission(PerOrder(buy_cost=0, sell_cost=0, min_cost=0))\n","type":"Literal","bound_global_parameter":null},{"name":"handle_data","value":"# 回测引擎:每日数据处理函数,每天执行一次\ndef bigquant_run(context, data):\n # 按日期过滤得到今日收盘后的下单股票\n trade_data = context.trade_data[context.trade_data.date == data.current_dt.strftime('%Y-%m-%d')]\n# print(\"handle_data:\", data.current_dt, \"\\n\", trade_data)\n \n for i in range(0, len(trade_data)):\n volume = trade_data['trade_volume'].iloc[i]\n cash = trade_data['total_cost'].iloc[i]\n if trade_data['side'].iloc[i] in ['BUY', '配售中签', '新股入帐']:\n try:\n# print('order -----> ', trade_data['instrument'].iloc[i], cash, trade_data['trade_price'].iloc[i], context.portfolio.cash)\n context.order_value(context.symbol(trade_data['instrument'].iloc[i]), cash)\n# print('-----'*10)\n except Exception as e:\n# print('警告:%s' % (e))\n# assert 1 > 2\n pass\n elif trade_data['side'].iloc[i] == 'SELL':\n volume = -volume\n try:\n# print('order -----> ', trade_data['instrument'].iloc[i], volume, trade_data['trade_price'].iloc[i], context.portfolio.cash)\n context.order_target(context.symbol(trade_data['instrument'].iloc[i]), 0)\n# print('-----'*10)\n except Exception as e:\n# print('警告:%s' % (e))\n# assert 1 > 2\n pass\n else:\n print('警告:未知的买卖标志 %s' % (trade_data['side'].iloc[i]))\n\n","type":"Literal","bound_global_parameter":null},{"name":"prepare","value":"# 回测引擎:准备数据,只执行一次\ndef bigquant_run(context):\n pass\n","type":"Literal","bound_global_parameter":null},{"name":"before_trading_start","value":"# 回测引擎:每个单位时间开始前调用一次,即每日开盘前调用一次。\ndef bigquant_run(context, data):\n pass\n","type":"Literal","bound_global_parameter":null},{"name":"volume_limit","value":"0","type":"Literal","bound_global_parameter":null},{"name":"order_price_field_buy","value":"open","type":"Literal","bound_global_parameter":null},{"name":"order_price_field_sell","value":"close","type":"Literal","bound_global_parameter":null},{"name":"capital_base","value":"1000000","type":"Literal","bound_global_parameter":null},{"name":"auto_cancel_non_tradable_orders","value":"False","type":"Literal","bound_global_parameter":null},{"name":"data_frequency","value":"daily","type":"Literal","bound_global_parameter":null},{"name":"price_type","value":"真实价格","type":"Literal","bound_global_parameter":null},{"name":"product_type","value":"股票","type":"Literal","bound_global_parameter":null},{"name":"plot_charts","value":"True","type":"Literal","bound_global_parameter":null},{"name":"backtest_only","value":"False","type":"Literal","bound_global_parameter":null},{"name":"benchmark","value":"000300.HIX","type":"Literal","bound_global_parameter":null}],"input_ports":[{"name":"instruments","node_id":"-36"},{"name":"options_data","node_id":"-36"},{"name":"history_ds","node_id":"-36"},{"name":"benchmark_ds","node_id":"-36"},{"name":"trading_calendar","node_id":"-36"}],"output_ports":[{"name":"raw_perf","node_id":"-36"}],"cacheable":false,"seq_num":3,"comment":"","comment_collapsed":true},{"node_id":"-50","module_id":"BigQuantSpace.cached.cached-v3","parameters":[{"name":"run","value":"# Python 代码入口函数,input_1/2/3 对应三个输入端,data_1/2/3 对应三个输出端\ndef bigquant_run(input_1, input_2, input_3):\n # 示例代码如下。在这里编写您的代码\n df = pd.read_csv('trade_list_1.csv').fillna(method='ffill')\n columns_map = {\n '日期': 'date',\n '股票代码': 'instrument', \n '买/卖': 'side', \n '成交价': 'trade_price', \n '数量': 'trade_volume',\n '总成本': 'total_cost'\n }\n \n df.rename(columns=columns_map, inplace=True)\n df.date = pd.to_datetime(df.date, format=\"%Y-%m-%d\")\n td = D.trading_days(start_date=df['date'].min() - pd.Timedelta(10, 'D'),\n end_date=df['date'].max())\n td['order_date'] = td['date'].shift(1)\n df = df.merge(td, on='date', how='left')\n df['date'] = df['order_date']\n del df['order_date']\n df.loc[df.side == '买入', 'side'] = 'BUY'\n df.loc[df.side == '卖出', 'side'] = 'SELL'\n \n \n instrument_data = {\n 'instruments': list(set(df['instrument'])),\n 'start_date': df['date'].min().strftime('%Y-%m-%d'),\n 'end_date': (df['date'].max() + pd.Timedelta(1, 'D')).strftime('%Y-%m-%d'),\n# 'end_date': '2015-05-11'\n }\n df.date = df.date.apply(lambda x: x.strftime('%Y-%m-%d'))\n trading_records_ds = DataSource.write_df(df)\n instruments_ds = DataSource.write_pickle(instrument_data)\n return Outputs(data_1=instruments_ds, data_2=trading_records_ds, data_3=None)\n","type":"Literal","bound_global_parameter":null},{"name":"post_run","value":"# 后处理函数,可选。输入是主函数的输出,可以在这里对数据做处理,或者返回更友好的outputs数据格式。此函数输出不会被缓存。\ndef bigquant_run(outputs):\n return outputs\n","type":"Literal","bound_global_parameter":null},{"name":"input_ports","value":"","type":"Literal","bound_global_parameter":null},{"name":"params","value":"{}","type":"Literal","bound_global_parameter":null},{"name":"output_ports","value":"","type":"Literal","bound_global_parameter":null}],"input_ports":[{"name":"input_1","node_id":"-50"},{"name":"input_2","node_id":"-50"},{"name":"input_3","node_id":"-50"}],"output_ports":[{"name":"data_1","node_id":"-50"},{"name":"data_2","node_id":"-50"},{"name":"data_3","node_id":"-50"}],"cacheable":true,"seq_num":4,"comment":"","comment_collapsed":true},{"node_id":"-58","module_id":"BigQuantSpace.N_days_performance_statistics.N_days_performance_statistics-v5","parameters":[{"name":"N","value":5,"type":"Literal","bound_global_parameter":null}],"input_ports":[{"name":"backtest_ds","node_id":"-58"}],"output_ports":[{"name":"evaluation_of_perf_indicator","node_id":"-58"},{"name":"analysis_of_stage_return_rat","node_id":"-58"}],"cacheable":true,"seq_num":1,"comment":"","comment_collapsed":true}],"node_layout":"<node_postions><node_position Node='-36' Position='600,504,200,200'/><node_position Node='-50' Position='605,406,200,200'/><node_position Node='-58' Position='551,656,200,200'/></node_postions>"},"nodes_readonly":false,"studio_version":"v2"}
    In [45]:
    # 本代码由可视化策略环境自动生成 2021年8月19日 16:39
    # 本代码单元只能在可视化模式下编辑。您也可以拷贝代码,粘贴到新建的代码单元或者策略,然后修改。
    
    
    # Python 代码入口函数,input_1/2/3 对应三个输入端,data_1/2/3 对应三个输出端
    def m4_run_bigquant_run(input_1, input_2, input_3):
        # 示例代码如下。在这里编写您的代码
        df = pd.read_csv('trade_list_1.csv').fillna(method='ffill')
        columns_map = {
            '日期': 'date',
            '股票代码': 'instrument', 
            '买/卖': 'side', 
            '成交价': 'trade_price', 
            '数量': 'trade_volume',
            '总成本': 'total_cost'
        }
        
        df.rename(columns=columns_map, inplace=True)
        df.date = pd.to_datetime(df.date, format="%Y-%m-%d")
        td = D.trading_days(start_date=df['date'].min() - pd.Timedelta(10, 'D'),
                            end_date=df['date'].max())
        td['order_date'] = td['date'].shift(1)
        df = df.merge(td, on='date', how='left')
        df['date'] = df['order_date']
        del df['order_date']
        df.loc[df.side == '买入', 'side'] = 'BUY'
        df.loc[df.side == '卖出', 'side'] = 'SELL'
        
        
        instrument_data = {
            'instruments': list(set(df['instrument'])),
            'start_date': df['date'].min().strftime('%Y-%m-%d'),
            'end_date': (df['date'].max() + pd.Timedelta(1, 'D')).strftime('%Y-%m-%d'),
    #         'end_date': '2015-05-11'
        }
        df.date = df.date.apply(lambda x: x.strftime('%Y-%m-%d'))
        trading_records_ds = DataSource.write_df(df)
        instruments_ds = DataSource.write_pickle(instrument_data)
        return Outputs(data_1=instruments_ds, data_2=trading_records_ds, data_3=None)
    
    # 后处理函数,可选。输入是主函数的输出,可以在这里对数据做处理,或者返回更友好的outputs数据格式。此函数输出不会被缓存。
    def m4_post_run_bigquant_run(outputs):
        return outputs
    
    # 回测引擎:初始化函数,只执行一次
    def m3_initialize_bigquant_run(context):
        context.show_debug_info = False
        context.trade_data = context.options['data'].read_df()
        context.set_long_only()
        from zipline.finance.slippage import SlippageModel
        class FixedPriceSlippage(SlippageModel):
            def process_order(self, data, order, bar_volume=0, trigger_check_price=0):
                if order.limit is None:
                    price_field = self._price_field_buy if order.amount > 0 else self._price_field_sell
                    price = data.current(order.asset, price_field)
                    return (price, order.amount)
                else:
                    price = order.limit
    #                 print("*********", order.asset, price, order.amount, "********")
                    return (price, order.amount)
        fix_slippage = FixedPriceSlippage()
        fix_slippage = FixedPriceSlippage(price_field_buy='open', price_field_sell='close')
        context.set_slippage(us_equities=fix_slippage)
        context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
    #     context.set_commission(PerOrder(buy_cost=0, sell_cost=0, min_cost=0))
    
    # 回测引擎:每日数据处理函数,每天执行一次
    def m3_handle_data_bigquant_run(context, data):
        # 按日期过滤得到今日收盘后的下单股票
        trade_data = context.trade_data[context.trade_data.date == data.current_dt.strftime('%Y-%m-%d')]
    #     print("handle_data:", data.current_dt, "\n", trade_data)
        
        for i in range(0, len(trade_data)):
            volume = trade_data['trade_volume'].iloc[i]
            cash = trade_data['total_cost'].iloc[i]
            if trade_data['side'].iloc[i] in ['BUY', '配售中签', '新股入帐']:
                try:
    #                 print('order -----> ', trade_data['instrument'].iloc[i], cash, trade_data['trade_price'].iloc[i], context.portfolio.cash)
                    context.order_value(context.symbol(trade_data['instrument'].iloc[i]), cash)
    #                 print('-----'*10)
                except Exception as e:
    #                 print('警告:%s' % (e))
    #                 assert 1 > 2
                    pass
            elif trade_data['side'].iloc[i] == 'SELL':
                volume = -volume
                try:
    #                 print('order -----> ', trade_data['instrument'].iloc[i], volume, trade_data['trade_price'].iloc[i], context.portfolio.cash)
                    context.order_target(context.symbol(trade_data['instrument'].iloc[i]), 0)
    #                 print('-----'*10)
                except Exception as e:
    #                 print('警告:%s' % (e))
    #                 assert 1 > 2
                    pass
            else:
                print('警告:未知的买卖标志 %s' % (trade_data['side'].iloc[i]))
    
    
    # 回测引擎:准备数据,只执行一次
    def m3_prepare_bigquant_run(context):
        pass
    
    # 回测引擎:每个单位时间开始前调用一次,即每日开盘前调用一次。
    def m3_before_trading_start_bigquant_run(context, data):
        pass
    
    
    m4 = M.cached.v3(
        run=m4_run_bigquant_run,
        post_run=m4_post_run_bigquant_run,
        input_ports='',
        params='{}',
        output_ports=''
    )
    
    m3 = M.trade.v4(
        instruments=m4.data_1,
        options_data=m4.data_2,
        start_date='',
        end_date='',
        initialize=m3_initialize_bigquant_run,
        handle_data=m3_handle_data_bigquant_run,
        prepare=m3_prepare_bigquant_run,
        before_trading_start=m3_before_trading_start_bigquant_run,
        volume_limit=0,
        order_price_field_buy='open',
        order_price_field_sell='close',
        capital_base=1000000,
        auto_cancel_non_tradable_orders=False,
        data_frequency='daily',
        price_type='真实价格',
        product_type='股票',
        plot_charts=True,
        backtest_only=False,
        benchmark='000300.HIX'
    )
    
    m1 = M.N_days_performance_statistics.v5(
        backtest_ds=m3.raw_perf,
        N=5
    )
    
    • 收益率306.69%
    • 年化收益率106.36%
    • 基准收益率-6.33%
    • 阿尔法1.2
    • 贝塔0.92
    • 夏普比率1.91
    • 胜率0.62
    • 盈亏比0.74
    • 收益波动率40.97%
    • 信息比率0.18
    • 最大回撤46.46%
    bigcharts-data-start/{"__type":"tabs","__id":"bigchart-f94c278f99264e8681a4304d4b9b1ba7"}/bigcharts-data-end
                         最近5日策略绩效统计
    return_ratio          -0.001191
    annual_return_ratio   -0.058296
    beta                  -0.052144
    alpha                 -0.565775
    sharp_ratio           -0.737618
    sortino               -1.300777
    return_volatility      0.120959
    max_drawdown          -0.012987
    
    In [ ]: