币安期货最近发起了第二次“千团大战”活动(活动地址:https://www.binancezh.com/cn/futures/activity/anniversary-competition/129-38599440 )。FMZ量化平台官方也组织了团队,直接搜索“发明者量化”就可以找到,目前刚刚有100多人,欢迎参与,参加后可加战队队长微信 fmz_zhangchao,回复“币安”拉微信群。
本次为参赛准备的策略为交割合约的蝶式对冲,本篇即为此策略的研究报告。注意策略只供参考,可以在此基础上提出自己的思路进行优化,也欢迎分享。 报告可直接在FMZ网站的研究环境直接使用(点击右上角下载,在研究环境中上传)。 <img src=“https://www.fmz.com/upload/asset/1b39347a88aa4cff916.jpg”>
1.策略缘由
对冲需要找到一个稳定的差价,当差价过高时做空差价,过低时做多差价,当差价回归平仓就赚到了其中的差价。如期现对冲,当未交割期货价格远高于现货时,可以做空期货合约,做多现货来做空差价。还有不同交割时间合约的跨期对冲,和期现对冲相比,还能做多差价。期现,跨期都是太常见的策略,竞争也很激烈,平时没有行情时,差价相对稳定,虽然可以做长期的大行情,但机会少,手动操作也可以。既然都是找稳定的差价,当一个标的物存在三个交易合约时,还有一个差价,即差价的差价,这就是蝶式对冲,也被称为套利的套利。
2.策略原理
币安币本位合约如BTC、ETH等同时存在三个合约,即永续BTCUSD_PERP、当季BTCUSD_200925、次季BTCUSD_201225。永续合约可以当作现货,一般两个合约做对冲共有三个差价:当季-永续、次季-永续、次季-当季。蝶式套利需要操作三个合约,差价为(次季-当季)-(当季-永续),即差价=次季+永续-2*当季。做多差价需要开做多一份的次季和永续合约,做空2份的当季合约。
3.对冲空间
数据我已经爬取了8月14至9月14日币安的5minK线,可以直接读取(由于时差,显示的时间差8h)。
# 需要导入的库 import pandas as pd import requests import matplotlib.pyplot as plt import seaborn as sns import numpy as np import time %matplotlib inline
#读取数据,大家也可以把数据上传到FMZ论坛,在研究环境中可以直接引用 df = pd.read_csv('https://www.fmz.com/upload/asset/1420b2081ecd122522d.csv',index_col = 0) df.index = pd.to_datetime(df.index) df.tail(3)
BTCUSD_200925 BTCUSD_201225 BTCUSD_PERP ETHUSD_200925 \ 2020-09-14 02:20:00 10369.9 10509.8 10367.1 366.37 2020-09-14 02:25:00 10366.4 10503.0 10360.4 366.80 2020-09-14 02:30:00 10362.8 10498.6 10356.8 366.13 ETHUSD_201225 ETHUSD_PERP ADAUSD_200925 ADAUSD_201225 \ 2020-09-14 02:20:00 367.78 366.31 0.09493 0.09529 2020-09-14 02:25:00 367.89 366.33 0.09471 0.09529 2020-09-14 02:30:00 367.44 365.91 0.09462 0.09520 LINKUSD_200925 LINKUSD_PERP BNBUSD_PERP TRXUSD_PERP \ 2020-09-14 02:20:00 12.040 12.017 29.759 0.03024 2020-09-14 02:25:00 12.075 12.048 29.507 0.03025 2020-09-14 02:30:00 12.056 12.024 29.493 0.03024 DOTUSD_PERP ADAUSD_PERP LINKUSD_201225 EOSUSD_PERP \ 2020-09-14 02:20:00 5.308 0.09471 12.117 2.719 2020-09-14 02:25:00 5.273 0.09453 12.141 2.719 2020-09-14 02:30:00 5.280 0.09435 12.118 2.719 LTCUSD_PERP BCHUSD_PERP XRPUSD_PERP ETCUSD_PERP 2020-09-14 02:20:00 48.19 223.21 0.2433 5.054 2020-09-14 02:25:00 48.11 223.25 0.2440 5.049 2020-09-14 02:30:00 48.09 223.10 0.2435 5.055
首先看一下比特币合约之间的差价,8月17日比特币价格快速涨了500u,一般为交割的合约相对于现货处于升水状态,现货价格上涨,对未来的预期会更加乐观,未交割合约和永续之间的差价会变大,如次季-永续的差价达到700u,随着9月份比特币价格的下跌,人们的预期迅速变差,次季-永续的差价跌至150u附近,当季-永续几乎没有了差价,如果做次季-永续的对冲,只能做长周期大差价的回归,如果8月决定做400-600之间的差价,现在显然处于被套牢的状态。
#永续价格 df['BTCUSD_PERP'].dropna().plot(figsize=(15,6),grid=True);
<Figure size 1080x432 with 1 Axes>
# 次季-永续的差价 (df['BTCUSD_201225']-df['BTCUSD_PERP']).dropna().plot(figsize=(15,6),grid=True);
<Figure size 1080x432 with 1 Axes>
# 当季-永续的差价 (df['BTCUSD_200925']-df['BTCUSD_PERP']).dropna().plot(figsize=(15,6),grid=True);
<Figure size 1080x432 with 1 Axes>
# 次季-当季的差价 (df['BTCUSD_201225']-df['BTCUSD_200925']).dropna().plot(figsize=(15,6),grid=True);
<Figure size 1080x432 with 1 Axes>
那么此时差价的差价是如何变动的呢? 下图可以看到,近期差价长期稳定在100-200u,即使9月初的大跌也没有影响很多,给了我们很多反复套利的空间,目前这个差价如果跌到100u,手动做多也是可以的。
当现货波动时,两个未到期合约同时反映了对未来的预期,差价减差价的过程可以很大程度抵消这种波动,表现的相对稳定。ETH的蝶式套利差价也有类似的表现。
#(次季-当季)-(当季-永续) (df['BTCUSD_201225']-df['BTCUSD_200925']-(df['BTCUSD_200925']-df['BTCUSD_PERP'])).dropna().plot(figsize=(15,6),grid=True);
<Figure size 1224x432 with 1 Axes>
#ETH的差价 (df['ETHUSD_201225']+df['ETHUSD_PERP']-2*df['ETHUSD_200925']).dropna().plot(figsize=(15,6),grid=True);
<Figure size 1080x432 with 1 Axes>
4.策略回测
为了省事(偷懒),回测还是用上次千团大战策略的USDT本位引擎,虽然会有一些误差,但也能说明问题。回测引擎放在本篇报告的最后,运行代码时要先到后面运行一下。币本位策略如果想赚USDT的话可以考虑对冲,也不复杂。
差价的中线用EMA追踪,采用网格的方式来控制仓位,即差价每拉开N份预定的差价(如30),就做空N份,反之依然。如差价中线为100u,当差价为90时,做空3份,差价变为60,平一份。格子的大小是一个关键参数。
下面是具体的BTC和ETH的回测代码和回测结果,表现还算符合预期,由于ETH、LINK的波动更大,差价也更加稳定,表现的好一些。注意这里的手续费用的是万2,币安默认的vip0的taker手续费是万4,手续费非常重要,接下来的章节专门分析。
trade_symbols = ['BTCUSD_201225', 'BTCUSD_200925', 'BTCUSD_PERP'] account = [] diff = df['BTCUSD_201225']+df['BTCUSD_PERP']-2*df['BTCUSD_200925'] diff_mean = diff.ewm(alpha=0.001).mean() e = Exchange(trade_symbols,initial_balance=10000,taker_fee=0.0002) for row in df[trade_symbols].dropna().iterrows(): date = row[0] prices = row[1] e.Update(date, trade_symbols, prices) account.append([e.account['USDT']['margin'],e.account['USDT']['realised_profit']+e.account['USDT']['unrealised_profit']]) aim_amount = -round((diff[date] - diff_mean[date])/30,1) now_amount = e.account['BTCUSD_PERP']['amount'] if aim_amount - now_amount < -1: trade_amount = now_amount - aim_amount e.Buy('BTCUSD_200925',prices['BTCUSD_200925'],2*trade_amount) e.Sell('BTCUSD_201225',prices['BTCUSD_201225'],trade_amount) e.Sell('BTCUSD_PERP',prices['BTCUSD_PERP'],trade_amount) if aim_amount - now_amount > 1: trade_amount = aim_amount - now_amount e.Sell('BTCUSD_200925',prices['BTCUSD_200925'],2*trade_amount) e.Buy('BTCUSD_201225',prices['BTCUSD_201225'],trade_amount) e.Buy('BTCUSD_PERP',prices['BTCUSD_PERP'],trade_amount) e.df = pd.DataFrame(index=df[trade_symbols].dropna().index,columns=['margin','profit'],data=account) e.df['profit'].plot(figsize=(15,6),grid=True);
<Figure size 1080x432 with 1 Axes>
symbol = 'ETH' trade_symbols = [symbol+'USD_201225', symbol+'USD_200925', symbol+'USD_PERP'] fee = 0.0002 account = [] diff = df[trade_symbols[0]]+df[trade_symbols[2]]-2*df[trade_symbols[1]] diff_mean = diff.ewm(alpha=0.001).mean() e = Exchange(trade_symbols,initial_balance=10000,taker_fee=fee) for row in df[trade_symbols].dropna().iloc[30:].iterrows(): date = row[0] prices = row[1] e.Update(date, trade_symbols, prices) account.append([e.account['USDT']['margin'],e.account['USDT']['realised_profit']+e.account['USDT']['unrealised_profit']]) aim_amount = -round((diff[date] - diff_mean[date])/(15*prices[trade_symbols[2]]*fee),1) now_amount = e.account[trade_symbols[2]]['amount'] if aim_amount - now_amount < -1: trade_amount = 1 e.Buy(trade_symbols[1],prices[trade_symbols[1]],2*trade_amount) e.Sell(trade_symbols[0],prices[trade_symbols[0]],trade_amount) e.Sell(trade_symbols[2],prices[trade_symbols[2]],trade_amount) if aim_amount - now_amount > 1: trade_amount = 1 e.Sell(trade_symbols[1],prices[trade_symbols[1]],2*trade_amount) e.Buy(trade_symbols[0],prices[trade_symbols[0]],trade_amount) e.Buy(trade_symbols[2],prices[trade_symbols[2]],trade_amount) e.df = pd.DataFrame(index=df[trade_symbols].dropna().iloc[30:].index,columns=['margin','profit'],data=account) e.df['profit'].plot(figsize=(15,6),grid=True);
<Figure size 1080x432 with 1 Axes>
symbol = 'LINK' trade_symbols = [symbol+'USD_201225', symbol+'USD_200925', symbol+'USD_PERP'] fee = 0.0002 account = [] diff = df[trade_symbols[0]]+df[trade_symbols[2]]-2*df[trade_symbols[1]] diff_mean = diff.ewm(alpha=0.001).mean() e = Exchange(trade_symbols,initial_balance=10000,taker_fee=fee) for row in df[trade_symbols].dropna().iloc[30:].iterrows(): date = row[0] prices = row[1] e.Update(date, trade_symbols, prices) account.append([e.account['USDT']['margin'],e.account['USDT']['realised_profit']+e.account['USDT']['unrealised_profit']]) aim_amount = -round((diff[date] - diff_mean[date])/(15*prices[trade_symbols[2]]*fee),1) now_amount = e.account[trade_symbols[2]]['amount'] if aim_amount - now_amount < -1: trade_amount = 1 e.Buy(trade_symbols[1],prices[trade_symbols[1]],2*trade_amount) e.Sell(trade_symbols[0],prices[trade_symbols[0]],trade_amount) e.Sell(trade_symbols[2],prices[trade_symbols[2]],trade_amount) if aim_amount - now_amount > 1: trade_amount = 1 e.Sell(trade_symbols[1],prices[trade_symbols[1]],2*trade_amount) e.Buy(trade_symbols[0],prices[trade_symbols[0]],trade_amount) e.Buy(trade_symbols[2],prices[trade_symbols[2]],trade_amount) e.df = pd.DataFrame(index=df[trade_symbols].dropna().iloc[30:].index,columns=['margin','profit'],data=account) e.df['profit'].plot(figsize=(15,6),grid=True);
<Figure size 1080x432 with 1 Axes>
5.手续费敏感性
由于同时需要操作3个合约,开仓后平仓共需要8份的手续费,因此手续费对策略的影响很大,如果有万1的手续费,可以进一步减小差价网格间距,BTC的回测结果如下图: <img src=“https://www.fmz.com/upload/asset/1d169ff6cb8e9c8165e.png”> 如果是万3的手续费,BTC回测结果如下图: <img src=“https://www.fmz.com/upload/asset/20c6b6d8de91f682f97.png”> ETH的回测结果: <img src=“https://www.fmz.com/upload/asset/208a70b018da8e37e57.png”>
新注册用户vip0基础的吃单费率为0.0004,被邀请首月减10%,返佣30%,消耗BNB减10%,这样最终手续费为0.0002268,最近币安交割合约交易额大的也有直接奖励。另外策略可以部分挂单,部分吃单,最终的综合费率可以降到万2。另外FMZ官方也在和币安讨论手续费优惠的问题,大家可以期待一下。
总结
套利的目的是寻找稳定的差价,差价的差价更稳定,因此蝶式套利的风险要比跨期、期现少很多,也可以手动操作。本策略只是起到抛砖引玉的作用,真正写成策略实盘运行要考虑很多问题,欢迎大家交流。
class Exchange: def __init__(self, trade_symbols, leverage=20, maker_fee=0.0002,taker_fee=0.0004,log='',initial_balance=10000): self.initial_balance = initial_balance #初始的资产 self.taker_fee = taker_fee self.maker_fee = maker_fee self.leverage = leverage self.trade_symbols = trade_symbols self.date = '' self.log = log self.df = pd.DataFrame() self.account = {'USDT':{'realised_profit':0, 'margin':0, 'unrealised_profit':0, 'total':initial_balance, 'leverage':0, 'fee':0,'maker_fee':0,'taker_fee':0}} for symbol in trade_symbols: self.account[symbol] = {'amount':0, 'hold_price':0, 'value':0, 'price':0, 'realised_profit':0, 'margin':0, 'unrealised_profit':0,'fee':0} def Trade(self, symbol, direction, price, amount, msg='', maker=True): if (self.date and symbol == self.log) or self.log == 'all': print('%-26s%-15s%-5s%-10.8s%-8.6s %s'%(str(self.date)[:24], symbol, 'buy' if direction == 1 else 'sell', price, amount, msg)) cover_amount = 0 if direction*self.account[symbol]['amount'] >=0 else min(abs(self.account[symbol]['amount']), amount) open_amount = amount - cover_amount if maker: self.account['USDT']['realised_profit'] -= price*amount*self.maker_fee #扣除手续费 self.account['USDT']['maker_fee'] += price*amount*self.maker_fee self.account['USDT']['fee'] += price*amount*self.maker_fee self.account[symbol]['fee'] += price*amount*self.maker_fee else: self.account['USDT']['realised_profit'] -= price*amount*self.taker_fee #扣除手续费 self.account['USDT']['taker_fee'] += price*amount*self.taker_fee self.account['USDT']['fee'] += price*amount*self.taker_fee self.account[symbol]['fee'] += price*amount*self.taker_fee if cover_amount > 0: #先平仓 self.account['USDT']['realised_profit'] += -direction*(price - self.account[symbol]['hold_price'])*cover_amount #利润 self.account['USDT']['margin'] -= cover_amount*self.account[symbol]['hold_price']/self.leverage #释放保证金 self.account[symbol]['realised_profit'] += -direction*(price - self.account[symbol]['hold_price'])*cover_amount self.account[symbol]['amount'] -= -direction*cover_amount self.account[symbol]['margin'] -= cover_amount*self.account[symbol]['hold_price']/self.leverage self.account[symbol]['hold_price'] = 0 if self.account[symbol]['amount'] == 0 else self.account[symbol]['hold_price'] if open_amount > 0: total_cost = self.account[symbol]['hold_price']*direction*self.account[symbol]['amount'] + price*open_amount total_amount = direction*self.account[symbol]['amount']+open_amount self.account['USDT']['margin'] += open_amount*price/self.leverage self.account[symbol]['hold_price'] = total_cost/total_amount self.account[symbol]['amount'] += direction*open_amount self.account[symbol]['margin'] += open_amount*price/self.leverage self.account[symbol]['unrealised_profit'] = (price - self.account[symbol]['hold_price'])*self.account[symbol]['amount'] self.account[symbol]['price'] = price self.account[symbol]['value'] = abs(self.account[symbol]['amount'])*price def Buy(self, symbol, price, amount, msg='', maker=False): self.Trade(symbol, 1, price, amount, msg, maker) def Sell(self, symbol, price, amount, msg='', maker=False): self.Trade(symbol, -1, price, amount, msg,maker) def Update(self, date, symbols, close_price): #对资产进行更新 self.date = date self.close = close_price self.account['USDT']['unrealised_profit'] = 0 for symbol in symbols: self.account[symbol]['unrealised_profit'] = (close_price[symbol] - self.account[symbol]['hold_price'])*self.account[symbol]['amount'] self.account[symbol]['price'] = close_price[symbol] self.account[symbol]['value'] = abs(self.account[symbol]['amount'])*close_price[symbol] self.account['USDT']['unrealised_profit'] += self.account[symbol]['unrealised_profit'] self.account['USDT']['total'] = round(self.account['USDT']['realised_profit'] + self.initial_balance + self.account['USDT']['unrealised_profit'],6) self.account['USDT']['leverage'] = round(self.account['USDT']['margin']*self.leverage/self.account['USDT']['total'],4)
kongbai979 请问草神,部分挂单部分吃单,能不能说说具体实施思路呢?感觉价差稍瞬即逝,只能是吃单
xukitty 如果仓位数小于trade-value,不会触发平仓。请问草总这个问题该咋解决呢?
caqzx 永续的大多数时候是开多,资金费率也是个大问题
xukitty 直接次季合约与当季合约的跨期套利,能达到同等效果吗?手续费还低一倍
homily 草神威武!
匯金 草神威武!
zhiwei1992 是真的牛逼啊!!!
fmzero 草神威武!
jingfengz 草神威武!
高吸低抛 这个无所谓吧,反正都有对冲。不平就不平呗。等着下次加仓
高吸低抛 我理解,他说的不是单腿问题吧,N+P-2C,单腿可以直接把2C按N+P的量对冲,而不是按trade-value计算下单
小草 单腿问题比较难处理,需要自己想办法
小草 跨期当然也可以,最常用的套利之一