复制链接
克隆策略

配对交易策略

版本 v1.0

目录

  • ### 配对交易策略的交易规则

  • ### 策略构建步骤

  • ### 策略的实现

正文

一、配对策略的交易规则

  • 对于股价有长期协整关系的两只股票X和Y, 可以通过历史数据回归计算两只股票的股价关系,即 Y = a*X + b, 得到相关系数a和残差项b;
  • 如果两个股票所属同一行业,我们可以认为两者的股价未来应该保持上述关系,即序列 zscore=(b-mean(b,N))/std(b,N) 存在比较稳定的均值回归特性,保持在-1和1之间往复震荡;
  • 当zscore小于-1时,Y股票低估,此时卖出X, 全仓买入Y;
  • 当zscore大于 1时,X股票低估,此时卖出Y, 全仓买入X;

二、策略构建步骤

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

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

2、获取历史价格数据

  • 通过输入特征列表输入 close_0/adjust_factor_0 因子,指定获取股票的收盘价真实价格。
  • 通过基础特征抽取模块获取股票的价格数据。
  • 通过缺失数据处理模块删去有缺失值的数据。

3、确定买卖原则

  • 当zscore小于-1时,卖出X股票, 全仓买入Y;
  • 当zscore大于 1时,卖出Y股票, 全仓买入X;

4、模拟回测

  • 通过 trade 模块中的初始化函数定义交易手续费和滑点;
  • 通过 trade 模块中的盘前处理函数每日用当日之前的历史数据计算一次zscore变量序列,并存放在context.zscore 变量中;
  • 回测第一天用过去240个自然日的历史数据计算,后面每日用于计算zscore的历史数据逐日递增;
  • 通过 trade 模块中的主函数(handle函数)查看每日zscore值,按照买卖原则执行相应的卖出/买入操作。

三、策略的实现

可视化策略实现如下:

    {"description":"实验创建于2017/8/26","graph":{"edges":[{"to_node_id":"-836:features","from_node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-24:data"},{"to_node_id":"-3301:features","from_node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-24:data"},{"to_node_id":"-836:instruments","from_node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-62:data"},{"to_node_id":"-3041:instruments","from_node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-62:data"},{"to_node_id":"-3301:input_data","from_node_id":"-836:data"},{"to_node_id":"-1755:input_data","from_node_id":"-3301:data"},{"to_node_id":"-3041:options_data","from_node_id":"-1755:data"}],"nodes":[{"node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-24","module_id":"BigQuantSpace.input_features.input_features-v1","parameters":[{"name":"features","value":"# #号开始的表示注释\n# 多个特征,每行一个,可以包含基础特征和衍生特征\nclose_0/adjust_factor_0\n","type":"Literal","bound_global_parameter":null}],"input_ports":[{"name":"features_ds","node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-24"}],"output_ports":[{"name":"data","node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-24"}],"cacheable":true,"seq_num":1,"comment":"","comment_collapsed":true},{"node_id":"287d2cb0-f53c-4101-bdf8-104b137c8601-62","module_id":"BigQuantSpace.instruments.instruments-v2","parameters":[{"name":"start_date","value":"2015-01-01","type":"Literal","bound_global_parameter":"交易日期"},{"name":"end_date","value":"2015-05-28","type":"Literal","bound_global_parameter":"交易日期"},{"name":"market","value":"CN_STOCK_A","type":"Literal","bound_global_parameter":null},{"name":"instrument_list","value":"601328.SHA\n601998.SHA","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":2,"comment":"预测数据,用于回测和模拟","comment_collapsed":false},{"node_id":"-836","module_id":"BigQuantSpace.general_feature_extractor.general_feature_extractor-v7","parameters":[{"name":"start_date","value":"","type":"Literal","bound_global_parameter":null},{"name":"end_date","value":"","type":"Literal","bound_global_parameter":null},{"name":"before_start_days","value":"300","type":"Literal","bound_global_parameter":null}],"input_ports":[{"name":"instruments","node_id":"-836"},{"name":"features","node_id":"-836"}],"output_ports":[{"name":"data","node_id":"-836"}],"cacheable":true,"seq_num":7,"comment":"","comment_collapsed":true},{"node_id":"-3041","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 # 系统已经设置了默认的交易手续费和滑点,要修改手续费可使用如下函数\n context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))","type":"Literal","bound_global_parameter":null},{"name":"handle_data","value":"# 回测引擎:每日数据处理函数,每天执行一次\ndef bigquant_run(context, data):\n today = data.current_dt.strftime('%Y-%m-%d')\n zscore_today =context.zscore.loc[today]\n #获取股票的列表\n stocklist=context.instruments\n # 转换成回测引擎所需要的symbol格式\n symbol_1 = context.symbol(stocklist[0]) \n symbol_2 = context.symbol(stocklist[1]) \n\n # 持仓\n cur_position_1 = context.portfolio.positions[symbol_1].amount\n cur_position_2 = context.portfolio.positions[symbol_2].amount\n \n # 交易逻辑\n # 如果zesore大于上轨(>1),则价差会向下回归均值,因此需要买入股票x,卖出股票y\n if zscore_today > 1 and cur_position_1 == 0 and data.can_trade(symbol_1) and data.can_trade(symbol_2): \n context.order_target_percent(symbol_2, 0)\n context.order_target_percent(symbol_1, 1)\n print(today, '全仓买入:',stocklist[0])\n \n # 如果zesore小于下轨(<-1),则价差会向上回归均值,因此需要买入股票y,卖出股票x\n elif zscore_today < -1 and cur_position_2 == 0 and data.can_trade(symbol_1) and data.can_trade(symbol_2): \n context.order_target_percent(symbol_1, 0) \n context.order_target_percent(symbol_2, 1)\n print(today, '全仓买入:',stocklist[1])\n \n \n","type":"Literal","bound_global_parameter":null},{"name":"prepare","value":"# 回测引擎:准备数据,只执行一次\ndef bigquant_run(context):\n pass\n \n","type":"Literal","bound_global_parameter":null},{"name":"before_trading_start","value":"def bigquant_run(context,data): \n # 加载股票历史数据\n df = context.options['data'].read_df()\n df['date'] = df['date'].apply(lambda x:x.strftime('%Y-%m-%d'))\n today = data.current_dt.strftime('%Y-%m-%d')\n # 获取前240个自然日的数据\n start_date = (pd.to_datetime(data.current_dt)-datetime.timedelta(days=240)).strftime('%Y-%m-%d')\n stock_data = df[df.date <= today]\n #获取股票的列表,由于可能上市天数不同,对缺失值填充处理\n stocklist=context.instruments\n prices_df=pd.pivot_table(stock_data, values='close_0', index=['date'], columns=['instrument'])\n prices_df.fillna(method='ffill',inplace=True)\n \n x = prices_df[stocklist[0]] # 股票1\n y = prices_df[stocklist[1]] # 股票2\n \n # 线性回归两个股票的股价 y=ax+b\n from pyfinance import ols\n model = ols.OLS(y=y, x=x)\n \n def zscore(series):\n return (series - series.mean()) / np.std(series)\n \n # 计算 y-a*x 序列的zscore值序列\n zscore_calcu = zscore(y-model.beta*x)\n context.zscore=zscore_calcu","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":"-3041"},{"name":"options_data","node_id":"-3041"},{"name":"history_ds","node_id":"-3041"},{"name":"benchmark_ds","node_id":"-3041"},{"name":"trading_calendar","node_id":"-3041"}],"output_ports":[{"name":"raw_perf","node_id":"-3041"}],"cacheable":false,"seq_num":3,"comment":"","comment_collapsed":true},{"node_id":"-3301","module_id":"BigQuantSpace.derived_feature_extractor.derived_feature_extractor-v3","parameters":[{"name":"date_col","value":"date","type":"Literal","bound_global_parameter":null},{"name":"instrument_col","value":"instrument","type":"Literal","bound_global_parameter":null},{"name":"drop_na","value":"False","type":"Literal","bound_global_parameter":null},{"name":"remove_extra_columns","value":"False","type":"Literal","bound_global_parameter":null},{"name":"user_functions","value":"{}","type":"Literal","bound_global_parameter":null}],"input_ports":[{"name":"input_data","node_id":"-3301"},{"name":"features","node_id":"-3301"}],"output_ports":[{"name":"data","node_id":"-3301"}],"cacheable":true,"seq_num":4,"comment":"","comment_collapsed":true},{"node_id":"-1755","module_id":"BigQuantSpace.dropnan.dropnan-v2","parameters":[],"input_ports":[{"name":"input_data","node_id":"-1755"},{"name":"features","node_id":"-1755"}],"output_ports":[{"name":"data","node_id":"-1755"}],"cacheable":true,"seq_num":5,"comment":"","comment_collapsed":true}],"node_layout":"<node_postions><node_position Node='287d2cb0-f53c-4101-bdf8-104b137c8601-24' Position='1306,105,200,200'/><node_position Node='287d2cb0-f53c-4101-bdf8-104b137c8601-62' Position='764,115,200,200'/><node_position Node='-836' Position='1079,236,200,200'/><node_position Node='-3041' Position='1002,519,200,200'/><node_position Node='-3301' Position='1088,305,200,200'/><node_position Node='-1755' Position='1081,397,200,200'/></node_postions>"},"nodes_readonly":false,"studio_version":"v2"}
    In [1]:
    # 本代码由可视化策略环境自动生成 2021年12月6日 22:15
    # 本代码单元只能在可视化模式下编辑。您也可以拷贝代码,粘贴到新建的代码单元或者策略,然后修改。
    
    
    # 回测引擎:初始化函数,只执行一次
    def m3_initialize_bigquant_run(context):
        # 系统已经设置了默认的交易手续费和滑点,要修改手续费可使用如下函数
        context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
    # 回测引擎:每日数据处理函数,每天执行一次
    def m3_handle_data_bigquant_run(context, data):
        today = data.current_dt.strftime('%Y-%m-%d')
        zscore_today =context.zscore.loc[today]
        #获取股票的列表
        stocklist=context.instruments
        # 转换成回测引擎所需要的symbol格式
        symbol_1 = context.symbol(stocklist[0]) 
        symbol_2 = context.symbol(stocklist[1])  
    
        # 持仓
        cur_position_1 = context.portfolio.positions[symbol_1].amount
        cur_position_2 = context.portfolio.positions[symbol_2].amount
           
        # 交易逻辑
        # 如果zesore大于上轨(>1),则价差会向下回归均值,因此需要买入股票x,卖出股票y
        if zscore_today > 1 and cur_position_1 == 0 and data.can_trade(symbol_1) and data.can_trade(symbol_2):  
            context.order_target_percent(symbol_2, 0)
            context.order_target_percent(symbol_1, 1)
            print(today, '全仓买入:',stocklist[0])
            
        # 如果zesore小于下轨(<-1),则价差会向上回归均值,因此需要买入股票y,卖出股票x
        elif zscore_today < -1 and cur_position_2 == 0 and data.can_trade(symbol_1) and data.can_trade(symbol_2):  
            context.order_target_percent(symbol_1, 0)  
            context.order_target_percent(symbol_2, 1)
            print(today, '全仓买入:',stocklist[1])
     
              
    
    # 回测引擎:准备数据,只执行一次
    def m3_prepare_bigquant_run(context):
        pass
        
    
    def m3_before_trading_start_bigquant_run(context,data):    
        # 加载股票历史数据
        df = context.options['data'].read_df()
        df['date'] = df['date'].apply(lambda x:x.strftime('%Y-%m-%d'))
        today = data.current_dt.strftime('%Y-%m-%d')
        # 获取前240个自然日的数据
        start_date = (pd.to_datetime(data.current_dt)-datetime.timedelta(days=240)).strftime('%Y-%m-%d')
        stock_data = df[df.date <= today]
        #获取股票的列表,由于可能上市天数不同,对缺失值填充处理
        stocklist=context.instruments
        prices_df=pd.pivot_table(stock_data, values='close_0', index=['date'], columns=['instrument'])
        prices_df.fillna(method='ffill',inplace=True)
        
        x = prices_df[stocklist[0]] # 股票1
        y = prices_df[stocklist[1]] # 股票2
        
        # 线性回归两个股票的股价 y=ax+b
        from pyfinance import ols
        model = ols.OLS(y=y, x=x)
     
        def zscore(series):
            return (series - series.mean()) / np.std(series)
        
        # 计算 y-a*x 序列的zscore值序列
        zscore_calcu = zscore(y-model.beta*x)
        context.zscore=zscore_calcu
    
    m1 = M.input_features.v1(
        features="""# #号开始的表示注释
    # 多个特征,每行一个,可以包含基础特征和衍生特征
    close_0/adjust_factor_0
    """
    )
    
    m2 = M.instruments.v2(
        start_date=T.live_run_param('trading_date', '2015-01-01'),
        end_date=T.live_run_param('trading_date', '2015-05-28'),
        market='CN_STOCK_A',
        instrument_list="""601328.SHA
    601998.SHA""",
        max_count=0
    )
    
    m7 = M.general_feature_extractor.v7(
        instruments=m2.data,
        features=m1.data,
        start_date='',
        end_date='',
        before_start_days=300
    )
    
    m4 = M.derived_feature_extractor.v3(
        input_data=m7.data,
        features=m1.data,
        date_col='date',
        instrument_col='instrument',
        drop_na=False,
        remove_extra_columns=False,
        user_functions={}
    )
    
    m5 = M.dropnan.v2(
        input_data=m4.data
    )
    
    m3 = M.trade.v4(
        instruments=m2.data,
        options_data=m5.data,
        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.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'
    )
    
    • 收益率-3.86%
    • 年化收益率-9.72%
    • 基准收益率36.8%
    • 阿尔法-0.87
    • 贝塔0.97
    • 夏普比率-0.18
    • 胜率0.0
    • 盈亏比0.0
    • 收益波动率36.47%
    • 信息比率-0.26
    • 最大回撤20.9%
    bigcharts-data-start/{"__type":"tabs","__id":"bigchart-ef80dec11e8c44aab57e3c5e5841c993"}/bigcharts-data-end