Recently, Binance futures launched the second “Binance Championship” (address: https://www.binancezh.com/cn/futures/activity/anniversary-competition/129-38599440). The official FMZ Quant platform has also organized a team, which can be found by searching “FMZ Quant” directly. At present, there are just more than 100 people. Welcome to join in us. After that, you can add the WeChat of team leader: fmz_zhangchao, reply “Binance”, and we will invite you to join in the WeChat group.
The strategy prepared for the Binance Championship is butterfly hedging of delivery contract. This article is the research report of the strategy. Attention: strategies are for reference only. You can put forward your own ideas for optimization on this basis. You are also welcome to share. The report can be used in the research environment of the FMZ website directly (click the top right corner to download, and upload in the “Analyze”).
Hedging needs to find a stable price difference. When the price difference is too large, going short the price difference. When the price difference is too small, going long the price difference. When the price difference returns to close the position, you will earn the price difference. If the futures and spots are hedging, when the price of undelivered futures is much higher than the spot price, you can go short the futures contract and go long the spot price to short the price difference. There are also intertemporal hedges of contracts with different delivery times, compared with futures and spots hedges, they can also go long price differences. Futures and spots and cross-futures are common strategies with fierce competition. When there is no market, the price difference is relatively stable. Although it can be a long-term market, there are few opportunities, and manual operation is also possible. Since they are all looking for stable price differentials, when there are three trading contracts for a subject matter, there is also a price difference. This is butterfly hedging, also known as arbitrage for arbitrage.
The Binance currency standard contracts, such as BTC and ETH, have three contracts at the same time, namely, perpetual BTCUSD_ PERP, BTCUSD_200925 of the current quarter, BTCUSD_ 201225 of the next quarter. Perpetual contracts can be used as spots. Generally, there are three price differentials for hedging two contracts: current quarter-perpetual, next quarter-perpetual, and next quarter-current quarter. Butterfly arbitrage requires three contracts. The difference is (next quarter - current quarter) - (current quarter - perpetual), that is, the difference=next quarter+perpetual - 2 * current quarter. To go long the price difference, you need to open a long position contract for the next quarter and perpetual contracts, and go short two contracts for the current quarter.
I have crawled the data of 5min K-line of Binance from August 14 to September 14, which can be read directly (due to time difference, the time difference shown is 8h).
In [4]:
# Libraries to be imported
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]:
#Read the data, you can also upload the data to the FMZ forum, which can be referenced in the "Analyze" directly
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]:
First, let’s take a look at the price difference between Bitcoin contracts. On August 17, the price of Bitcoin rose rapidly by 500u. Generally speaking, the contract delivered was at a premium compared with the spot price, and the spot price rose. The expectation for the future will be more optimistic. The price difference between the undelivered contract and the perpetuity will become larger. For example, the price difference between the next quarter and the perpetuity will be 700u. With the decrease of the price of Bitcoin in September, people’s expectations will deteriorate quickly, the price difference between the next quarter and perpetuity fell to around 150u, and there was almost no price difference between the current quarter and perpetuity. If the hedging between the next quarter and perpetuity was carried out, only the return of long term large price difference could be carried out. If the price difference between 400-600 was decided to be carried out in August, it is obviously in a locked state now.
In [18]:
#Perpetual price
df['BTCUSD_PERP'].dropna().plot(figsize=(15,6),grid=True);
Out[18]:
In [15]:
# Price difference of next quarter - perpetual
(df['BTCUSD_201225']-df['BTCUSD_PERP']).dropna().plot(figsize=(15,6),grid=True);
Out[15]:
In [16]:
# Price difference of current quarter - perpetual
(df['BTCUSD_200925']-df['BTCUSD_PERP']).dropna().plot(figsize=(15,6),grid=True);
Out[16]:
In [17]:
# Price difference of next quarter - current quarter
(df['BTCUSD_201225']-df['BTCUSD_200925']).dropna().plot(figsize=(15,6),grid=True);
Out[17]:
So how does the price difference change at this time? As it can be seen from the figure below, the recent price difference has been stable at 100-200u for a long time. Even the sharp drop at the beginning of September did not affect much, giving us much space for repeated arbitrage. At present, if the price difference falls to 100u, it is OK to go long manually.
When the spot price fluctuates, the two unexpired contracts reflect the expectation of the future at the same time. The process of price difference reduction can offset this fluctuation to a large extent, and the performance is relatively stable. The butterfly arbitrage spread of ETH has a similar performance.
In [19]:
#(next quarter - current quarter)-(current quarter - perpetual)
(df['BTCUSD_201225']-df['BTCUSD_200925']-(df['BTCUSD_200925']-df['BTCUSD_PERP'])).dropna().plot(figsize=(15,6),grid=True);
Out[19]:
In [22]:
#The price difference of ETH
(df['ETHUSD_201225']+df['ETHUSD_PERP']-2*df['ETHUSD_200925']).dropna().plot(figsize=(15,6),grid=True);
Out[22]:
In order to save time (just laziness), the backtest still uses the USDT standard engine of the last Binance Championship strategy. Although there may be some errors, it can also explain the problem. The backtesting engine is placed at the end of this report. When running the code, you should see the end of the article. The currency standard strategy can consider hedging if you want to earn USDT, and it is not complicated.
The midline of the price difference is tracked by EMA, and the position is controlled by grid, that is, every time the difference is opened (such as 30), go short N shares, and vice versa. If the midline of the price difference is 100u, when the price difference is 90, go short 3 shares, and the price difference becomes 60, close one share. The size of the grid is a key parameter.
The following are the specific BTC and ETH backtesting codes and results. The performance is in line with expectations. Because ETH and LINK have greater volatility and the price difference is more stable, the performance is better. Note that the service charge here is 0.02%, and the default vip0 taker service charge in Binance is 0.04%. The service charge is very important, and the following chapters will analyze it.
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]:
Since 3 contracts need to be operated at the same time, 8 service charges are required to close the position after opening, so the service charges have a great impact on the strategy. If there is a service charge of 0.01%, the gap between price difference grids can be further reduced. The results of BTC’s backtesting are shown in the following figure:
If the comission is 0.03%, the BTC backtest results are as follows:
The backtest results of ETH:
The taker rate of vip0 for new registered users is 0.0004, 10% will be reduced in the first month of being invited, 30% will be returned, and 10% will be reduced for BNB consumption. Thus, the final handling fee is 0.0002268. There will also be a direct reward for the recent large transaction amount of the Binance delivery contract. In addition, part of the bill can be placed and part of the bill can be taken, and the final comprehensive rate can be reduced to 0.02%. In addition, the FMZ official is also discussing the issue of service charge discount with Binance. You can look forward to it.
The purpose of arbitrage is to find a stable price difference. The price difference of the price difference is more stable. Therefore, butterfly arbitrage is much less risky than cross period and future-spot, and it can also be operated manually. This strategy only serves as an introduction. Many issues need to be considered when writing in the real bot. Welcome you all to communicate.
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 #Initial assets
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 #Deduct service charge
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 #Deduct service charge
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: #Close the position first
self.account['USDT']['realised_profit'] += -direction*(price - self.account[symbol]['hold_price'])*cover_amount #Profit
self.account['USDT']['margin'] -= cover_amount*self.account[symbol]['hold_price']/self.leverage #Release margin
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): #Update the assets
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 [ ]: