跨品种套利
1. 原理
什么是套利?
套利是指在买入或卖出一种金融资产的同时卖出或买入另一种相关的金融资产从中利用价差获得套利的过程。
什么是跨品种套利?
当两个合约有很强的相关性时,可能存在相似的变动关系,两种合约之间的价差会维持在一定的水平上。当市场出现变化时,两种合约之间的价差会偏离均衡水平。此时,可以买入其中一份合约同时卖出其中一份合约,当价差恢复到正常水平时平仓,获取收益。
以大商所玉米和淀粉为例,合约分别为DCE.c1801和DCE.cs1801。二者之间相关性为0.7333,价差处于相对稳定合理区间。如图所示。
二者价差整体处于250-350之间。当价差偏离此区间时,可以进行跨品种套利。
跨品种套利有以下几个特点:
1.套利的两种资产必须有一定的相关性。
2.两种合约标的不同,到期时间相同。
3.两种资产之间的价差呈现一定规律。
怎样确定合约之间有相关性?
最常用的方法是利用EG两步法对两个序列做协整检验,判断两个序列是否平稳。只有单整阶数相同,二者才有可能存在一定的关系。
以大豆和豆粕为例,选取其在2017年1月1日至2018年1月1日的主力合约价格时间序列,利用statsmodels包进行协整检验。
检验结果为:
焦炭的t = -1.7886,1%置信区间的临界值为-3.4576,说明该序列在99%的置信水平下平稳。
焦煤的t = -2.0500,1%置信区间的临界值为-3.4576,说明该序列在99%的置信水平下平稳。
因此,二者都为平稳序列。
利用OLS回归检残差序列是否平稳,残差的t=-2.3214,临界值为-3.4577,说明残差平稳。因此,可以认为二者之间存在一定关系。
回归后的残差图如下:
对残差进行ks检验,检验结果p=0,说明残差分布为正态分布。
策略设计
传统利用价差进行跨品种套利的方法是计算出均值和方差,设定开仓、平仓和止损阈值。当新的价格达到阈值时,进行相应的开仓和平仓操作。
应该怎样确定均值?
均值的选取主要有两种方法,第一种方法是固定均值。先历史价格计算相应的阈值(比如利用2017年2月-2017年6月的数据计算阈值,在2019年7月进行套利),再用最新价差进行比较,会发现前后均值差异很大。如图所示。
因此,常用变动的均值设定阈值。即用过去N天两个标的之间差值的均值和方差。
2. 策略思路
第一步:选择相关性较高的两个合约,本例选择大商所的焦炭和焦煤。
第二步:以过去30个的1d频率bar的均值正负0.75个标准差作为开仓阈值,以正负2个标准差作为止损阈值。
第三步:最新价差上穿上界时做空价差,回归到均值附近平仓;下穿下界时做多价差,回归到均值附近平仓。设定止损点,触发止损点则全部平仓。
回测期:2018-02-01 8:00:00 至 2018-12-31 16:00:00
回测标的:DCE.j1901, DCE.jm1901
回测初始资金:200万
注意:若修改回测期,需要修改对应的回测标的。
3. 策略代码
# coding=utf-8
from __future__ import print_function, absolute_import, unicode_literals
from gm.api import *
import numpy as np
def init(context):
# 选择的两个合约
context.symbol = ['DCE.j1901', 'DCE.jm1901']
# 订阅历史数据
subscribe(symbols=context.symbol,frequency='1d',count=11,wait_group=True)
def on_bar(context, bars):
# 数据提取
j_close = context.data(symbol=context.symbol[0],frequency='1d',fields='close',count=31).values
jm_close = context.data(symbol=context.symbol[1],frequency='1d',fields='close',count=31).values
# 提取最新价差
new_price = j_close[-1] - jm_close[-1]
# 计算历史价差,上下限,止损点
spread_history = j_close[:-2] - jm_close[:-2]
context.spread_history_mean = np.mean(spread_history)
context.spread_history_std = np.std(spread_history)
context.up = context.spread_history_mean + 0.75 * context.spread_history_std
context.down = context.spread_history_mean - 0.75 * context.spread_history_std
context.up_stoppoint = context.spread_history_mean + 2 * context.spread_history_std
context.down_stoppoint = context.spread_history_mean - 2 * context.spread_history_std
# 查持仓
position_jm_long = context.account().position(symbol=context.symbol[0],side=1)
position_jm_short = context.account().position(symbol=context.symbol[0],side=2)
# 设计买卖信号
# 设计开仓信号
if not position_jm_short and not position_jm_long:
if new_price > context.up:
print('做空价差组合')
order_volume(symbol=context.symbol[0],side=OrderSide_Sell,volume=1,order_type=OrderType_Market,position_effect=1)
order_volume(symbol=context.symbol[1], side=OrderSide_Buy, volume=1, order_type=OrderType_Market, position_effect=PositionEffect_Open)
if new_price < context.down:
print('做多价差组合')
order_volume(symbol=context.symbol[0], side=OrderSide_Buy, volume=1, order_type=OrderType_Market, position_effect=PositionEffect_Open)
order_volume(symbol=context.symbol[1], side=OrderSide_Sell, volume=1, order_type=OrderType_Market, position_effect=PositionEffect_Open)
# 设计平仓信号
# 持jm多仓时
if position_jm_long:
if new_price >= context.spread_history_mean:
# 价差回归到均值水平时,平仓
print('价差回归到均衡水平,平仓')
order_volume(symbol=context.symbol[0], side=OrderSide_Sell, volume=1, order_type=OrderType_Market, position_effect=PositionEffect_Close)
order_volume(symbol=context.symbol[1], side=OrderSide_Buy, volume=1, order_type=OrderType_Market, position_effect=PositionEffect_Close)
if new_price < context.down_stoppoint:
# 价差达到止损位,平仓止损
print('价差超过止损点,平仓止损')
order_volume(symbol=context.symbol[0], side=OrderSide_Sell, volume=1, order_type=OrderType_Market, position_effect=PositionEffect_Close)
order_volume(symbol=context.symbol[1], side=OrderSide_Buy, volume=1, order_type=OrderType_Market, position_effect=PositionEffect_Close)
# 持jm空仓时
if position_jm_short:
if new_price <= context.spread_history_mean:
# 价差回归到均值水平时,平仓
print('价差回归到均衡水平,平仓')
order_volume(symbol=context.symbol[0], side=OrderSide_Buy, volume=1, order_type=OrderType_Market, position_effect=PositionEffect_Close)
order_volume(symbol=context.symbol[1], side=OrderSide_Sell, volume=1, order_type=OrderType_Market, position_effect=PositionEffect_Close)
if new_price > context.up_stoppoint:
# 价差达到止损位,平仓止损
print('价差超过止损点,平仓止损')
order_volume(symbol=context.symbol[0], side=OrderSide_Buy, volume=1, order_type=OrderType_Market, position_effect=PositionEffect_Close)
order_volume(symbol=context.symbol[1], side=OrderSide_Sell, volume=1, order_type=OrderType_Market, position_effect=PositionEffect_Close)
if __name__ == '__main__':
'''
strategy_id策略ID,由系统生成
filename文件名,请与本文件名保持一致
mode实时模式:MODE_LIVE回测模式:MODE_BACKTEST
token绑定计算机的ID,可在系统设置-密钥管理中生成
backtest_start_time回测开始时间
backtest_end_time回测结束时间
backtest_adjust股票复权方式不复权:ADJUST_NONE前复权:ADJUST_PREV后复权:ADJUST_POST
backtest_initial_cash回测初始资金
backtest_commission_ratio回测佣金比例
backtest_slippage_ratio回测滑点比例
'''
run(strategy_id='strategy_id',
filename='main.py',
mode=MODE_BACKTEST,
token='token',
backtest_start_time='2018-02-01 08:00:00',
backtest_end_time='2018-12-31 16:00:00',
backtest_adjust=ADJUST_PREV,
backtest_initial_cash=2000000,
backtest_commission_ratio=0.0001,
backtest_slippage_ratio=0.0001)
4. 回测结果与稳健性分析
设定初始资金200万,手续费率为0.01%,滑点比率为0.01%。回测结果如下图所示。
回测期累计收益率2.80%,年化收益率为3.06%,沪深300收益率为-29.09%,策略收益跑赢基准收益。最大回撤率为2.03%,胜率为48.25%。
为了检验策略的稳健性,改变数据的频率和均线的计算周期,结果如下。
数据频率 | 均线周期 | 年化收益率 | 最大回撤 |
---|---|---|---|
1d | 10 | 3.06% | 2.30% |
1d | 20 | 3.51% | 2.53% |
1d | 30 | 0.55% | 2.45% |
3600s | 10 | -7.84% | 7.40% |
3600s | 20 | -4.11% | 5.28% |
3600s | 30 | -2.89% | 3.91% |
900s | 10 | -10.07% | 9.38% |
900s | 20 | -9.39% | 8.82% |
900s | 30 | -7.65% | 7.32% |
可以看出,该策略只在1d的频率下实现了盈利,在其他频率下,收益均为负,说明该策略对于高频场景的适用有一定限制。