币安期货最近发起了第二次“千团大战”活动(活动地址: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)
कोंगबाई979कृपया घास देवता से पूछें, क्या आप एक विचार को लागू करने के बारे में कुछ कह सकते हैं?
चुकिटीयदि पदों की संख्या व्यापार-मूल्य से कम है, तो कोई पट्टेबाजी नहीं होगी।
ककज़क्सयह एक बहुत बड़ा मुद्दा है, और पूंजी की दर भी एक बड़ी समस्या है
चुकिटीक्या सीधी दूसरी तिमाही के अनुबंध के समान प्रभाव हो सकता है जैसे कि उस तिमाही के अनुबंध के लिए ट्रांस्फर सूट?
उपदेशघास के देवता!
धातुघास के देवता!
ज़ीवेई1992यह बहुत ही कठोर है!
fmzeroघास के देवता!
झिंगफेंगज़घास के देवता!
ऊंचा उठाना कम फेंकनायह कोई फर्क नहीं पड़ता, वैसे भी एक हेजिंग है.
ऊंचा उठाना कम फेंकनामैं समझता हूँ कि वह एक पैर की समस्या नहीं है, N + P-2C, एक पैर सीधे 2C को N + P के हिसाब से कवर कर सकता है, व्यापार-मूल्य के हिसाब से नहीं
घासएक पैर की समस्या से निपटना मुश्किल है, इसे खुद से हल करने की जरूरत है
घासऔर निश्चित रूप से, सबसे आम विकल्पों में से एक है