币安交割合约策略3.ipynb下载
币安期货最近发起了第二次“千团大战”活动(活动地址:https://www.binancezh.com/cn/futures/activity/anniversary-competition/129-38599440 )。FMZ量化平台官方也组织了团队,直接搜索“发明者量化”就可以找到,目前刚刚有100多人,欢迎参与,参加后可加战队队长微信 fmz_zhangchao,回复“币安”拉微信群。
本次为参赛准备的策略为交割合约的蝶式对冲,本篇即为此策略的研究报告。注意策略只供参考,可以在此基础上提出自己的思路进行优化,也欢迎分享。 报告可直接在FMZ网站的研究环境直接使用(点击右上角下载,在研究环境中上传)。
对冲需要找到一个稳定的差价,当差价过高时做空差价,过低时做多差价,当差价回归平仓就赚到了其中的差价。如期现对冲,当未交割期货价格远高于现货时,可以做空期货合约,做多现货来做空差价。还有不同交割时间合约的跨期对冲,和期现对冲相比,还能做多差价。期现,跨期都是太常见的策略,竞争也很激烈,平时没有行情时,差价相对稳定,虽然可以做长期的大行情,但机会少,手动操作也可以。既然都是找稳定的差价,当一个标的物存在三个交易合约时,还有一个差价,即差价的差价,这就是蝶式对冲,也被称为套利的套利。
币安币本位合约如BTC、ETH等同时存在三个合约,即永续BTCUSD_PERP、当季BTCUSD_200925、次季BTCUSD_201225。永续合约可以当作现货,一般两个合约做对冲共有三个差价:当季-永续、次季-永续、次季-当季。蝶式套利需要操作三个合约,差价为(次季-当季)-(当季-永续),即差价=次季+永续-2*当季。做多差价需要开做多一份的次季和永续合约,做空2份的当季合约。
数据我已经爬取了8月14至9月14日币安的5minK线,可以直接读取(由于时差,显示的时间差8h)。
In [4]:
# 需要导入的库
import pandas as pd
import requests
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import time
%matplotlib inline
In [12]:
#读取数据,大家也可以把数据上传到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)
Out[12]:
BTCUSD_200925 | BTCUSD_201225 | BTCUSD_PERP | ETHUSD_200925 | ETHUSD_201225 | ETHUSD_PERP | ADAUSD_200925 | ADAUSD_201225 | LINKUSD_200925 | LINKUSD_PERP | BNBUSD_PERP | TRXUSD_PERP | DOTUSD_PERP | ADAUSD_PERP | LINKUSD_201225 | EOSUSD_PERP | LTCUSD_PERP | BCHUSD_PERP | XRPUSD_PERP | ETCUSD_PERP | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2020-09-14 02:20:00 | 10369.9 | 10509.8 | 10367.1 | 366.37 | 367.78 | 366.31 | 0.09493 | 0.09529 | 12.040 | 12.017 | 29.759 | 0.03024 | 5.308 | 0.09471 | 12.117 | 2.719 | 48.19 | 223.21 | 0.2433 | 5.054 |
2020-09-14 02:25:00 | 10366.4 | 10503.0 | 10360.4 | 366.80 | 367.89 | 366.33 | 0.09471 | 0.09529 | 12.075 | 12.048 | 29.507 | 0.03025 | 5.273 | 0.09453 | 12.141 | 2.719 | 48.11 | 223.25 | 0.2440 | 5.049 |
2020-09-14 02:30:00 | 10362.8 | 10498.6 | 10356.8 | 366.13 | 367.44 | 365.91 | 0.09462 | 0.09520 | 12.056 | 12.024 | 29.493 | 0.03024 | 5.280 | 0.09435 | 12.118 | 2.719 | 48.09 | 223.10 | 0.2435 | 5.055 |
首先看一下比特币合约之间的差价,8月17日比特币价格快速涨了500u,一般为交割的合约相对于现货处于升水状态,现货价格上涨,对未来的预期会更加乐观,未交割合约和永续之间的差价会变大,如次季-永续的差价达到700u,随着9月份比特币价格的下跌,人们的预期迅速变差,次季-永续的差价跌至150u附近,当季-永续几乎没有了差价,如果做次季-永续的对冲,只能做长周期大差价的回归,如果8月决定做400-600之间的差价,现在显然处于被套牢的状态。
In [18]:
#永续价格
df[\'BTCUSD_PERP\'].dropna().plot(figsize=(15,6),grid=True);
Out[18]:
In [15]:
# 次季-永续的差价
(df[\'BTCUSD_201225\']-df[\'BTCUSD_PERP\']).dropna().plot(figsize=(15,6),grid=True);
Out[15]:
In [16]:
# 当季-永续的差价
(df[\'BTCUSD_200925\']-df[\'BTCUSD_PERP\']).dropna().plot(figsize=(15,6),grid=True);
Out[16]:
In [17]:
# 次季-当季的差价
(df[\'BTCUSD_201225\']-df[\'BTCUSD_200925\']).dropna().plot(figsize=(15,6),grid=True);
Out[17]:
那么此时差价的差价是如何变动的呢? 下图可以看到,近期差价长期稳定在100-200u,即使9月初的大跌也没有影响很多,给了我们很多反复套利的空间,目前这个差价如果跌到100u,手动做多也是可以的。
当现货波动时,两个未到期合约同时反映了对未来的预期,差价减差价的过程可以很大程度抵消这种波动,表现的相对稳定。ETH的蝶式套利差价也有类似的表现。
In [19]:
#(次季-当季)-(当季-永续)
(df[\'BTCUSD_201225\']-df[\'BTCUSD_200925\']-(df[\'BTCUSD_200925\']-df[\'BTCUSD_PERP\'])).dropna().plot(figsize=(15,6),grid=True);
Out[19]:
In [22]:
#ETH的差价
(df[\'ETHUSD_201225\']+df[\'ETHUSD_PERP\']-2*df[\'ETHUSD_200925\']).dropna().plot(figsize=(15,6),grid=True);
Out[22]:
为了省事(偷懒),回测还是用上次千团大战策略的USDT本位引擎,虽然会有一些误差,但也能说明问题。回测引擎放在本篇报告的最后,运行代码时要先到后面运行一下。币本位策略如果想赚USDT的话可以考虑对冲,也不复杂。
差价的中线用EMA追踪,采用网格的方式来控制仓位,即差价每拉开N份预定的差价(如30),就做空N份,反之依然。如差价中线为100u,当差价为90时,做空3份,差价变为60,平一份。格子的大小是一个关键参数。
下面是具体的BTC和ETH的回测代码和回测结果,表现还算符合预期,由于ETH、LINK的波动更大,差价也更加稳定,表现的好一些。注意这里的手续费用的是万2,币安默认的vip0的taker手续费是万4,手续费非常重要,接下来的章节专门分析。
In [39]:
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);
Out[39]:
In [59]:
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);
Out[59]:
In [60]:
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);
Out[60]:
由于同时需要操作3个合约,开仓后平仓共需要8份的手续费,因此手续费对策略的影响很大,如果有万1的手续费,可以进一步减小差价网格间距,BTC的回测结果如下图:
如果是万3的手续费,BTC回测结果如下图:\”>
ETH的回测结果:\”>
新注册用户vip0基础的吃单费率为0.0004,被邀请首月减10%,返佣30%,消耗BNB减10%,这样最终手续费为0.0002268,最近币安交割合约交易额大的也有直接奖励。另外策略可以部分挂单,部分吃单,最终的综合费率可以降到万2。另外FMZ官方也在和币安讨论手续费优惠的问题,大家可以期待一下。
套利的目的是寻找稳定的差价,差价的差价更稳定,因此蝶式套利的风险要比跨期、期现少很多,也可以手动操作。本策略只是起到抛砖引玉的作用,真正写成策略实盘运行要考虑很多问题,欢迎大家交流。
In [23]:
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)
In [ ]:
买好币上币库:https://www.kucoin.com/r/1f7w3