Pesquisa avançada de plataformas Análise de dados e estratégia Python Backtest

Autora:Ninabadass, Criado: 2022-04-13 09:12:47, Atualizado: 2022-04-28 11:06:13

Pesquisa avançada de plataformas Análise de dados e estratégia Python Backtest.ipynb

Pesquisa avançada de plataformas

O FMZ possui um notebook jupyter incorporado para ajudar os usuários a se familiarizarem com a API da plataforma e conduzir pesquisas de estratégia, e suporta os ambientes de aprendizagem de Python3 C++11/17 e Javascript. Notebook+Python é uma ferramenta muito poderosa, que é quase indispensável para análise de dados e pesquisa de estratégia. Embora o backtest que vem com a plataforma FMZ seja muito útil, ele não é adequado para estratégias com volumes de dados complexos e grandes.

Utilização do Jupyter

O ambiente de pesquisa dentro do FMZ pode ser usado, mas a rede é inconveniente. Recomenda-se instalar no seu próprio dispositivo o anaconda3, com notebook e bibliotecas relacionadas com o uso comum para cálculos matemáticos; ele pode compartilhar o ambiente da rede local e ter melhor desempenho. Também é recomendável usar o Google colab. Embora haja algumas limitações de armazenamento, é gratuito e poderoso, adequado para a pesquisa relacionada ao estudo de robôs.

Tutorial

Há muitos tutoriais on-line para habilidades específicas de uso de notebook e Python. Você pode encontrar muitas informações pesquisando palavras-chave, como quantificação de Python e tutorial de notebook jupyter. Você precisa aprender e dominar uma série de conceitos básicos, como rastreador, processamento de dados, backtest, design de estratégia e plot.

Aquisição de dados

As plataformas geralmente fornecem APIs para obter K-lines com dados de histórico, e alguns também fornecem dados de execução de comércio por comércio.

Em seguida, vamos demonstrar como obter e armazenar os dados da linha K de contratos perpétuos no Binance.

Primeiro, encontre a documentação do Binance Perpetual Swap:https://binance-docs.github.io/apidocs/futures/cn/#c59e471e81. Você pode ver os parâmetros necessários e os formatos de dados retornados. Normalmente, o número de K-lines adquiridas pela API é limitado, e o Binance tem um máximo de 1000, por isso precisa ser adquirido por iteração de loop. A situação em outras plataformas é semelhante à do Binance.

Os períodos suportados pelo Binance: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M.

Em [24]: solicitações de importação # solicitações de rede para a biblioteca comum a partir da data-hora data de importação, data-hora Tempo de importação importar pandas como pd Em [160]: def GetKlines ((símbolo=BTC,start=2020-8-10,end=2021-8-10,periodo=1h): Cravos = [] start_time = int(time.mktime(datetime.strptime(start, %Y-%m-%d).timetuple))) *1000 end_time = int(time.mktime(datetime.strptime(end, %Y-%m-%d).timetuple))) *1000 enquanto start_time < end_time: res = requests.get ((https://fapi.binance.com/fapi/v1/klines?simbolo=%sUSDT&interval=%s&startTime=%s&limit=1000% ((simbolo,período,start_time)) res_list = res.json() Klines += res_list #print ((datetime.utcfromtimestamp ((start_time/1000).strftime ((%Y-%m-%d %H:%M:%S), len ((res_list)) start_time = res_list[-1][0] return pd.DataFrame ((Klines,columns=[time,open,high,low,close,amount,end_time,volume,count,buy_amount,buy_volume,null]).astype)) Em [85]: df = GetKlines ((símbolo=BTC,start=2021-1-1,end=2021-8-10,período=1h)

O armazenamento e leitura de dados pode usar as funções dentro da biblioteca panda.

Além do preço mais alto, o preço mais baixo, o preço de abertura, o preço de fechamento e o volume executado, os dados da linha K devolvidos pela Binance também incluem o valor total das negociações, o valor da iniciativa de compra, o valor da execução, etc. Estas são informações valiosas que podem ser usadas para construir estratégias.

Em [86]: Df.to_csv ((btc_klines.csv) df = pd.read_csv ((btc_klines.csv,index_col=0) Em [87]: df Fora[87]: Não, não, não, não, não, não, não, não, não, não, não, não, não, não, não, não, não, não, não, não... tempo de abertura alto baixo quantidade de fechamento final_conta de volume de tempo buy_amount buy_volume null 0 1596988800000 11575.08 11642.00 11566.07 11591.37 6541.466 1596992399999 7.592336e+07 25724 3127.898 3.630633e+07 0 1 1596992400000 11591.39 11610.23 11526.90 11534.39 6969.252 1596995999999 8.057780e+07 27403 3390.424 3.920162e+07 0 2 1596996000000 11534.39 11656.69 11527.93 11641.07 6439.365 1596999599999 7.469135e+07 25403 3446.186 3.997906e+07 0 3 1596999600000 11641.06 11665.90 11624.20 11635.30 3911.582 1597003199999 4.555459e+07 17820 1842.413 2.145768e+07 0 4 1597003200000 11635.29 11684.00 11635.29 11673.81 3461.004 1597006799999 4.036804e+07 15513 1660.575 1.936981e+07 0 ........................................................................................................................................................................................................................................................................................................................... 8805 1628658000000 45627.72 45894.53 45540.00 45801.45 10296.202 1628661599999 4.710187e+08 112187 4988.565 2.282399e+08 0 8806 1628661600000 45801.46 46270.00 45800.01 46087.86 26409.962 1628665199999 1.215164e+09 247170 13696.301 6.302708e+08 0 8807 1628665200000 46087.87 46450.00 46087.87 46367.38 23969.309 1628668799999 1.110210e+09 232348 11990.951 5.554267e+08 0 8808 1628668800000 46367.37 46643.13 46002.01 46217.01 23472.769 1628672399999 1.086549e+09 229533 12334.292 5.711837e+08 0 8809 1628672400000 46217.01 46329.69 46046.54 46297.16 6579.477 16286759999 - Não. 8810 linhas × 12 colunas

- Não. Em [88]: df.index = pd.to_datetime ((df.time,unit=ms) #converter o índice em uma data, que é conveniente para o gráfico Em [89]: df.close.plot ((figsize=(15,6), grid = True); #close price Fora[89]:imgEm [92]: (df.buy_amount.rolling(150).mean()/df.amount.rolling(150.mean)).plot ((figsize=(15,6),grid = True); #depois de plano, a proporção do montante da compra de iniciativa # a situação em que a proporção do montante da iniciativa de compra aumenta após atingir o fundo normalmente responde à situação de aumento de preços, mas a média a longo prazo da proporção do montante da iniciativa de compra é de 49% Fora[92]:imgEm [93]: (df[count].rolling(100).mean (()).plot ((figsize=(15,6),grid = True); #o valor executado após o plano,e as cotações de mercado podem ser preparadas em um local baixo Fora[93]:img

Motor de teste de retorno

O artigo anterior também deu o mecanismo de backtest Python, mas aqui está uma versão otimizada. Contratos perpétuos com margem USDT (ou outra moeda de cotação com margem) são muito semelhantes aos contratos spot. A diferença é que os contratos perpétuos podem ser alavancados e manter quantidade negativa (equivalente a fazer curto), e podem compartilhar um mecanismo de backtest. Contratos de entrega cripto-marginados são especiais, pois são liquidados em moeda e exigem backtest específico.

Aqui é dado um exemplo simples, que pode implementar spot multi-símbolo ou backtesting perpétuo multi-símbolo. Muitos detalhes são ignorados: como alavancagem de futuros, ocupação de margem, taxa de financiamento, mecanismo de liquidação, criação de mercado e transações de tomadores de ordens, bem como manutenção de ordens, mas geralmente não afeta os resultados normais do backtest. E o preço e a quantidade da correspondência e a atualização da conta precisam ser importados externamente. Os leitores podem melhorá-lo nesta base.

Introdução à classe de intercâmbio:

  • conta:USDT indica a moeda de base, que não é necessária; realized_profit: os lucros e perdas já realizados; unrealised_profit: os lucros e perdas ainda não realizados; total: o património total; taxa: a taxa de manipulação.

  • trade_symbols: matriz de pares de negociação; você também pode passar em um par de negociação; a moeda de cotação padrão é USDT, mas você também pode usar outros símbolos de moeda de cotação para backtest.

  • Taxa: a taxa de entrega; para simplificar, não se faz distinção entre quem faz e quem recebe.

  • inicial_saldo: os activos iniciais; o montante inicial dos pares de negociação por defeito é 0.

  • Função de compra: comprar, o que corresponde a fazer compras e fechar contratos perpétuos, sem um mecanismo de correspondência.

  • Função de venda: vender.

  • Função de atualização: atualizar as informações da conta, que devem ser inseridas no dicionário de preços de todos os pares de negociação. Em [98]: classe Intercâmbio:

    def Iniciar(self, trade_symbols, fee=0.0004, initial_balance=10000): self.initial_balance = inicial_balance #balance inicial auto.fee = taxa self.trade_symbols = trade_symbols O montante total do saldo inicial é calculado em função da taxa de câmbio. para símbolo em trade_symbols: Self.account[simbolo] = {amount:0, hold_price:0, value:0, price:0, realised_profit:0,unrealised_profit:0,fee:0}

    def Comércio (próprio, símbolo, direcção, preço, montante):

      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.fee #take out the fee 
      self.account['USDT']['fee'] += price*amount*self.fee
      self.account[symbol]['fee'] += price*amount*self.fee
    
      if cover_amount > 0: #close first 
          self.account['USDT']['realised_profit'] += -direction*(price - self.account[symbol]['hold_price'])*cover_amount  #profit 
          self.account[symbol]['realised_profit'] += -direction*(price - self.account[symbol]['hold_price'])*cover_amount
          
          self.account[symbol]['amount'] -= -direction*cover_amount
          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[symbol]['hold_price'] = total_cost/total_amount
          self.account[symbol]['amount'] += direction*open_amount
    

    def Comprar ((self, símbolo, preço, montante):self.Trade(símbolo 1, preço, montante)

    def Vender ((self, símbolo, preço, montante):self.Trade(símbolo -1, preço, montante)

    def Atualização ((self, close_price): #actualizar os ativos conta própria[USDT][lucro não realizado] = 0 para símbolo em self.trade_symbols: self.account[símbolo][unrealised_profit] = (close_price[símbolo] - self.account[símbolo][hold_price]) *self.account[símbolo][amount] self.account[símbolo][price] = close_price[símbolo] self.account[simbolo][value] = abs(self.account[simbolo][amount]) *close_price[simbolo] auto.conta[USDT][unrealised_profit] += auto.conta[símbolo][unrealised_profit] self.account[USDT][total] = redondo(self.account[USDT][realised_profit] + self.initial_balance + self.account[USDT][unrealised_profit],6) Em [117]: #No teste, você pode ver que não há ênfase em se a plataforma é USDT-marginada ou spot. e = Exchange([BTC], fee=0.0004, initial_balance=10000) #criar um objeto Exchange, e apenas um par de negociação de BTC e.Comprar ((BTC,40000, 0.1) #comprar 0.1 BTC ao preço de 40.000 e.Vender ((BTC,41000, 0.1) # vender 0,1 BTC ao preço de 41.000 e.Atualizar (({BTC:41000}) # atualizar as informações da conta Imprimir ((e.conta) #a informação final da conta print('Profito: ',round(e.account[USDT][total]-e.initial_balance,2)) Out[117]:{USDT: {benefício realizado: 96.76, benefício não realizado: 0.0, total: 10096.76, taxa: 3.24}, BTC: {montante: 0.0, preço de retenção: 0, valor: 0.0, preço: 000, 41 benefício realizado: 100.0, benefício não realizado: 0.0, taxa: 3.24}} lucro: 96,76

Teste de retorno da estratégia da grade

Em primeiro lugar, vamos fazer um backtest de uma estratégia de rede perpétua clássica. Esta estratégia é muito popular na nossa plataforma recentemente. Em comparação com a rede spot, não precisa manter moeda e pode adicionar alavancagem, o que é muito mais conveniente do que a rede spot. No entanto, como não pode ser diretamente testado, não é propício para selecionar símbolos de moeda. Aqui usamos o mecanismo de backtest agora para testá-lo.

No topo do Live, há um bot oficial, iniciado em 4 de abril de 2021; o valor da posição é de 150, o espaçamento da grade é de 0,01, e o lucro atual é de 3600USDT. Usando os mesmos parâmetros e a linha K de 5min para backtest, o lucro é de 3937USDT. Como o valor da posição no início do bot é inferior a 150 USDT, o resultado é bastante preciso. Se você mudar o espaçamento da grade para 0,005, o ganho será de 5226U. Um espaçamento da grade de 0,005 é obviamente um parâmetro melhor do que 0,01, que precisa ser backtestado para descobrir.

Quanto mais curto o período da linha K, mais precisos são os resultados correspondentes do backtest e maior a quantidade de dados necessários.

Em [241]: símbolo = TRX df = GetKlines ((simbolo=simbolo,start=2021-4-4,end=2021-8-11,periodo=5m) Em [286]: Valor = 150 pct = 0,01

e = Exchange (([símbolo], taxa=0.0002, inicial_saldo=10000) init_price = df.loc[0,close] res_list = [] # usado para armazenar o resultado do meio para a linha em df.iterrows(): kline = linha [1] #que só testará uma K-line e só receberá uma ordem de compra ou uma ordem de venda, o que não é muito preciso buy_price = (valor / pct - valor) / ((valor / pct) / init_price + e.account[símbolo][amount]) #sell preço da ordem, pois é uma execução do fabricante, é também o preço final de correspondência preço de venda = (valor / pct + valor) / ((valor / pct) / init_price + e.account[símbolo][montante])

if kline.low < buy_price: #the lowest price of K-line is less than the current maker price; the buy order is executed 
    e.Buy(symbol,buy_price,value/buy_price)
if kline.high > sell_price:
    e.Sell(symbol,sell_price,value/sell_price)
e.Update({symbol:kline.close})
res_list.append([kline.time, kline.close, e.account[symbol]['amount'], e.account['USDT']['total']-e.initial_balance])

res = pd.DataFrame ((data=res_list, colunas=[tempo,preço,montante,lucro]) res.index = pd.to_datetime ((res.time,unit=ms) Em [287]: e.conta Out[287]:{USDT: {realizado_lucro: 3866.633149565143, lucro não realizado: 70.54622281993666, total: 13937.179372, taxa: 177,51000000000596}, TRX: {montante: 36497.43208747655, hold_price: 0,08203709078461048, valor: 3064.689372385406, preço: 0,08397, lucro realizado: 4044.143149565462, lucro não realizado: 70.54622281993666, fees: 177.51000000000596}} Em [288]: res.profit.plot ((figsize=(15,6), grelha = True); Fora[288]:imgEm [170]: res.price.plot ((figsize=(15,6), grelha = True); #preço final Fora[170]:img

Estratégia de Equilíbrio Spot Backtest

Este tipo de estratégia também é relativamente popular, mas a plataforma FMZ não é muito boa no backtesting de estratégias de múltiplos símbolos, basta usar este mecanismo de backtest para experimentar.

Primeiro, obtenha os preços de fechamento dos quatro símbolos no ano passado. Pode-se ver que o ETH tem o maior aumento, e os outros três têm aumentos semelhantes. Se você manter esses quatro símbolos em média, o valor líquido final é de 4,5.

Em [290]: símbolos = [BTC,ETH,LTC,XRP] dados = {} Para símbolos em símbolos: df = GetKlines ((simbolo=simbolo,start=2020-8-11,end=2021-8-11,periodo=1h) dados[símbolo] = df.close Em [291]: df = pd.DataFrame (([dados[símbolo].valores do símbolo em símbolos],index=símbolos).T Em [302]: e = intercâmbio (simbolos, taxa=0.0004, saldo inicial=10000) res_list = [] para a linha em df.iterrows(): preços = linha [1] Total = e.conta[USDT][total] e.Atualização dos preços Para símbolos em símbolos: pct = e.conta[símbolo][valor]/total Se o pct for > 0,26: e.Vender (símbolo,preços (símbolo), ponto 0.25) *total/preços (símbolo) se o pct < 0,24: e.Comprar (símbolo,preços (símbolo), (símbolo, 0,25%) *total/preços (símbolo)) res_list.append (([e.account[simbolo][value] para símbolo em símbolos] + [e.account[USDT][total]]) res = pd.DataFrame ((data=res_list, colunas=símbolos+[total]) Em [303]: (df/df.iloc[0,:]).plot(figsize=(15,6),grid = True); #plot da tendência por normalização Fora[303]:imgEm [304]: (res.total/10000-(df/df.iloc[0,:]).mean(axis=1)).plot(figsize=(15,6),grid = True); #enheance o efeito Fora[304]:img

Estratégia da Tartaruga

A estratégia da tartaruga é uma estratégia de tendência clássica, que inclui uma lógica completa de stop-loss para adicionar posições.https://zhuanlan.zhihu.com/p/27987938Vamos implementar uma versão simples aqui para backtest.

O período da estratégia de tartaruga tem uma grande influência na estratégia, e não é aconselhável escolher um período que seja muito curto. Aqui, escolhemos 6h. O período do canal de Donchian é selecionado como 5, e a relação de posição é selecionada como 0,003 de acordo com o backtest. Quando o preço atravessa a upBand do canal para abrir 1 unidade de posição longa, e o preço continua a aumentar em volatilidade de 0,3 após a abertura das posições, continue a adicionar 1 unidade, e o preço cai abaixo de 2,5 Volatilidade do último preço aberto para parar a perda. O princípio da ordem curta é o mesmo. Devido ao grande mercado de alta do ETH, a estratégia de tartaruga capturou a tendência principal e finalmente alcançou 27 vezes os lucros, com uma alavancagem máxima de 4 vezes durante o período.

Os parâmetros da estratégia da tartaruga estão estreitamente relacionados com o período e devem ser selecionados através de backtest.

Pode-se ver a partir do gráfico final do valor líquido que a estratégia de tartaruga é uma estratégia de longo prazo, durante a qual pode não haver lucro por 3 a 4 meses, e perdas de parada repetidas, mas uma vez que há uma grande cotação de mercado de um lado, a estratégia de tartaruga pode aproveitar a tendência para acumular uma grande posição, mantê-la até o final da tendência, ganhar muitos lucros. No final do aumento, a estratégia vai acumular muitas posições. Neste momento, a volatilidade será relativamente grande, e muitas vezes grandes lucros serão retirados. Usar a estratégia de tartaruga requer que você aceite suas deficiências e sua paciência.

Em [424]: símbolo = ETH df = GetKlines ((simbolo=simbolo,start=2019-8-11,end=2021-8-11,periodo=6h) Em [425]: df.index = pd.to_datetime ((df.time,unit=ms) Em [568]: M = 5 # volume do canal de Donchian pct = 0,003 #a percentagem das posições adicionadas no total das posições df[up] = df[high].rolling ((M).max().shift(1) #upBand do canal de Donchian, usado para fazer longo e julgar para quebrar t df[down] = df[low].rolling(M).max().shift(1) df[middle] = (df[up]+df[down])/2 df[true_range] = pd.concat([df[high]-df[low],df[high]-df[close].shift(1),df[close].shift(1)-df[low], eixo=1).max (eixo=1) df[N] = df[true_range].rolling(50).mean() #N é igual à volatilidade recente, usada para julgar a compra e a parada de perdas Em [572]: Open_times = 0.3 #julgamento da abertura de uma posição Stop_times = 2,5 #stop loss e = Exchange([símbolo], fee=0.0004, initial_balance=10000) #set the taker to 0.0004 res_list = [] último_preço = 0 #último preço da posição aberta para a linha em df.iterrows(): linha = linha[1] se kline.isnull().sum() > 0: #salte a seção sem dados Continuar unidade = e.conta[USDT][total]*pct/kline.N #valor unitário da posição aberta

if kline.high >  kline.up and e.account[symbol]['amount'] == 0: #first time to open long position 
    e.Buy(symbol,kline.up,unit) #notice the trading price here
    last_price = kline.up
if e.account[symbol]['amount'] > 0 and kline.high > last_price + open_times*kline.N: #long position, buy in 
    e.Buy(symbol,last_price + open_times*kline.N,unit)
    last_price = last_price + open_times*kline.N
if e.account[symbol]['amount'] > 0 and kline.low < last_price - stop_times*kline.N: #long position, stop loss
    e.Sell(symbol,last_price - stop_times*kline.N,e.account[symbol]['amount'])
    
if kline.low <  kline.down and e.account[symbol]['amount'] == 0: #open short
    e.Sell(symbol,kline.down,unit)
    last_price = kline.down
if e.account[symbol]['amount'] < 0 and kline.low < last_price - open_times*kline.N: #short position, buy in 
    e.Sell(symbol,last_price - open_times*kline.N,unit)
    last_price = last_price - open_times*kline.N
if e.account[symbol]['amount'] < 0 and kline.high > last_price + stop_times*kline.N: #short position, stop loss
    e.Buy(symbol,last_price + stop_times*kline.N,-e.account[symbol]['amount'])
    
e.Update({symbol:kline.close})
res_list.append([kline.time, kline.close, e.account[symbol]['amount']*kline.close, e.account['USDT']['total']])

res = pd.DataFrame ((data=res_list, colunas=[tempo,preço,valor,total]) res.index = pd.to_datetime ((res.time,unit=ms) print(Valor de mercado final:,res[total][-1]) Out[572]:Valor de mercado final: 280760.566996 Em [573]: Res.total.plot ((figsize=(15,6), grelha = True); Fora[573]:imgEm [571]: (res.value/res.total).plot(figsize=(15,6),grid = True); Fora [1]:img

Conclusão

Se você é proficiente no uso da plataforma de pesquisa do notebook jupyter, pode facilmente realizar operações, como aquisição de dados, análise de dados, backtest de estratégia, exibição de gráficos, etc., o que é o caminho inevitável para a negociação quantitativa.

Use o Python para realizar análise de dados:https://wizardforcel.gitbooks.io/pyda-2e/content/

Tutorial Quantitativo Python:https://wizardforcel.gitbooks.io/python-quant-uqer/content/

Em [ ]:


Mais.