Strategy Principle

The CoinSafe coin localization contracts such as BTC, ETH, etc. exist in three contracts at the same time, namely the perpetual BTCUSD_PERP, the current season BTCUSD_200925, the next season BTCUSD_201225.

Perpetual contract can be treated as spot, generally two contracts to do hedging a total of three spreads: the current season - perpetual, the next season - perpetual, the next season - the current season. Butterfly arbitrage need to operate 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 spread you need to open long one of the next quarter and perpetual contracts and short 2 of the current quarter contracts.

Strategy parameters

  • Currency of transaction: the need to exist at the same time the perpetual, the current quarter, the next quarter of the three varieties.
  • Number of orders: the number of orders per grid.
  • Grid opening spread: for each deviation from the spread, go long or short by one.
  • Spread smoothing parameter Alpha: used to calculate the average value of the spread, available by default, you can also backtest.
  • Iceberg commissioned by the number of sheets: If the number of open positions is too large, in order to reduce the phenomenon of single-leg, you can set the minimum number of sheets each time, the disadvantage is that the grab spreads are not timely. Do not need Iceberg entrusted this parameter settings and the same number of sheets.

If the average of the spread is 100, the current spread is 200, the number of orders is 2, the grid open single spread is 30, then the position at this time: the next quarter empty 6, perpetual empty 6, more than 12 in the current season.

Notes:

  • Delivery contracts need to be one-way position, can not hold both long and short.
  • Margin is in full position mode.
  • The margin is in full position mode. It is not a no-brainer to run the strategy, and it is necessary to understand the principle and test it carefully.
  • Backtesting is not the same as real trading, no need to over-optimize the parameters.
  • Grid opening spread to cover the handling fee, such as the handling fee of 12,000, bitcoin 10,000, it is greater than 16.
  • As we approach settlement, the time difference between the current quarter and the perpetual is widening, and the current quarter is close to the perpetual, which is actually an arbitrage between the next quarter and the perpetual, so we need to stop or observe. The same applies to new contracts.
  • With IOC orders, the part of the order that cannot be fully filled is canceled directly, without the need to manually cancel orders.
  • Can be simply modified to current quarter and perpetual or next quarter and perpetual arbitrage.
  • The strategy does not open and close positions with high frequency, and it is possible that there will be no orders for a day.
  • The average spread is calculated from the beginning of the run and does not go back in history.
  • Single-leg situation may exist, can be optimized.
  • Slippage has no effect on small positions, but large positions need to be optimized, such as iceberg commissions.

Historical data observation

Long spread = Long 1 next quarter + perpetual short 2 current quarter contracts.

Short spread = 1 point short next quarter + perpetual short 2 current quarter contracts

Historical data analysis

The source of profit for Disc Arbitrage is that the next quarter’s contract will fall ahead of the bull to bear panic, the spread widens dramatically, and after the panic is overreacted, the spread returns to its original trajectory.

After the panic is overreacted, the spreads are found to have overreacted and return to their original trajectory, and the spreads are drastically narrowed again, bringing in profits.

1
2
3
df['ETHUSD_PERP'].dropna().plot(figsize=(15,6),grid=True) 
df['ETHUSD_210625'].dropna().plot(figsize=(15,6),grid=True)
df['ETHUSD_210924'].dropna().plot(figsize=(15,6),grid=True)

Output:

Backtesting engine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

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)

The code for calculating the return on the traded variety (btc for example):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
trade_symbols = ['BTCUSD_210924', 'BTCUSD_210625', 'BTCUSD_PERP']
account = []
diff = df['BTCUSD_210924']+df['BTCUSD_PERP']-2*df['BTCUSD_210625']
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_210625',prices['BTCUSD_210625'],2*trade_amount)
e.Sell('BTCUSD_210924',prices['BTCUSD_210924'],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_210625',prices['BTCUSD_210625'],2*trade_amount)
e.Buy('BTCUSD_210924',prices['BTCUSD_210924'],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);

BTC Spreads

ETH Spreads

BTC Spreads

ETH earnings curve

LTC earnings curve

XRP earnings curve

xrp收益.png

BNB earnings curve

BCH earnings curve

ADA earnings curve

DOT earnings curve

Conclusion

With well-written code, this is a profitable strategy to test the nature of the engagement