可能有人对“套利”这个词很陌生,但“套利”在现实生活中却很常见。比如:便利店老板从批发市场以 0.5 元买入一瓶矿泉水,然后在店里以 1 元的价格出售,最后赚取 0.5 元的差价。这个过程其实就类似套利。金融市场上的套利跟这个道理差不多,只不过套利的形式有多种多样。
在商品期货市场中,理论上 5 月份交割的苹果合约价格减去 10 月交割的苹果合约价格,其结果应该接近于 0 或者稳定在一定的价格区间内。但事实上由于受到天气、市场供需等诸多因素的原因,近期和远期合约价格在一段时间内会分别受到不同程度的影响,价差也会出现较大幅度的波动。 但无论如何,价差通常最终会回归到一定的价格区间内,那么如果价差大于这个区间,就卖 5 月份合约,同时买 10 月份合约,做空价差赚取利润;如果价差小于这个区间,就买 5 月份合约,同时卖 10 月份合约,做多价差赚取利润。这就是通过买卖同一个品种但不同交割月份的跨期套利。
除了跨期套利外,还有买入出口国大豆同时卖出进口国大豆,或者卖出出口国大豆同时买入进口国大豆的跨市场套利;买入上游原材料铁矿石同时卖出下游成品螺纹钢,或者卖出上游原材料铁矿石同时买入下游成品螺纹钢的跨品种套利等等。
但是上面这几种套利方法,虽然字面上是“套利”,并不属于纯粹意义上的套利,它们本质上还是属于有风险的投机,只不过这种投机的方式是做多或做空价差。虽然价差在大部分时间内趋于稳定,但也可能出现很长时间不回归的行情。 期现套利的核心原理是,同一个商品在同一个时间点只能有一个价格,期货到了交割时间就变成现货,所以在临近交割时会强制回归。这个与跨期套利完全不同,跨期套利是两个不同交割月的合约,到期时也就是两个不同月份的现货,当然可以是两个价格。
期现套利最大的特点是理论上无风险,主要是根据基差状态,计算利润区间。如果基差过大,就可以买入现货,同时做空期货,等待基差重新归零,就可以期货和现货双边平仓,赚取基差的利润。主要有两种方法:双平套利和交割套利。
说起来简单,最复杂的一个环节是商品现货交易,这其中涉及到仓单、税务等等一系列问题。首先需要一个与投资范围相关的公司,如果是交割套利期货账户必须是公司法人的,如果双平套利就需要一个可靠的销售渠道。网上有很多现货交易的网站,如:卓创网、上海有色网等等。
需要注意的是,现货交易通常有 17%~20%的增值税,所以如果是双平套利,在买入现货后需要做空 1.2~1.25 倍的期货。如果是交割套利,在买入现货后需要做空相同比例的期货,还需要考虑手续费、运输、仓库等成本。当然这一切的前提是期现的基差足够大,有足够的边界。 除此之外,由于上海黄金交易所黄金(T+D)的存在,使得黄金期现套利不仅可以正向套利,还可以无需黄金租赁进行反向套利操作。上海黄金交易所的现货黄金(T+D)延期交易不仅交易方便,而且成交量和持仓量大,流动性非常适合期现套利。
现货和基差的数据网上有很多种,大部分是以表格的形式呈现,这显然不合适用来分析判断行情。优宽量化(youquant.com)已经内置了商品期货基本面数据,包括现货数据和基差数据。只需要调用一个函数就可以获取每个品种的现货和基差价格,并且支持 2016 年至今的历史数据。
# 回测配置
'''backtest
start: 2020-06-01 00:00:00
end: 2020-06-02 00:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
'''
# 策略入口
def main():
while True:
ret = exchange.GetData("GDP") # 调用国内生产总值数据
Log(ret) # 打印数据
Sleep(1000 * 60 * 60 * 24 * 30)
返回结果
{
"季度": "2006年第1季度",
"国内生产总值": {
"绝对值(亿元)": 47078.9,
"同比增长": 0.125
},
"第一产业": {
"绝对值(亿元)": 3012.7,
"同比增长": 0.044
},
"第三产业": {
"绝对值(亿元)": 22647.4,
"同比增长": 0.131
},
"第二产业": {
"绝对值(亿元)": 21418.7,
"同比增长": 0.131
}
}
让我们用优宽量化,以图表的形式,把现货价格和基差价格实现出来。首先注册并登陆优宽量化官网(youquant.com),点击控制中心,点击策略库+新建策略。在左上角下拉菜单中选择 Python,并填入策略的名字。
第一步:编写策略框架
# 策略主函数
def onTick():
pass
# 策略入口
def main():
while True: # 进入循环模式
onTick() # 执行策略主函数
Sleep(1000 * 60 * 60 * 24) # 策略休眠一天
策略框架是两个函数,main 函数是策略的入口,主要功能是交易之前的预处理,程序会先从 main 函数开始执行,然后进入无线循环模式,重复执行 onTick 函数,onTick 函数是策略的主函数,主要执行核心代码。
第二步:增加图表功能
# 全局变量
# 期现图表
cfgA = {
"extension": {
"layout": 'single',
"col": 6,
"height": "500px",
},
"title": {
"text": "期现图表"
},
"xAxis": {
"type": "datetime"
},
"series": [{
"name": "期货价格",
"data": [],
}, {
"name": "现货价格",
"data": [],
}
]
}
# 基差图表
cfgB = {
"extension": {
"layout": 'single',
"col": 6,
"height": "500px",
},
"title": {
"text": "基差图表"
},
"xAxis": {
"type": "datetime"
},
"series": [{
"name": "基差价格",
"data": [],
}]
}
chart = Chart([cfgA, cfgB]) # 创建一个图表对象
# 策略主函数
def onTick():
chart.add(0, []) # 绘制图表
chart.add(1, []) # 绘制图表
chart.add(2, []) # 绘制图表
chart.update([cfgA, cfgB]) # 更新图表
# 策略入口
def main():
LogReset() # 运行前先清空之前的Log日志信息
chart.reset() # 运行前先清空之前的图表信息
while True: # 进入循环模式
onTick() # 执行策略主函数
Sleep(1000 * 60 * 60 * 24) # 策略休眠一天
在这个策略中,一共创建了 2 个图表,并左右分布排列。其中左图 cfgA 是期现图表,包含期货价格和现货价格,右图 cfgB 是基差图表。然后调用优宽量化内置的 Python 版画线类库创建一个 chart 对象。最后是在 onTick 函数中实时更新图表中的数据。
第三步:获取数据
last_spot_price = 0 # 保存最后一个有效的现货价格
last_basis_price = 0 # 保存最后一个有效的基差价格
def onTick():
global last_basis_price, last_spot_price # 导入全局变量
exchange.SetContractType("i888") # 订阅期货品种
futures = _C(exchange.GetRecords)[-1] # 获取最新K线数据
futures_ts = futures.Time # 获取最新K线期货时间戳
futures_price = futures.Close # 获取最新K线收盘价
spot = exchange.GetData("SPOTPRICE") # 获取现货数据
spot_ts = spot.Time # 获取现货时间戳
if '铁矿石' in spot.Data:
spot_price = spot.Data['铁矿石']
last_spot_price = spot_price
else:
spot_price = last_spot_price
basis = exchange.GetData("BASIS") # 获取基差数据
basis_ts = basis.Time # 获取基差时间戳
if '铁矿石' in basis.Data:
basis_price = basis.Data['铁矿石']
last_basis_price = basis_price
else:
basis_price = last_basis_price
我们一共需要获取三种数据:期货价格、现货价格、基差价格。获取期货价格很简单,直接使用 SetContractType 函数订阅期货品种,再使用 GetRecords 函数就可以获取 K 线的收盘价。现货和基差的价格,可以使用之前介绍的方法,使用 GetData 函数调用基本面数据代码,返回的是包含时间戳的字典数据。
套利的没有想象中那么复杂,它不需要太多的金融理论知识,也不需要太过复杂的数学或统计模型,套利本质上就是赚取价格从不合理到回归合理之间的利润而已。市场行情每年都在变化,对于交易者而言,最好不要把历史数据完全照搬到现在,而是结合当下数据研究基差是否合理。
'''backtest start: 2017-01-01 00:00:00 end: 2020-06-01 00:00:00 period: 1d basePeriod: 1d exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}] ''' # 全局变量 # 期现图表 cfgA = { "extension": { "layout": 'single', "col": 6, "height": "500px", }, "title": { "text": "期现图表" }, "xAxis": { "type": "datetime" }, "series": [{ "name": "期货价格", "data": [], }, { "name": "现货价格", "data": [], } ] } # 基差图表 cfgB = { "extension": { "layout": 'single', "col": 6, "height": "500px", }, "title": { "text": "基差图表" }, "xAxis": { "type": "datetime" }, "series": [{ "name": "基差价格", "data": [], }] } last_spot_price = 0 # 保存最后一个有效的现货价格 last_basis_price = 0 # 保存最后一个有效的基差价格 chart = Chart([cfgA, cfgB]) # 创建一个图表对象 def onTick(): global last_basis_price, last_spot_price # 导入全局变量 exchange.SetContractType("i888") # 订阅期货品种 futures = _C(exchange.GetRecords)[-1] # 获取最新K线数据 futures_ts = futures.Time # 获取最新K线期货时间戳 futures_price = futures.Close # 获取最新K线收盘价 Log('期货价格:', futures_ts, futures_price) spot = exchange.GetData("SPOTPRICE") # 获取现货数据 spot_ts = spot.Time # 获取现货时间戳 if '铁矿石' in spot.Data: spot_price = spot.Data['铁矿石'] last_spot_price = spot_price else: spot_price = last_spot_price Log('现货价格:', spot_ts, spot_price) basis = exchange.GetData("BASIS") # 获取基差数据 basis_ts = basis.Time # 获取基差时间戳 if '铁矿石' in basis.Data: basis_price = basis.Data['铁矿石'] last_basis_price = basis_price else: basis_price = last_basis_price Log('基差价格:', basis_ts, basis_price) chart.add(0, [futures_ts, futures_price]) # 绘制图表 chart.add(1, [spot_ts, spot_price]) # 绘制图表 chart.add(2, [basis_ts, basis_price]) # 绘制图表 chart.update([cfgA, cfgB]) # 更新图表 Log('---------') # 策略入口 def main(): LogReset() # 运行前先清空之前的Log日志信息 chart.reset() # 运行前先清空之前的图表信息 while True: # 进入循环模式 onTick() # 执行策略主函数 Sleep(1000 * 60 * 60 * 24) # 策略休眠一天template: strategy.tpl:40:21: executing "strategy.tpl" at <.api.GetStrategyListByName>: wrong number of args for GetStrategyListByName: want 7 got 6