Русский
preview
Redes neurais em trading: Modelo multidimensional de ponta a ponta para previsão de séries temporais (Conclusão)

Redes neurais em trading: Modelo multidimensional de ponta a ponta para previsão de séries temporais (Conclusão)

MetaTrader 5Sistemas de negociação |
23 8
Dmitriy Gizlyk
Dmitriy Gizlyk

Introdução

Nos artigos anteriores, percorremos o caminho desde a introdução aos aspectos teóricos até a implementação prática das abordagens propostas, revelando passo a passo a arquitetura e a lógica do GinAR, um framework original de redes neurais desenvolvido especialmente para analisar e prever séries temporais em condições de incerteza estrutural e alto nível de ruído. Baseado em uma arquitetura multicamadas e multicomponente, o GinAR combina com solidez princípios de atenção, transformações baseadas em grafos e dinâmica recorrente, adquirindo assim a capacidade não apenas de perceber a estrutura dos dados, mas também de se adaptar a ela em tempo real.

Chegamos à parte mais importante e, provavelmente, a mais interessante: a avaliação do funcionamento do framework GinAR em condições reais. Ficaram para trás as etapas de projeto da arquitetura, implementação dos componentes-chave e construção dos algoritmos de propagação para frente e de propagação reversa. Todos os elementos do sistema foram detalhadamente desenvolvidos, verificados e adaptados às particularidades das séries temporais financeiras. Agora é hora de reuni-los em um todo único e, por assim dizer, colocar em prática para verificar com que eficiência o GinAR lida com tarefas práticas de análise e previsão.

Neste artigo, não apenas concluímos o desenvolvimento, mas também fechamos o ciclo de um conceito cuja implementação exigiu um esforço considerável. O foco principal estará no treinamento e no teste do modelo em condições próximas às reais de operação.

Antes de passar à avaliação, vamos relembrar brevemente como o próprio framework GinAR é estruturado. Em sua base está a célula original GinARCell, que combina de uma só vez vários componentes-chave:

  • Interpolation Attention (IA) é um mecanismo de atenção contextual que pré-processa o fluxo de entrada e faz o modelo se concentrar nos elementos relevantes. Trata-se de uma espécie de filtro de percepção primária, que permite destacar padrões significativos e restaurar elementos ausentes antes da transformação principal.
  • Blocos AGCN (Adaptive Graph Convolution Network) são três módulos, dos quais um é responsável pelo processamento principal (cX_AGNC), enquanto os outros dois respondem pelos sinais de controle (ForgetGate e ResetGate). Sua função é extrair informações estruturais e topológicas, bem como modelar o fluxo com base em dependências temporais e estruturais.
  • O bloco de contexto (Context) é um repositório do estado temporal, responsável por fornecer memória profunda e continuidade ao fluxo de processamento. Sua atualização ocorre segundo um princípio ponderado, baseado na interação com os gates de controle.
  • O mecanismo duplo de controle é composto por ForgetGate e ResetGate, que operam segundo o princípio de descarte e reescrita do contexto. Eles são implementados como células AGCN independentes, o que permite gerenciar com flexibilidade o estado interno do modelo.

O resultado da célula é formado com base na ativação ELU e em fatores de atenção contextual, o que garante uma transição suave, porém sensível, entre os intervalos temporais.

Essa combinação torna o GinAR verdadeiramente único. Ao contrário das redes recorrentes clássicas, ele não apenas se lembra do passado, mas também sabe estruturá-lo. Ao contrário dos modelos GCN padrão, ele não opera com uma topologia fixa, e sim a aprende dinamicamente. E, por fim, ao contrário dos transformadores generalizados, o GinAR preserva a localidade e a adaptação dinâmica, duas características vitais para a análise de séries temporais financeiras.

A visualização autoral do framework GinAR é apresentada abaixo.

Visualização autoral do framework GinAR

Este artigo serve não apenas como conclusão da parte técnica, mas também como uma espécie de demonstração da maturidade de toda a construção. Veremos como o framework se comporta na prática, quais gargalos surgem e qual é a precisão e a robustez do modelo em novos dados. E, o que é especialmente importante, avaliaremos se ele é realmente capaz de oferecer ao trader justamente a vantagem que justifica toda essa complexidade arquitetural.



Bloco GinAR

A arquitetura do GinAR é estruturada segundo um princípio hierárquico. Ela é composta por várias camadas sequenciais de GinAR e termina com um Decoder, construído com base em um perceptron multicamadas (MLP). Cada camada GinAR, por sua vez, contém um conjunto de células GinARCell, que realizam o processamento passo a passo da sequência temporal, de forma análoga à maneira como os quadros de um filme formam uma cena contínua.

Na saída de cada camada, preserva-se apenas o estado oculto final da última célula. Esses estados são reunidos em um tensor final único hⁿₐₗₗ, que contém uma representação compacta e maximamente informativa de toda a sequência analisada. É precisamente esse tensor que é enviado à entrada do MLP-Decoder, composto por duas camadas totalmente conectadas com função de ativação ReLU entre elas.

Vale dar atenção especial a um importante aspecto arquitetural relacionado à forma como o fluxo de dados é estruturado na arquitetura multicamadas do GinAR. Por si só, a aplicação sequencial de várias camadas, em que cada camada seguinte recebe como entrada a saída da anterior, é uma prática bastante padrão e se encaixa bem na lógica de um modelo linear.

No entanto, o modelo original do GinAR utiliza um esquema mais complexo: após o processamento sequencial por várias camadas, o resultado não é simplesmente a saída da última camada, mas sim uma representação unificada, obtida pela concatenação das saídas de todas as camadas. Essa abordagem permite que o Decoder utilize de uma só vez todo o espectro de atributos internos formados em diferentes níveis de abstração, o que, sem dúvida, fortalece o modelo. Ao mesmo tempo, porém, ela rompe a estrutura linear de transmissão de dados que está na base de nossas implementações e exige uma lógica adicional para coletar e unir os dados provenientes de diferentes camadas.

O objeto de alto nível usado por nós na construção de modelos originalmente não previa operações de fusão das saídas de várias camadas em um único tensor, o que torna impossível a incorporação direta dessa parte da arquitetura original do GinAR. Para contornar essa limitação, desenvolvemos um bloco GinAR especial que gerencia o processamento sequencial dos dados de entrada com a ajuda de um conjunto de células CNeuronGinARCell e, em seguida, acumula os resultados de todas as células em um único tensor. Esse tensor final é encaminhado à entrada do Decoder, reproduzindo uma característica-chave do modelo original: a integração de atributos obtidos em diferentes níveis de abstração.

Ao passar à análise da arquitetura do bloco GinAR, vale destacar uma diferença importante em relação ao modelo original. Além da estrutura básica, adicionamos uma matriz treinável de dependências predefinidas entre os componentes dos dados analisados. Ela se baseia em um mecanismo de aprendizado adaptativo, semelhante ao utilizado nas matrizes de dependências estruturais dentro de cada célula GinARCell. No entanto, a diferença central está no escopo de aplicação. Se as matrizes adaptativas são formadas separadamente em cada célula, refletindo padrões locais, a matriz introduzida por nós tem caráter global e é comum a todas as células dentro de um mesmo bloco GinAR. Isso permite fixar correlações estáveis e recorrentes entre as variáveis ao longo de toda a análise, reforçando assim a coerência estrutural do modelo.

Para implementar o bloco GinAR em nossa arquitetura, foi construída uma classe especial CNeuronGinAR, herdada de CNeuronSwiGLUOCL. Essa hierarquia permite preservar a compatibilidade com o restante do modelo e aproveitar as vantagens da ativação convolucional com a não linearidade flexível SwiGLU. No entanto, os elementos funcionais-chave e a lógica de funcionamento foram substancialmente ampliados e redefinidos. A estrutura do novo objeto é apresentada abaixo.

class CNeuronGinAR   :  public CNeuronSwiGLUOCL
  {
protected:
   CParams              cEa;
   CNeuronSwiGLUOCL     cWx;
   CNeuronSwiGLUOCL     cWe;
   CNeuronBaseOCL       cWconcat_ex;
   CNeuronConvOCL       cEn;
   CNeuronTransposeOCL  cEnT;
   CNeuronBaseOCL       cEnEnT;
   CNeuronSoftMaxOCL    cApre;
   CNeuronGinARCell     caCells[4];
   CNeuronBaseOCL       cConcat;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronGinAR(void) {};
                    ~CNeuronGinAR(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units_count, uint dimension,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronGinAR; }
   virtual void      TrainMode(bool flag) override;
   virtual void      SetOpenCL(COpenCLMy *obj);
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau);
   //---
   virtual bool      Clear(void) override;
  };

A classe CNeuronGinAR representa um bloco completo do framework GinAR, implementado em MQL5 com o uso de OpenCL. Ela é destinada ao processamento de séries temporais e à extração de padrões complexos com base em uma hierarquia de inter-relações lineares. Dentro da classe, estão concentrados de uma só vez vários componentes-chave, responsáveis pela geração e interpretação da matriz global de dependências estruturais, bem como um array de células CNeuronGinARCell, que realizam o processamento sequencial dos dados de entrada. Seus resultados são posteriormente concatenados, formando a representação final da sequência analisada.

A estrutura da classe foi construída com base nos princípios de modularidade e reutilização. Todos os elementos internos são declarados estaticamente. Isso significa que a gestão de seu ciclo de vida (criação, destruição e limpeza da memória) é realizada automaticamente, sem necessidade de intervenção explícita por parte do construtor ou do destrutor.

Para configurar todos os componentes internos, utiliza-se o método Init, que atua como um ponto único de entrada para a inicialização. Ele recebe os parâmetros que definem a arquitetura do objeto e o tipo de otimização dos parâmetros.

bool CNeuronGinAR::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                        uint units_count, uint dimension,
                        ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronSwiGLUOCL::Init(numOutputs, myIndex, open_cl, caCells.Size()*dimension,
                              caCells.Size()*dimension, dimension, units_count, 1,
                              optimization_type, batch))
      return false;

Primeiro, é chamado o método homônimo da classe pai CNeuronSwiGLUOCL, que, em nossa implementação, forma as representações agregadas dos resultados gerados pelas células GinARCell. Depois disso, os componentes recém-declarados que integram a arquitetura do bloco são inicializados em sequência.

O primeiro deles é a matriz treinável de dependências estruturais globais entre as variáveis da série temporal.

int index = 0;
if(!cEa.Init(0, index, OpenCL, Neurons(), optimization, iBatch))
   return false;
cEa.SetActivationFunction(None);

Ao contrário da abordagem original, em que a matriz de covariância predefinida é formada com base em conhecimento a priori ou em uma análise prévia da estrutura da sequência a ser prevista, em nossa implementação é usada uma matriz treinável de dependências estruturais, comum a todas as células GinARCell dentro de um mesmo bloco. Essa abordagem permite dispensar o ajuste manual ou a análise externa dos dados e proporciona uma configuração mais flexível e adaptativa das inter-relações entre as variáveis durante o treinamento.

Em seguida, inicializamos os transformadores lineares cWx e cWe. Sua função é levar os dados de entrada e a matriz de covariância a uma dimensionalidade comum antes da união em um único tensor cWconcat_ex.

   index++;
   if(!cWx.Init(0, index, OpenCL, dimension, dimension, dimension, units_count, 1,
                                                            optimization, iBatch))
      return false;
   index++;
   if(!cWe.Init(0, index, OpenCL, dimension, dimension, dimension, units_count, 1,
                                                            optimization, iBatch))
      return false;
   if(!cWconcat_ex.Init(0, index, OpenCL, 2 * Neurons(), optimization, iBatch))
      return false;
   cWconcat_ex.SetActivationFunction(None);

A camada convolucional cEn é destinada a realizar a mistura profunda dos atributos e de suas dependências.

   index++;
   if(!cEn.Init(0, index, OpenCL, 2 * dimension, 2 * dimension, dimension, units_count,
                                                              1, optimization, iBatch))
      return false;
   cEn.SetActivationFunction(None);
   index++;
   if(!cEnT.Init(0, index, OpenCL, units_count, dimension, optimization, iBatch))
      return false;
   index++;
   if(!cEnEnT.Init(0, index, OpenCL, units_count * units_count, optimization, iBatch))
      return false;
   cEnEnT.SetActivationFunction(GELU);

Os dados obtidos são primeiro transpostos e, em seguida, multiplicamos a representação original pela transposta para obter a matriz simétrica de interdependências entre as variáveis cEnEnT. Para a ativação, é usada a função GELU, que reforça as inter-relações significativas.

Depois disso, normalizamos a matriz de dependências com a função SoftMax, convertendo os dados em uma distribuição de probabilidade.

   index++;
   if(!cApre.Init(0, index, OpenCL, units_count * units_count, optimization, iBatch))
      return false;
   cApre.SetHeads(units_count);

Na etapa seguinte, é inicializado o array de células CNeuronGinARCell, cada uma das quais é responsável pelo processamento da sequência analisada em seu próprio nível hierárquico. Apesar da independência lógica, todas as células têm arquitetura idêntica, o que permite simplificar sua inicialização: basta um único loop bem estruturado.

   for(uint i = 0; i < caCells.Size(); i++)
     {
      index++;
      if(!caCells[i].Init(0, index, OpenCL, units_count, dimension, optimization, iBatch))
         return false;
     }
   index++;
   if(!cConcat.Init(0, index, OpenCL, GetWindow()*units_count, optimization, iBatch))
      return false;
//---
   return true;
  }

Os resultados gerados pelas células são agregados no objeto cConcat. E, após a inicialização bem-sucedida de todos os componentes internos, retornamos ao programa chamador o resultado lógico da execução das operações.

É importante destacar que o método Init não gerencia o ciclo de vida dos objetos listados. Sua tarefa é estritamente configurar os parâmetros e estruturar a arquitetura da camada, transferindo o controle para o ciclo de execução do modelo. Essa abordagem garante alta estabilidade, previsibilidade de comportamento e uso eficiente da memória.

Após a conclusão da etapa de inicialização, todos os componentes do bloco CNeuronGinAR estão prontos para operar na propagação para frente. É justamente aqui que se manifesta a interação entre os módulos estruturais, os estados ocultos das células e a matriz treinável de dependências. O método feedForward é responsável pela execução do ciclo completo de processamento da sequência de entrada, incluindo a construção da matriz de dependências estruturais e a geração do tensor de saída final, que será encaminhado ao próximo nível da arquitetura de rede neural.

bool CNeuronGinAR::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//--- Calculate Apre
   if(bTrain)
     {
      if(!cEa.FeedForward())
         return false;
      if(!cWe.FeedForward(cEa.AsObject()))
         return false;
     }

Na primeira etapa, ocorre a construção da matriz treinável de dependências. Se o modelo estiver em modo de treinamento (bTrain == true), primeiro é ativado o tensor de dependências estruturais globais e, em seguida, sua saída é processada pela camada de projeção linear.

Em paralelo, o fluxo principal de informações dos dados de entrada também é processado, passando pela camada de projeção linear cWx.

//---
   if(!cWx.FeedForward(NeuronOCL))
      return false;
   if(!Concat(cWx.getOutput(), cWe.getOutput(), cWconcat_ex.getOutput(), 
              cWx.GetWindowOut(), cWe.GetWindowOut(), cWx.GetUnits()))
      return false;

Em seguida, os resultados de ambas as direções são unidos em um único tensor por meio da operação Concat, cujo resultado é enviado à camada convolucional cEn.

   if(!cEn.FeedForward(cWconcat_ex.AsObject()))
      return false;
   if(!cEnT.FeedForward(cEn.AsObject()))
      return false;
   if(!MatMul(cEn.getOutput(), cEnT.getOutput(), cEnEnT.getOutput(), cEnT.GetCount(),
                                                  cEnT.GetWindow(), cEnT.GetCount()))
      return false;
   if(cEnEnT.Activation() != None)
      if(!Activation(cEnEnT.getOutput(), cEnEnT.getOutput(), cEnEnT.Activation()))
         return false;

Na sequência, o tensor obtido é transposto (cEnT) e, depois, realiza-se a multiplicação matricial do original por sua versão transposta. Nessa etapa, forma-se de fato a matriz de covariância, à qual, se necessário, é adicionada uma não linearidade com o uso da função de ativação definida.

A matriz de covariância final é construída na camada cApre, em que os dados são convertidos em uma representação probabilística por meio da função SoftMax.

if(!cApre.FeedForward(cEnEnT.AsObject()))
   return false;
if(!IdentSum(cApre.getOutput(), cApre.getOutput(), cApre.Heads()))
   return false;

Para garantir a robustez e um nível básico de autodependência (self-connection), essa matriz é complementada por uma matriz diagonal unitária. Ou seja, cada variável recebe não apenas informações sobre suas inter-relações com as demais, mas também preserva uma orientação básica para si mesma. Como resultado, obtém-se uma estrutura equilibrada de pesos de atenção, capaz de considerar tanto as dependências globais quanto a relevância local de cada elemento. É justamente essa matriz final que, em seguida, é encaminhada à entrada de cada uma das células GinARCell, atuando como um mecanismo universal de reforço seletivo dos sinais.

Em seguida, começa o processamento principal da sequência de entrada por meio do array de células. Na entrada da primeira célula, são fornecidos os dados analisados, recebidos do programa externo. E cada célula subsequente analisa os resultados gerados pela anterior, elevando o nível de detalhamento da representação latente. Dessa forma, forma-se uma sequência de representações ocultas que reflete a natureza hierárquica da análise da série temporal.

//--- GimAR Cells
   CNeuronBaseOCL *temp = NeuronOCL;
   for(uint i = 0; i < caCells.Size(); i++)
     {
      if(!caCells[i].FeedForward(temp, cApre.getOutput()))
         return false;
      temp = caCells[i].AsObject();
     }
//---
   if(!Concat(caCells[0].getOutput(), caCells[1].getOutput(), caCells[2].getOutput(),
              caCells[3].getOutput(), cConcat.getOutput(), GetWindow() / 4,
              GetWindow() / 4, GetWindow() / 4, GetWindow() / 4, GetUnits()))
      return false;
//---
   return CNeuronSwiGLUOCL::feedForward(cConcat.AsObject());
  }

A etapa final do método consiste em unir as saídas de todas as células em um único tensor comum (cConcat), que depois é enviado à entrada do método homônimo da classe pai CNeuronSwiGLUOCL para processamento adicional. Essa estrutura assegura uma passagem consistente e eficiente das informações através do bloco GinAR, aproximando ao máximo o comportamento do sistema da arquitetura original, apesar das limitações arquiteturais do framework utilizado.

Após concluir a propagação para frente e o cálculo dos resultados do bloco, passamos à segunda etapa-chave: a propagação do gradiente de erro (Backpropagation). É justamente aqui que começa a verdadeira engenharia reversa da previsão: o erro vindo do Decoder retorna a cada elemento da arquitetura com o objetivo de ajustar com precisão os pesos e otimizar o modelo.

O método calcInputGradients é responsável por implementar a propagação reversa completa em toda a arquitetura do bloco, começando pela saída e terminando nos dados de entrada. Como a estrutura do bloco é complexa e consiste tanto em operações sequenciais quanto em operações de ramificação e fusão de tensores, a propagação reversa exige uma execução em etapas e estritamente coordenada.

bool CNeuronGinAR::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
//---
   if(!CNeuronSwiGLUOCL::calcInputGradients(cConcat.AsObject()))
      return false;
   if(!DeConcat(caCells[0].getPrevOutput(), caCells[1].getPrevOutput(),
                caCells[2].getPrevOutput(), caCells[3].getGradient(),
                cConcat.getGradient(), GetWindow() / 4,
                GetWindow() / 4, GetWindow() / 4,
                GetWindow() / 4, GetUnits()))
      return false;

O método começa com a chamada do método homônimo da classe pai CNeuronSwiGLUOCL, que transmite o gradiente ao tensor concatenado dos resultados gerados pelas células internas, cConcat. Como, durante a propagação para frente, esse tensor foi formado pela concatenação das saídas de quatro GinAR células, agora é necessário redistribuir corretamente o gradiente de volta entre os quatro fluxos de informação. Com a ajuda do método DeConcat, transmitimos a cada célula o gradiente de erro de acordo com sua influência sobre o resultado final.

Cabe destacar especialmente um ponto importante relacionado à lógica de propagação do gradiente de erro dentro do bloco GinAR. Apesar da simetria externa das células, sua carga funcional durante o treinamento não é a mesma.

Em primeiro lugar, apenas a última célula é usada exclusivamente para formar o tensor final de resultados do bloco por meio da concatenação. Todas as células anteriores têm dupla finalidade: suas saídas não apenas participam da concatenação, mas também servem como dados de entrada para a GinARCell seguinte na hierarquia. Isso significa que o gradiente de erro deve chegar a cada uma dessas células a partir de duas fontes. Portanto, ao calcular o gradiente das três primeiras células, reunimos o gradiente das duas direções de uso dos dados e os somamos corretamente. Isso assegura a continuidade do fluxo de erro e uma retroalimentação precisa em cada nível da hierarquia.

//--- GimAR Cells
   cApre.getGradient().Fill(0);
   for(uint i = caCells.Size() - 1; i > 0; i--)
      if(!caCells[i - 1].CalcHiddenGradients(caCells[i].AsObject(), cApre.getOutput(),
                                             cApre.getPrevOutput(),
                                             (ENUM_ACTIVATION)cApre.Activation()) ||
         !SumAndNormilize(caCells[i - 1].getGradient(), caCells[i - 1].getPrevOutput(),
                          caCells[i - 1].getGradient(), cWx.GetWindow(), false, 0, 0, 0, 1) ||
         !SumAndNormilize(cApre.getGradient(), cApre.getPrevOutput(),
                          cApre.getGradient(), GetUnits(), false, 0, 0, 0, 1))
         return false;
   if(!NeuronOCL.CalcHiddenGradients(caCells[0].AsObject(), cApre.getOutput(),
                                     cApre.getPrevOutput(), (ENUM_ACTIVATION)cApre.Activation()) ||
      !SumAndNormilize(cApre.getGradient(), cApre.getPrevOutput(),
                       cApre.getGradient(), GetUnits(), false, 0, 0, 0, 1))
      return false;

Em segundo lugar, uma situação ainda mais sutil surge ao calcular o gradiente da matriz global de covariância, representada no objeto cApre. Essa matriz foi passada simultaneamente para todas as quatro GinARCell. Portanto, durante a propagação reversa, seu gradiente deve acumular informações provenientes de quatro direções independentes, uma de cada célula. Isso exige a adição sequencial de todos os gradientes recebidos, com preservação de forma e escala consistentes. Erros nessa parte podem levar a saltos bruscos nos pesos e, como consequência, à instabilidade do treinamento.

Em seguida, começa a propagação reversa através do bloco de construção da matriz de dependências. Primeiro, calcula-se o gradiente de cEnEnT, após o que os valores são ajustados pela derivada da função de ativação correspondente.

//---
   if(!cEnEnT.CalcHiddenGradients(cApre.AsObject()))
      return false;
   if(cEnEnT.Activation() != None)
      if(!DeActivation(cEnEnT.getOutput(), cEnEnT.getGradient(),
                       cEnEnT.getGradient(), cEnEnT.Activation()))
         return false;
   if(!MatMulGrad(cEn.getOutput(), cEn.getPrevOutput(),
                  cEnT.getOutput(), cEnT.getGradient(),
                  cEnEnT.getGradient(), cEnT.GetCount(),
                  cEnT.GetWindow(), cEnT.GetCount()))
      return false;
   if(!cEn.CalcHiddenGradients(cEnT.AsObject()))
      return false;
   if(!SumAndNormilize(cEn.getGradient(), cEn.getPrevOutput(),
                       cEn.getGradient(), cEnT.GetWindow(), false, 0, 0, 0, 1))
      return false;
   if(cEn.Activation() != None)
      if(!DeActivation(cEn.getOutput(), cEn.getGradient(),
                       cEn.getGradient(), cEn.Activation()))
         return false;

Depois disso, calcula-se o gradiente da operação de multiplicação matricial MatMulGrad, distribuindo o erro entre a camada convolucional cEn e sua cópia transposta cEnT. Ao mesmo tempo, o gradiente de erro da representação transposta também é retropropagado até o nível da camada convolucional, e somamos os valores obtidos a partir dos dois fluxos de informação. Esse esquema permite reunir uma representação diferenciável completa da matriz simétrica de covariância, preservando assim a precisão e a integridade da transmissão do erro mesmo dentro de uma topologia complexa.

Os valores obtidos são então retropropagados até o nível do tensor cWconcat_ex.

if(!cWconcat_ex.CalcHiddenGradients(cEn.AsObject()))
   return false;
if(!DeConcat(cWx.getGradient(), cWe.getGradient(), cWconcat_ex.getGradient(),
             cWx.GetWindowOut(), cWe.GetWindowOut(), cWx.GetUnits()))
   return false;

A saída de cWconcat_ex foi formada a partir dos resultados de cWx e cWe. Agora, seus gradientes são novamente separados, passando pela possível desativação. Depois disso, inicia-se a propagação reversa de cWx até o objeto de dados de entrada NeuronOCL.

No entanto, aqui vale lembrar que, anteriormente, o gradiente de erro da primeira célula já havia sido transmitido ao nível dos dados de entrada. Por isso, usaremos o recurso de substituição dos ponteiros para os buffers de dados, seguido pela soma dos valores obtidos a partir dos dois fluxos de informação.

   if(cWx.Activation() != None)
      if(!DeActivation(cWx.getOutput(), cWx.getGradient(),
                       cWx.getGradient(), cWx.Activation()))
         return false;
   if(cWe.Activation() != None)
      if(!DeActivation(cWe.getOutput(), cWe.getGradient(),
                       cWe.getGradient(), cWe.Activation()))
         return false;
   CBufferFloat* temp = NeuronOCL.getGradient();
   if(!NeuronOCL.SetGradient(NeuronOCL.getPrevOutput(), false))
      return false;
   if(!NeuronOCL.CalcHiddenGradients(cWx.AsObject()))
      return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), temp,
                       NeuronOCL.getGradient(), cWx.GetWindow(), false, 0, 0, 0, 1))
      return false;
   if(!NeuronOCL.SetGradient(temp, false))
      return false;

O método é concluído com a transmissão do gradiente de erro ao nível da matriz de dependências globais cEa.

   if(!cEa.CalcHiddenGradients(cWe.AsObject()))
      return false;
//---
   return true;
  }

E, após a conclusão bem-sucedida de todas as iterações, retornamos ao programa chamador o resultado lógico da execução do método.

Assim, o método calcInputGradients assegura a propagação correta do gradiente por toda a estrutura do bloco GinAR. A arquitetura da propagação reversa reflete com exatidão a estrutura da propagação para frente, como em um espelho, o que é especialmente importante ao utilizar mecanismos complexos de atenção e componentes aninhados.

O código completo dessa classe e de todos os seus métodos é apresentado no anexo e está disponível para estudo independente.



Arquitetura do modelo

Após a criação de todos os componentes necessários para a construção do framework GinAR, passamos à etapa seguinte: a descrição da arquitetura dos modelos treináveis. Aqui, a lógica de construção vai muito além da simples previsão de séries temporais: nossa tarefa é construir um agente de trading completo, no qual o framework GinAR desempenha o papel de Encoder do estado do ambiente.

Cada componente do modelo responde por uma função estritamente definida. Encoder é responsável por extrair e generalizar informações a partir dos dados históricos. Os módulos de previsão, divididos em três modelos paralelos, realizam a previsão em diferentes horizontes temporais. Em seguida, vêm o Actor e o Critic, blocos correspondentes à arquitetura de Reinforcement Learning: o primeiro gera as ações de trading, enquanto o segundo avalia a eficácia esperada do comportamento do modelo no estado atual do mercado.

Para descrever a arquitetura, utiliza-se o método CreateDescriptions, que forma as configurações de todos os módulos. Na primeira etapa, o método inicializa os contêineres de cada modelo: Encoder, módulos de previsão, Actor e Critic. Esses contêineres são implementados como arrays de objetos que descrevem as camadas neurais.

bool CreateDescriptions(CArrayObj *&encoder,
                        CArrayObj *&forecast1,
                        CArrayObj *&forecast2,
                        CArrayObj *&forecast3,
                        CArrayObj *&actor,
                        CArrayObj *&critic
                       )
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!forecast1)
     {
      forecast1 = new CArrayObj();
      if(!forecast1)
         return false;
     }
   if(!forecast2)
     {
      forecast2 = new CArrayObj();
      if(!forecast2)
         return false;
     }
   if(!forecast3)
     {
      forecast3 = new CArrayObj();
      if(!forecast3)
         return false;
     }
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!critic)
     {
      critic = new CArrayObj();
      if(!critic)
         return false;
     }

Após a inicialização, começa a formação da estrutura dos modelos. Encoder começa com uma camada totalmente conectada básica, que simplesmente recebe os dados históricos na entrada do modelo.

//--- Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   uint prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormWithNoise;
   descr.count = prev_count;
   descr.batch = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Em seguida, vem uma camada de normalização em lote com adição de ruído, cuja função é estabilizar a distribuição dos dados de entrada e introduzir no sistema uma augmentação das representações para aumentar a robustez do modelo ao overfitting.

Depois disso, são adicionados canais de atributos diferenciais entre barras adjacentes. Isso permite ao sistema capturar não apenas os valores absolutos, mas também a direção e a intensidade das mudanças. Isso é especialmente importante em condições de comportamento de mercado de alta frequência e irregular.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatDiff;
   prev_count = descr.count = HistoryBars;
   descr.layers = BarDescr;
   descr.step = 1;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

A camada seguinte é responsável por enriquecer as representações com harmônicos das marcas temporais.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defMamba4CastEmbeding;
   prev_count = descr.count = HistoryBars;
   descr.window = 2 * BarDescr;
   uint prev_out = descr.window_out = NSkills;
     {
      uint temp[] = {PeriodSeconds(PERIOD_H1), PeriodSeconds(PERIOD_D1)};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Os dados preparados passam então por uma camada de transposição, que altera os eixos da representação: as sequências temporais transformam-se em características espaciais e são enviadas à entrada do filtro convolucional. A camada convolucional com ativação tanh destaca os padrões-chave e as dependências estruturais nos embeddings, obtendo assim uma representação comprimida e concentrada dos padrões ocultos do mercado.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   prev_count = descr.window = prev_out;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_out = descr.count;
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.variables = 1;
   prev_out = descr.window_out = EmbeddingSize;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

A culminação do Encoder é a camada que implementa o framework GinAR. É justamente aqui que todos os atributos obtidos anteriormente são reunidos, e sua interpretação ocorre por meio da hierarquia de células GinAR. Elas operam de forma sincronizada, usando uma matriz de covariância treinável comum para modelar as conexões estruturais ocultas nos dados. Essa camada não apenas transmite a informação adiante, mas forma uma representação ativa do estado do mercado, que já pode ser considerada pronta para uso na tomada de decisões de trading.

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronGinAR;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Assim, as abordagens do framework GinAR são incorporadas à arquitetura do modelo de forma sequencial, lógica e sem excessos. O estado oculto final formado pelo Encoder representa uma descrição de alto nível da situação de mercado, adequada tanto para previsão quanto para a geração de decisões de trading.

Para o treinamento do Encoder do estado do ambiente aplica-se uma estratégia de propagação de ponta a ponta do gradiente de erro a partir de várias subsistemas independentes ao mesmo tempo. Em primeiro lugar, temos três modelos de previsão, cada um especializado em projetar a continuação da série temporal em seu próprio horizonte de planejamento. Esses módulos analisam o mesmo estado oculto formado pelo Encoder, mas o interpretam de maneiras diferentes, dependendo do horizonte de planejamento definido. Com isso, alcança-se uma compreensão multiescala da situação atual do mercado.

Além deles, também entra em cena o Actor, componente responsável por gerar a decisão de trading. Ele se apoia na mesma representação compacta do estado do ambiente e já a interpreta como um guia para a ação. Assim, o Encoder fica sob pressão simultânea de quatro frentes: sua saída deve ser, ao mesmo tempo, informativa para a previsão e adequada para a aplicação prática em condições de incerteza de mercado.

É importante destacar que a arquitetura de todos esses modelos foi integralmente herdada do trabalho anterior. Por isso, no âmbito do estudo atual, não vamos nos deter em sua descrição detalhada. Toda a atenção está concentrada na parte nova: a integração do framework GinAR à composição do sistema treinável.

O código completo de todos os componentes, incluindo as configurações das camadas, é apresentado no anexo.



Treinamento

Tendo concluído a construção de todos os componentes e a configuração da arquitetura dos modelos treináveis, passamos à etapa final e, provavelmente, a mais responsável: o treinamento dos modelos. No âmbito do nosso sistema, usamos uma abordagem combinada que reúne as vantagens dos modos offline e online de treinamento. Isso permite não apenas garantir uma preparação inicial estável do modelo, mas também adaptar seu comportamento às condições específicas de trading, incluindo mudanças na fase de mercado, no nível de volatilidade e na frequência de surgimento dos sinais de trading.

O processo de treinamento, como nos trabalhos anteriores, é implementado em duas etapas. A primeira etapa é o treinamento offline. Aqui, os modelos são treinados com base em dados históricos. Além disso, usamos uma abordagem sem preparação prévia do conjunto de treinamento. Em vez disso, formamos o estado do ambiente diretamente a partir dos dados históricos do terminal e, em paralelo, modelamos o estado da conta. Essa abordagem oferece alta flexibilidade e permite usar quaisquer trechos históricos sem necessidade de preparar manualmente os datasets.

A segunda etapa é o ajuste online. Ela já ocorre em condições o mais próximas possível da negociação real no testador de estratégias do MetaTrader 5. O modelo continua aprendendo ao se deparar com novos cenários de mercado, antes não encontrados, e se adapta a eles em tempo real. Essa fase permite eliminar desalinhamentos residuais acumulados durante o treinamento offline e preparar o sistema para o uso autônomo pleno em uma conta real.

É importante observar que o treinamento em duas etapas não é um conceito novo no âmbito das nossas pesquisas. Já aplicamos essa estratégia com sucesso em trabalhos anteriores. No entanto, no projeto atual, na fase de treinamento offline, foram introduzidas mudanças de importância fundamental, motivadas tanto por considerações teóricas quanto pela experiência prática de implementação dos modelos anteriores.

Você provavelmente percebeu que, na arquitetura atual, está ausente o componente chamado Diretor. Vale lembrar que, em vários dos trabalhos mais recentes, o Diretor era usado como um modelo avaliativo adicional, operando em paralelo ao Critic. Sua função principal consistia na classificação binária das ações do Agente em lucrativas e deficitárias. Essa forma rígida de retroalimentação mostrou bons resultados nas fases iniciais do treinamento, ajudando a acelerar a adaptação inicial da política.

Ainda assim, na implementação atual seguimos por outro caminho. Decidimos combinar duas abordagens de aprendizado, aprendizado por reforço e aprendizado supervisionado, diretamente na fase de treinamento offline. Essa decisão se mostrou mais elegante e também mais eficaz, tanto na qualidade quanto na direção do sinal de treinamento.

A essência da mudança é a seguinte: em paralelo com a avaliação das ações atuais do Agente, realizada pelo Critic, fornecemos ao Actor ações de referência formadas com base na análise do movimento futuro dos preços. Como, na fase de treinamento offline, temos acesso ao trecho completo da série temporal, incluindo o movimento posterior dos preços, podemos calcular a chamada trajetória quase ideal de ações. Essas ações, formadas a posteriori, não apenas indicam a direção, mas se tornam uma espécie de referência para a qual a política do Actor busca convergir.

Assim, implementamos um mecanismo em que o Agente aprende tanto com seus próprios erros quanto com o exemplo de estratégias lucrativas previamente calculadas. Isso permite abandonar a retroalimentação binária rígida do Diretor sem perder seu efeito de treinamento. Em contrapartida, a qualidade do sinal de orientação aumenta, e o aprendizado de uma política lucrativa se torna mais controlável e estável.

A abordagem proposta foi implementada no método Train do Expert Advisor "…\MQL5\Experts\GinAR\Study.mq5". O algoritmo do método representa a etapa central do treinamento offline, na qual se combinam métodos de aprendizado por reforço e aprendizado supervisionado. Em sua base está a simulação sequencial de situações de mercado com base em dados históricos e o treinamento da estratégia de trading a partir da análise tanto dos estados atuais quanto das trajetórias idealizadas de decisões de trading.

Tudo começa com a preparação do conjunto de treinamento: a partir do histórico de cotações, seleciona-se o intervalo definido pelo usuário, e, para cada trecho temporal, são calculados os indicadores técnicos analisados. Ao mesmo tempo, controlamos que todos os indicadores tenham sido calculados e estejam prontos para uso; caso contrário, a execução é interrompida.

void Train(void)
  {
   int start = iBarShift(Symb.Name(), TimeFrame, Start);
   int end = iBarShift(Symb.Name(), TimeFrame, End);
   int bars = CopyRates(Symb.Name(), TimeFrame, 0, start, Rates);
//---
   if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) ||
      !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
//---
   int count = -1;
   bool calculated = false;
   do
     {
      count++;
      calculated = (RSI.BarsCalculated() >= bars &&
                    CCI.BarsCalculated() >= bars &&
                    ATR.BarsCalculated() >= bars &&
                    MACD.BarsCalculated() >= bars
                   );
      Sleep(100);
      count++;
     }
   while(!calculated && count < 100);
   if(!calculated)
     {
      PrintFormat("%s -> %d The training data has not been loaded", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
//---
   if(!ArraySetAsSeries(Rates, true))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
   bars -= end + HistoryBars + NForecast;
   if(bars < 0)
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }

São formados buffers que serão usados para armazenar os estados de entrada, as marcas temporais e os valores-alvo.

Em seguida, começa o ciclo principal de treinamento. O treinamento é organizado na forma de uma série de épocas, ou seja, passagens completas pelo histórico selecionado. Dentro de cada época, é realizada a simulação sequencial de situações de mercado.

//---
   vector<float> result, target, neg_target;
   bool Stop = false;
//---
   uint ticks = GetTickCount();
//---
   for(int epoch = 0; (epoch < Epochs && !IsStopped() && !Stop); epoch ++)
     {
      if(!cEncoder.Clear())
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         ExpertRemove();
         return;
        }
      for(int posit = start - HistoryBars - NForecast - 1; posit >= end; posit--)
        {
         if(!CreateBuffers(posit, GetPointer(bState), GetPointer(bTime), Result))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            ExpertRemove();
            return;
           }
         const vector<float> account = SampleAccount(GetPointer(bState), datetime(bTime[0]));
         const vector<float> target_action = OraculAction(account, Result);
         if(!bAccount.AssignArray(account))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            ExpertRemove();
            return;
           }

Em cada etapa, forma-se o estado atual do mercado, representado na forma de um vetor numérico, e determina-se a escala temporal correspondente. Esses dados são fornecidos à entrada do Encoder, que transforma o estado em uma representação oculta compacta, uma espécie de quintessência do quadro de mercado.

//--- Feed Forward
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false,
                                   (CBufferFloat*)GetPointer(bTime)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
 }

O vetor latente obtido é usado imediatamente em três direções. Por um lado, ele é encaminhado a três modelos de previsão, cada um responsável por seu próprio horizonte de planejamento: curto prazo, médio prazo e longo prazo. Esses modelos geram suas previsões para os períodos mais próximos.

for(uint f = 0; f < caForecast.Size(); f++)
   if(!caForecast[f].feedForward(GetPointer(cEncoder), -1, (CBufferFloat*)NULL))
     {
      PrintFormat("%s -> %d - Forecast %d", __FUNCTION__, __LINE__, f);
      Stop = true;
      break;
     }

Por outro lado, a mesma representação oculta é encaminhada ao Actor, responsável pela tomada da decisão de trading. Com base no estado do mercado e no estado atual da conta, ele forma um tensor de ações.

if(!cActor.feedForward(GetPointer(bAccount), 1, false, GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

A decisão tomada é avaliada pelo Critic, que determina o quão razoável ela é nessas condições.

if(!cCritic.feedForward(GetPointer(cActor), -1, GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Após a realização da propagação para frente, precisamos fornecer retroalimentação aos modelos. Primeiro, são ajustados os parâmetros dos modelos de previsão.

//--- Study
for(uint f = 0; f < caForecast.Size(); f++)
   if(!caForecast[f].backProp(Result, (CBufferFloat*)NULL) ||
     !cEncoder.backPropGradient((CBufferFloat*)NULL))
     {
     PrintFormat("%s -> %d - Forecast %d", __FUNCTION__, __LINE__, f);
      Stop = true;
      break;
     }

A avaliação das ações do Actor é construída com base no cálculo da recompensa: a variação do capital, normalizada em relação ao saldo de referência. Em caso de prejuízo, a penalização é duplicada, para tornar a retroalimentação mais rígida e convincente. A recompensa obtida é transmitida ao Critic. E, a partir dele, o gradiente de erro é retropropagado até o Actor e o Encoder.

//---
cActor.getResults(Action);
double equity = bAccount[2] * bAccount[0] * EtalonBalance / (1 + bAccount[1]);
double reward = CheckAction(Action, equity, posit - NForecast + 1) / EtalonBalance;
if(reward < 0)
   reward *= 2;
Result.Clear();
if(!Result.Add(float(reward)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cCritic.backProp(Result, GetPointer(cEncoder), LatentLayer) ||
  !cActor.backPropGradient(GetPointer(cEncoder), LatentLayer, -1, false) ||
  !cEncoder.backPropGradient((CBufferFloat*)NULL, NULL, LatentLayer, true))
 {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
  break;
  }

É assim que ocorre o primeiro aprendizado: com base na reação real do mercado à ação escolhida.

Mas a execução não termina aí. Entra em cena a segunda parte da abordagem: o aprendizado supervisionado. Em vez de se orientar apenas pela resposta real do mercado, o sistema usa uma informação que não está disponível no trading real: o conhecimento do futuro. Para cada estado, calcula-se uma trajetória de referência quase ideal, isto é, a ação que teria levado ao melhor resultado, a julgar pelo movimento posterior do preço. Essa ação é transmitida ao Actor, e ele a compara com sua própria política. Dessa forma, treinamos a estratégia não apenas pelo método de tentativa e erro, mas também lhe damos uma referência clara à qual ela deve se alinhar. Esse esquema em duas camadas, controle factual e idealizado, acelera o treinamento e torna o comportamento do modelo mais robusto e racional.

//--- Oracul
if(!Action.AssignArray(target_action))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
reward = CheckAction(Action, equity, posit - NForecast + 1) / EtalonBalance;
if(!cActor.backProp(Action, GetPointer(cEncoder), LatentLayer) ||
   !cEncoder.backPropGradient((CBufferFloat*)NULL, NULL, LatentLayer, true))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

O uso da ação de referência não se limita ao treinamento do Actor. Também encaminhamos essa ação à entrada do Critic que, diferentemente da primeira passagem, avalia um comportamento sabidamente correto. Após a propagação para frente, com essa ação idealizada, é realizada a propagação reversa do erro até o Encoder. Isso permite ao Critic captar com mais precisão a estrutura da função de recompensa, obter informações adicionais sobre aquelas regiões do espaço de estados em que o comportamento do Agente deve ser especialmente preciso e, como consequência, aumentar sua eficiência no treinamento posterior do Actor.

if(!cCritic.feedForward(Action, 1, false, GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!Result.Update(0, float(reward)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cCritic.backProp(Result, GetPointer(cEncoder), LatentLayer) ||
   !cEncoder.backPropGradient((CBufferFloat*)NULL, NULL, LatentLayer, true))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
//---

Assim, conclui-se o ciclo completo de treinamento para um estado. Além disso, o treinamento ocorre simultaneamente por duas trajetórias: pela via do comportamento real e pela via da referência. Esse duplo circuito permite não apenas reduzir o erro mais rapidamente, mas também formar representações internas resistentes ao ruído do mercado e às flutuações locais.

Ao longo de todo o procedimento, em uma frequência predefinida, um relatório visual é atualizado no terminal. Na tela, são exibidos os erros médios de todos os modelos. Esses dados dão ao desenvolvedor a possibilidade de acompanhar, em tempo real, o progresso do treinamento, avaliar a estabilidade da execução e identificar possíveis falhas ou anomalias.

    if(GetTickCount() - ticks > 500)
      {
       double percent = (epoch + 1.0 - double(posit - end) / 
                        (start - end - HistoryBars - NForecast))
                        / Epochs * 100.0;
       string str = "";
       for(uint f = 0; f < caForecast.Size(); f++)
          str += StringFormat("%-12s%d %6.2f%% -> Error %15.8f\n", "Forecast", f,
                                 percent, caForecast[f].getRecentAverageError());
       str += StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Actor", percent, 
                                                 cActor.getRecentAverageError());
       str += StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Critic", percent, 
                                                cCritic.getRecentAverageError());
       Comment(str);
       ticks = GetTickCount();
      }
   }
}

Quando o treinamento em todas as épocas é concluído, o algoritmo faz o balanço final e mostra o erro médio de cada um dos modelos. Depois disso, o Expert Advisor é desativado.

   Comment("");
//---
   for(uint f = 0; f < caForecast.Size(); f++)
      PrintFormat("%s -> %d -> %-15s%d %10.7f", __FUNCTION__, __LINE__, "Forecast", f, 
                                               caForecast[f].getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Actor", 
                                                      cActor.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, 
                                            "Critic",cCritic.getRecentAverageError());
   ExpertRemove();
//---
  }

No fim, o método Train demonstra como é possível construir o treinamento de uma estratégia de trading apoiando-se ao mesmo tempo nos resultados reais das ações e em referências ideais obtidas do futuro. Essa arquitetura garante não apenas a adaptação aos dados passados, mas um movimento direcionado rumo ao lucro potencial.

O código completo desse Expert Advisor, assim como de todos os programas usados na preparação do artigo, é apresentado no anexo.



Teste

Como já foi mencionado, todo o treinamento do modelo foi estruturado em duas etapas sequenciais. Na primeira delas, utilizamos treinamento offline realizado com dados históricos do par EURUSD no timeframe H1 ao longo de todo o ano de 2024. Esse período abrange um amplo espectro de cenários de mercado, desde longos movimentos laterais até movimentos direcionais rápidos, desde períodos apáticos e lentos até explosões de volatilidade. Essa diversidade permitiu que o modelo entrasse em contato com situações típicas e atípicas, o que é criticamente importante para sua robustez e universalidade.

Durante o treinamento, o Encoder aprendeu a extrair, das informações de mercado, padrões recorrentes e a compactá-los em uma representação compacta, mas rica em atributos. Esse estado interno do mercado tornou-se a base sobre a qual o Actor, usando a retroalimentação do Critic, formou uma estratégia robusta, capaz de operar com eficiência em diferentes condições. Além disso, na etapa de treinamento offline, foram fornecidas a ele as chamadas trajetórias quase ideais, ações de referência formadas com base no conhecimento do movimento futuro dos preços. Essas sinalizações não apenas direcionavam a política para o aumento do lucro, como também ofereciam uma referência confiável na formação da estratégia, especialmente nas etapas iniciais do treinamento, quando a experiência do próprio modelo ainda era limitada.

Após a conclusão do treinamento offline, passamos à segunda etapa, o ajuste fino online, já realizado em condições próximas às do mercado real. O treinamento foi conduzido no testador de estratégias do MetaTrader 5, em que o modelo, passo a passo, candle a candle, analisava o mercado em modo de fluxo. Isso não apenas permitiu testar a robustez do modelo diante de ruído, distorções de mercado e oscilações aleatórias, como também se tornou uma importante ferramenta de adaptação: o modelo não apenas memorizava, mas realmente aprendia a operar em condições reais e imprevisíveis. Essa abordagem aumentou substancialmente sua resiliência, reduziu o overfitting e melhorou sua capacidade de generalização.

A etapa final foi o teste do modelo em dados totalmente novos, cotações referentes ao período de janeiro a março de 2025. Todos os parâmetros e ajustes internos usados durante o treinamento foram mantidos sem alterações. Assim, os resultados obtidos permitem avaliar objetivamente não apenas a precisão, mas também a robustez prática da abordagem proposta.

Os resultados do teste do modelo permitem obter uma visão objetiva de sua eficácia real e de sua robustez fora da amostra de treinamento. O depósito inicial de $100 foi elevado para $1087.74, o que equivale a um aumento de capital de mais de 10 vezes, um resultado que, à primeira vista, impressiona. No entanto, ao analisar mais a fundo, torna-se perceptível uma característica marcante: o modelo demonstra alta eficiência no início do período de teste, mas depois, à medida que se afasta da amostra de treinamento, seu desempenho começa a cair.

O gráfico de saldo reflete claramente esse efeito. O primeiro terço do período é acompanhado por um crescimento rápido, quase uma trajetória de referência, com drawdowns mínimos e aumento constante. No entanto, a partir de meados de fevereiro e, sobretudo, em março, a curva entra em um platô e até apresenta sinais de deterioração: aumentam as oscilações, tornam-se mais frequentes as séries de operações deficitárias e o ritmo de lucro cai de forma visível.

Esse efeito também é confirmado pelas métricas numéricas. O fator de recuperação (Recovery Factor) é inferior a 1 (0.96), o que indica uma relação não ideal entre lucro e profundidade do drawdown. O drawdown relativo por equity atinge 65.59%, um nível de risco bastante elevado, especialmente considerando que o drawdown absoluto no início do teste era praticamente nulo. O fator de lucro (Profit Factor) é de 1.12, valor minimamente aceitável para uma estratégia que pretende operar com robustez. O equilíbrio entre operações lucrativas e deficitárias também é desfavorável ao modelo: a proporção de trades lucrativos fica abaixo de 48%.

Esse comportamento é explicado de forma lógica pela limitação da amostra de treinamento. O modelo foi treinado apenas com dados de 2024 e, embora eles contemplassem diferentes fases de mercado, o mercado continua sendo um organismo vivo e dinâmico. À medida que se afasta do período conhecido, o modelo se depara cada vez mais com situações que não conhece. Sem episódios adicionais de treinamento, seu comportamento se torna cada vez menos relevante, especialmente em novas condições de volatilidade.

Um dos caminhos evidentes para resolver esse problema é ampliar a amostra de treinamento. A inclusão de dados de anos anteriores, por exemplo, de 2020 ou até mesmo de 2015, pode permitir que o modelo cubra um espectro muito mais amplo de cenários de mercado. Isso garantirá não apenas maior capacidade de generalização, mas também uma melhor compreensão de padrões de comportamento do preço raros, porém criticamente importantes. Outra alternativa pode ser o treinamento online gradual com atualização periódica dos pesos do modelo à medida que novos dados chegam, o que permitirá manter sua atualidade sem necessidade de retreinamento completo.



Considerações finais

Ao longo deste trabalho, demonstramos a aplicabilidade prática do framework GinAR em condições reais de mercado. Da construção da arquitetura e do treinamento em etapas até o teste final, todo o percurso confirmou a viabilidade da abordagem proposta.

Apesar dos indicadores elevados no início do período de teste, a dinâmica posterior revelou limitações naturais do modelo, relacionadas à estreiteza da amostra de treinamento. Isso ressalta um princípio fundamental do trabalho com séries temporais: a robustez da estratégia depende diretamente de sua capacidade de generalizar o conhecimento para além do contexto de treinamento.

Os resultados obtidos oferecem uma base sólida para pesquisas e aperfeiçoamentos futuros. Em particular, uma direção evidente de desenvolvimento é a ampliação do volume de dados de treinamento e a implementação de um mecanismo de aprendizado contínuo. Tudo isso abre perspectivas para a construção de sistemas de trading verdadeiramente adaptativos e inteligentes, capazes não apenas de sobreviver, mas também de gerar lucros de forma estável em condições de mercado complexas.


Links


Programas utilizados no artigo

#NomeTipoDescrição
1Study.mq5Expert AdvisorEA de treinamento offline dos modelos
2StudyOnline.mq5 Expert Advisor EA de treinamento online dos modelos
3Test.mq5Expert AdvisorExpert Advisor para testar o modelo
4Trajectory.mqhBiblioteca de classeEstrutura de descrição do estado do sistema e da arquitetura dos modelos
5NeuroNet.mqhBiblioteca de classeBiblioteca de classes para criação de redes neurais
6NeuroNet.clBibliotecaBiblioteca de código de programas OpenCL

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/18914

Arquivos anexados |
MQL5.zip (2935.06 KB)
Últimos Comentários | Ir para discussão (8)
Andrew_7543
Andrew_7543 | 8 ago. 2025 em 12:51
sps) de fato, e o que mais restou - foi, portanto, o caminho...
Andrew_7543
Andrew_7543 | 8 ago. 2025 em 16:05
Dmitriy Gizlyk MathPow por ::MathPow, o que permitirá acessar as funções do compilador em vez das declaradas na classe.
Além disso, os mesmos problemas no módulo NeuroNet.mqh

 call resolves to 'bool CNeuronPSBlock::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,float,ENUM_OPTIMIZATION,uint)' instead of 'bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 3513 25
   see declaration of function 'CNeuronPSBlock::Init' NeuroNet.mqh 48451 22
   see declaration of function 'CNeuronConvSAMOCL::Init' NeuroNet.mqh 11308 22
no one of the overloads can be applied to the function call NeuroNet.mqh 21835 18
could be one of 2 function(s) NeuroNet.mqh 21835 18
   bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 11308 22
   bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,float,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 11309 22
call resolves to '<NA>' instead of 'bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 21835 18
   see declaration of function 'CNeuronConvOCL::Init' NeuroNet.mqh 10973 22
no one of the overloads can be applied to the function call NeuroNet.mqh 21840 18
could be one of 2 function(s) NeuroNet.mqh 21840 18
   bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 11308 22
   bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,float,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 11309 22
call resolves to '<NA>' instead of 'bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 21840 18
   see declaration of function 'CNeuronConvOCL::Init' NeuroNet.mqh 10973 22
no one of the overloads can be applied to the function call NeuroNet.mqh 21846 18
could be one of 2 function(s) NeuroNet.mqh 21846 18
   bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 11308 22
   bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,float,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 11309 22
call resolves to '<NA>' instead of 'bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 21846 18
   see declaration of function 'CNeuronConvOCL::Init' NeuroNet.mqh 10973 22
no one of the overloads can be applied to the function call NeuroNet.mqh 30722 16
could be one of 2 function(s) NeuroNet.mqh 30722 16
   bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 10973 22
   bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 10974 22
call resolves to '<NA>' instead of 'bool CNeuronProofOCL::Init(uint,uint,COpenCLMy*,int,int,int,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 30722 16
   see declaration of function 'CNeuronProofOCL::Init' NeuroNet.mqh 10856 22
no one of the overloads can be applied to the function call NeuroNet.mqh 30730 16
could be one of 2 function(s) NeuroNet.mqh 30730 16
   bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 10973 22
   bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 10974 22
call resolves to '<NA>' instead of 'bool CNeuronProofOCL::Init(uint,uint,COpenCLMy*,int,int,int,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 30730 16
   see declaration of function 'CNeuronProofOCL::Init' NeuroNet.mqh 10856 22
no one of the overloads can be applied to the function call NeuroNet.mqh 30755 16
could be one of 2 function(s) NeuroNet.mqh 30755 16
   bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 10973 22
   bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 10974 22
call resolves to '<NA>' instead of 'bool CNeuronProofOCL::Init(uint,uint,COpenCLMy*,int,int,int,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 30755 16
   see declaration of function 'CNeuronProofOCL::Init' NeuroNet.mqh 10856 22
wrong parameters count, 12 passed, but 15 requires NeuroNet.mqh 64222 22
   bool CNeuronTimeMoEAttention::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 63529 22
call resolves to 'bool CNeuronTimeMoEAttention::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint)' instead of 'bool CNeuronCrossDMHAttention::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 64222 22
   see declaration of function 'CNeuronTimeMoEAttention::Init' NeuroNet.mqh 63529 22
   see declaration of function 'CNeuronCrossDMHAttention::Init' NeuroNet.mqh 50759 22
7 errors, 8 warnings 7 8
Dmitriy Gizlyk
Dmitriy Gizlyk | 9 ago. 2025 em 07:57
Andrew_7543 #:
Em seguida, os mesmos problemas no módulo NeuroNet.mqh

Biblioteca corrigida no artigo Redes neurais na negociação: decomposição em vez de escalonamento - Building Modules - MQL5 Articles

Andrew_7543
Andrew_7543 | 9 ago. 2025 em 20:10
obrigado)tudo começou como estava antes da atualização do terminal... e para o artigo um respeito especial!!!!
Andrew_7543
Andrew_7543 | 10 ago. 2025 em 08:12
@Dmitriy Gizlyk saudações... não está muito claro o que acontece no treinamento on-line: qual período você escolheu, quais configurações do testador de estratégia você usou para se aproximar das condições de combate?
Caminhe em novos trilhos: Personalize indicadores no MQL5 Caminhe em novos trilhos: Personalize indicadores no MQL5
Vou agora listar todas as possibilidades novas e recursos do novo terminal e linguagem. Elas são várias, e algumas novidades valem a discussão em um artigo separado. Além disso, não há códigos aqui escritos com programação orientada ao objeto, é um tópico muito importante para ser simplesmente mencionado em um contexto como vantagens adicionais para os desenvolvedores. Neste artigo vamos considerar os indicadores, sua estrutura, desenho, tipos e seus detalhes de programação em comparação com o MQL4. Espero que este artigo seja útil tanto para desenvolvedores iniciantes quanto para experientes, talvez alguns deles encontrem algo novo.
Técnicas do MQL5 Wizard que você deve conhecer (Parte 56): Fractais de Bill Williams Técnicas do MQL5 Wizard que você deve conhecer (Parte 56): Fractais de Bill Williams
Os Fractais de Bill Williams são um indicador poderoso que é fácil de ignorar quando inicialmente observado em um gráfico de preços. Ele parece muito carregado e provavelmente não é suficientemente incisivo. Nosso objetivo é remover essa impressão sobre este indicador, examinando o que seus diversos padrões podem realizar quando avaliados com testes forward walk em todos eles, utilizando um Expert Advisor montado pelo Wizard.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Redes neurais em trading: Modelo multidimensional de ponta a ponta para previsão de séries temporais (Componentes principais) Redes neurais em trading: Modelo multidimensional de ponta a ponta para previsão de séries temporais (Componentes principais)
Apresentamos a nova implementação dos principais componentes do framework GinAR, um algoritmo adaptativo para trabalhar com séries temporais baseadas em grafos. Neste artigo, analisamos passo a passo a arquitetura e os algoritmos de propagação para frente e de retropropagação do erro.