The original research report address: https://www.fmz.com/digest-topic/5584 You can read it first, this article won't have duplicate content. This article will highlights the optimization process of the second strategy. After the optimization, the second strategy is improved obviously, it is recommended to upgrade the strategy according to this article. The backtest engine added the statistics of handling fee.
# Libraries to import import pandas as pd import requests import matplotlib.pyplot as plt import seaborn as sns import numpy as np %matplotlib inline
symbols = ['ETH', 'BCH', 'XRP', 'EOS', 'LTC', 'TRX', 'ETC', 'LINK', 'XLM', 'ADA', 'XMR', 'DASH', 'ZEC', 'XTZ', 'BNB', 'ATOM', 'ONT', 'IOTA', 'BAT', 'VET', 'NEO', 'QTUM', 'IOST']
price_usdt = pd.read_csv('https://www.fmz.com/upload/asset/20227de6c1d10cb9dd1.csv ', index_col = 0) price_usdt.index = pd.to_datetime(price_usdt.index)
price_usdt_norm = price_usdt/price_usdt.fillna(method='bfill').iloc[0,]
price_usdt_btc = price_usdt.divide(price_usdt['BTC'],axis=0) price_usdt_btc_norm = price_usdt_btc/price_usdt_btc.fillna(method='bfill').iloc[0,]
class Exchange: def __init__(self, trade_symbols, leverage=20, commission=0.00005, initial_balance=10000, log=False): self.initial_balance = initial_balance # Initial asset self.commission = commission self.leverage = leverage self.trade_symbols = trade_symbols self.date = '' self.log = log self.df = pd.DataFrame(columns=['margin','total','leverage','realised_profit','unrealised_profit']) self.account = {'USDT':{'realised_profit':0, 'margin':0, 'unrealised_profit':0, 'total':initial_balance, 'leverage':0, '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=''): if self.date and self.log: print('%-20s%-5s%-5s%-10.8s%-8.6s %s'%(str(self.date), 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 self.account['USDT']['realised_profit'] -= price*amount*self.commission # Minus handling fee self.account['USDT']['fee'] += price*amount*self.commission self.account[symbol]['fee'] += price*amount*self.commission if cover_amount > 0: # close 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 # Free 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 return True def Buy(self, symbol, price, amount, msg=''): self.Trade(symbol, 1, price, amount, msg) def Sell(self, symbol, price, amount, msg=''): self.Trade(symbol, -1, price, amount, msg) def Update(self, date, close_price): # Update assets self.date = date self.close = close_price self.account['USDT']['unrealised_profit'] = 0 for symbol in self.trade_symbols: if np.isnan(close_price[symbol]): continue 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'] if self.date.hour in [0,8,16]: pass self.account['USDT']['realised_profit'] += -self.account[symbol]['amount']*close_price[symbol]*0.01/100 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.account['USDT']['total'],4)*self.leverage self.df.loc[self.date] = [self.account['USDT']['margin'],self.account['USDT']['total'],self.account['USDT']['leverage'],self.account['USDT']['realised_profit'],self.account['USDT']['unrealised_profit']]
The performance of the original strategy, after the currency type selection, performed well, but there are still many holding positions, generally around 4 times
Principle:
- Update the market quotes and account holding positions, the initial price will be recorded in the first run (newly added currencies are calculated according to the time of joining)
- Update the index, the index is the altcoin-bitcoin price index = mean (sum ((altcoin price / bitcoin price) / (altcoin initial price / bitcoin initial price)))
- Judging long and short operation according to the deviation index, and judging the position size according to the deviation size
- Placing orders, the order quantity is determined by the iceberg commission strategy, and the transaction is executed according newest executable price
- Loop again
trade_symbols = list(set(symbols)-set(['LINK','XTZ','BCH', 'ETH'])) # Remaining currencies price_usdt_btc_norm_mean = price_usdt_btc_norm[trade_symbols].mean(axis=1) e = Exchange(trade_symbols,initial_balance=10000,commission=0.0005,log=False) trade_value = 300 for row in price_usdt.iloc[:].iterrows(): e.Update(row[0], row[1]) empty_value = 0 for symbol in trade_symbols: price = row[1][symbol] if np.isnan(price): continue diff = price_usdt_btc_norm.loc[row[0],symbol] - price_usdt_btc_norm_mean[row[0]] aim_value = -trade_value*round(diff/0.01,1) now_value = e.account[symbol]['value']*np.sign(e.account[symbol]['amount']) empty_value += now_value if aim_value - now_value > 20: e.Buy(symbol, price, round((aim_value - now_value)/price, 6),round(e.account[symbol]['realised_profit']+e.account[symbol]['unrealised_profit'],2)) if aim_value - now_value < -20: e.Sell(symbol, price, -round((aim_value - now_value)/price, 6),round(e.account[symbol]['realised_profit']+e.account[symbol]['unrealised_profit'],2)) stragey_2b = e (stragey_2b.df['total']/stragey_2b.initial_balance).plot(figsize=(17,6),grid = True);
stragey_2b.df['leverage'].plot(figsize=(18,6),grid = True); # leverage
pd.DataFrame(e.account).T.apply(lambda x:round(x,3)) # holding position
Why improve
The original biggest problem is the comparison between the latest price and the initial price started by the strategy. As the time passes, it will become more and more deviated. We will accumulate a lot of positions in these currencies. The biggest problem with filtering currencies is that we may still have unique currencies in the future based on our past experience. The following is the performance of non-filtering mode. In fact, when trade_value = 300, in the middle stage of the strategy running, it has already lost everything. Even if it is not, LINK and XTZ also hold positions above 10000USDT, which is too large. Therefore, we must solve this problem in the backtest and pass the test of all currencies.
trade_symbols = list(set(symbols)) # Remaining currencies price_usdt_btc_norm_mean = price_usdt_btc_norm[trade_symbols].mean(axis=1) e = Exchange(trade_symbols,initial_balance=10000,commission=0.0005,log=False) trade_value = 300 for row in price_usdt.iloc[:].iterrows(): e.Update(row[0], row[1]) empty_value = 0 for symbol in trade_symbols: price = row[1][symbol] if np.isnan(price): continue diff = price_usdt_btc_norm.loc[row[0],symbol] - price_usdt_btc_norm_mean[row[0]] aim_value = -trade_value*round(diff/0.01,1) now_value = e.account[symbol]['value']*np.sign(e.account[symbol]['amount']) empty_value += now_value if aim_value - now_value > 20: e.Buy(symbol, price, round((aim_value - now_value)/price, 6),round(e.account[symbol]['realised_profit']+e.account[symbol]['unrealised_profit'],2)) if aim_value - now_value < -20: e.Sell(symbol, price, -round((aim_value - now_value)/price, 6),round(e.account[symbol]['realised_profit']+e.account[symbol]['unrealised_profit'],2)) stragey_2c = e (stragey_2c.df['total']/stragey_2c.initial_balance).plot(figsize=(17,6),grid = True);
pd.DataFrame(stragey_2c.account).T.apply(lambda x:round(x,3)) # Last holding position
((price_usdt_btc_norm.iloc[-1:] - price_usdt_btc_norm_mean[-1]).T) # Each currency deviates from the initial situation
Since the cause of the problem is to compare with the initial price, it may be more and more biased. We can compare it with the moving average of the past period of time, backtest the full currency and see the results below.
Alpha = 0.05 #price_usdt_btc_norm2 = price_usdt_btc/price_usdt_btc.rolling(20).mean() #Ordinary moving average price_usdt_btc_norm2 = price_usdt_btc/price_usdt_btc.ewm(alpha=Alpha).mean() # Here is consistent with the strategy, using EMA trade_symbols = list(set(symbols))#All currencies price_usdt_btc_norm_mean = price_usdt_btc_norm2[trade_symbols].mean(axis=1) e = Exchange(trade_symbols,initial_balance=10000,commission=0.0005,log=False) trade_value = 300 for row in price_usdt.iloc[:].iterrows(): e.Update(row[0], row[1]) empty_value = 0 for symbol in trade_symbols: price = row[1][symbol] if np.isnan(price): continue diff = price_usdt_btc_norm2.loc[row[0],symbol] - price_usdt_btc_norm_mean[row[0]] aim_value = -trade_value*round(diff/0.01,1) now_value = e.account[symbol]['value']*np.sign(e.account[symbol]['amount']) empty_value += now_value if aim_value - now_value > 20: e.Buy(symbol, price, round((aim_value - now_value)/price, 6),round(e.account[symbol]['realised_profit']+e.account[symbol]['unrealised_profit'],2)) if aim_value - now_value < -20: e.Sell(symbol, price, -round((aim_value - now_value)/price, 6),round(e.account[symbol]['realised_profit']+e.account[symbol]['unrealised_profit'],2)) stragey_2d = e #print(N,stragey_2d.df['total'][-1],pd.DataFrame(stragey_2d.account).T.apply(lambda x:round(x,3))['value'].sum())
The performance of the strategy has fully met our expectations, and the returns are almost the same. The situation of bursting account positions in the original currency of the entire currencies has also smoothly transitioned, and there is almost no retracement. The same opening position size, almost all leverage is below 1 times, on 12th March 2020 price plunged extreme case, it still does not exceed 4 times, which means that we can increase trade_value, and under the same leverage, double the profit. The final holding position is only BCH exceeding 1000USDT, which is very good.
Why would the position be lowered? Imagine joining the altcoin index unchanged, one coin has increased by 100%, and it will be maintained for a long time. The original strategy will hold short positions of 300 * 100 = 30000USDT for a long time, and the new strategy will eventually track the benchmark price At the latest price, you will not hold any position at the end.
(stragey_2d.df['total']/stragey_2d.initial_balance).plot(figsize=(17,6),grid = True); #(stragey_2c.df['total']/stragey_2c.initial_balance).plot(figsize=(17,6),grid = True);
stragey_2d.df['leverage'].plot(figsize=(18,6),grid = True); stragey_2b.df['leverage'].plot(figsize=(18,6),grid = True); # Screen currency strategy leverage
pd.DataFrame(stragey_2d.account).T.apply(lambda x:round(x,3))
What will happen to the currency with the screening mechanism, with the same parameters, the earlier stage profits performs better, the retracement is smaller, but the overall returns are slightly lower. Therefore, it is recommended to have a screening mechanism.
#price_usdt_btc_norm2 = price_usdt_btc/price_usdt_btc.rolling(50).mean() price_usdt_btc_norm2 = price_usdt_btc/price_usdt_btc.ewm(alpha=0.05).mean() trade_symbols = list(set(symbols)-set(['LINK','XTZ','BCH', 'ETH'])) # Remaining currencies price_usdt_btc_norm_mean = price_usdt_btc_norm2[trade_symbols].mean(axis=1) e = Exchange(trade_symbols,initial_balance=10000,commission=0.0005,log=False) trade_value = 300 for row in price_usdt.iloc[:].iterrows(): e.Update(row[0], row[1]) empty_value = 0 for symbol in trade_symbols: price = row[1][symbol] if np.isnan(price): continue diff = price_usdt_btc_norm2.loc[row[0],symbol] - price_usdt_btc_norm_mean[row[0]] aim_value = -trade_value*round(diff/0.01,1) now_value = e.account[symbol]['value']*np.sign(e.account[symbol]['amount']) empty_value += now_value if aim_value - now_value > 20: e.Buy(symbol, price, round((aim_value - now_value)/price, 6),round(e.account[symbol]['realised_profit']+e.account[symbol]['unrealised_profit'],2)) if aim_value - now_value < -20: e.Sell(symbol, price, -round((aim_value - now_value)/price, 6),round(e.account[symbol]['realised_profit']+e.account[symbol]['unrealised_profit'],2)) stragey_2e = e
#(stragey_2d.df['total']/stragey_2d.initial_balance).plot(figsize=(17,6),grid = True); (stragey_2e.df['total']/stragey_2e.initial_balance).plot(figsize=(17,6),grid = True);
stragey_2e.df['leverage'].plot(figsize=(18,6),grid = True);
pd.DataFrame(stragey_2e.account).T.apply(lambda x:round(x,3))
Parameter optimization
The larger the setting of the Alpha parameter of the exponential moving average, the more sensitive the benchmark price tracking, the less transactions, the lower the final holding position. when lower the leverage, the return also reduced. Lower the maximum retracement, it can increase transaction volume. The specific balance operations need based on the backtest results.
Since the backtest is a 1h K line, it can only be updated once an hour, the real market can be updated faster, and it is necessary to weigh the specific settings comprehensively.
This is the result of optimization:
for Alpha in [i/100 for i in range(1,30)]: #price_usdt_btc_norm2 = price_usdt_btc/price_usdt_btc.rolling(20).mean() # Ordinary moving average price_usdt_btc_norm2 = price_usdt_btc/price_usdt_btc.ewm(alpha=Alpha).mean() # Here is consistent with the strategy, using EMA trade_symbols = list(set(symbols))# All currencies price_usdt_btc_norm_mean = price_usdt_btc_norm2[trade_symbols].mean(axis=1) e = Exchange(trade_symbols,initial_balance=10000,commission=0.0005,log=False) trade_value = 300 for row in price_usdt.iloc[:].iterrows(): e.Update(row[0], row[1]) empty_value = 0 for symbol in trade_symbols: price = row[1][symbol] if np.isnan(price): continue diff = price_usdt_btc_norm2.loc[row[0],symbol] - price_usdt_btc_norm_mean[row[0]] aim_value = -trade_value*round(diff/0.01,1) now_value = e.account[symbol]['value']*np.sign(e.account[symbol]['amount']) empty_value += now_value if aim_value - now_value > 20: e.Buy(symbol, price, round((aim_value - now_value)/price, 6),round(e.account[symbol]['realised_profit']+e.account[symbol]['unrealised_profit'],2)) if aim_value - now_value < -20: e.Sell(symbol, price, -round((aim_value - now_value)/price, 6),round(e.account[symbol]['realised_profit']+e.account[symbol]['unrealised_profit'],2)) stragey_2d = e # These are the final net value, the initial maximum backtest, the final position size, and the handling fee print(Alpha, round(stragey_2d.account['USDT']['total'],1), round(1-stragey_2d.df['total'].min()/stragey_2d.initial_balance,2),round(pd.DataFrame(stragey_2d.account).T['value'].sum(),1),round(stragey_2d.account['USDT']['fee']))
0.01 21116.2 0.14 15480.0 2178.0 0.02 20555.6 0.07 12420.0 2184.0 0.03 20279.4 0.06 9990.0 2176.0 0.04 20021.5 0.04 8580.0 2168.0 0.05 19719.1 0.03 7740.0 2157.0 0.06 19616.6 0.03 7050.0 2145.0 0.07 19344.0 0.02 6450.0 2133.0 0.08 19174.0 0.02 6120.0 2117.0 0.09 18988.4 0.01 5670.0 2104.0 0.1 18734.8 0.01 5520.0 2090.0 0.11 18532.7 0.01 5310.0 2078.0 0.12 18354.2 0.01 5130.0 2061.0 0.13 18171.7 0.01 4830.0 2047.0 0.14 17960.4 0.01 4770.0 2032.0 0.15 17779.8 0.01 4531.3 2017.0 0.16 17570.1 0.01 4441.3 2003.0 0.17 17370.2 0.01 4410.0 1985.0 0.18 17203.7 0.0 4320.0 1971.0 0.19 17016.9 0.0 4290.0 1955.0 0.2 16810.6 0.0 4230.6 1937.0 0.21 16664.1 0.0 4051.3 1921.0 0.22 16488.2 0.0 3930.6 1902.0 0.23 16378.9 0.0 3900.6 1887.0 0.24 16190.8 0.0 3840.0 1873.0 0.25 15993.0 0.0 3781.3 1855.0 0.26 15828.5 0.0 3661.3 1835.0 0.27 15673.0 0.0 3571.3 1816.0 0.28 15559.5 0.0 3511.3 1800.0 0.29 15416.4 0.0 3481.3 1780.0