Русский
preview
Redes neurais no trading: modelos de refinamento iterativo de previsões (Conclusão)

Redes neurais no trading: modelos de refinamento iterativo de previsões (Conclusão)

MetaTrader 5Sistemas de negociação |
20 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Introdução

O mercado não tem pressa em revelar suas intenções. Ele está em constante movimento, flexível, fluido e muitas vezes imprevisível. Cada variação de preço não é uma oscilação aleatória, mas o reflexo de inúmeras interações entre os participantes do mercado e os fluxos de informação. Enquanto os modelos clássicos tentam capturar esse momento em um único retrato, a abordagem moderna RAFT (Recurrent All-Pairs Field Transforms) vai além. Em vez de fazer uma única previsão, ela a refina repetidamente, estabelecendo uma espécie de diálogo com os dados.

Essa ideia chega a parecer filosófica. Estamos acostumados a ver um modelo gerar um resultado e encerrar sua participação. O RAFT, por sua vez, continua refinando a previsão, como um analista experiente que verifica suas hipóteses passo a passo, confrontando cada nova observação com o panorama geral do mercado. Em vez de um mapeamento direto do tipo Entrada - Saída, constrói-se aqui um campo de interações no qual cada elemento influencia os demais. Essa abordagem não apenas aumenta a precisão, mas também transforma a própria concepção de previsão. O resultado deixa de ser apenas um número e passa a representar um estado do modelo, refletindo seu grau de coerência interna com o mercado.

Em sua versão original, o RAFT foi utilizado para calcular o fluxo óptico, ou seja, estimar o movimento entre dois quadros de uma imagem. No entanto, deixando de lado os detalhes técnicos, a própria lógica do método apresenta uma afinidade surpreendente com as aplicações de análise financeira. Afinal, aqui também temos dois estados: o passado e o presente. E tentamos compreender para onde o movimento se dirige. No lugar dos pixels, temos as cotações. No lugar das estruturas ópticas, os padrões de preço. E, no lugar dos quadros, as fatias temporais do mercado. Dessa forma, adaptar o RAFT às tarefas de trading surge como uma evolução natural das ideias de refinamento iterativo de previsões.

Nos trabalhos anteriores, já estabelecemos essa base. O primeiro artigo foi dedicado ao conceito de múltiplas iterações e à descrição da arquitetura geral do framework. O segundo analisou seus principais componentes, desde a construção do campo de correlações até os blocos recorrentes que conduzem o refinamento. Vimos como o modelo pode aprimorar gradualmente suas previsões, retornando aos dados originais e reavaliando cada hipótese.

Antes de prosseguirmos, vale refletir mais uma vez sobre o motivo pelo qual o RAFT representou um avanço tão significativo. Sua principal força está na capacidade de modelar um campo completo de interações entre todos os pares de elementos. Em vez de buscar correlações locais, ele compara todos os elementos entre si, criando algo semelhante a um mapa de forças de atração e repulsão entre os elementos. No contexto financeiro, isso significa que cada barra, cada atributo ou indicador não é analisado isoladamente, mas no contexto de todo o histórico e de todas as relações possíveis. Isso torna a previsão mais robusta e confere ao modelo uma capacidade de raciocínio sistêmico.

Outro elemento fundamental é o refinamento iterativo. Diferentemente das redes neurais de uma única etapa, o RAFT atualiza repetidamente a estimativa de movimento ou, no nosso caso, a previsão. Após cada iteração, as novas informações são confrontadas com o mapa global de interações, e o modelo ajusta a direção. Essa estratégia lembra o raciocínio analítico: primeiro surge uma hipótese, depois vêm a verificação, o refinamento e uma nova verificação. Graças a isso, o RAFT consegue manter o foco nos padrões mais relevantes sem perder o contexto nem sofrer sobreajuste.

O bloco recorrente desempenha um papel especial. Sua função é preservar o estado interno, isto é, a memória do modelo, e controlar a velocidade de adaptação. Em aplicações financeiras, isso pode ser interpretado como um mecanismo interno que regula o grau de confiança: até que ponto o modelo confia na estimativa atual, se deve revisá-la e como deve reagir a uma mudança no regime de mercado. Dessa forma, o componente recorrente se torna algo maior do que um simples elemento técnico. Ele atua como um regulador da intuição, mantendo o equilíbrio entre os novos dados e a experiência acumulada.

Outro princípio igualmente importante é a abordagem multiescala. Na arquitetura original do RAFT, utiliza-se uma compressão piramidal das correlações entre os atributos. Em cada nível, as informações são agregadas e refinadas em diferentes resoluções. Esse princípio é particularmente adequado ao mercado. Afinal, as flutuações de curto prazo e as tendências de longo prazo se desenvolvem em horizontes temporais distintos, mas exercem influência umas sobre as outras. Um modelo capaz de considerar essa interação se aproxima mais da dinâmica real. Ele percebe tanto os movimentos individuais quanto o ritmo geral do mercado.

A implementação dessas ideias em MQL5 torna o projeto especialmente valioso. O MQL5 oferece ferramentas poderosas para trabalhar com séries temporais e acesso direto aos dados de trading, enquanto o OpenCL amplia as possibilidades de computação paralela. Ao combinar essas tecnologias, obtemos um ambiente experimental para testar conceitos fundamentais com dados reais, sem nos afastarmos da prática do trading. Cada iteração do modelo pode ser testada em tempo real.

O framework RAFT também se destaca por sua versatilidade. Seus princípios, como a construção de um campo de interações, o refinamento iterativo e a memória recorrente, não dependem de uma área específica. No trading, essas ideias permitem construir um sistema que não apenas faz uma previsão, mas aprende a refiná-la. Um sistema que não se precipita na decisão, mas verifica cada etapa. Em uma época em que velocidade e precisão determinam o sucesso no mercado, essa característica se torna uma vantagem rara.

A visualização do framework RAFT proposta pelo autor é apresentada a seguir.



Objeto de nível superior

No artigo anterior, concluímos a implementação dos principais componentes do framework RAFT. Agora chegou o momento de avançar para a etapa final: integrar tudo em uma estrutura única e coerente, na qual cada componente não apenas cumpra sua função, mas também interaja com os demais, formando um mecanismo completo. Antes, porém, de iniciarmos essa etapa, vale refletir por um momento sobre o que já construímos e quais questões permanecem em aberto.

Na arquitetura original do RAFT, há uma simetria elegante. A mesma arquitetura de codificador de atributos é utilizada em dois fluxos: o principal e o de contexto. O codificador do fluxo principal analisa um par de imagens, extrai seus mapas de atributos e, em seguida, determina as correlações entre eles. Já o codificador de contexto trabalha com apenas uma imagem, a imagem de origem, criando uma espécie de base a partir da qual se inicia a previsão. É justamente essa divisão fundamental de funções que torna a arquitetura RAFT tão robusta: um fluxo observa as mudanças, enquanto o outro mantém um ponto de referência.

No universo financeiro, porém, tudo é muito mais dinâmico. Enquanto na visão computacional os quadros registram uma sequência de eventos com limites bem definidos, o mercado não conhece pausas. Ele não espera que o modelo tenha tempo de capturar um retrato, mas flui continuamente, passando de um estado a outro como um rio depois da chuva, alterando seu curso sob a influência de novos fluxos de informação. Nesse cenário, a ideia de um contexto fixo começa a apresentar limitações. Não é possível congelar o estado do mercado e usá-lo como base para construir uma previsão. O que era relevante há poucos minutos pode perder o sentido uma hora depois.

Já nos deparamos com um problema semelhante ao discutir o uso de um estado inicial fixo na previsão de séries temporais. Na ocasião, a solução surgiu de forma natural: introduzimos uma pilha de estados históricos, permitindo que o modelo se apoiasse não em um único instante, mas em toda uma sequência, formando uma matriz dinâmica de correlações. Essa abordagem tornou possível observar o mercado não como um quadro estático, mas em movimento, acompanhando a interação entre o passado e o presente.

No fluxo de contexto, porém, a situação se torna ainda mais interessante. O contexto não é apenas uma informação auxiliar. Ele representa a própria compreensão do regime de mercado vigente. E os regimes, como sabemos, mudam constantemente. A volatilidade aumenta, a distribuição da liquidez muda, e as correlações entre ativos ora se intensificam, ora desaparecem. Fixar o contexto nessas condições significa tornar o modelo incapaz de acompanhar essas mudanças. Por outro lado, tentar monitorá-lo por meio de uma pilha de estados históricos adicionaria complexidade excessiva e reduziria o desempenho. O resultado seria uma estrutura pesada, comprometendo justamente uma das principais vantagens do RAFT: a velocidade e a eficiência de suas iterações.

Precisamos, portanto, de outro caminho, mais natural e mais flexível. Uma abordagem na qual o próprio modelo seja capaz de compreender como o contexto evolui e atualizá-lo sem intervenção externa. A ideia é simples, mas profunda: complementar o codificador de contexto com um bloco recorrente capaz de acompanhar o estado do mercado em tempo real. Esse bloco poderá manter na memória as mudanças recentes, identificar padrões recorrentes e ajustar o contexto à medida que novos dados forem chegando.

Na prática, essa solução torna o modelo autoajustável. O modelo passa a ser capaz não apenas de reagir aos dados originais, mas também de se adaptar, reorganizando de forma coerente sua própria interpretação do que está acontecendo.

Se quisermos fazer uma analogia, o bloco recorrente funciona como a memória de curto prazo de um analista: ele se lembra do que aconteceu recentemente, mas não carrega consigo todo o histórico. Mantém o foco no que é relevante no momento, permitindo que a previsão continue precisa mesmo quando a dinâmica do mercado muda rapidamente.

Esse é um avanço importante. Passamos da simples construção mecânica da arquitetura para uma modelagem deliberada do comportamento adaptativo. Afinal, no trading, não basta calcular o movimento: é preciso compreender por que ele muda e conseguir se adaptar a um novo regime. O codificador de contexto recorrente permitirá aproximar o framework RAFT desse objetivo.

Implementamos as ideias propostas no objeto CNeuronRAFT, que se torna o elemento central da nossa implementação. É nele que todos os principais componentes do framework, como os codificadores, o bloco de correlação, o mecanismo recorrente e as conexões residuais, convergem em uma única estrutura, formando o núcleo dinâmico do modelo.

class CNeuronRAFT :  public CNeuronSpikeActivation
  {
protected:
   CLayer                              cEncoder;
   CLayer                              cContext;
   CLayer                              cCorrelation;
   CNeuronBaseOCL                      cConcatenated;
   CNeuronSpikeConvGRU2D               cGRU;
   CNeuronSpikeResNeXtResidual         cResidual;
   CLayer                              cG;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronRAFT(void) {};
                    ~CNeuronRAFT(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint &units[], uint &chanels[], uint group_size,
                          uint groups, uint stack_size, uint levels,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronRAFT;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   //---
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual void      TrainMode(bool flag) override;
   virtual bool      Clear(void) override;
  };

A estrutura apresentada não é apenas um conjunto de objetos. Trata-se de um ecossistema arquitetural no qual cada componente desempenha seu papel na dinâmica geral.

cEncoder é responsável pela extração inicial de atributos dos dados de origem. Ele corresponde ao codificador visual do RAFT original, adaptado às séries temporais financeiras. O componente transforma o fluxo de preços e os valores dos indicadores em uma representação multidimensional compacta, na qual cada coordenada deixa de ser apenas um número e passa a representar um componente semântico do movimento do mercado.

cContext forma o fluxo de contexto. Sua arquitetura é complementada por um módulo recorrente destinado a acompanhar a evolução do estado do mercado ao longo do tempo. Graças a ele, o contexto deixa de ser estático: passa a acompanhar os novos eventos e a se reajustar gradualmente junto com o mercado, permitindo que o modelo mantenha na memória informações recentes sobre a dinâmica do mercado.

Em seguida vem cCorrelation, responsável pela construção da matriz de relações entre os atributos. É o próprio campo de interações no qual se fundamenta toda a filosofia do RAFT. Aqui, os atributos do estado atual são comparados com a pilha histórica de dados acumulada nos períodos anteriores. Dessa comparação surge o campo de correlações, a base que permite ao RAFT captar tanto deslocamentos locais quanto a dinâmica global das mudanças observadas.

O componente cConcatenated combina os resultados do codificador e do módulo de correlação com o contexto atualizado, preparando-os para alimentar o bloco recorrente cGRU. Esse módulo é responsável pelo refinamento iterativo da previsão. Ele avalia a diferença entre os estados anteriores e o estado atual, aplica as correções necessárias e, com isso, conduz o modelo gradualmente a uma solução mais precisa.

O módulo cResidual atua como elemento estabilizador. Diferentemente do que ocorre nas redes residuais clássicas, ele não propaga o gradiente, mas mantém a consistência entre as iterações e evita a deriva dos atributos ao longo das atualizações sucessivas. Aqui é importante compreender uma diferença fundamental entre o RAFT e os modelos tradicionais de previsão. O algoritmo não prevê diretamente um novo estado. Em vez disso, gera um delta dos atributos, isto é, um refinamento que descreve a alteração do estado atual. Ao adicionar esse delta previsto aos dados de origem, obtemos o resultado desejado.

Os autores do framework RAFT optaram deliberadamente por não propagar o gradiente do erro pelo fluxo das conexões residuais. O estado inicial é tratado como um fato consumado e não está sujeito a correções. A otimização se propaga apenas pelos fluxos atualizáveis, passando pelos blocos de correlação e recorrentes. Essa abordagem torna o treinamento mais estável, evita que a propagação reversa distorça os dados iniciais e permite que o modelo se concentre na dinâmica observada.

O componente cG é o bloco final de processamento dos dados, responsável por gerar o mapa de saída dos valores previstos. É nele que convergem todos os caminhos do modelo, e o resultado de cada iteração passa a alimentar a etapa seguinte de refinamento.

A inicialização de todos os componentes internos do novo objeto é realizada no método Init. Essa etapa pode ser comparada ao momento em que uma orquestra se reúne antes de uma apresentação: cada instrumento precisa estar afinado, e o maestro deve estar pronto para marcar o ritmo. É aqui que todo o conjunto de blocos neurais é criado e integrado, dando origem posteriormente à dinâmica coordenada do modelo.

bool CNeuronRAFT::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                       uint &units[], uint &chanels[], uint group_size,
                       uint groups, uint stack_size, uint levels,
                       ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(units.Size() != chanels.Size())
      return false;
   uint layers = units.Size() - 1;
   if(!CNeuronSpikeActivation::Init(numOutputs, myIndex, open_cl, 
                units[layers]*chanels[layers], optimization_type, batch))
      return false;

A rotina começa pela verificação de que os parâmetros recebidos estão corretos e pela chamada do método de inicialização da classe pai. Em seguida, são criados os principais componentes do framework: os codificadores, o módulo de correlação, o bloco recorrente e o módulo estabilizador de conexões residuais.

A estrutura de dois fluxos, cEncoder e cContext, é criada primeiro. Em um laço, são criadas as camadas CNeuronSpikeResNeXtBlock, responsáveis pela extração sequencial dos atributos.

   int index = 0;
   CNeuronSpikeResNeXtBlock*           resblock = NULL;
   CNeuronSpikeConvGRU2D*              gru = NULL;
   CNeuronMultiScaleStackCorrelation*  correlation = NULL;
   CNeuronBaseOCL*                     neuron = NULL;
   CNeuronBatchNormOCL*                norm = NULL;
//---
   cEncoder.Clear();
   cContext.Clear();
   cCorrelation.Clear();
   cG.Clear();
   cEncoder.SetOpenCL(OpenCL);
   cContext.SetOpenCL(OpenCL);
   cCorrelation.SetOpenCL(OpenCL);
   cG.SetOpenCL(OpenCL);
//---
   for(uint i = 0; i < layers; i++)
     {
      resblock = new CNeuronSpikeResNeXtBlock();
      if(!resblock ||
         !resblock.Init(0, index, OpenCL, chanels[i], chanels[i + 1], units[i], units[i + 1],
                                                 group_size, groups, optimization, iBatch) ||
         !cEncoder.Add(resblock))
        {
         DeleteObj(resblock)
         return false;
        }
      index++;
      resblock = new CNeuronSpikeResNeXtBlock();
      if(!resblock ||
         !resblock.Init(0, index, OpenCL, chanels[i], chanels[i + 1], units[i], units[i + 1],
                                                 group_size, groups, optimization, iBatch) ||
         !cContext.Add(resblock))
        {
         DeleteObj(resblock)
         return false;
        }
      index++;
     }

O fluxo cEncoder analisa os atributos do estado atual, enquanto cContext constrói um espaço de contexto adaptativo, complementado pelo bloco recorrente CNeuronSpikeConvGRU2D. Esse módulo GRU dá ao framework a capacidade de acompanhar a dinâmica temporal e ajustar o contexto sem recorrer a um estado fixo, uma propriedade essencial ao trabalhar com dados de mercado contínuos.

   gru = new CNeuronSpikeConvGRU2D();
   if(!gru ||
      !gru.Init(0, index, OpenCL, units[layers], chanels[layers], chanels[layers], optimization, iBatch) ||
      !cContext.Add(gru))
     {
      DeleteObj(gru)
      return false;
     }
   index++;

Em seguida, é criado o módulo CNeuronMultiScaleStackCorrelation, que constrói a matriz de correlações entre os atributos do estado atual e a pilha histórica de dados. Esse é o elemento central da arquitetura, análogo a um campo computacional de forças no qual se manifesta a relação entre o passado e o presente.

   correlation = new CNeuronMultiScaleStackCorrelation();
   if(!correlation ||
      !correlation.Init(0, index, OpenCL, stack_size, chanels[layers], units[layers], levels,
                                                                     optimization, iBatch) ||
      !cCorrelation.Add(correlation))
     {
      DeleteObj(correlation)
      return false;
     }
   index++;
   resblock = new CNeuronSpikeResNeXtBlock();
   if(!resblock ||
      !resblock.Init(0, index, OpenCL, correlation.Neurons() / units[layers], chanels[layers],
                    units[layers], units[layers], group_size, groups, optimization, iBatch) ||
      !cCorrelation.Add(resblock))
     {
      DeleteObj(resblock)
      return false;
     }
   index++;

Na saída do bloco, é adicionada uma camada CNeuronSpikeResNeXtBlock, que aumenta a seletividade do modelo em relação aos atributos mais relevantes.

Após o bloco de geração do campo de correlações, é criado o nó de agregação cConcatenated, que reúne em um único tensor os resultados dos três fluxos: atributos, contexto e correlações.

   if(!cConcatenated.Init(0, index, OpenCL, 3 * chanels[layers]*units[layers], optimization, iBatch))
      return false;
   index++;

Em seguida, os dados são passados para cGRU, onde ocorre o refinamento iterativo do delta previsto dos atributos.

   if(!cGRU.Init(0, index, OpenCL, units[layers], 3 * chanels[layers], chanels[layers], optimization, iBatch))
      return false;
   index++;

Na sequência, é inicializado o módulo estabilizador cResidual. Sua função é preservar a estabilidade do espaço de atributos ao longo de múltiplas iterações. Diferentemente das conexões residuais clássicas, aqui não ocorre retropropagação do gradiente: o estado-base não é corrigido, mas utilizado como ponto de referência, ao qual é adicionado o delta dos atributos calculado pelo modelo.

   if(!cResidual.Init(0, index, OpenCL, chanels[0], chanels[layers], units[0], units[layers], optimization, iBatch))
      return false;
   index++;

O bloco final cG integra todos os componentes em um único sistema, normalizando o fluxo de saída e preparando-o para o nível seguinte. Nessa etapa também são criados objetos auxiliares, como o neurônio básico e a camada de normalização CNeuronBatchNormOCL, responsável por estabilizar a distribuição de saída.

   neuron = new CNeuronBaseOCL();
   if(!neuron ||
      !neuron.Init(0, index, OpenCL, Neurons(), optimization, iBatch) ||
      !cG.Add(neuron))
     {
      DeleteObj(neuron)
      return false;
     }
   neuron.SetActivationFunction(None);
   if(!neuron.SetGradient(cGRU.getGradient(), true) ||
      !cResidual.SetGradient(cGRU.getGradient(), true))
      return false;
//---
   index++;
   norm = new CNeuronBatchNormOCL();
   if(!norm ||
      !norm.Init(0, index, OpenCL, Neurons(), iBatch, optimization) ||
      !cG.Add(norm))
     {
      DeleteObj(norm)
      return false;
     }
//---
   return true;
  }

Dessa forma, o método de inicialização estabelece uma sequência lógica de etapas computacionais. Cada módulo possui uma função bem definida: extração de atributos, construção das correlações, refinamento recorrente do contexto e geração do delta previsto. Todos esses elementos são integrados em uma única estrutura, pronta para o treinamento em condições reais de mercado.

A propagação para frente é implementada no método feedForward. Não se trata apenas de um laço computacional, mas de um processo dinâmico. É aqui que o modelo ganha vida e começa a perceber o mercado. Cada bloco desempenha sua função e, em conjunto, eles constroem uma visão dinâmica das mudanças futuras.

bool CNeuronRAFT::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cResidual.FeedForward(NeuronOCL))
      return false;

A execução começa pelo bloco cResidual. Podemos imaginá-lo como uma base sólida ou uma âncora. Ele fixa o estado atual dos atributos, estabelecendo um ponto de referência para todas as alterações subsequentes.

Em seguida entra em ação o fluxo principal de atributos, cEncoder. Cada bloco ResNeXt funciona como um observador atento, examinando o mercado camada após camada e identificando padrões e dependências relevantes. Na saída, forma-se um retrato do estado atual, completo e detalhado, como um mapa panorâmico do mercado naquele instante.

   CNeuronBaseOCL* current = NULL;
   CNeuronBaseOCL* prev = NeuronOCL;
   for(int i = 0; i < cEncoder.Total(); i++)
     {
      current = cEncoder[i];
      if(!current ||
         !current. Feedforward(prev))
         return false;
      prev = current;
     }

Em paralelo, atua o fluxo de contexto cContext. Se o codificador registra o momento atual, o contexto funciona como um observador que mantém na memória os estados anteriores e acompanha as tendências. Seu bloco GRU recorrente monitora como o mercado se alterou nos passos temporais mais recentes e ajusta a previsão a essas mudanças. O contexto evolui junto com o mercado, permitindo que o modelo se adapte de forma contínua.

   prev = NeuronOCL;
   for(int i = 0; i < cContext.Total(); i++)
     {
      current = cContext[i];
      if(!current ||
         !current. Feedforward(prev))
         return false;
      prev = current;
     }

Em seguida, os resultados do codificador são passados para o bloco cCorrelation. Ali, eles são adicionados à pilha que armazena o histórico dos estados do mercado, e é construída uma matriz de correlações multiescala entre os atributos. Podemos compará-la a um mapa estratégico no qual cada ponto mostra como os eventos anteriores se relacionam com o estado atual. É nesse estágio que o modelo capta padrões ocultos e identifica a intensidade e a direção do movimento.

   prev = cEncoder[-1];
   for(int i = 0; i < cCorrelation.Total(); i++)
     {
      current = cCorrelation[i];
      if(!current ||
         !current. Feedforward(prev))
         return false;
      prev = current;
     }

Os três fluxos, codificador, contexto e correlações, convergem no nó cConcatenated. Ele funciona como um centro de consolidação, reunindo todas as informações antes da etapa de refinamento.

   uint units = cGRU.GetUnits();
   if(!Concat(cEncoder[-1].getOutput(), cContext[-1].getOutput(), cCorrelation[-1].getOutput(),
              cConcatenated.getOutput(), cEncoder[-1].Neurons() / units, cContext[-1].Neurons() / units,
              cCorrelation[-1].Neurons() / units, units))
      return false;

O bloco recorrente cGRU assume então o papel central. Ele analisa o sinal combinado e calcula o delta dos atributos, isto é, a alteração prevista que será adicionada ao estado-base para formar a previsão final.

   if(!cGRU.FeedForward(cConcatenated.AsObject()))
      return false;
//---
   current = cG[0];
   if(!current ||
      !SumAndNormilize(cGRU.getOutput(), cResidual.getOutput(), current.getOutput(),
                       cGRU.GetChanels(), false, 0, 0, 0, 1))
      return false;

Por fim, o sinal é passado ao bloco cG, onde é somado à saída da conexão residual e normalizado. Esse estágio pode ser visto como um ajuste fino do equilíbrio entre os diferentes fluxos: eles são sincronizados, vieses e ruídos são eliminados e a estabilidade da previsão é reforçada. A passagem sequencial por todas as camadas de cG conclui a geração dos resultados, que agora representam não um estado estático, mas um panorama do mercado refinado dinamicamente.

   prev = current;
   for(int i = 1; i < cG.Total(); i++)
     {
      current = cG[i];
      if(!current ||
         !current. Feedforward(prev))
         return false;
      prev = current;
     }
//---
   return CNeuronSpikeActivation::feedforward(prev);
  }

Ao final, feedForward transforma os dados analisados em uma projeção dinâmica das mudanças, na qual cada bloco atua como um analista independente. Em conjunto, porém, eles formam uma equipe capaz de identificar tendências, antecipar movimentos e se adaptar a um ambiente de mercado em constante transformação. O modelo não tenta prever diretamente o futuro. Em vez disso, calcula o delta e refina gradualmente a representação, permitindo que a previsão permaneça plausível e robusta.

Depois que a previsão é formada, chegamos à próxima etapa fundamental: a distribuição do gradiente do erro. Essa etapa foi implementada no método calcInputGradients. Se a propagação para frente pode ser comparada a uma orquestra executando uma obra, a retropropagação funciona como o maestro que avalia cada instrumento e ajusta sua execução para que todo o conjunto alcance a precisão necessária.

bool CNeuronRAFT::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
//---
   if(!CNeuronSpikeActivation::calcInputGradients(cG[-1]))
      return false;

O método começa verificando se o ponteiro para o objeto de dados iniciais NeuronOCL está correto. Sem ele, o modelo não conseguirá distribuir adequadamente os gradientes, e toda a execução será interrompida, como uma orquestra sem partitura.

Em seguida, é chamado o método homônimo da classe pai, que estabelece o ponto inicial da retropropagação. Podemos compará-lo ao primeiro pulso do metrônomo: os gradientes começam a percorrer a cadeia de componentes de cG, do último para o primeiro.

   CNeuronBaseOCL* current = NULL;
   CNeuronBaseOCL* next = cG[-1];
   for(int i = cG.Total() - 2; i >= 0; i--)
     {
      current = cG[i];
      if(!current ||
         !current.CalcHiddenGradients(next))
         return false;
      next = current;
     }

O laço percorre os elementos do módulo cG em ordem inversa. Cada camada recebe o sinal da camada seguinte e distribui cuidadosamente o erro. Esse processo lembra o raciocínio de um analista experiente, que avalia cada nível do modelo confrontando a previsão com os resultados reais.

Após as camadas de cG, o gradiente é passado ao bloco cResidual. Nesse ponto, o erro é distribuído em relação ao estado-base, que permanece preservado como um dado imutável. Esse é um aspecto essencial: os atributos de base não são modificados, mas servem como referência para o cálculo das correções.

   if(!NeuronOCL.CalcHiddenGradients(cResidual.AsObject()))
      return false;

Em seguida, os gradientes chegam ao nó de agregação cConcatenated. A partir dele, são divididos entre três fluxos: cEncoder, cContext e cCorrelation. Podemos imaginar essa etapa como uma equipe que distribui tarefas entre três analistas, atribuindo a cada um sua parcela dos dados a serem processados.

   if(!cConcatenated.CalcHiddenGradients(cGRU.AsObject()))
      return false;
   uint units = cGRU.GetChanels();
   if(!DeConcat(cEncoder[-1].getGradient(), cContext[-1].getGradient(), cCorrelation[-1].getGradient(),
                cConcatenated.getGradient(), cEncoder[-1].Neurons() / units, cContext[-1].Neurons() / units,
                cCorrelation[-1].Neurons() / units, units))
      return false;

O próximo bloco é cCorrelation. O laço percorre suas camadas em ordem inversa, e cada uma ajusta seu gradiente com base no sinal recebido da camada seguinte. É nessa etapa que o modelo avalia de que forma a interação entre os atributos atuais e históricos influenciou a previsão final.

   next = cCorrelation[-1];
   for(int i = cCorrelation.Total() - 2; i >= 0; i--)
     {
      current = cCorrelation[i];
      if(!current ||
         !current.CalcHiddenGradients(next))
         return false;
      next = current;
     }

Depois, o gradiente é passado ao codificador cEncoder. Aqui há uma particularidade interessante. O codificador recebe os gradientes do erro por dois fluxos de informação. Parte desses gradientes já havia sido recebida durante a separação dos gradientes de erro provenientes do bloco recorrente. Para não perder os valores já armazenados, a última camada do codificador salva temporariamente o ponteiro para o buffer de gradientes em uma variável local, temp, enquanto um buffer livre passa a ocupar seu lugar. Em seguida, calculamos os gradientes provenientes do fluxo principal do módulo de correlação e os somamos aos valores previamente armazenados.

   current = cEncoder[-1];
   if(!current)
      return false;
   CBufferFloat* temp = current.getGradient();
   if(!current.SetGradient(current.getPrevOutput(), false) ||
      !current.CalcHiddenGradients(next) ||
      !SumAndNormilize(temp, current.getGradient(), temp, temp.Total() / units, false, 0, 0, 0, 1) ||
      !current.SetGradient(temp, false))
      return false;

Depois disso, os ponteiros para os buffers são restaurados ao estado original, e os gradientes atravessam todas as demais camadas do codificador.

   next = current;
   for(int i = cEncoder.Total() - 2; i >= 0; i--)
     {
      current = cEncoder[i];
      if(!current ||
         !current.CalcHiddenGradients(next))
         return false;
      next = current;
     }
   if(!NeuronOCL.CalcHiddenGradients(next))
      return false;

Após o processamento do codificador, passamos ao fluxo de contexto cContext. O laço percorre seus componentes em ordem inversa.

   next = cContext[-1];
   for(int i = cContext.Total() - 2; i >= 0; i--)
     {
      current = cContext[i];
      if(!current ||
         !current.CalcHiddenGradients(next))
         return false;
      next = current;
     }

A operação final consiste em propagar os gradientes até o nível do objeto de dados iniciais NeuronOCL, onde também é feita a soma cuidadosa dos gradientes provenientes dos dois fluxos de informação.

   temp = NeuronOCL.getGradient();
   if(!NeuronOCL.SetGradient(NeuronOCL.getPrevOutput(), false) ||
      !NeuronOCL.CalcHiddenGradients(next) ||
      !SumAndNormilize(temp, NeuronOCL.getGradient(), temp, 1, false, 0, 0, 0, 1) ||
      !NeuronOCL.SetGradient(temp, false))
      return false;
//---
   return true;
  }

Como resultado, o método calcInputGradients distribui o erro por toda a arquitetura do objeto, desde a camada de saída até o codificador, o contexto e as correlações, ajustando apenas os componentes sujeitos ao treinamento. Cada bloco desempenha sua função: os codificadores analisam os atributos locais, o contexto considera o histórico, as correlações registram as relações entre os elementos e os blocos residuais mantêm a estabilidade de toda a estrutura. Essa abordagem garante um treinamento eficiente, sem distorcer os estados iniciais, preservando a dinâmica do modelo observada durante a propagação para frente.

Chegamos então à etapa final da otimização dos parâmetros. É nesse momento que o modelo ajusta seus pesos internos para tornar a previsão mais precisa. O algoritmo é implementado no método updateInputWeights, que pode ser comparado à afinação de todos os instrumentos após um ensaio.

bool CNeuronRAFT::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cResidual.UpdateInputWeights(NeuronOCL))
      return false;

A execução começa pela otimização do bloco residual cResidual. Ele ajusta seus parâmetros, mantendo um ponto de referência estável para toda a arquitetura.

Em seguida, os pesos são ajustados no fluxo principal do codificador cEncoder. Cada camada recebe sequencialmente o sinal do bloco anterior e atualiza seus parâmetros. Podemos imaginar essa etapa como uma equipe de pesquisadores que, após receber feedback, calibra seus instrumentos para melhorar a qualidade da extração de atributos na próxima propagação para frente.

   CNeuronBaseOCL* current = NULL;
   CNeuronBaseOCL* prev = NeuronOCL;
   for(int i = 0; i < cEncoder.Total(); i++)
     {
      current = cEncoder[i];
      if(!current ||
         !current.UpdateInputWeights(prev))
         return false;
      prev = current;
     }

O fluxo de contexto cContext é atualizado de forma semelhante. Nesse caso, o GRU recorrente utiliza os gradientes acumulados para ajustar seus pesos levando em conta a dinâmica temporal. Cada camada do contexto recalibra a influência dos estados anteriores sobre as previsões atuais, permitindo que o modelo responda de maneira mais flexível às mudanças do mercado.

   prev = NeuronOCL;
   for(int i = 0; i < cContext.Total(); i++)
     {
      current = cContext[i];
      if(!current ||
         !current.UpdateInputWeights(prev))
         return false;
      prev = current;
     }

Na etapa seguinte, a otimização percorre o bloco de correlações cCorrelation. Cada camada ajusta seus parâmetros de acordo com os sinais recebidos, aumentando a precisão da matriz de interações entre os atributos atuais e históricos. Dessa forma, o modelo aprende a identificar as relações mais relevantes e a considerar com maior precisão as tendências observadas no passado.

   prev = cEncoder[-1];
   for(int i = 0; i < cCorrelation.Total(); i++)
     {
      current = cCorrelation[i];
      if(!current ||
         !current.UpdateInputWeights(prev))
         return false;
      prev = current;
     }

O nó recorrente cGRU também atualiza seus pesos com base no tensor combinado cConcatenated. É aqui que ocorre um ajuste essencial: os parâmetros do GRU são ajustados para que o delta dos atributos calculado represente com a maior precisão possível as mudanças do estado atual.

   if(!cGRU.UpdateInputWeights(cConcatenated.AsObject()))
      return false;

Por fim, o bloco cG passa pela atualização sequencial de todas as suas camadas, somando a influência das correlações, do contexto e do fluxo principal de atributos, além de normalizar o sinal de saída. Essa etapa final garante que todos os componentes do modelo permaneçam sincronizados e preparados para a próxima propagação para frente.

   prev = cG[0];
   for(int i = 1; i < cG.Total(); i++)
     {
      current = cG[i];
      if(!current ||
         !current.UpdateInputWeights(prev))
         return false;
      prev = current;
     }
//---
   return CNeuronSpikeActivation::updateInputWeights(prev);
  }

Ao término, é chamado o método homônimo da classe pai, encerrando de forma coordenada o ciclo de otimização. Com isso, os pesos de todos os componentes são atualizados, e o modelo fica pronto para a próxima etapa de treinamento ou para gerar previsões com novos dados.

No anexo, é apresentado o código-fonte completo do objeto, com a implementação detalhada de todos os seus métodos, demonstrando o funcionamento interno do RAFT.



Arquitetura dos modelos

Concluímos a construção dos principais componentes do framework RAFT e agora passamos à definição da arquitetura dos modelos treináveis. Como antes, nosso objetivo é criar um sistema de trading capaz de desenvolver sua própria estratégia e realizar operações no mercado de forma autônoma.

Para treinar os modelos, utilizamos a abordagem Ator-Crítico, integrando as capacidades do RAFT diretamente ao codificador do estado do ambiente. Essa abordagem permite que o modelo não apenas reaja aos atributos atuais, mas também considere sua dinâmica e as relações identificadas pelo framework. Ao mesmo tempo, utilizamos o módulo STFS para selecionar os atributos mais relevantes, concentrando a atenção do modelo nos aspectos que realmente influenciam o resultado.

É justamente aqui que surge o primeiro efeito concreto da implementação dos princípios do RAFT na implementação do objeto de nível superior. Para integrá-lo às soluções arquiteturais existentes, basta conectar o novo objeto, e o modelo fica praticamente pronto para o treinamento, sem necessidade de alterações adicionais significativas.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronRAFT;
     {
      uint temp[] = {prev_out,            // Chanels In
                     32,
                     64,
                     128            // Chanels Out
                    };
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
     {
      uint temp[] = {prev_count,          // Units In
                     128,
                     64,
                     32          // Units Out
                    };
      if(ArrayCopy(descr.units, temp) < (int)temp.Size())
         return false;
     }
   descr.window = EmbeddingSize / 2;         // Group size
   descr.step = NHeads;                      // Groups
   descr.count = StackSize;                  // Stack size
   descr.layers = 4;                         // Correlation's Levels
   descr.optimization = ADAM;
   descr.batch = BatchSize;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   iLatentLayer++;
//---
   uint window = descr.windows[descr.windows.Size()-1];
   uint count = descr.units[descr.units.Size()-1];

Todos os demais componentes da arquitetura permanecem inalterados, o que simplifica consideravelmente todo o processo e acelera a transição do desenvolvimento para os testes.

Como resultado, obtemos um sistema modular, flexível e treinável, no qual o RAFT atua como núcleo central da análise dos atributos e da previsão da dinâmica do mercado, enquanto os demais elementos da arquitetura continuam desempenhando suas funções habituais, mantendo o funcionamento estável e o comportamento previsível do modelo.

A descrição completa da arquitetura dos modelos treináveis está disponível no anexo.



Testes

Depois que a arquitetura do modelo treinável é definida e o framework RAFT é integrado ao codificador de estado, passamos às etapas de treinamento e teste. É nesse momento que avaliamos até que ponto o sistema consegue se adaptar às condições reais do mercado e desenvolver sua própria estratégia de forma autônoma.

A primeira etapa do treinamento pode ser comparada ao ensaio de um iniciante no ambiente histórico do mercado. Utilizamos dados do par de moedas EURUSD no timeframe H1, referentes ao período de janeiro de 2024 a junho de 2025. Nesse intervalo, o modelo recebe um panorama histórico do mercado e aprende a reconhecer padrões: dinâmica dos preços, volume de negociações e relações ocultas entre os principais atributos. O RAFT atua aqui como um mentor experiente: ele gera uma representação informativa do estado para o Ator e o Crítico, ajuda o modelo a desenvolver sua própria intuição de trading e a acumular gradualmente experiência estratégica, permitindo prever os movimentos do mercado e avaliar o risco de cada operação potencial.

A etapa seguinte é o ajuste online no Testador de Estratégias do MetaTrader 5. O treinamento passa então para um ambiente dinâmico, muito próximo das condições reais. O modelo processa os dados candle a candle, reagindo a cada movimento do preço. O RAFT permite manter a estabilidade diante das flutuações provocadas pelo ruído de mercado, adaptar-se a picos repentinos e ajustar as ações em períodos de baixa liquidez. A estrutura básica construída a partir dos dados históricos continua servindo como referência, mas o modelo aprende a reagir de forma flexível às condições atuais do mercado, evitando sobreajuste e aumentando a precisão das previsões mesmo em ambientes instáveis.

A etapa final consiste no teste com dados totalmente novos, referentes ao período de julho a setembro de 2025. Todos os parâmetros obtidos nas etapas anteriores são carregados sem qualquer alteração, garantindo uma avaliação imparcial da capacidade de generalização do modelo. Os resultados dos testes são apresentados a seguir.

Os resultados demonstram a estabilidade e a capacidade de adaptação do modelo construído com o framework RAFT. No gráfico Balance/Equity, observa-se que o modelo consegue manter uma evolução positiva do capital, apesar das oscilações periódicas do mercado. Os dois indicadores apresentam uma trajetória coerente, o que indica um funcionamento equilibrado do modelo e uma gestão de risco adequada.

Durante os testes, com um depósito inicial de 100,0 USD, o modelo obteve um lucro líquido de 5,33 USD. O drawdown máximo do saldo foi de 7,17%, enquanto o drawdown máximo da equity chegou a 10,53%, indicando uma estratégia razoavelmente conservadora e um nível de risco baixo em relação ao capital empregado. O Profit Factor foi de 1,34, confirmando uma relação positiva entre os lucros obtidos e as perdas incorridas.

A análise das operações mostra que, das 30 negociações realizadas, 17 foram lucrativas, correspondendo a 56,67%. As posições vendidas apresentaram desempenho melhor, com 64,29% de operações vencedoras, enquanto nas posições compradas esse percentual foi de 50,0%. O lucro médio por operação foi de 1,24 USD, e a perda média, de 1,21 USD. Esses resultados demonstram o equilíbrio da estratégia. O maior lucro acumulado em uma sequência foi de 5,24 USD em 4 operações, enquanto a maior perda acumulada em uma sequência foi de 6,78 USD em 2 operações.

De modo geral, o modelo apresenta uma evolução positiva e estável do capital, capacidade de responder de forma flexível às mudanças nas condições de mercado e uma gestão de risco eficiente. O RAFT, integrado ao codificador de estado, fornece uma representação informativa dos atributos do mercado e permite que o sistema tome decisões fundamentadas mesmo em condições de instabilidade. No entanto, antes de utilizar o modelo em mercados reais, é necessário realizar testes adicionais e abrangentes da estratégia com uma amostra mais representativa.



Conclusão

O framework RAFT demonstrou ser uma ferramenta poderosa para análise e previsão de séries temporais financeiras. Sua arquitetura flexível e otimizada oferece precisão satisfatória nas previsões, estabilidade operacional e facilidade de adaptação a novas aplicações. O RAFT integra os principais componentes em um único sistema, acelera o processamento dos dados, reduz o risco de erros e facilita o desenvolvimento de estratégias de trading eficientes. Essas vantagens fazem dele uma ferramenta indispensável para traders e pesquisadores que buscam soluções rápidas e confiáveis.


Referências


Programas utilizados no artigo

#NomeTipoDescrição
1Study.mq5Expert AdvisorEA para treinamento offline de modelos
2StudyOnline.mq5Expert AdvisorEA para treinamento online de modelos
3Test.mq5Expert AdvisorEA para teste do modelo
4Trajectory.mqhBiblioteca de classesEstrutura de descrição do estado do sistema e da arquitetura dos modelos
5NeuroNet.mqhBiblioteca de classesBiblioteca de classes para criação de redes neurais
6NeuroNet.clBibliotecaBiblioteca de código do programa OpenCL

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

Arquivos anexados |
MQL5.zip (3279.6 KB)
Sistema de autoaprendizado por reforço para trading algorítmico em MQL5 Sistema de autoaprendizado por reforço para trading algorítmico em MQL5
Neste artigo, desenvolvemos um sistema multiagente de aprendizado de máquina para trading algorítmico no MetaTrader 5 com base em aprendizado por reforço. O sistema possui uma arquitetura de três níveis: os neurônios de memória armazenam a experiência, os agentes tomam decisões de forma independente e a inteligência coletiva combina essas decisões por meio de votação ponderada. O sistema se aperfeiçoa continuamente por meio de Q-learning, pruning de neurônios ineficientes e redução evolutiva do nível de exploração.
Métodos de amostragem MCMC: algoritmo de amostragem por fatias (Slice Sampling) Métodos de amostragem MCMC: algoritmo de amostragem por fatias (Slice Sampling)
Neste artigo, estudamos o método de amostragem por fatias (slice sampling), um algoritmo MCMC adaptativo que ajusta automaticamente os parâmetros de amostragem. Sua eficiência é demonstrada em modelos bayesianos de regressão linear e logística, e os resultados são comparados com métodos frequentistas clássicos.
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: modelos de refinamento iterativo de previsões (RAFT) Redes neurais em trading: modelos de refinamento iterativo de previsões (RAFT)
O framework RAFT propõe uma abordagem fundamentalmente diferente para prever a dinâmica do mercado: em vez de produzir uma previsão única, ele refina iterativamente o estado em tempo real. Ao mesmo tempo, leva em conta mudanças locais e globais, mantendo alta precisão mesmo diante de estruturas de preços complexas.