English Русский 中文 Español Deutsch 日本語
preview
Usando o Algoritmo de Aprendizado de Máquina PatchTST para Prever a Ação do Preço nas Próximas 24 Horas

Usando o Algoritmo de Aprendizado de Máquina PatchTST para Prever a Ação do Preço nas Próximas 24 Horas

MetaTrader 5Negociação | 3 dezembro 2024, 10:07
248 0
Shashank Rai
Shashank Rai

Introdução

Eu encontrei pela primeira vez um algoritmo chamado PatchTST quando comecei a explorar os avanços da IA relacionados às previsões de séries temporais no Huggingface.co. Como qualquer pessoa que trabalhou com grandes modelos de linguagem (LLMs) sabe, a invenção dos transformers foi um divisor de águas para o desenvolvimento de ferramentas para processamento de linguagem natural, imagens e vídeos. Mas e quanto às séries temporais? Isso é algo que foi simplesmente deixado de lado? Ou a maior parte da pesquisa está apenas por trás de portas fechadas? Acontece que existem muitos modelos mais novos que aplicam transformers com sucesso para prever séries temporais. Neste artigo, veremos uma implementação como essa.

O que é impressionante sobre o PatchTST é quão rápido ele treina um modelo e quão fácil é usar o modelo treinado com MQL. Admito abertamente que sou novo no conceito de redes neurais. Mas ao passar por esse processo e abordar a implementação do PatchTST descrita neste artigo para MQL5, senti que dei um grande salto no meu aprendizado e na minha compreensão de como essas redes neurais complexas são desenvolvidas, depuradas, treinadas e usadas. É como pegar uma criança, que mal está aprendendo a andar, e colocá-la em um time profissional de futebol, esperando que ela marque o gol da vitória na final da Copa do Mundo. 


Visão Geral do PatchTST

Após descobrir o PatchTST, comecei a olhar o artigo que explica seu design: "Uma Série Temporal Vale 64 Palavras: Previsões de Longo Prazo com Transformers". O título era interessante. Quando comecei a ler mais sobre o artigo, pensei: uau, isso parece uma estrutura fascinante - tem muitos elementos que sempre quis aprender. Então, naturalmente, quis experimentar e ver como as previsões funcionam. Aqui está o que me deixou ainda mais interessado nesse algoritmo:

  • Você pode prever open, high, low e close usando o PatchTST. Com o PatchTST, senti que você poderia fornecer todos os dados conforme eles chegam - open, high, low, close e até volume. Você pode esperar que ele encontre os padrões nos dados porque todos os dados são convertidos em algo chamado "patches". Mais sobre o que são patches um pouco mais tarde neste artigo. Por enquanto, é importante saber que patches são atraentes e ajudam a melhorar as previsões.
  • Requisitos mínimos de pré-processamento de dados com o PatchTST. Quando comecei a explorar mais a fundo o algoritmo, percebi que os autores usam algo chamado "RevIn", que é normalização reversa de instâncias. RevIn vem de um artigo intitulado: "REVERSIBLE INSTANCE NORMALIZATION FOR ACCURATE TIME-SERIES FORECASTING AGAINST DISTRIBUTION SHIFT". RevIn tenta resolver o problema de mudança de distribuição nas previsões de séries temporais. Como traders algorítmicos, estamos todos muito familiarizados com a sensação quando nosso EA treinado parece não prever mais o mercado e somos forçados a reotimizar e atualizar nossos parâmetros. Considere o RevIn como uma forma de fazer a mesma coisa. 

  • Este método basicamente pega os dados passados para ele e os normaliza usando a seguinte fórmula:

    x = (x - média) / desvio

    Então, quando o modelo precisa fazer uma previsão, ele desnormaliza os dados usando a propriedade oposta:

    x = x * desvio + média

    RevIn também tem outra propriedade chamada affine_bias. Em termos mais simples, este é um parâmetro aprendível que cuida da assimetria, curtose, etc., que pode estar presente no conjunto de dados. 

    x = x * affine_weight + affine_bias

    A estrutura do PatchTST pode ser resumida da seguinte forma: 

    Dados de Entrada -> RevIn -> Decomposição da Série -> Componente de Tendência -> Backbone do PatchTST -> TSTiEncoder -> Flatten_Head -> Previsor de Tendência -> Componente Residual -> Adicionar Tendência e Residual -> Previsão Final

    Entendemos que nossos dados serão extraídos usando MT5. Também discutimos como o RevIn funciona.

    Aqui está como o PatchTST funciona: digamos que você pegue 80.000 barras de dados do EURUSD para o intervalo de tempo H1. Isso equivale a cerca de 13 anos de dados. Com o PatchTST, você segmenta os dados em algo chamado “patches”. Como analogia, pense nos patches como algo semelhante ao funcionamento dos Vision Transformers (ViTs) para imagens, mas adaptado para dados de séries temporais. Por exemplo, se o comprimento do patch for 16, então cada patch conterá 16 valores consecutivos de preço. Isso é como olhar pequenos pedaços da série temporal de cada vez, o que ajuda o modelo a focar em padrões locais antes de considerar o padrão global.

    Em seguida, os patches incluem codificação posicional para preservar a ordem da sequência, o que ajuda o modelo a lembrar a posição de cada patch na sequência.

    O transformer passa os patches normalizados e codificados por uma pilha de camadas de codificação. Cada camada de codificação contém uma camada de atenção multi-cabeça e uma camada feed-forward. A camada de atenção multi-cabeça permite que o modelo se concentre em diferentes partes da sequência de entrada, enquanto a camada feed-forward permite que o modelo aprenda transformações não-lineares complexas dos dados.

    Por fim, temos os componentes de tendência e residual. O mesmo processo de patching, normalização, codificação posicional e camadas de transformer é aplicado tanto ao componente de tendência quanto ao componente residual. Depois, somamos as saídas dos componentes de tendência e residual para produzir a previsão final.


    Problemas do Repositório Oficial do PatchTST

    O repositório oficial do PatchTST pode ser encontrado no GitHub no seguinte link: PatchTST (ICLR 2023). Existem duas versões diferentes disponíveis - supervisionada e não supervisionada. Para este artigo, usaremos a abordagem de aprendizado supervisionado. Como sabemos, para usar qualquer modelo com MQL5, precisamos de uma forma de convertê-lo para o formato ONNX. No entanto, os autores do PatchTST não levaram isso em consideração. Tive que fazer as seguintes modificações no código base deles para fazer o modelo funcionar com MQL5: 

    Código Original: 

    class PatchTST_backbone(nn.Module):
        def __init__(self, c_in:int, context_window:int, target_window:int, patch_len:int, stride:int, max_seq_len:Optional[int]=1024, 
                     n_layers:int=3, d_model=128, n_heads=16, d_k:Optional[int]=None, d_v:Optional[int]=None,
                     d_ff:int=256, norm:str='BatchNorm', attn_dropout:float=0., dropout:float=0., act:str="gelu", key_padding_mask:bool='auto',
                     padding_var:Optional[int]=None, attn_mask:Optional[Tensor]=None, res_attention:bool=True, pre_norm:bool=False,                          
                     store_attn:bool=False,
                     pe:str='zeros', learn_pe:bool=True, fc_dropout:float=0., head_dropout = 0, padding_patch = None,
                     pretrain_head:bool=False, head_type = 'flatten', individual = False, revin = True, affine = True, subtract_last = False,
                     verbose:bool=False, **kwargs):
            
            super().__init__()
            
            # RevIn
            self.revin = revin
            if self.revin: self.revin_layer = RevIN(c_in, affine=affine, subtract_last=subtract_last)
            
            # Patching
            self.patch_len = patch_len
            self.stride = stride
            self.padding_patch = padding_patch
            patch_num = int((context_window - patch_len)/stride + 1)
            if padding_patch == 'end': # can be modified to general case
                self.padding_patch_layer = nn.ReplicationPad1d((0, stride)) 
                patch_num += 1
            
            # Backbone 
            self.backbone = TSTiEncoder(c_in, patch_num=patch_num, patch_len=patch_len, max_seq_len=max_seq_len,
                                    n_layers=n_layers, d_model=d_model, n_heads=n_heads, d_k=d_k, d_v=d_v, d_ff=d_ff,
                                    attn_dropout=attn_dropout, dropout=dropout, act=act, key_padding_mask=key_padding_mask, padding_var=padding_var,
                                    attn_mask=attn_mask, res_attention=res_attention, pre_norm=pre_norm, store_attn=store_attn,
                                    pe=pe, learn_pe=learn_pe, verbose=verbose, **kwargs)
    
            # Head
            self.head_nf = d_model * patch_num
            self.n_vars = c_in
            self.pretrain_head = pretrain_head
            self.head_type = head_type
            self.individual = individual
    
            if self.pretrain_head: 
                self.head = self.create_pretrain_head(self.head_nf, c_in, fc_dropout) # custom head passed as a partial func with all its kwargs
            elif head_type == 'flatten': 
                self.head = Flatten_Head(self.individual, self.n_vars, self.head_nf, target_window, head_dropout=head_dropout)
            
        
        def forward(self, z):                                                                   # z: [bs x nvars x seq_len]
            # norm
            if self.revin: 
                z = z.permute(0,2,1)
                z = self.revin_layer(z, 'norm')
                z = z.permute(0,2,1)
                
            # do patching
            if self.padding_patch == 'end':
                z = self.padding_patch_layer(z)
            z = z.unfold(dimension=-1, size=self.patch_len, step=self.stride)                   # z: [bs x nvars x patch_num x patch_len]
            z = z.permute(0,1,3,2)                                                              # z: [bs x nvars x patch_len x patch_num]
            
            # model
            z = self.backbone(z)                                                                # z: [bs x nvars x d_model x patch_num]
            z = self.head(z)                                                                    # z: [bs x nvars x target_window] 
            
            # denorm
            if self.revin: 
                z = z.permute(0,2,1)
                z = self.revin_layer(z, 'denorm')
                z = z.permute(0,2,1)
            return z

    O código acima é o principal backbone. Como você pode ver, o código usa uma função chamada Unfold na linha: 

     z = z.unfold(dimension=-1, size=self.patch_len, step=self.stride)                   # z: [bs x nvars x patch_num x patch_len]

    A conversão de Unfold não é suportada pelo ONNX. Você receberá um erro como: 

    Unsupported: ONNX export of operator Unfold, input size not accessible. Por favor, sinta-se à vontade para solicitar suporte ou enviar um pull request no GitHub do PyTorch: https://github.com/pytorch/pytorch/issues

    Então, tive que substituir esta seção do código por: 

    # Manually unfold the input tensor
        batch_size, n_vars, seq_len = z.size()
        patches = []
        for i in range(0, seq_len - self.patch_len + 1, self.stride):
            patches.append(z[:, :, i:i+self.patch_len])

    Observe que a substituição acima é um pouco menos eficiente porque usa um loop for para treinar uma Rede Neural. As ineficiências podem se acumular ao longo de muitas épocas e com conjuntos de dados grandes. Mas isso é necessário, caso contrário, o modelo simplesmente falhará na conversão e não poderemos usá-lo com MQL5. 

    Abordei especificamente esse problema. Fazer isso levou o maior tempo. Depois, coloquei tudo junto em um arquivo chamado patchTST.py, que pode ser encontrado no arquivo zip anexado a este artigo. Este é o arquivo que usaremos para o treinamento do nosso modelo. 


    Requisitos para Trabalhar com PatchTST em Python

    Nesta seção, fornecerei os requisitos para trabalhar com o PatchTST em Python. Esses requisitos podem ser resumidos abaixo: 

    Crie um ambiente virtual: 

    python -m venv myenv

    Ative o ambiente virtual (Windows)

    .\myenv\Scripts\activate
    

    Instale o arquivo requirements.txt incluído no arquivo zip anexado a este artigo: 

    pip install -r requirements.txt

    Especificamente, os requisitos para rodar este projeto são: 

    MetaTrader5
    pandas
    numpy
    torch
    plotly
    datetime


    Desenvolvimento do Código de Treinamento do Modelo Passo a Passo

    Para o código a seguir, você pode acompanhar comigo usando um notebook Jupyter que eu incluí no arquivo zip: PatchTST Step-By-Step.ipynb. Resumiremos os passos abaixo: 

    1. Importar Bibliotecas Necessárias: Importando as bibliotecas necessárias, incluindo MetaTrader 5, Pandas, Numpy, Torch e o modelo PatchTST.

      # Step 1: Import necessary libraries
      import MetaTrader5 as mt5
      import pandas as pd
      import numpy as np
      import torch
      from torch.utils.data import TensorDataset, DataLoader
      from patchTST import Model as PatchTST
    2. Inicializar e Obter Dados do MetaTrader 5: A função fetch_mt5_data inicializa o MT5, obtém os dados para o símbolo fornecido, intervalo de tempo e número de barras, e retorna um dataframe com as colunas open, high, low e close.

      # Step 2: Initialize and fetch data from MetaTrader 5
      def fetch_mt5_data(symbol, timeframe, bars):
          if not mt5.initialize():
              print("MT5 initialization failed")
              return None
      
          timeframe_dict = {
              'M1': mt5.TIMEFRAME_M1,
              'M5': mt5.TIMEFRAME_M5,
              'M15': mt5.TIMEFRAME_M15,
              'H1': mt5.TIMEFRAME_H1,
              'D1': mt5.TIMEFRAME_D1
          }
      
          rates = mt5.copy_rates_from_pos(symbol, timeframe_dict[timeframe], 0, bars)
          mt5.shutdown()
      
          df = pd.DataFrame(rates)
          df['time'] = pd.to_datetime(df['time'], unit='s')
          df.set_index('time', inplace=True)
          return df[['open', 'high', 'low', 'close']]
      
      # Fetch data
      data = fetch_mt5_data('EURUSD', 'H1', 80000)
    3. Preparar Dados para Previsão Usando Janela Deslizante: A função prepare_forecasting_data cria o conjunto de dados usando uma abordagem de janela deslizante, gerando sequências de dados históricos (X) e os dados futuros correspondentes (y).

      # Step 3: Prepare forecasting data using sliding window
      def prepare_forecasting_data(data, seq_length, pred_length):
          X, y = [], []
          for i in range(len(data) - seq_length - pred_length):
              X.append(data.iloc[i:(i + seq_length)].values)
              y.append(data.iloc[(i + seq_length):(i + seq_length + pred_length)].values)
          return np.array(X), np.array(y)
      
      seq_length = 168  # 1 week of hourly data
      pred_length = 24  # Predict next 24 hours
      
      X, y = prepare_forecasting_data(data, seq_length, pred_length)
    4. Dividir Dados em Conjuntos de Treinamento e Teste: Dividindo os dados em conjuntos de treinamento e teste, com 80% para treinamento e 20% para teste. 

      # Step 4: Split data into training and testing sets
      split = int(len(X) * 0.8)
      X_train, X_test = X[:split], X[split:]
      y_train, y_test = y[:split], y[split:]
      
    5. Converter Dados para Tensores PyTorch: Convertendo os arrays NumPy para tensores PyTorch, que são necessários para o treinamento com PyTorch. Define uma semente manual para o torch, para reprodução dos resultados. 

      # Step 5: Convert data to PyTorch tensors
      X_train = torch.tensor(X_train, dtype=torch.float32)
      y_train = torch.tensor(y_train, dtype=torch.float32)
      X_test = torch.tensor(X_test, dtype=torch.float32)
      y_test = torch.tensor(y_test, dtype=torch.float32)
      
      torch.manual_seed(42)
    6. Definir Dispositivo para Cálculo: Definindo o dispositivo para CUDA, se disponível, caso contrário, usando o CPU. Isso é essencial para aproveitar a aceleração da GPU durante o treinamento, especialmente se estiver disponível.

      # Step 6: Set device for computation
      device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
      print(f"Using device: {device}")
    7. Criar Data Loader para Dados de Treinamento: Criando um data loader para lidar com o agrupamento e embaralhamento dos dados de treinamento.

      # Step 7: Create DataLoader for training data
      train_dataset = TensorDataset(X_train, y_train)
      train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
    8. Definir a Classe de Configuração para o Modelo: Definindo uma classe de configuração Config para armazenar todos os hiperparâmetros e configurações necessárias para o modelo PatchTST.

      # Step 8: Define the configuration class for the model
      class Config:
          def __init__(self):
              self.enc_in = 4  # Adjusted for 4 columns (open, high, low, close)
              self.seq_len = seq_length
              self.pred_len = pred_length
              self.e_layers = 3
              self.n_heads = 4
              self.d_model = 64
              self.d_ff = 256
              self.dropout = 0.1
              self.fc_dropout = 0.1
              self.head_dropout = 0.1
              self.individual = False
              self.patch_len = 24
              self.stride = 24
              self.padding_patch = True
              self.revin = True
              self.affine = False
              self.subtract_last = False
              self.decomposition = True
              self.kernel_size = 25
      
      configs = Config()
    9. Inicializar o Modelo PatchTST: Inicializando o modelo PatchTST com a configuração definida e movendo-o para o dispositivo selecionado.

      # Step 9: Initialize the PatchTST model
      model = PatchTST(
          configs=configs,
          max_seq_len=1024,
          d_k=None,
          d_v=None,
          norm='BatchNorm',
          attn_dropout=0.1,
          act="gelu",
          key_padding_mask='auto',
          padding_var=None,
          attn_mask=None,
          res_attention=True,
          pre_norm=False,
          store_attn=False,
          pe='zeros',
          learn_pe=True,
          pretrain_head=False,
          head_type='flatten',
          verbose=False
      ).to(device)
    10. Definir Otimizador e Função de Perda: Configurando o otimizador (Adam) e a função de perda (Erro Quadrático Médio) para treinar o modelo.

      # Step 10: Define optimizer and loss function
      optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
      loss_fn = torch.nn.MSELoss()
      num_epochs = 100
    11. Treinar o Modelo: Treinando o modelo pelo número especificado de épocas. Para cada lote de dados, o modelo realiza uma passagem direta, calcula a perda, realiza uma passagem reversa para calcular os gradientes e atualiza os parâmetros do modelo.

      # Step 11: Train the model
      for epoch in range(num_epochs):
          model.train()
          total_loss = 0
          for batch_X, batch_y in train_loader:
              optimizer.zero_grad()
              batch_X = batch_X.to(device)
              batch_y = batch_y.to(device)
      
              outputs = model(batch_X)
              outputs = outputs[:, -pred_length:, :4]
      
              loss = loss_fn(outputs, batch_y)
      
              loss.backward()
              optimizer.step()
              total_loss += loss.item()
      
          print(f"Epoch {epoch+1}/{num_epochs}, Loss: {total_loss/len(train_loader):.10f}")
    12. Salvar o Modelo no Formato PyTorch: Salvando o dicionário de estado do modelo treinado em um arquivo. Podemos usar este arquivo para fazer previsões diretamente no Python. 

      # Step 12: Save the model in PyTorch format
      torch.save(model.state_dict(), 'patchtst_model.pth')
    13. Preparar uma Entrada Fictícia para Exportação para ONNX: Criando um tensor de entrada fictício para usar na exportação do modelo para o formato ONNX. 

      # Step 13: Prepare a dummy input for ONNX export
      dummy_input = torch.randn(1, seq_length, 4).to(device)
    14. Exportar o Modelo para o Formato ONNX: Exportando o modelo treinado para o formato ONNX. Precisaremos deste arquivo para fazer previsões com MQL5. 

      # Step 14: Export the model to ONNX format
      torch.onnx.export(model, dummy_input, "patchtst_model.onnx",
                        opset_version=13,
                        input_names=['input'],
                        output_names=['output'],
                        dynamic_axes={'input': {0: 'batch_size'},
                                      'output': {0: 'batch_size'}})
      
      print("Model trained and saved in PyTorch and ONNX formats.")


    Resultados do Treinamento do Modelo

    Aqui estão os resultados que obtive ao treinar o modelo. 

    Epoch 1/100, Loss: 0.0000283705
    Epoch 2/100, Loss: 0.0000263274
    Epoch 3/100, Loss: 0.0000256321
    Epoch 4/100, Loss: 0.0000252389
    Epoch 5/100, Loss: 0.0000249340
    Epoch 6/100, Loss: 0.0000246715
    Epoch 7/100, Loss: 0.0000244293
    Epoch 8/100, Loss: 0.0000241942
    Epoch 9/100, Loss: 0.0000240157
    Epoch 10/100, Loss: 0.0000236776
    Epoch 11/100, Loss: 0.0000233954
    Epoch 12/100, Loss: 0.0000230437
    Epoch 13/100, Loss: 0.0000226635
    Epoch 14/100, Loss: 0.0000221875
    Epoch 15/100, Loss: 0.0000216960
    Epoch 16/100, Loss: 0.0000213242
    Epoch 17/100, Loss: 0.0000208693
    Epoch 18/100, Loss: 0.0000204956
    Epoch 19/100, Loss: 0.0000200573
    Epoch 20/100, Loss: 0.0000197222
    Epoch 21/100, Loss: 0.0000193516
    Epoch 22/100, Loss: 0.0000189223
    Epoch 23/100, Loss: 0.0000186635
    Epoch 24/100, Loss: 0.0000184025
    Epoch 25/100, Loss: 0.0000180468
    Epoch 26/100, Loss: 0.0000177854
    Epoch 27/100, Loss: 0.0000174621
    Epoch 28/100, Loss: 0.0000173247
    Epoch 29/100, Loss: 0.0000170032
    Epoch 30/100, Loss: 0.0000168594
    Epoch 31/100, Loss: 0.0000166609
    Epoch 32/100, Loss: 0.0000164818
    Epoch 33/100, Loss: 0.0000162424
    Epoch 34/100, Loss: 0.0000161265
    Epoch 35/100, Loss: 0.0000159775
    Epoch 36/100, Loss: 0.0000158510
    Epoch 37/100, Loss: 0.0000156571
    Epoch 38/100, Loss: 0.0000155327
    Epoch 39/100, Loss: 0.0000154742
    Epoch 40/100, Loss: 0.0000152778
    Epoch 41/100, Loss: 0.0000151757
    Epoch 42/100, Loss: 0.0000151083
    Epoch 43/100, Loss: 0.0000150182
    Epoch 44/100, Loss: 0.0000149140
    Epoch 45/100, Loss: 0.0000148057
    Epoch 46/100, Loss: 0.0000147672
    Epoch 47/100, Loss: 0.0000146499
    Epoch 48/100, Loss: 0.0000145281
    Epoch 49/100, Loss: 0.0000145298
    Epoch 50/100, Loss: 0.0000144795
    Epoch 51/100, Loss: 0.0000143969
    Epoch 52/100, Loss: 0.0000142840
    Epoch 53/100, Loss: 0.0000142294
    Epoch 54/100, Loss: 0.0000142159
    Epoch 55/100, Loss: 0.0000140837
    Epoch 56/100, Loss: 0.0000140005
    Epoch 57/100, Loss: 0.0000139986
    Epoch 58/100, Loss: 0.0000139122
    Epoch 59/100, Loss: 0.0000139010
    Epoch 60/100, Loss: 0.0000138351
    Epoch 61/100, Loss: 0.0000138050
    Epoch 62/100, Loss: 0.0000137636
    Epoch 63/100, Loss: 0.0000136853
    Epoch 64/100, Loss: 0.0000136191
    Epoch 65/100, Loss: 0.0000136272
    Epoch 66/100, Loss: 0.0000135552
    Epoch 67/100, Loss: 0.0000135439
    Epoch 68/100, Loss: 0.0000135200
    Epoch 69/100, Loss: 0.0000134461
    Epoch 70/100, Loss: 0.0000133950
    Epoch 71/100, Loss: 0.0000133979
    Epoch 72/100, Loss: 0.0000133059
    Epoch 73/100, Loss: 0.0000133242
    Epoch 74/100, Loss: 0.0000132816
    Epoch 75/100, Loss: 0.0000132145
    Epoch 76/100, Loss: 0.0000132803
    Epoch 77/100, Loss: 0.0000131212
    Epoch 78/100, Loss: 0.0000131809
    Epoch 79/100, Loss: 0.0000131538
    Epoch 80/100, Loss: 0.0000130786
    Epoch 81/100, Loss: 0.0000130651
    Epoch 82/100, Loss: 0.0000130255
    Epoch 83/100, Loss: 0.0000129917
    Epoch 84/100, Loss: 0.0000129804
    Epoch 85/100, Loss: 0.0000130086
    Epoch 86/100, Loss: 0.0000130156
    Epoch 87/100, Loss: 0.0000129557
    Epoch 88/100, Loss: 0.0000129013
    Epoch 89/100, Loss: 0.0000129018
    Epoch 90/100, Loss: 0.0000128864
    Epoch 91/100, Loss: 0.0000128663
    Epoch 92/100, Loss: 0.0000128411
    Epoch 93/100, Loss: 0.0000128514
    Epoch 94/100, Loss: 0.0000127915
    Epoch 95/100, Loss: 0.0000127778
    Epoch 96/100, Loss: 0.0000127787
    Epoch 97/100, Loss: 0.0000127623
    Epoch 98/100, Loss: 0.0000127452
    Epoch 99/100, Loss: 0.0000127141
    Epoch 100/100, Loss: 0.0000127229

    Os resultados podem ser visualizados da seguinte forma: 

    Progresso do Treinamento ao Longo de 100 Épocas

    Também obtemos a seguinte saída sem erros ou avisos, indicando que nosso modelo foi convertido com sucesso para o formato ONNX. 

    Modelo treinado e salvo nos formatos PyTorch e ONNX.
    


    Gerando Previsões Usando Python Passo a Passo

    Agora, vamos dar uma olhada no código de previsão: 

    1. Etapa 1. Importar Bibliotecas Necessárias: começamos importando todas as bibliotecas necessárias.
      # Import required libraries
      import MetaTrader5 as mt5
      import pandas as pd
      import numpy as np
      import torch
      from datetime import datetime, timedelta
      import plotly.graph_objects as go
      from plotly.subplots import make_subplots
      from patchTST import Model as PatchTST
    2. Etapa 2. Buscar Dados do MetaTrader 5: definimos uma função para buscar dados do MetaTrader 5 e convertê-los em um DataFrame. Buscamos 168 barras anteriores porque isso é o que é necessário para obter uma previsão com nosso modelo. 
      # Function to fetch data from MetaTrader 5
      def fetch_mt5_data(symbol, timeframe, bars):
          if not mt5.initialize():
              print("MT5 initialization failed")
              return None
      
          timeframe_dict = {
              'M1': mt5.TIMEFRAME_M1,
              'M5': mt5.TIMEFRAME_M5,
              'M15': mt5.TIMEFRAME_M15,
              'H1': mt5.TIMEFRAME_H1,
              'D1': mt5.TIMEFRAME_D1
          }
      
          rates = mt5.copy_rates_from_pos(symbol, timeframe_dict[timeframe], 0, bars)
          mt5.shutdown()
      
          df = pd.DataFrame(rates)
          df['time'] = pd.to_datetime(df['time'], unit='s')
          df.set_index('time', inplace=True)
          return df[['open', 'high', 'low', 'close']]
      
      # Fetch the latest week of data
      historical_data = fetch_mt5_data('EURUSD', 'H1', 168)
    3. Etapa 3. Preparar Dados de Entrada: definimos uma função para preparar os dados de entrada para o modelo, pegando as últimas linhas de dados de comprimento seq_length. Ao puxar os dados, precisamos apenas das últimas 168 horas de dados de 1h para fazer previsões para as próximas 24 horas. Isso ocorre porque foi assim que treinamos o modelo. 
      # Function to prepare input data
      def prepare_input_data(data, seq_length):
          X = []
          X.append(data.iloc[-seq_length:].values)
          return np.array(X)
      
      # Prepare the input data
      seq_length = 168  # 1 week of hourly data
      input_data = prepare_input_data(historical_data, seq_length)
      
    4. Etapa 4. Definir Configuração: definimos uma classe de configuração para configurar os parâmetros do modelo. Essas configurações são as mesmas que usamos para treinar o modelo. 
      # Define the configuration class
      class Config:
          def __init__(self):
              self.enc_in = 4  # Adjusted for 4 columns (open, high, low, close)
              self.seq_len = seq_length
              self.pred_len = 24  # Predict next 24 hours
              self.e_layers = 3
              self.n_heads = 4
              self.d_model = 64
              self.d_ff = 256
              self.dropout = 0.1
              self.fc_dropout = 0.1
              self.head_dropout = 0.1
              self.individual = False
              self.patch_len = 24
              self.stride = 24
              self.padding_patch = True
              self.revin = True
              self.affine = False
              self.subtract_last = False
              self.decomposition = True
              self.kernel_size = 25
      
      # Initialize the configuration
      config = Config()
    5. Etapa 5. Carregar o Modelo Treinado: definimos uma função para carregar o modelo PatchTST treinado. Essas são as mesmas configurações que usamos para treinar o modelo. 
      # Function to load the trained model
      def load_model(model_path, config):
          model = PatchTST(
              configs=config,
              max_seq_len=1024,
              d_k=None,
              d_v=None,
              norm='BatchNorm',
              attn_dropout=0.1,
              act="gelu",
              key_padding_mask='auto',
              padding_var=None,
              attn_mask=None,
              res_attention=True,
              pre_norm=False,
              store_attn=False,
              pe='zeros',
              learn_pe=True,
              pretrain_head=False,
              head_type='flatten',
              verbose=False
          )
          model.load_state_dict(torch.load(model_path))
          model.eval()
          return model
      
      # Load the trained model
      model_path = 'patchtst_model.pth'
      device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
      model = load_model(model_path, config).to(device)
      
    6. Etapa 6. Fazer Previsões: definimos uma função para fazer previsões usando o modelo carregado e os dados de entrada.
      # Function to make predictions
      def predict(model, input_data, device):
          with torch.no_grad():
              input_data = torch.tensor(input_data, dtype=torch.float32).to(device)
              output = model(input_data)
          return output.cpu().numpy()
      
      # Make predictions
      predictions = predict(model, input_data, device)
      
    7. Etapa 7. Pós-processamento e Visualização: processamos as previsões, criamos um data frame e visualizamos os dados históricos e previstos usando Plotly.
      # Ensure predictions have the correct shape
      if predictions.shape[2] != 4:
          predictions = predictions[:, :, :4]  # Adjust based on actual number of columns required
      
      # Check the shape of predictions
      print("Shape of predictions:", predictions.shape)
      
      # Create a DataFrame for predictions
      pred_index = pd.date_range(start=historical_data.index[-1] + pd.Timedelta(hours=1), periods=24, freq='H')
      pred_df = pd.DataFrame(predictions[0], columns=['open', 'high', 'low', 'close'], index=pred_index)
      
      # Combine historical data and predictions
      combined_df = pd.concat([historical_data, pred_df])
      
      # Create the plot
      fig = make_subplots(rows=1, cols=1, shared_xaxes=True, vertical_spacing=0.03, subplot_titles=('EURUSD OHLC'))
      
      # Add historical candlestick
      fig.add_trace(go.Candlestick(x=historical_data.index,
                                   open=historical_data['open'],
                                   high=historical_data['high'],
                                   low=historical_data['low'],
                                   close=historical_data['close'],
                                   name='Historical'))
      
      # Add predicted candlestick
      fig.add_trace(go.Candlestick(x=pred_df.index,
                                   open=pred_df['open'],
                                   high=pred_df['high'],
                                   low=pred_df['low'],
                                   close=pred_df['close'],
                                   name='Predicted'))
      
      # Add a vertical line to separate historical data from predictions
      fig.add_vline(x=historical_data.index[-1], line_dash="dash", line_color="gray")
      
      # Update layout
      fig.update_layout(title='EURUSD OHLC Chart with Predictions',
                        yaxis_title='Price',
                        xaxis_rangeslider_visible=False)
      
      # Show the plot
      fig.show()
      
      # Print predictions (optional)
      print("Predicted prices for the next 24 hours:", predictions)
      


    Código de Treinamento e Previsão em Python

    Se você não estiver interessado em rodar o código em um notebook Jupyter, forneci alguns arquivos que você pode rodar diretamente nos anexos: 

    • model_training.py
    • model_prediction.py

    Você pode configurar o modelo como desejar e executá-lo sem usar o Jupyter. 


    Resultados da Previsão

    Após treinar o modelo e executar o código de previsão em Python, obtive o seguinte gráfico. As previsões foram criadas por volta das 12:30 AM (CEST + 3) do dia 7/8/2024. Este é exatamente o momento da Abertura de Domingo à Noite / Segunda-feira de manhã. Podemos ver um gap no gráfico porque o EURUSD abriu com um gap. O modelo prevê que o EURUSD deverá experimentar uma tendência de alta durante a maior parte do tempo, possivelmente preenchendo esse gap. Após o gap ser preenchido, a ação do preço deverá virar para baixo perto do final do dia. 

    Previsões de Python

    Também imprimimos o valor bruto dos resultados, que pode ser visto abaixo: 

    Predicted prices for the next 24 hours: [[[1.0789319 1.08056   1.0789403 1.0800443]
      [1.0791171 1.080738  1.0791024 1.0802013]
      [1.0792702 1.0807946 1.0792127 1.0802455]
      [1.0794896 1.0809869 1.07939   1.0804181]
      [1.0795166 1.0809793 1.0793561 1.0803629]
      [1.0796498 1.0810834 1.079427  1.0804263]
      [1.0798903 1.0813211 1.0795883 1.0805805]
      [1.0800778 1.081464  1.0796818 1.0806502]
      [1.0801392 1.0815498 1.0796598 1.0806476]
      [1.0802988 1.0817037 1.0797216 1.0807337]
      [1.080521  1.0819166 1.079835  1.08086  ]
      [1.0804708 1.0818571 1.079683  1.0807351]
      [1.0805807 1.0819991 1.079669  1.0807738]
      [1.0806456 1.0820425 1.0796478 1.0807805]
      [1.080733  1.0821087 1.0796758 1.0808226]
      [1.0807986 1.0822101 1.0796862 1.08086  ]
      [1.0808219 1.0821983 1.0796905 1.0808747]
      [1.0808604 1.082247  1.0797052 1.0808727]
      [1.0808146 1.082188  1.0796149 1.0807893]
      [1.0809066 1.0822624 1.0796828 1.0808471]
      [1.0809724 1.0822903 1.0797662 1.0808889]
      [1.0810378 1.0823163 1.0797914 1.0809084]
      [1.0810691 1.0823379 1.0798224 1.0809308]
      [1.0810966 1.0822875 1.0797993 1.0808865]]]


    Levando o Modelo Pré-treinado para MQL5

    Nesta seção, criaremos um precursor de um indicador que nos ajudará a visualizar a ação do preço prevista em nossos gráficos. Deliberadamente, fiz o script rudimentar e aberto, porque nossos leitores podem ter objetivos diferentes e estratégias diferentes para como usar essas redes neurais complexas. O indicador é desenvolvido no Formato de Consultor Especial do MQL5. Aqui está o script completo: 

    //+------------------------------------------------------------------+
    //|                                            PatchTST Predictor    |
    //|                                                   Copyright 2024 |
    //+------------------------------------------------------------------+
    #property copyright "Copyright 2024"
    #property link      "https://www.mql5.com"
    #property version   "1.00"
    
    #resource "\\PatchTST\\patchtst_model.onnx" as uchar PatchTSTModel[]
    
    #define SEQ_LENGTH 168
    #define PRED_LENGTH 24
    #define INPUT_FEATURES 4
    
    long ModelHandle = INVALID_HANDLE;
    datetime ExtNextBar = 0;
    
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
    {
       // Load the ONNX model
       ModelHandle = OnnxCreateFromBuffer(PatchTSTModel, ONNX_DEFAULT);
       if (ModelHandle == INVALID_HANDLE)
       {
          Print("Error creating ONNX model: ", GetLastError());
          return(INIT_FAILED);
       }
    
       // Set input shape
       const long input_shape[] = {1, SEQ_LENGTH, INPUT_FEATURES};
       if (!OnnxSetInputShape(ModelHandle, ONNX_DEFAULT, input_shape))
       {
          Print("Error setting input shape: ", GetLastError());
          return(INIT_FAILED);
       }
    
       // Set output shape
       const long output_shape[] = {1, PRED_LENGTH, INPUT_FEATURES};
       if (!OnnxSetOutputShape(ModelHandle, 0, output_shape))
       {
          Print("Error setting output shape: ", GetLastError());
          return(INIT_FAILED);
       }
    
       return(INIT_SUCCEEDED);
    }
    
    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
    {
       if (ModelHandle != INVALID_HANDLE)
       {
          OnnxRelease(ModelHandle);
          ModelHandle = INVALID_HANDLE;
       }
    }
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
    {
       if (TimeCurrent() < ExtNextBar)
          return;
    
       ExtNextBar = TimeCurrent();
       ExtNextBar -= ExtNextBar % PeriodSeconds();
       ExtNextBar += PeriodSeconds();
    
       // Prepare input data
       float input_data[];
       if (!PrepareInputData(input_data))
       {
          Print("Error preparing input data");
          return;
       }
    
       // Make prediction
       float predictions[];
       if (!MakePrediction(input_data, predictions))
       {
          Print("Error making prediction");
          return;
       }
    
       // Draw hypothetical future bars
       DrawFutureBars(predictions);
    }
    
    //+------------------------------------------------------------------+
    //| Prepare input data for the model                                 |
    //+------------------------------------------------------------------+
    bool PrepareInputData(float &input_data[])
    {
       MqlRates rates[];
       ArraySetAsSeries(rates, true);
       int copied = CopyRates(_Symbol, PERIOD_H1, 0, SEQ_LENGTH, rates);
       
       if (copied != SEQ_LENGTH)
       {
          Print("Failed to copy rates data. Copied: ", copied);
          return false;
       }
    
       ArrayResize(input_data, SEQ_LENGTH * INPUT_FEATURES);
       for (int i = 0; i < SEQ_LENGTH; i++)
       {
          input_data[i * INPUT_FEATURES + 0] = (float)rates[SEQ_LENGTH - 1 - i].open;
          input_data[i * INPUT_FEATURES + 1] = (float)rates[SEQ_LENGTH - 1 - i].high;
          input_data[i * INPUT_FEATURES + 2] = (float)rates[SEQ_LENGTH - 1 - i].low;
          input_data[i * INPUT_FEATURES + 3] = (float)rates[SEQ_LENGTH - 1 - i].close;
       }
    
       return true;
    }
    
    //+------------------------------------------------------------------+
    //| Make prediction using the ONNX model                             |
    //+------------------------------------------------------------------+
    bool MakePrediction(const float &input_data[], float &output_data[])
    {
       ArrayResize(output_data, PRED_LENGTH * INPUT_FEATURES);
    
       if (!OnnxRun(ModelHandle, ONNX_NO_CONVERSION, input_data, output_data))
       {
          Print("Error running ONNX model: ", GetLastError());
          return false;
       }
    
       return true;
    }
    
    //+------------------------------------------------------------------+
    //| Draw hypothetical future bars                                    |
    //+------------------------------------------------------------------+
    void DrawFutureBars(const float &predictions[])
    {
       datetime current_time = TimeCurrent();
       for (int i = 0; i < PRED_LENGTH; i++)
       {
          datetime bar_time = current_time + PeriodSeconds(PERIOD_H1) * (i + 1);
          double open = predictions[i * INPUT_FEATURES + 0];
          double high = predictions[i * INPUT_FEATURES + 1];
          double low = predictions[i * INPUT_FEATURES + 2];
          double close = predictions[i * INPUT_FEATURES + 3];
    
          string obj_name = "FutureBar_" + IntegerToString(i);
          ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, bar_time, low, bar_time + PeriodSeconds(PERIOD_H1), high);
          ObjectSetInteger(0, obj_name, OBJPROP_COLOR, close > open ? clrGreen : clrRed);
          ObjectSetInteger(0, obj_name, OBJPROP_FILL, true);
          ObjectSetInteger(0, obj_name, OBJPROP_BACK, true);
       }
    
       ChartRedraw();
    }

    Para rodar o script acima, observe como a seguinte linha está definida: 

    #resource "\\PatchTST\\patchtst_model.onnx" as uchar PatchTSTModel[]

    Isso significa que dentro da pasta do Consultor Especial, precisaremos criar uma subpasta chamada PatchTST. Dentro da subpasta PatchTST, precisaremos salvar o arquivo ONNX do treinamento do modelo. No entanto, o EA principal será armazenado na pasta raiz. 

    Os parâmetros que usamos para treinar nosso modelo também estão definidos no topo do script:

    #define SEQ_LENGTH 168
    #define PRED_LENGTH 24
    #define INPUT_FEATURES 4

    No nosso caso, queremos usar 168 barras anteriores, alimentá-las no modelo ONNX e obter uma previsão para as próximas 24 barras no futuro. Temos 4 características de entrada: open, high, low e close. 

    Além disso, observe o seguinte código dentro da função OnTick(): 

    if (TimeCurrent() < ExtNextBar)
       return;
    
    ExtNextBar = TimeCurrent();
    ExtNextBar -= ExtNextBar % PeriodSeconds();
    ExtNextBar += PeriodSeconds();

    Como os modelos ONNX são intensivos em poder de processamento de um computador, esse código garantirá que uma nova previsão seja gerada apenas uma vez por barra. No nosso caso, como estamos trabalhando com barras horárias, as previsões serão atualizadas uma vez por hora. 

    Finalmente, neste código, desenharemos as barras futuras na tela, através dos recursos de desenho do MQL5: 

    void DrawFutureBars(const float &predictions[])
    {
       datetime current_time = TimeCurrent();
       for (int i = 0; i < PRED_LENGTH; i++)
       {
          datetime bar_time = current_time + PeriodSeconds(PERIOD_H1) * (i + 1);
          double open = predictions[i * INPUT_FEATURES + 0];
          double high = predictions[i * INPUT_FEATURES + 1];
          double low = predictions[i * INPUT_FEATURES + 2];
          double close = predictions[i * INPUT_FEATURES + 3];
    
          string obj_name = "FutureBar_" + IntegerToString(i);
          ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, bar_time, low, bar_time + PeriodSeconds(PERIOD_H1), high);
          ObjectSetInteger(0, obj_name, OBJPROP_COLOR, close > open ? clrGreen : clrRed);
          ObjectSetInteger(0, obj_name, OBJPROP_FILL, true);
          ObjectSetInteger(0, obj_name, OBJPROP_BACK, true);
       }
    
       ChartRedraw();
    }

    Após implementar este código em MQL5, compilar o modelo e colocar o EA resultante no intervalo de tempo H1, você deverá ver algumas barras adicionais no futuro em seu gráfico. No meu caso, isso se parece com o seguinte: 


    Observe que se você não vir as barras recém-desenhadas à direita, pode ser necessário clicar no botão "Shift end of chart from right border".

    Conclusão

    Neste artigo, tomamos uma abordagem passo a passo para treinar o modelo PatchTST, que foi introduzido em 2023. Obtemos uma visão geral de como o algoritmo PatchTST funciona. O código base teve alguns problemas relacionados à conversão para ONNX. Especificamente, o operador "Unfold" não é suportado, então resolvemos esse problema para tornar o código mais compatível com ONNX. Também mantivemos o propósito do artigo voltado para o trader, focando nos conceitos básicos do modelo, como puxar os dados, treinar o modelo e obter uma previsão para as próximas 24 horas. Depois, implementamos a previsão em MQL5, para que possamos usar o modelo completamente treinado com nossos indicadores e consultores especializados favoritos. Estou feliz em compartilhar todo o meu código com a comunidade MQL nos arquivos zip anexados. Por favor, me avise se tiver alguma dúvida ou comentário. 


    Traduzido do Inglês pela MetaQuotes Ltd.
    Artigo original: https://www.mql5.com/en/articles/15198

    Arquivos anexados |
    PatchTST.zip (4876.35 KB)
    Técnicas do MQL5 Wizard que você deve conhecer (Parte 26): Médias Móveis e o Exponente de Hurst Técnicas do MQL5 Wizard que você deve conhecer (Parte 26): Médias Móveis e o Exponente de Hurst
    O Exponente de Hurst é uma medida de quanto uma série temporal se autocorrela ao longo do tempo. Entende-se que ele captura as propriedades de longo prazo de uma série temporal e, portanto, tem um peso significativo na análise de séries temporais, mesmo fora do contexto econômico/financeiro. No entanto, focamos em seu potencial benefício para os traders ao analisar como essa métrica poderia ser combinada com médias móveis para construir um sinal potencialmente robusto.
    Desenvolvendo um EA multimoeda (Parte 17): Preparação adicional para o trading real Desenvolvendo um EA multimoeda (Parte 17): Preparação adicional para o trading real
    Atualmente, nosso EA utiliza um banco de dados para obter as strings de inicialização de instâncias individuais de estratégias de trading. No entanto, o banco de dados é bastante volumoso e contém muitas informações desnecessárias para a operação real do EA. Tentaremos garantir o funcionamento do EA sem a necessidade de conexão obrigatória ao banco de dados.
    Usando a API de Dados JSON em seus projetos MQL Usando a API de Dados JSON em seus projetos MQL
    Imagine que você pode usar dados que não estão disponíveis no MetaTrader, você só obtém dados de indicadores por análise de preços e análise técnica. Agora imagine que você pode acessar dados que levarão seu poder de negociação a um novo nível. Você pode multiplicar o poder do software MetaTrader se misturar a saída de outros softwares, métodos de análise macroeconômica e ferramentas ultra-avançadas por meio da API de dados. Neste artigo, vamos ensinar como usar APIs e apresentar serviços de dados API úteis e valiosos.
    Análise de Sentimento e Deep Learning para Trading com EA e Backtesting com Python Análise de Sentimento e Deep Learning para Trading com EA e Backtesting com Python
    Neste artigo, vamos introduzir a Análise de Sentimento e Modelos ONNX com Python para serem usados em um EA. Um script executa um modelo ONNX treinado do TensorFlow para previsões de deep learning, enquanto outro busca manchetes de notícias e quantifica o sentimento usando IA.