商品期货跨品种配对交易

该策略为分钟级别回测。运用了简单的移动平均以及布林带(Bollinger Bands)作为交易信号产生源。有关对冲比率(HedgeRatio)的确定,您可以在我们的研究平台上面通过import statsmodels.api as sm引入 statsmodels 中的OLS方法进行线性回归估计。具体估计窗口,您可以根据自己策略需要自行选择。

策略中的移动窗口选择为60分钟,即在每天开盘60分钟内不做任何交易,积累数据计算移动平均值。当然,这一移动窗口也可以根据自身需要进行灵活选择。下面例子中使用了黄金与白银两种商品期货进行配对交易。简单起见,例子中期货的价格并未做对数差处理。

  1. import numpy as np
  2.  
  3.  
  4. # 在这个方法中编写任何的初始化逻辑。context对象将会在你的算法策略的任何方法之间做传递。
  5. def init(context):
  6. context.s1 = 'AG1612'
  7. context.s2 = 'AU1612'
  8.  
  9. # 设置全局计数器
  10. context.counter = 0
  11.  
  12. # 设置滚动窗口
  13. context.window = 60
  14.  
  15. # 设置对冲手数,通过研究历史数据进行价格序列回归得到该值
  16. context.ratio = 15
  17.  
  18. context.up_cross_up_limit = False
  19. context.down_cross_down_limit = False
  20.  
  21. # 设置入场临界值
  22. context.entry_score = 2
  23.  
  24. # 初始化时订阅合约行情。订阅之后的合约行情会在handle_bar中进行更新
  25. subscribe([context.s1, context.s2])
  26.  
  27.  
  28. # before_trading此函数会在每天交易开始前被调用,当天只会被调用一次
  29. def before_trading(context):
  30. # 样例商品期货在回测区间内有夜盘交易,所以在每日开盘前将计数器清零
  31. context.counter = 0
  32.  
  33.  
  34. # 你选择的期货数据更新将会触发此段逻辑,例如日线或分钟线更新
  35. def handle_bar(context, bar_dict):
  36.  
  37. # 获取当前一对合约的仓位情况。如尚未有仓位,则对应持仓量都为0
  38. position_a = context.portfolio.positions[context.s1]
  39. position_b = context.portfolio.positions[context.s2]
  40.  
  41. context.counter += 1
  42. # 当累积满一定数量的bar数据时候,进行交易逻辑的判断
  43. if context.counter > context.window:
  44.  
  45. # 获取当天历史分钟线价格队列
  46. price_array_a = history_bars(context.s1, context.window, '1m', 'close')
  47. price_array_b = history_bars(context.s2, context.window, '1m', 'close')
  48.  
  49. # 计算价差序列、其标准差、均值、上限、下限
  50. spread_array = price_array_a - context.ratio * price_array_b
  51. std = np.std(spread_array)
  52. mean = np.mean(spread_array)
  53. up_limit = mean + context.entry_score * std
  54. down_limit = mean - context.entry_score * std
  55.  
  56. # 获取当前bar对应合约的收盘价格并计算价差
  57. price_a = bar_dict[context.s1].close
  58. price_b = bar_dict[context.s2].close
  59. spread = price_a - context.ratio * price_b
  60.  
  61. # 如果价差低于预先计算得到的下限,则为建仓信号,'买入'价差合约
  62. if spread <= down_limit and not context.down_cross_down_limit:
  63. # 可以通过logger打印日志
  64. logger.info('spread: {}, mean: {}, down_limit: {}'.format(spread, mean, down_limit))
  65. logger.info('创建买入价差中...')
  66.  
  67. # 获取当前剩余的应建仓的数量
  68. qty_a = 1 - position_a.buy_quantity
  69. qty_b = context.ratio - position_b.sell_quantity
  70.  
  71. # 由于存在成交不超过下一bar成交量25%的限制,所以可能要通过多次发单成交才能够成功建仓
  72. if qty_a > 0:
  73. buy_open(context.s1, qty_a)
  74. if qty_b > 0:
  75. sell_open(context.s2, qty_b)
  76. if qty_a == 0 and qty_b == 0:
  77. # 已成功建立价差的'多仓'
  78. context.down_cross_down_limit = True
  79. logger.info('买入价差仓位创建成功!')
  80.  
  81. # 如果价差向上回归移动平均线,则为平仓信号
  82. if spread >= mean and context.down_cross_down_limit:
  83. logger.info('spread: {}, mean: {}, down_limit: {}'.format(spread, mean, down_limit))
  84. logger.info('对买入价差仓位进行平仓操作中...')
  85.  
  86. # 由于存在成交不超过下一bar成交量25%的限制,所以可能要通过多次发单成交才能够成功建仓
  87. qty_a = position_a.buy_quantity
  88. qty_b = position_b.sell_quantity
  89. if qty_a > 0:
  90. sell_close(context.s1, qty_a)
  91. if qty_b > 0:
  92. buy_close(context.s2, qty_b)
  93. if qty_a == 0 and qty_b == 0:
  94. context.down_cross_down_limit = False
  95. logger.info('买入价差仓位平仓成功!')
  96.  
  97. # 如果价差高于预先计算得到的上限,则为建仓信号,'卖出'价差合约
  98. if spread >= up_limit and not context.up_cross_up_limit:
  99. logger.info('spread: {}, mean: {}, up_limit: {}'.format(spread, mean, up_limit))
  100. logger.info('创建卖出价差中...')
  101. qty_a = 1 - position_a.sell_quantity
  102. qty_b = context.ratio - position_b.buy_quantity
  103. if qty_a > 0:
  104. sell_open(context.s1, qty_a)
  105. if qty_b > 0:
  106. buy_open(context.s2, qty_b)
  107. if qty_a == 0 and qty_b == 0:
  108. context.up_cross_up_limit = True
  109. logger.info('卖出价差仓位创建成功')
  110.  
  111. # 如果价差向下回归移动平均线,则为平仓信号
  112. if spread < mean and context.up_cross_up_limit:
  113. logger.info('spread: {}, mean: {}, up_limit: {}'.format(spread, mean, up_limit))
  114. logger.info('对卖出价差仓位进行平仓操作中...')
  115. qty_a = position_a.sell_quantity
  116. qty_b = position_b.buy_quantity
  117. if qty_a > 0:
  118. buy_close(context.s1, qty_a)
  119. if qty_b > 0:
  120. sell_close(context.s2, qty_b)
  121. if qty_a == 0 and qty_b == 0:
  122. context.up_cross_up_limit = False
  123. logger.info('卖出价差仓位平仓成功!')