O endereço do relatório de investigação original:https://www.fmz.com/digest-topic/5584Você pode lê-lo primeiro, este artigo não terá conteúdo duplicado. Este artigo irá destacar o processo de otimização da segunda estratégia. Após a otimização, a segunda estratégia é melhorada obviamente, recomenda-se atualizar a estratégia de acordo com este artigo. O mecanismo de backtest adicionou as estatísticas de taxa de manuseio.
# 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']]
O desempenho da estratégia original, após a selecção do tipo de moeda, foi bom, mas ainda existem muitas posições de detenção, geralmente cerca de 4 vezes
Princípio:
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
O maior problema original é a comparação entre o preço mais recente e o preço inicial iniciado pela estratégia. À medida que o tempo passa, ele se tornará mais e mais desviado. Vamos acumular muitas posições nessas moedas. O maior problema com a filtragem de moedas é que ainda podemos ter moedas únicas no futuro com base em nossa experiência passada. O seguinte é o desempenho do modo de não filtragem. Na verdade, quando o trade_value = 300, no estágio médio da execução da estratégia, ele já perdeu tudo. Mesmo que não seja, o LINK e o XTZ também mantêm posições acima de 10000USDT, o que é muito grande. Portanto, devemos resolver esse problema no backtest e passar no teste de todas as moedas.
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
Uma vez que a causa do problema é comparar com o preço inicial, ele pode ser cada vez mais tendencioso.
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())
O desempenho da estratégia atendeu plenamente às nossas expectativas e os retornos são quase os mesmos. A situação de posições de conta quebradas na moeda original de todas as moedas também passou suavemente, e quase não há retração. O mesmo tamanho da posição de abertura, quase toda a alavancagem está abaixo de 1 vez, em 12 de março de 2020 o preço caiu caso extremo, ainda não excede 4 vezes, o que significa que podemos aumentar o trade_value e, sob a mesma alavancagem, dobrar o lucro. A posição final de detenção é apenas BCH superior a 1000USDT, o que é muito bom.
A estratégia original manterá posições curtas de 300 * 100 = 30000USDT por um longo tempo, e a nova estratégia eventualmente rastreará o preço de referência No último preço, você não manterá nenhuma posição no final.
(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))
O que acontecerá com a moeda com o mecanismo de triagem, com os mesmos parâmetros, os lucros da fase anterior apresentam melhor desempenho, o retração é menor, mas os retornos globais são ligeiramente menores.
#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))
Quanto maior a configuração do parâmetro Alpha da média móvel exponencial, mais sensível o rastreamento do preço de referência, menos transações, menor a posição de detenção final. quando a alavancagem é menor, o retorno também é reduzido. Baixar o retracement máximo, pode aumentar o volume de transações. As operações de saldo específicas precisam ser baseadas nos resultados do backtest.
Uma vez que o backtest é uma linha de 1h K, só pode ser actualizado uma vez por hora, o mercado real pode ser actualizado mais rapidamente e é necessário ponderar as configurações específicas de forma abrangente.
Este é o resultado da otimização:
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