复制链接
克隆策略

股票事件驱动策略

版本 v1.0

目录

  • ### 事件驱动策略的交易规则

  • ### 策略构建步骤

  • ### 策略的实现

正文

一、事件驱动策略的交易规则

  • 由于财务公告通常在晚上发布,在财务报表公告的第二日开盘买入归属母公司股东的净利润同比增长率百分比大于30%的且降序排名靠前股票(总持仓量不超过50只);
  • 买入并持有40个交易日后,以第二日开盘价卖出;

二、策略构建步骤

1、确定股票池和回测时间

  • 通过证券代码列表输入要回测的单只/多只股票,以及回测的起止日期

2、确定买卖条件信号

  • 通过自定义Python模块m4获取对应股票起止时间范围内的财务数据,通过自定义Python模块m9获取对应股票起止时间范围内的财报发布日期并将其前移1天来模拟前晚发布。
  • 通过自定义Python模块m7整合各股票的财务数据和财报发布日期。
  • 通过数据过滤模块实现财务数据归属母公司股东的净利润同比增长率的条件过滤,通过排序模块实现净利润同比增长率的逐日降序排名。
  • 通过缺失数据处理模块删去有缺失值的数据。

3、确定买卖原则

  • 根据持仓股票买入时的日期戳计算已有持仓中满足持有天数达到40天的股票,需执行卖出操作,更新持仓股票总数和可用现金;
  • 满足买入条件的股票如果当前持仓总数不足50只,则执行买入操作;
  • 本策略根据需要买入的股票总数量,通过可用现金等资金比例仓位买入,并记录买入各股票时的日期戳。

4、模拟回测

  • 通过 trade 模块中的初始化函数定义交易手续费和滑点,设置持仓天数 context.hold_periods,设置最大持仓股票数量 context.stock_max_num ,初始化记录已有持仓股票开仓日期戳的字典 context.hold_days;
  • 通过 trade 模块中的准备函数定义 context.daily_stock_buy 变量来获取并存放每日满足买入条件的股票;
  • 通过 trade 模块中的主函数(handle函数)查看每日满足买入条件的股票,按照买卖原则执行相应的卖出/买入操作。

三、策略的实现

可视化策略实现如下:

    {"description":"实验创建于2017/8/26","graph":{"edges":[{"to_node_id":"-201:input_1","from_node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-62:data"},{"to_node_id":"-178:instruments","from_node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-62:data"},{"to_node_id":"-259:input_1","from_node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-62:data"},{"to_node_id":"-235:input_1","from_node_id":"-201:data_1"},{"to_node_id":"-178:options_data","from_node_id":"-218:sorted_data"},{"to_node_id":"-218:input_ds","from_node_id":"-226:data"},{"to_node_id":"-226:input_data","from_node_id":"-235:data_1"},{"to_node_id":"-235:input_2","from_node_id":"-259:data_1"}],"nodes":[{"node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-62","module_id":"BigQuantSpace.instruments.instruments-v2","parameters":[{"name":"start_date","value":"2017-07-01","type":"Literal","bound_global_parameter":"交易日期"},{"name":"end_date","value":"2017-12-18","type":"Literal","bound_global_parameter":"交易日期"},{"name":"market","value":"CN_STOCK_A","type":"Literal","bound_global_parameter":null},{"name":"instrument_list","value":"","type":"Literal","bound_global_parameter":null},{"name":"max_count","value":"0","type":"Literal","bound_global_parameter":null}],"input_ports":[{"name":"rolling_conf","node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-62"}],"output_ports":[{"name":"data","node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-62"}],"cacheable":true,"seq_num":1,"comment":"预测数据,用于回测和模拟","comment_collapsed":false},{"node_id":"-178","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.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5)) \n context.hold_periods = 40 # 持有40天,固定持仓期\n context.stock_max_num = 50 # 最大持仓数量为50只\n \n\n \n ","type":"Literal","bound_global_parameter":null},{"name":"handle_data","value":"# 回测引擎:每日数据处理函数,每天执行一次\ndef bigquant_run(context, data):\n \n today = data.current_dt.strftime('%Y-%m-%d') # 日期\n # 通过positions对象,使用列表生成式的方法获取目前持仓的股票列表和对应的最新市值\n equities = {e.symbol: p for e, p in context.portfolio.positions.items() if p.amount>0}\n\n # 记录持仓股票数量\n hold_num = len(equities)\n # 记录用于买入股票的可用现金\n cash_for_buy = context.portfolio.cash\n for instrument in equities.keys():\n # 如果持仓时间大于40天\n sid = equities[instrument].sid # 交易标的\n # 今天和上次交易的时间相隔hold_days就全部卖出\n dt = pd.to_datetime(D.trading_days(end_date = today).iloc[-context.hold_periods].values[0])\n if pd.to_datetime(equities[instrument].last_sale_date.strftime('%Y-%m-%d')) <= dt and data.can_trade(context.symbol(instrument)):\n # 卖出股票并更新持股数量\n context.order_target_percent(sid, 0)\n hold_num -= 1\n # 因为是早盘卖股票早盘买股票,需要记录卖出的股票市值并在买入下单前更新可用现金;\n # 如果是早盘买尾盘卖,则卖出时不需更新可用现金,因为尾盘卖出股票所得现金无法使用\n cash_for_buy += equities[instrument].amount * equities[instrument].last_sale_price\n\n \n # 当日还允许买入建仓的股票数目\n stock_can_buy_num = context.stock_max_num - hold_num\n \n try:\n daily_stock_buy = context.daily_stock_buy[today] # 当日符合买入条件的股票\n except:\n daily_stock_buy = [] # 如果没有符合条件的股票,就设置为空\n \n stock_to_buy = [k for k in daily_stock_buy if k not in equities]\n stock_to_buy_num = min(stock_can_buy_num,len(stock_to_buy))\n \n # 如果不需要买入股票就返回,当日不执行买入操作\n if stock_to_buy_num > 0:\n buy_num = 0\n # 买入满足条件的股票,使用可用现金等额买入\n for stock in stock_to_buy:\n cash = cash_for_buy / stock_to_buy_num\n if data.can_trade(context.symbol(stock)) and buy_num<stock_to_buy_num:\n context.order_target_value(context.symbol(stock), cash)\n buy_num += 1\n\n\n","type":"Literal","bound_global_parameter":null},{"name":"prepare","value":"# 回测引擎:准备数据,只执行一次\ndef bigquant_run(context):\n # 加载预测数据\n df = context.options['data'].read_df()\n df['date']=df['date'].apply(lambda x:x.strftime('%Y-%m-%d'))\n # 函数:求满足开仓条件的股票列表\n def open_pos_con(df):\n return list(df.instrument)\n \n # 每日买入股票的数据框\n context.daily_stock_buy= df.groupby('date').apply(open_pos_con)\n \n","type":"Literal","bound_global_parameter":null},{"name":"before_trading_start","value":"","type":"Literal","bound_global_parameter":null},{"name":"volume_limit","value":0.025,"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":"open","type":"Literal","bound_global_parameter":null},{"name":"capital_base","value":"1000000","type":"Literal","bound_global_parameter":null},{"name":"auto_cancel_non_tradable_orders","value":"True","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":"-178"},{"name":"options_data","node_id":"-178"},{"name":"history_ds","node_id":"-178"},{"name":"benchmark_ds","node_id":"-178"},{"name":"trading_calendar","node_id":"-178"}],"output_ports":[{"name":"raw_perf","node_id":"-178"}],"cacheable":false,"seq_num":3,"comment":"","comment_collapsed":true},{"node_id":"-201","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 #start_date=input_1.read_pickle()['start_date']\n start_date=(pd.to_datetime(input_1.read_pickle()['start_date']) - datetime.timedelta(days=100)).strftime('%Y-%m-%d')\n end_date=input_1.read_pickle()['end_date']\n instruments=input_1.read_pickle()['instruments']\n df = DataSource('financial_statement_CN_STOCK_A').read(instruments,start_date,end_date,\\\n ['fs_publish_date','fs_quarter_year','fs_quarter_index','fs_net_profit_yoy'])\n data_1 = DataSource.write_df(df)\n return Outputs(data_1=data_1, data_2=None, 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":"-201"},{"name":"input_2","node_id":"-201"},{"name":"input_3","node_id":"-201"}],"output_ports":[{"name":"data_1","node_id":"-201"},{"name":"data_2","node_id":"-201"},{"name":"data_3","node_id":"-201"}],"cacheable":true,"seq_num":4,"comment":"","comment_collapsed":true},{"node_id":"-218","module_id":"BigQuantSpace.sort.sort-v4","parameters":[{"name":"sort_by","value":"fs_net_profit_yoy","type":"Literal","bound_global_parameter":null},{"name":"group_by","value":"date","type":"Literal","bound_global_parameter":null},{"name":"keep_columns","value":"--","type":"Literal","bound_global_parameter":null},{"name":"ascending","value":"False","type":"Literal","bound_global_parameter":null}],"input_ports":[{"name":"input_ds","node_id":"-218"},{"name":"sort_by_ds","node_id":"-218"}],"output_ports":[{"name":"sorted_data","node_id":"-218"}],"cacheable":true,"seq_num":5,"comment":"","comment_collapsed":true},{"node_id":"-226","module_id":"BigQuantSpace.filter.filter-v3","parameters":[{"name":"expr","value":"fs_net_profit_yoy>30","type":"Literal","bound_global_parameter":null},{"name":"output_left_data","value":"False","type":"Literal","bound_global_parameter":null}],"input_ports":[{"name":"input_data","node_id":"-226"}],"output_ports":[{"name":"data","node_id":"-226"},{"name":"left_data","node_id":"-226"}],"cacheable":true,"seq_num":6,"comment":"","comment_collapsed":true},{"node_id":"-235","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 df1 = input_1.read_df()\n df2 = input_2.read_df()\n df_merge=pd.merge(df1,df2,on='date',how='left')#.drop(['date'],axis=1).rename(columns={'shift_date':'date'})\n data_1 = DataSource.write_df(df_merge)\n return Outputs(data_1=data_1, data_2=None, 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":"-235"},{"name":"input_2","node_id":"-235"},{"name":"input_3","node_id":"-235"}],"output_ports":[{"name":"data_1","node_id":"-235"},{"name":"data_2","node_id":"-235"},{"name":"data_3","node_id":"-235"}],"cacheable":false,"seq_num":7,"comment":"","comment_collapsed":true},{"node_id":"-259","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 # 获取交易日历\n start_date=(pd.to_datetime(input_1.read_pickle()['start_date']) - datetime.timedelta(days=100)).strftime('%Y-%m-%d')\n end_date=input_1.read_pickle()['end_date']\n df = D.trading_days(start_date=start_date,end_date=end_date)\n # 为尽量接近实盘,事件日期应为财报公布日的前一天\n df['shift_date']=df.shift(1)\n data_1 = DataSource.write_df(df)\n return Outputs(data_1=data_1, data_2=None, 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":"-259"},{"name":"input_2","node_id":"-259"},{"name":"input_3","node_id":"-259"}],"output_ports":[{"name":"data_1","node_id":"-259"},{"name":"data_2","node_id":"-259"},{"name":"data_3","node_id":"-259"}],"cacheable":false,"seq_num":9,"comment":"","comment_collapsed":true}],"node_layout":"<node_postions><node_position Node='287d2cb0-f53c-4101-bdf8-104b137c8601-62' Position='937,-90,200,200'/><node_position Node='-178' Position='1197,547.6558837890625,200,200'/><node_position Node='-201' Position='1091,106,200,200'/><node_position Node='-218' Position='1179,439,200,200'/><node_position Node='-226' Position='1219,328,200,200'/><node_position Node='-235' Position='1224,226,200,200'/><node_position Node='-259' Position='1427,102,200,200'/></node_postions>"},"nodes_readonly":false,"studio_version":"v2"}
    In [1]:
    # 本代码由可视化策略环境自动生成 2021年12月6日 22:16
    # 本代码单元只能在可视化模式下编辑。您也可以拷贝代码,粘贴到新建的代码单元或者策略,然后修改。
    
    
    # Python 代码入口函数,input_1/2/3 对应三个输入端,data_1/2/3 对应三个输出端
    def m4_run_bigquant_run(input_1, input_2, input_3):
        # 示例代码如下。在这里编写您的代码
        #start_date=input_1.read_pickle()['start_date']
        start_date=(pd.to_datetime(input_1.read_pickle()['start_date']) - datetime.timedelta(days=100)).strftime('%Y-%m-%d')
        end_date=input_1.read_pickle()['end_date']
        instruments=input_1.read_pickle()['instruments']
        df = DataSource('financial_statement_CN_STOCK_A').read(instruments,start_date,end_date,\
                                                               ['fs_publish_date','fs_quarter_year','fs_quarter_index','fs_net_profit_yoy'])
        data_1 = DataSource.write_df(df)
        return Outputs(data_1=data_1, data_2=None, data_3=None)
    
    # 后处理函数,可选。输入是主函数的输出,可以在这里对数据做处理,或者返回更友好的outputs数据格式。此函数输出不会被缓存。
    def m4_post_run_bigquant_run(outputs):
        return outputs
    
    # Python 代码入口函数,input_1/2/3 对应三个输入端,data_1/2/3 对应三个输出端
    def m9_run_bigquant_run(input_1, input_2, input_3):
        # 示例代码如下。在这里编写您的代码
        # 获取交易日历
        start_date=(pd.to_datetime(input_1.read_pickle()['start_date']) - datetime.timedelta(days=100)).strftime('%Y-%m-%d')
        end_date=input_1.read_pickle()['end_date']
        df = D.trading_days(start_date=start_date,end_date=end_date)
        # 为尽量接近实盘,事件日期应为财报公布日的前一天
        df['shift_date']=df.shift(1)
        data_1 = DataSource.write_df(df)
        return Outputs(data_1=data_1, data_2=None, data_3=None)
    
    # 后处理函数,可选。输入是主函数的输出,可以在这里对数据做处理,或者返回更友好的outputs数据格式。此函数输出不会被缓存。
    def m9_post_run_bigquant_run(outputs):
        return outputs
    
    # Python 代码入口函数,input_1/2/3 对应三个输入端,data_1/2/3 对应三个输出端
    def m7_run_bigquant_run(input_1, input_2, input_3):
        # 示例代码如下。在这里编写您的代码
        df1 = input_1.read_df()
        df2 = input_2.read_df()
        df_merge=pd.merge(df1,df2,on='date',how='left')#.drop(['date'],axis=1).rename(columns={'shift_date':'date'})
        data_1 = DataSource.write_df(df_merge)
        return Outputs(data_1=data_1, data_2=None, data_3=None)
    
    # 后处理函数,可选。输入是主函数的输出,可以在这里对数据做处理,或者返回更友好的outputs数据格式。此函数输出不会被缓存。
    def m7_post_run_bigquant_run(outputs):
        return outputs
    
    # 回测引擎:初始化函数,只执行一次
    def m3_initialize_bigquant_run(context):   
        context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5)) 
        context.hold_periods = 40 # 持有40天,固定持仓期
        context.stock_max_num = 50 # 最大持仓数量为50只
         
    
        
        
    # 回测引擎:每日数据处理函数,每天执行一次
    def m3_handle_data_bigquant_run(context, data):
        
        today  = data.current_dt.strftime('%Y-%m-%d') # 日期
        # 通过positions对象,使用列表生成式的方法获取目前持仓的股票列表和对应的最新市值
        equities = {e.symbol: p for e, p in context.portfolio.positions.items() if p.amount>0}
    
        # 记录持仓股票数量
        hold_num = len(equities)
        # 记录用于买入股票的可用现金
        cash_for_buy = context.portfolio.cash
        for instrument in equities.keys():
            # 如果持仓时间大于40天
            sid = equities[instrument].sid  # 交易标的
            # 今天和上次交易的时间相隔hold_days就全部卖出
            dt = pd.to_datetime(D.trading_days(end_date = today).iloc[-context.hold_periods].values[0])
            if  pd.to_datetime(equities[instrument].last_sale_date.strftime('%Y-%m-%d')) <= dt and data.can_trade(context.symbol(instrument)):
                # 卖出股票并更新持股数量
                context.order_target_percent(sid, 0)
                hold_num -= 1
                # 因为是早盘卖股票早盘买股票,需要记录卖出的股票市值并在买入下单前更新可用现金;
                # 如果是早盘买尾盘卖,则卖出时不需更新可用现金,因为尾盘卖出股票所得现金无法使用
                cash_for_buy += equities[instrument].amount *  equities[instrument].last_sale_price
    
                
        # 当日还允许买入建仓的股票数目
        stock_can_buy_num = context.stock_max_num - hold_num
        
        try:
            daily_stock_buy = context.daily_stock_buy[today]  # 当日符合买入条件的股票
        except:
            daily_stock_buy = []  # 如果没有符合条件的股票,就设置为空
            
        stock_to_buy = [k for k in daily_stock_buy if k not in equities]
        stock_to_buy_num = min(stock_can_buy_num,len(stock_to_buy))
        
        # 如果不需要买入股票就返回,当日不执行买入操作
        if stock_to_buy_num > 0:
            buy_num = 0
            # 买入满足条件的股票,使用可用现金等额买入
            for stock in stock_to_buy:
                cash = cash_for_buy / stock_to_buy_num
                if data.can_trade(context.symbol(stock)) and buy_num<stock_to_buy_num:
                    context.order_target_value(context.symbol(stock), cash)
                    buy_num += 1
    
    
    
    # 回测引擎:准备数据,只执行一次
    def m3_prepare_bigquant_run(context):
        # 加载预测数据
        df = context.options['data'].read_df()
        df['date']=df['date'].apply(lambda x:x.strftime('%Y-%m-%d'))
        # 函数:求满足开仓条件的股票列表
        def open_pos_con(df):
            return list(df.instrument)
        
        # 每日买入股票的数据框
        context.daily_stock_buy= df.groupby('date').apply(open_pos_con)
        
    
    
    m1 = M.instruments.v2(
        start_date=T.live_run_param('trading_date', '2017-07-01'),
        end_date=T.live_run_param('trading_date', '2017-12-18'),
        market='CN_STOCK_A',
        instrument_list='',
        max_count=0
    )
    
    m4 = M.cached.v3(
        input_1=m1.data,
        run=m4_run_bigquant_run,
        post_run=m4_post_run_bigquant_run,
        input_ports='',
        params='{}',
        output_ports=''
    )
    
    m9 = M.cached.v3(
        input_1=m1.data,
        run=m9_run_bigquant_run,
        post_run=m9_post_run_bigquant_run,
        input_ports='',
        params='{}',
        output_ports='',
        m_cached=False
    )
    
    m7 = M.cached.v3(
        input_1=m4.data_1,
        input_2=m9.data_1,
        run=m7_run_bigquant_run,
        post_run=m7_post_run_bigquant_run,
        input_ports='',
        params='{}',
        output_ports='',
        m_cached=False
    )
    
    m6 = M.filter.v3(
        input_data=m7.data_1,
        expr='fs_net_profit_yoy>30',
        output_left_data=False
    )
    
    m5 = M.sort.v4(
        input_ds=m6.data,
        sort_by='fs_net_profit_yoy',
        group_by='date',
        keep_columns='--',
        ascending=False
    )
    
    m3 = M.trade.v4(
        instruments=m1.data,
        options_data=m5.sorted_data,
        start_date='',
        end_date='',
        initialize=m3_initialize_bigquant_run,
        handle_data=m3_handle_data_bigquant_run,
        prepare=m3_prepare_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='000300.HIX'
    )
    
    • 收益率-2.96%
    • 年化收益率-6.33%
    • 基准收益率8.69%
    • 阿尔法-0.17
    • 贝塔0.55
    • 夏普比率-0.45
    • 胜率0.56
    • 盈亏比0.41
    • 收益波动率17.65%
    • 信息比率-0.09
    • 最大回撤8.2%
    bigcharts-data-start/{"__type":"tabs","__id":"bigchart-67b99ad354e74519b78cf4bb7fabda83"}/bigcharts-data-end