股指期货跨期套利策略示例

模型简介

套利是期货交易中一种常见的投资方式,其原理是在标的产品间的市场价格关系处于“异常”状态时进行双边交易,以获取低风险价差。相比来说,投机交易的风险太大,套期保则值是为了规避市场风险,无法获取最大收益。而套利交易的收益往往独立于市场,无需关注市场行情涨跌,都可以在价差中获利,且一般波动较小,风险较低。

期货套利交易种类繁多,按品种可分为股指期货套利和商品期货套利;按套利方式又可以分为期现套利、跨期套利、跨市套利、跨品种套利等。在这里,我们选取比较有代表性的股指期货跨期套利向大家介绍,并在之后向大家展示如何在云宽客投研平台加以实现。

跨期套利是指投资者以赚取期货合约价差为目的,在同一期货品种的不同月份合约上建立数量相等、方向相反的交易头寸,最后以对冲或交割方式结束交易、获得收益的方式。最简单的跨期套利就是买入近期的期货品种,卖出远期的期货品种。因此跨期套利所关注的是合约之间的价差,能够一定程度上避免单个期货合约价格剧烈变动所带来的风险。

对于同一个投资标的,市场上一般有若干张不同到期月份的期货合约。以沪深 300 指数为例,同一时刻,市场上存在当月,下月,当季,下季四个股指期货。这些合约均有同一现货标的,受相同因素的影响,所以合约价格表现出一定的相关性。在市场预期一致的情况下,它们之间的价差应该是稳定的,一旦发生了偏离,那么就出现了套利的机会。

相比期现套利来说,跨期套利并不是无风险套利。期现套利的价差会在合约到期时收敛,但是跨期套利的价差可能会随着股指的上升而上升,实际中也与投资者对中期或短期股指的预期相关,如果价差在一个新的水平上达到平衡,那么我们的策略便面临着风险。

因此,如何判断价差走势,是股指期货跨期套利的关键所在。

制定策略

本篇文章中,我们选取了沪深 300 股指期货作为投资品种,选择当月主力合约及下月合约(如果当月合约到期,则取下月合约及下个季月合约)作为投资标的。对于价差的,我们采用考察其均值和标准差的方法选择进场时点。

我们的策略具体分为以下几个步骤:
1)选择品种、标的;
2)获取历史数据,计算标的价差(合约 B- 合约 A),以及其均值、标准差,设定上界为均值 + 标准差 _ 进场参数,下界为均值 - 标准差 _ 离场参数,这里进场、离场参数都设为 1;
3)根据价格、保证金率、合约乘数设定交易量,留存部分现金防止爆仓;
4)当价差上穿上界时,卖出组合,即开多合约 A,开空合约 B,并在价差下穿均线时平仓;当价差下穿下界时,买入组合,即开空合约 A,开多合约 B;,并在价差上穿均线时平仓;

收益曲线

收益归因

小结

我们可以看到,对沪深 300 股指期货的跨期套利策略,在近一年的回测期中表现十分优异,有着稳健且高额的收益率,较低的波动和很小的回撤。这证明了期货套利策略在实际应用上的可行性。

当然,本文着重介绍的只是改策略的思想以及具体的实现过程。在实际应用中,更困难的问题是发掘和验证不同合约直接的相关性与稳定性。传统的跨期套利中投资者需要预期价差(spread)的走势来建立套利头寸,在主观性的影响下这种方法局限性很大。现代投资体系中往往采用统计套利(Statistical Arbitrage)的方法发现价差的稳定性以及变量间的长期均衡关系,用实际的价格与数量模型所预测的价值进行对比,制定统计方法下相对客观的跨期套利策略。这点就留给读者继续探究了。

Code:

# -*- coding:utf-8 -*-

from CloudQuant import SDKCoreEngine  # 导入量子金服SDK
from CloudQuant import AssetType
from CloudQuant import QuoteCycle
from CloudQuant import OrderType
import numpy as np  # 使用numpy

np.seterr(invalid='ignore')

MARKET="IF" #设定交易品种
WINDOW=10 #设定观察窗口
ENTRY_SCORE=1 #设定入场参数
EXIT_SCORE=1 #设定离场参数

config = {
'username': 'pengkun',
'password': '111111',
'rootpath': 'c:/cStrategy',  # 客户端所在路径
'assetType': AssetType.Future,
'initCapitalFuture': 10000000,  # 初始资金
'startDate': 20160101,  # 交易开始日期
'endDate': 20170110,  # 交易结束日期
'cycle': QuoteCycle.D,  # 回放粒度为1分钟线
'feeRate': 0.001,
'feeLimit': 5,
'strategyName': 'arbitrage',  # 策略名
"logfile": "ma.log",
'dealByVolume': False
}

def dateAdd(year,month,add): #自定义月份加法
month=month+add
if month>12:
year+=1
month-=12
return str(year*100+month)

def initial(sdk):
flag=0 #标记状态
sdk.setGlobal('flag',flag)

def initPerDay(sdk):
#选择标的,当月主力及下月合约
delivery_date = sdk.getFactorData("LZ_CN_FF_LASTTRADE_DATE")
delivery_date_dict = dict(line for line in delivery_date)
todayDate = sdk.getNowDate()
this_month = str(todayDate)[2:6]
year = int(this_month[0:2])
month = int(this_month[2:4])
next_month = dateAdd(year, month, 1)
next_quarter_month = dateAdd(year, month, (3 - month % 3))
contract_this_month = MARKET + str(this_month)
contract_next_month =MARKET + str(next_month)
contract_next_quarter_month=MARKET+str(next_quarter_month)
delivery_date_this_month = delivery_date_dict[contract_this_month + '.CFE']
# 如果当月合约到期,选择下月即下个季月
if todayDate <= int(delivery_date_this_month):
contract_a = contract_this_month
contract_b = contract_next_month
else:
contract_a = contract_next_month
contract_b = contract_next_quarter_month
sdk.setGlobal("c_a",contract_a)
def strategy(sdk):
flag=sdk.getGlobal('flag')
contract_a=sdk.getGlobal("c_a")
contract_b=sdk.getGlobal("c_b")
contract=[contract_a,contract_b]
#获取历史数据,计算价差及其均值、标准差
hist=sdk.getLatest(contract,WINDOW,"D",2)
if len(hist[contract_a])>=WINDOW and len(hist[contract_b])>=WINDOW:
hist_a=[0.0]*WINDOW
hist_b=[0.0]*WINDOW
for i in range(WINDOW):
    hist_a[i]=hist[contract_a][i].close
    hist_b[i]=hist[contract_b][i].close
hist_a=np.array(hist_a)
hist_b=np.array(hist_b)
spread=hist_b-hist_a
mean=np.nanmean(spread)
std=np.std(spread)

#获取标的报价
q=sdk.getQuotes(contract,2)
price_a=q[contract_a].current
price_b=q[contract_b].current

#设定保证金率
margin_a=40
margin_b=40

#计算持仓量,投入60%资金,留存40%防止行情震荡爆仓
volume_a= (sdk.getAccountInfo(tsInd=2).availableCash*0.6) //((price_a*margin_a*0.01+price_b*margin_b*0.01)*300)
volume_b=volume_a

pos=sdk.getPositions(tsInd=2)
if pos:
    pos_a=pos[0].optPosition
    if len(pos)>1:
        pos_b=pos[1].optPosition
    else:
        pos_b=0
else:
    pos_a=0
    pos_b=0

if spread[-1] < mean and flag > 0:  # 下穿均线,平仓
    sdk.makeOrder(contract_a, price_a, pos_a, -1, tsInd=2)
    sdk.makeOrder(contract_b, price_b, pos_b, -2, tsInd=2)
    sdk.sdklog("------------------------------------------")
    sdk.sdklog(sdk.getNowDate(), "DATE")
    sdk.sdklog([contract_a, price_a, pos_a, -1], 'CLOSE_LONG')
    sdk.sdklog([contract_b, price_b, pos_b, -2], 'CLOSE_SHORT')

if spread[-1]>mean and flag<0:     #上穿均线,平仓
    sdk.makeOrder(contract_a, price_a, pos_a, -2, tsInd=2)
    sdk.makeOrder(contract_b, price_b, pos_b, -1, tsInd=2)
    sdk.sdklog("------------------------------------------")
    sdk.sdklog(sdk.getNowDate(), "DATE")
    sdk.sdklog([contract_a, price_a, pos_a, -2], 'CLOSE_SHORT')
    sdk.sdklog([contract_b, price_b, pos_b, -1], 'CLOSE_LONG')

if spread[-1]<(mean-ENTRY_SCORE*std) and flag>-1:     #下穿下界,买入
    sdk.makeOrder(contract_a, price_a, volume_a, 2, tsInd=2)
    sdk.makeOrder(contract_b, price_b, volume_b, 1, tsInd=2)
    sdk.sdklog("------------------------------------------")
    sdk.sdklog(sdk.getNowDate(), "DATE")
    sdk.sdklog([contract_a, price_a, volume_a, 2], 'OPEN_SHORT')
    sdk.sdklog([contract_b, price_b, volume_b, 1], 'OPEN_LONG')

if spread[-1]>(mean+EXIT_SCORE*std) and flag<1:     #上穿上界,卖出
    sdk.makeOrder(contract_a, price_a, volume_a, 1, tsInd=2)
    sdk.makeOrder(contract_b, price_b, volume_b, 2, tsInd=2)
    sdk.sdklog("------------------------------------------")
    sdk.sdklog(sdk.getNowDate(), "DATE")
    sdk.sdklog([contract_a, price_a, volume_a, 1], 'OPEN_LONG')
    sdk.sdklog([contract_b, price_b, volume_b, 2], 'OPEN_SHORT')

#更新状态
if spread[-1]>(mean+EXIT_SCORE*std):
    flag=1.5
elif spread[-1]>mean:
    flag=0.5
elif spread[-1]>(mean-ENTRY_SCORE*std):
    flag=-0.5
else:
    flag=-1.5

sdk.setGlobal("flag",flag)

def main():
# 将策略函数加入
config['initial'] = initial
config['strategy'] = strategy
config['preparePerDay'] = initPerDay
# 启动SDK
SDKCoreEngine(**config).run()

if __name__ == "__main__":
main()