[千团大战]币安交割合约策略3——蝶式对冲

Author: 小草, Created: 2020-09-14 15:24:43, Updated: 2023-09-27 19:35:50

img

币安期货最近发起了第二次“千团大战”活动(活动地址: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)


More

kongbai979 请问草神,部分挂单部分吃单,能不能说说具体实施思路呢?感觉价差稍瞬即逝,只能是吃单

xukitty 如果仓位数小于trade-value,不会触发平仓。请问草总这个问题该咋解决呢?

caqzx 永续的大多数时候是开多,资金费率也是个大问题

xukitty 直接次季合约与当季合约的跨期套利,能达到同等效果吗?手续费还低一倍

homily 草神威武!

匯金 草神威武!

zhiwei1992 是真的牛逼啊!!!

fmzero 草神威武!

jingfengz 草神威武!

高吸低抛 这个无所谓吧,反正都有对冲。不平就不平呗。等着下次加仓

高吸低抛 我理解,他说的不是单腿问题吧,N+P-2C,单腿可以直接把2C按N+P的量对冲,而不是按trade-value计算下单

小草 单腿问题比较难处理,需要自己想办法

小草 跨期当然也可以,最常用的套利之一