The resource loading... loading...

[Millennium War] Bitcoin exchange rate approximate strategy 3 butterfly hedging

Author: The grass, Created: 2020-09-14 15:24:43, Updated: 2024-12-10 10:11:51

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

kongbai979Please ask the grasshopper, can you tell me about the specific implementation of the idea, partly hanging partly eating alone?

xukittyIf the number of positions is less than the trade-value, no settlement is triggered.

caqzxMost of the time, it's too much, and the interest rate is a big problem.

xukittyCan a direct sub-quarterly contract have the same effect as a cross-quarterly contract?

homilyThe grass god is fighting!

PlatinumThe grass god is fighting!

zhiwei1992It's really tough!

fmzeroThe grass god is fighting!

jingfengzThe grass god is fighting!

High suction low throwThis doesn't matter, there is a hedge anyway.

High suction low throwI understand that he's not talking about a one-legged problem, N+P-2C, one-legged can directly hedge 2C against the quantity of N+P instead of trading-value.

The grassOne-leggedness is more difficult to deal with, and you need to find your own solution.

The grassAnd, of course, cross-currency is one of the most commonly used options.