Русский
preview
Redes neurais em trading: Framework de previsão cross-domain de séries temporais (Conclusão)

Redes neurais em trading: Framework de previsão cross-domain de séries temporais (Conclusão)

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

Introdução

Nas últimas décadas, o trading financeiro passou por mudanças fundamentais, transformando-se em uma área altamente tecnológica e rigidamente disciplinada. Se antes o sucesso do trader dependia em grande parte da intuição, da experiência e de observações pessoais, hoje ganha destaque a capacidade de processar rapidamente e com qualidade enormes volumes de dados, construir previsões precisas e tomar decisões com base em algoritmos avançados. Com o desenvolvimento do poder computacional e o surgimento de métodos modernos de aprendizado de máquina, traders e pesquisadores passaram a contar com novas ferramentas capazes de identificar padrões e tendências no aparente caos dos dados de mercado. No entanto, apesar do progresso significativo, a previsão de curto e médio prazo de séries temporais dos mercados financeiros continua sendo uma tarefa com nível extremamente elevado de complexidade.

A razão está na natureza única e complexa dos mercados financeiros. São sistemas caracterizados por alto nível de ruído, quando os sinais reais frequentemente ficam ocultos sob uma massa de flutuações aleatórias e artefatos. Os dados de mercado são não estacionários: suas propriedades estatísticas mudam ao longo do tempo, o que se manifesta na alternância de regimes de mercado, de fases calmas para fases voláteis e vice-versa. Além disso, os mercados estão sujeitos a eventos repentinos, como notícias, crises ou intervenções de reguladores, que rompem os padrões habituais de comportamento dos preços. Tudo isso cria um ambiente no qual os modelos tradicionais de séries temporais enfrentam limitações fundamentais.

Os métodos estatísticos clássicos demonstraram bons resultados na análise de processos estacionários, porém sua aplicabilidade aos dados de mercado modernos é limitada. Por outro lado, as redes neurais, incluindo LSTM e GRU, tornaram-se amplamente utilizadas como ferramentas mais flexíveis para modelagem de dependências temporais complexas. Ainda assim, elas exigem grandes volumes de dados para treinamento e, ao serem aplicadas a novos ativos ou novas condições de mercado, frequentemente perdem precisão e estabilidade. A razão é que a maioria dos modelos está vinculada a determinadas estruturas e padrões de dados que nem sempre são universais.

Nesse contexto, surge um problema central: como construir um modelo capaz de generalização e adaptação sem treinamento adicional, o chamado Zero-Shot Forecasting. Tal modelo, treinado com grande quantidade de séries temporais diversas provenientes de diferentes domínios, deve possuir uma representação interna que lhe permita reconhecer padrões e regularidades semelhantes em dados completamente novos, nunca vistos anteriormente. Isso altera de forma fundamental a abordagem de previsão: o modelo deixa de ser um especialista em um único mercado e passa a se tornar um analista universal, capaz de transferir conhecimento entre diferentes tarefas.

Foi exatamente sob essa concepção que foi desenvolvido o framework TimeFound, cujo estudo iniciamos no artigo anterior. TimeFound é um modelo cross-domain poderoso e universal para séries temporais, baseado na arquitetura Transformer. O Transformer, um método revolucionário inicialmente proposto para tarefas de processamento de linguagem natural, ganhou ampla disseminação graças à sua capacidade de capturar de forma eficiente dependências de longo alcance em sequências. No TimeFound, seu potencial é utilizado para a análise de séries financeiras, porém com um complemento importante: a aplicação do mecanismo Multi-Resolution Patching.

A ideia do Multi-Resolution Patching consiste em dividir a série temporal original em patches, fragmentos de diferentes comprimentos que refletem distintas escalas temporais. Por exemplo, patches curtos capturam oscilações locais e rápidas, enquanto patches longos capturam tendências mais lentas e globais. Essa abordagem multinível permite que o modelo considere simultaneamente várias camadas de informação, o que é criticamente importante para dados financeiros, nos quais os sinais se manifestam em diferentes horizontes temporais.

Do ponto de vista técnico, a arquitetura do TimeFound inclui um conjunto de módulos de projeção independentes, um para cada grupo de patches de determinada escala. Cada módulo desse tipo representa um pequeno perceptron de duas camadas com conexões residuais. Ele recebe como entrada os patches e máscaras binárias de pontos válidos, o que ajuda a ignorar de forma eficiente o padding e preenchimentos artificiais de lacunas. Graças aos módulos de projeção separados e às particularidades da repetição dos patches, o modelo diferencia as contribuições de cada escala temporal, o que proporciona robustez a deslocamentos de fase, variações no comprimento dos dados brutos e valores ausentes.

Para o treinamento do TimeFound, foi reunido e cuidadosamente preparado um amplo conjunto de séries temporais provenientes das mais diversas áreas, desde indicadores macroeconômicos e cotações de mercado até processos físicos. Isso permitiu que o modelo desenvolvesse capacidade de generalização, operando com sucesso em dados novos, nunca antes vistos.

A visualização original do framework é apresentada a seguir.

Na parte prática do artigo anterior, analisamos detalhadamente a implementação do módulo de patching multiescalar, componente-chave do bloco de preparação de dados do framework TimeFound. Esse módulo é responsável pela formação eficiente de tokens informativos, representações compactas e densas de fragmentos individuais da série temporal, divididos em patches de diferentes comprimentos e escalas. É justamente graças a esse mecanismo que o modelo passa a ter a capacidade de perceber e processar os dados considerando tanto as características locais quanto as globais do mercado.

Hoje, continuaremos o trabalho iniciado.



Codificador-Decodificador TimeFound

O tensor de tokens formado na etapa de patching é enviado para análise adicional ao bloco Transformer. É exatamente aqui que começa o processamento inteligente dos dados, a identificação de padrões ocultos, a previsão da dinâmica e a formação de sinais.

Os autores do framework TimeFound propõem usar, nessa parte da arquitetura, o esquema clássico Codificador–Decodificador, bem conhecido em modelos de tradução automática e análise de sequências. No entanto, no contexto de séries temporais, esse esquema foi adaptado e reforçado com uma série de complementos para considerar as especificidades dos dados.

No Codificador, é aplicada a Self-Attention bidirecional. Isso significa que cada token é analisado no contexto de todos os demais, tanto para frente quanto para trás na escala temporal. Assim, o modelo pode considerar não apenas o que ocorreu antes do momento atual, mas também dependências potenciais com eventos que ocorrerão depois. Isso é especialmente útil ao analisar trechos de histórico já concluídos. Essa abordagem permite enxergar relações mais profundas.

No Decodificador, ao contrário, a visibilidade é intencionalmente limitada, cada token pode ver apenas aqueles que o precedem no tempo. O elemento-chave do Decodificador é o módulo de Cross-Attention (Cross-Attention). Aqui, os dados obtidos do Codificador são fornecidos como contexto, e o Decodificador relaciona os tokens recebidos na entrada com essa informação de contexto. Isso permite formar previsões de saída com mais precisão.

Essa separação de tarefas entre Codificador e Decodificador, bem como o uso dos mecanismos de atenção, tornam a arquitetura TimeFound especialmente flexível. Ela é capaz não apenas de reagir aos movimentos mais recentes de preço, mas de analisar o comportamento do ativo em um amplo contexto histórico, considerar padrões de mercado e distinguir ruído comum de sinais relevantes.

No arsenal da nossa biblioteca, já reunimos várias variações de módulos de Self- e Cross-Attention, desde os básicos até os mais avançados, com mascaramento e seleção dinâmica de cabeças de atenção. E, naturalmente, não vamos perder a oportunidade de aproveitar esses desenvolvimentos no âmbito da implementação do modelo TimeFound. No entanto, aqui é importante considerar uma nuance conceitual: os autores do framework propõem usar uma abordagem autorregressiva para a geração da previsão.

O modelo não constrói a previsão para todo o intervalo temporal em um único passo. Em vez disso, ele atua de forma iterativa. A cada iteração, o Transformer recebe na entrada os dados históricos, um tensor de tokens que reflete diferentes escalas de padrões de mercado, e gera um token que corresponde ao próximo segmento. Esse token é interpretado como um estado previsto. O resultado obtido é adicionado aos dados históricos, e o procedimento se repete. Assim, passo a passo, constrói-se a previsão para todo o horizonte de interesse.

No nosso caso, não buscamos fazer previsões longas para frente. Não brincamos de adivinhação, e sim construímos um algoritmo sistemático de gerenciamento de posição. O modelo é executado a cada nova barra, recalcula o estado com base nos dados factuais, já conhecidos, e forma uma decisão: manter, fechar, inverter, aumentar o volume. Claro, ao realizar uma operação de trade fazemos uma previsão em certa perspectiva, mas a cada nova barra a ajustamos de acordo com a realidade atualizada.

No entanto, o próprio princípio da autorregressão impõe exigências arquiteturais. Em cada etapa, é necessário alimentar o mesmo conjunto de dados históricos tanto no Codificador, quanto no Decodificador. O Codificador os analisa no contexto de todo o histórico, identificando padrões gerais, enquanto o Decodificador utiliza o estado atual, que é parte da mesma sequência histórica, para analisá-lo no contexto das dependências históricas identificadas pelo Codificador e construir valores previstos. Assim, obtemos dois fluxos paralelos de trabalho com os mesmos dados.

Para garantir tal sincronia, criaremos o objeto CNeuronTimeFoundTransformerUnit. Sua tarefa é gerenciar de forma centralizada o fluxo de informações. Isso permitirá manter a pureza arquitetural e simplificar futuras ampliações ou substituições de componentes. A estrutura do novo objeto é apresentada abaixo.

class CNeuronTimeFoundTransformerUnit  :  public CNeuronCrossDMHAttention
  {
protected:
   CNeuronMVMHAttentionMLKV   cSelfAttention;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override
                                                              { return feedForward(NeuronOCL); }
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput,
                                        CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None)
                                        override
                                        { return calcInputGradients(NeuronOCL); }
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override
                                        { return updateInputWeights(NeuronOCL); }

public:
                     CNeuronTimeFoundTransformerUnit(void) {};
                    ~CNeuronTimeFoundTransformerUnit(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window,
                          uint window_key, uint heads, uint heads_kv, uint units_count,
                          uint layers, uint layers_to_one_kv, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override const { return defNeuronTimeFoundTransformerUnit; }
   //---
   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;
  }; 

Na estrutura apresentada da nova classe, observamos apenas um objeto interno, o módulo Self-Attention, que desempenha as funções de Codificador do modelo em construção. A funcionalidade do Decodificador é implementada por meio da classe pai, na qual é utilizado o módulo de atenção cruzada.

Entretanto, por trás da simplicidade externa, encontra-se um algoritmo cuidadosamente elaborado. No papel de Codificador atua o bloco de análise independente de canais CNeuronMVMHAttentionMLKV. Sua principal característica é a capacidade de realizar análise independente de cada sequência unitária, isto é, de cada variável ou indicador separadamente. Se na entrada do modelo chegam, por exemplo, preços, volumes e volatilidade, o CNeuronMVMHAttentionMLKV estabelece relações de causa e efeito dentro de cada série temporal, sem misturar os sinais em estágio inicial. Isso minimiza o ruído cruzado e permite que o modelo compreenda melhor a estrutura de cada fonte de dados, aumentando significativamente a interpretabilidade dos resultados obtidos.

Eficiência adicional é proporcionada pela aplicação do mecanismo Multi-Layer Key-Value (MLKV) , uma das ideias mais elegantes para otimização da complexidade computacional do Transformer. Nas implementações clássicas, cada camada de Self-Attention utiliza seus próprios Key e Value. Aqui, esses tensores são armazenados e reutilizados por várias camadas, enquanto o Query é refinado em cada nível. Essa abordagem reduz a carga computacional e, ao mesmo tempo, permite refinar o significado de forma incremental, como se estivéssemos relendo um texto familiar, percebendo novos detalhes a cada leitura. Isso é especialmente valioso no trading: na primeira propagação para frente, o modelo pode identificar padrões locais, e depois estruturas de mercado cada vez mais gerais.

Assim, o Codificador nesta implementação desempenha dupla função:

  1. Por um lado, Self-Attention bidirecional clássico, que identifica relações entre tokens em ambas as direções na escala temporal.
  2. Por outro, análise modular de cada sequência unitária com possibilidade de compreensão multilayer de sua estrutura.

Agora, passemos ao Decodificador. Nesta parte, utilizamos o módulo CNeuronCrossDMHAttention, que implementa atenção cruzada multi-head diversificada. Ele também possui uma série de vantagens arquiteturais.

Em primeiro lugar, o Decodificador, em conformidade com a estrutura clássica do Transformer, inclui módulos de dois tipos de atenção:

  • Self-Attention analisa as dependências entre os tokens dos dados de entrada do fluxo principal de informações;
  • Cross-Attention enriquece esses tokens com o contexto obtido do Codificador.

Em ambos os casos, são aplicados módulos de codificação posicional relativa. Isso significa que o bloco de atenção considera não apenas a posição absoluta do token, mas também sua posição relativa em relação aos demais. Além disso, a atenção é complementada por três tipos de deslocamentos:

  • deslocamento posicional dependente do conteúdo,
  • deslocamento global de contexto,
  • deslocamento posicional global.

Isso permite que o modelo considere com maior precisão não apenas as distâncias entre eventos, mas também as relações qualitativas entre eles, o que é crítico para a análise de dados sequenciais de mercado.

Após o módulo de atenção, segue o bloco multi-head FeedForward. Sua tarefa é realizar processamento não linear independente em diferentes subespaços de características. Isso possibilita identificar uma ampla gama de padrões de mercado, sem perder elementos estruturais importantes da sequência.

Vale também destacar uma diferença importante entre os componentes da arquitetura. Diferentemente do Codificador, o Decodificador não separa as sequências unitárias. Se no Codificador a análise de cada variável ocorre de forma isolada, com preservação de sequências unitárias independentes, no Decodificador todos os tokens são reunidos em um único fluxo. Aqui já não existem fronteiras entre as séries unitárias, o modelo as percebe como um todo unificado.

Graças a isso, no Decodificador torna-se possível a análise de dependências cross-domain. isto é, dependências entre variáveis que poderiam não ser evidentes quando consideradas separadamente. O preço pode ser explicado não apenas pelo histórico do próprio preço, mas também pela volatilidade, pelo volume ou, por exemplo, pela atividade do fluxo de notícias. Todas essas dependências começam a se manifestar precisamente nos blocos de atenção cruzada, onde os tokens do estado atual são enriquecidos com as informações codificadas pelo Codificador.

Como resultado, é criada uma arquitetura poderosa e interpretável, na qual o módulo de Codificador CNeuronMVMHAttentionMLKV é combinado com um Decodificador flexível e adaptativo baseado no CNeuronCrossDMHAttention. Tal modelo escala bem, é adequado para diferentes tipos de dados de mercado e mantém alta eficiência computacional.

A declaração do Codificador ocorre de forma estática, o que nos permite deixar o construtor e o destrutor da classe vazios. A inicialização dos componentes herdados e declarados é realizada de forma centralizada no método Init. Nos parâmetros do método, recebemos um conjunto de constantes que permitem interpretar de forma unívoca a arquitetura do objeto em construção.

bool CNeuronTimeFoundTransformerUnit::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window,
                                           uint window_key, uint heads, uint heads_kv, uint units_count,
                                           uint layers, uint layers_to_one_kv, uint variables,
                                           ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronCrossDMHAttention::Init(numOutputs, myIndex, open_cl, window, window_key, variables, window,
                                      units_count * variables, heads, layers, optimization_type, batch))
      return false;
   SetActivationFunction(None);

No corpo do método, primeiramente chamamos o método com o mesmo nome da classe pai, no qual já está implementado o algoritmo de inicialização dos objetos e interfaces herdados.

Aqui vale a pena detalhar os parâmetros de inicialização passados ao método da classe pai. Antes de tudo, é preciso dizer que o objeto pai, diferentemente do que estamos criando, requer 2 fluxos de dados brutos: os dados analisados e o contexto. Já nós planejamos receber na entrada do objeto apenas um. O contexto é formado pelo Codificador dentro do módulo.

Além disso, como dados brutos, o objeto recebe uma sequência multimodal de tokens que descrevem a janela analisada de dados históricos. Cada sequência unitária é representada por uma série inteira de tokens, em que cada token corresponde a um patch, um segmento local da série temporal. Esses patches permitem que o modelo se concentre em mudanças e padrões locais, identificando dependências de curto prazo dentro de cada variável.

Na saída do objeto, espera-se apenas 1 token para cada sequência unitária, representando a previsão codificada do próximo segmento da série temporal multimodal analisada. Ao mesmo tempo, a arquitetura Transformer prevê a preservação das dimensões do fluxo principal de informações.

Considerando o exposto, decidimos alimentar pela via principal do Decodificador apenas os tokens que descrevem o último segmento do estado do ambiente, um para cada sequência unitária. Ao mesmo tempo, pelo fluxo adicional de informações é transmitido todo o volume de dados obtidos, enriquecidos pelas dependências internas no Codificador.

Após a execução bem-sucedida do método da classe pai, passamos à inicialização do Codificador. Aqui, transmitimos informações sobre o tamanho completo da sequência analisada.

   if(!cSelfAttention.Init(0, 0, OpenCL, window, window_key, heads, heads_kv, units_count, layers, layers_to_one_kv,
                           variables, optimization, iBatch))
      return false;
   cSelfAttention.SetActivationFunction(None);
//---
   return true;
  }

Observe que o Codificador e o Decodificador, em nossa implementação, utilizam objetos de arquitetura multilayer. E usamos o mesmo número de camadas em ambos os módulos.

A próxima etapa do nosso trabalho é a construção do algoritmo de propagação para frente do nosso objeto. O algoritmo é bastante simples, basta chamar os métodos com o mesmo nome, primeiro do Codificador e depois do Decodificador. No entanto, é necessário prestar atenção a algumas nuances dos fluxos de informação. Nos parâmetros do método, recebemos um ponteiro para o objeto de dados brutos. Nele, esperamos obter os dados na forma de um tensor tridimensional [Comprimento da série × Variável × Tamanho do token]. Essa é exatamente a ordem de dados necessária para o Codificador, e imediatamente passamos para ele o ponteiro para o objeto.

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

Como dados brutos do fluxo principal de informações do Decodificador, combinamos utilizar os tokens do segmento que contêm o último estado do ambiente. Porém, sabemos que alimentar o modelo com os dados analisados ocorre em ordem cronológica inversa, primeiro vêm os dados mais recentes. Consequentemente, os tokens de que precisamos estão localizados no início do buffer de dados brutos. Aproveitando essa circunstância, ignoramos a etapa de cópia do fragmento necessário de dados e simplesmente passamos o ponteiro para o objeto de dados brutos pelo fluxo principal de informações, e os resultados do trabalho do Codificador pelo fluxo auxiliar.

   if(!CNeuronCrossDMHAttention::feedForward(NeuronOCL, cSelfAttention.getOutput()))
      return false;
//---
   return true;
  }

E concluímos o método, retornando previamente o resultado lógico da execução das operações ao programa chamador.

Com o algoritmo dos métodos de propagação reversa, proponho que você se familiarize de forma independente. O código completo da classe CNeuronCrossDMHAttention e de todos os seus métodos é apresentado no anexo.



Arquitetura do modelo

Após a construção de todos os objetos necessários, que representam os componentes estruturais do nosso modelo, passamos à etapa seguinte, a construção da arquitetura completa. Aqui é importante destacar que, assim como em nossos desenvolvimentos anteriores, estamos criando um Agente de trading que opera dentro do conceito de aprendizado por reforço.

No contexto da tarefa proposta, o framework TimeFound desempenha apenas a função de análise do estado do ambiente, pré-processamento das informações de mercado e identificação de padrões ocultos em séries temporais históricas. É nessa qualidade que seu componente será integrado à arquitetura do Codificador do ambiente, que fornece ao restante do modelo uma representação estruturada, limpa e interpretável da situação atual do mercado.

No âmbito do Agente de trading como um todo, seguimos a estrutura Actor–Director–Critic, na qual cada componente resolve uma tarefa estritamente definida. Na implementação atual, o sistema inclui 4 modelos autônomos que trabalham em conjunto:

  • Codificador do estado do ambiente (Encoder) é responsável pela análise profunda dos dados de mercado recebidos. Ele identifica padrões estáveis nas séries temporais analisadas, criando uma representação rica do contexto atual de trading. É essa representação que posteriormente é transmitida ao Ator.
  • Ator (Actor) é o elemento central do Agente. Recebendo como entrada as informações do Codificador, ele forma decisões concretas de trading.
  • Diretor (Director) e Crítico (Critic) são subsistemas de avaliação. Eles avaliam as decisões do Ator utilizando um modelo interno do futuro e preveem possíveis consequências. O Director fornece uma avaliação binária mais rígida, se tal comportamento deve ou não ser considerado, enquanto o Critic fornece uma métrica quantitativa da recompensa esperada. Juntos, eles formam o sinal de retroalimentação necessário para o treinamento ou aprendizado e adaptação do Agente às condições mutáveis do mercado.

Graças a essa abordagem, alcança-se modularidade e flexibilidade do sistema: cada componente pode ser melhorado ou adaptado de forma independente para uma classe específica de estratégias de trading.

A arquitetura de todos os modelos é definida no método CreateDescriptions, em cujos parâmetros são passados ponteiros para 4 arrays dinâmicos (de acordo com o número de modelos). Em cada um deles será armazenada uma sequência única de objetos que fornece uma representação completa do modelo criado.

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

Comecemos pelo Codificador. Planejamos alimentar o modelo com um tensor contendo o histórico das séries temporais, cujos dados transferimos imediatamente para uma camada totalmente conectada de dimensão suficiente. Essa camada serve como uma espécie de porta de entrada do modelo. Definimos o número de neurônios como o produto do número de barras do histórico pelo número de descritores de cada barra, o que permite preservar todas as informações sobre a dinâmica de preço, volumes e outros indicadores.

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

Em seguida vem a camada BatchNormWithNoise. Ela tem como objetivo estabilizar o treinamento ou aprendizado e adicionar um pequeno componente estocástico, o que ajuda o modelo a não ficar preso em mínimos locais durante a otimização. É exatamente aqui que introduzimos o controle de ruído: durante o processo de normalização são adicionadas flutuações aleatórias, garantindo melhor capacidade de generalização em dados reais de mercado, que frequentemente apresentam ruído.

//--- 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;
     }

Após a normalização dos dados, adicionamos um pouco de informação sobre sua dinâmica no módulo ConcatDiff. Essa camada calcula as primeiras diferenças na escala temporal e as adiciona como novos canais aos dados originais, permitindo que o modelo capture melhor o ritmo de variação dos indicadores.

//--- 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;
     }

Em seguida vem o Mamba4CastEmbedding, aqui incorporamos a codificação temporal de cada barra do histórico. Definimos harmônicos de duas periodicidades, horária e diária, para que o modelo capture padrões diários e oscilações intradiárias. Depois disso, as incorporações de cada barra são combinadas em uma representação geral.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defMamba4CastEmbeding;
   prev_count = descr.count = HistoryBars;
   descr.window = 2 * BarDescr;
   int prev_out = descr.window_out = NSkills;
     {
      int 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;
     }

Quando a incorporação está pronta, passamos ao TimeFoundPatching. O módulo divide o tensor obtido em um número previamente definido de segmentos, patches, de tamanho igual, organizando-os em uma única matriz. Cada patch carrega informações sobre a dinâmica local, e seu conjunto serve como entrada para o bloco Transformer.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTimeFoundPatching;
   descr.count = prev_count;
   prev_count=descr.window = Segments;
   descr.variables = prev_out;
   descr.window_out = EmbeddingSize;
   descr.step = 8;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

E é aqui que surge nossa camada-chave, TimeFoundTransformerUnit. Esse objeto permite que o modelo analise simultaneamente cada sequência unitária separadamente e capture dependências cross-domain.

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTimeFoundTransformerUnit;
   descr.count = prev_count;
   descr.window = EmbeddingSize;
   descr.window_out =descr.window/4;
   {
      int temp[]={4,2};
      if(ArrayCopy(descr.heads,temp)<ArraySize(temp))
        return false;
   }
   descr.layers=3;
   descr.step=3;
   descr.variables=prev_out;
   descr.batch = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Na saída do Transformer, esperamos obter tokens previstos do patch subsequente para cada canal analisado. Recordo que, na etapa de pré-processamento, o número de canais foi significativamente ampliado. Agora, para cada canal, temos um novo token que reflete a previsão de seu comportamento no próximo intervalo de tempo.

Para expandir esse recorte compacto novamente em uma série prevista completa para o horizonte de planejamento definido, aplicamos uma camada convolucional. Ela recebe o conjunto de tokens previstos, passa-os por vários filtros e os transforma em uma sequência de elementos do comprimento necessário, pronta para o trabalho posterior do robô de trading. Essa abordagem permite preservar as vantagens da previsão autorregressiva no nível do token, mas obter na saída uma série temporal completa para uso nas estratégias.

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 1;
   descr.step = EmbeddingSize;
   prev_count=descr.layers = prev_out;
   descr.window = EmbeddingSize;
   prev_out=descr.window_out = NForecast;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = SoftPlus;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     } 

Na saída da camada convolucional, obtemos um conjunto de sequências unitárias do comprimento necessário. Cada uma delas reflete a previsão de seu respectivo canal. Porém, para comparação com os dados reais, precisamos não de um conjunto de séries separadas, mas de uma única sequência multimodal, na qual elementos adjacentes correspondam a diferentes fontes de informação no mesmo momento do tempo. Por isso, no passo seguinte, transponhamos o resultado. Como resultado, obtemos uma matriz de dimensão [Horizonte de planejamento × Número de canais].

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

Em seguida, aplicamos à matriz obtida mais uma camada convolucional, cuja tarefa é retornar o número de canais à dimensão original. 

//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_out;
   descr.window = prev_count;
   descr.step = prev_count;
   descr.layers = 1;
   prev_out=descr.window_out = BarDescr;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_count=descr.count;

E, finalmente, para que nossas previsões adquiram valores reais, nós as passamos por uma camada de normalização reversa. Ela recebe cada uma das sequências multimodais obtidas e restaura os parâmetros estatísticos originais, média e variância, dos mesmos canais com os quais começamos. Assim, transformamos os tokens padronizados novamente em unidades de medida habituais.

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronRevInDenormOCL;
   descr.count = prev_count * prev_out;
   descr.layers = 1;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Assim, como resultado do trabalho do Codificador, obtemos na saída valores previstos completos de toda a série multimodal nas unidades de medida familiares ao trader, preços, volumes, indicadores. Esses dados podem ser visualizados, comparados com as cotações reais e utilizados para interpretar as ações do Agente.

Além disso, isso nos permite treinar o Codificador em regime de Self-Supervised Learning, prevendo o movimento futuro com base em um grande volume de dados históricos não rotulados. O que, por sua vez, permitirá formar representações latentes informativas dos tokens previstos. São justamente essas representações que planejamos transmitir ao Ator, ao Diretor e ao Crítico, garantindo estabilidade, adaptabilidade e capacidade de generalização do modelo.

CLayerDescription *latent = encoder. At(LatentLayer);

Em seguida, passamos à descrição da arquitetura do Ator. Como foi dito anteriormente, esse modelo analisa o estado atual da conta no contexto da situação de mercado e toma uma decisão de trading. Pela via principal de informações, planejamos alimentar o modelo com um tensor que descreve a representação da conta, saldo, equity, posições abertas.

Para obter os dados brutos e realizar seu pré-processamento, utilizamos a combinação de uma camada totalmente conectada e uma camada de normalização em lote. Apenas que, diferentemente do Codificador, não adicionamos ruído, pois nosso estado financeiro não admite flutuações artificiais. Primeiro, o tensor da conta é passado por uma camada totalmente conectada de dimensão suficiente e, em seguida, é normalizado, garantindo uma representação estável e determinística.

//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = AccountDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = AccountDescr;
   descr.batch = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Os dados preparados são enriquecidos com o contexto das informações de mercado no módulo de atenção multi-head diversificada, que permite considerar as inter-relações entre o estado da conta e os padrões globais do mercado, proporcionando uma escolha mais fundamentada da estratégia de trading.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronCrossDMHAttention;
     {
      int temp[] = {AccountDescr,    // Inputs window
                    latent.window    // Cross window
                   };
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
     {
      int temp[] = {1,                 // Inputs units
                    latent.variables    // Cross units
                   };
      if(ArrayCopy(descr.units, temp) < (int)temp.Size())
         return false;
     }
   descr.step = 4;                  // Heads
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.layers = 3;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Para a tomada final de decisão, é utilizado um MLP de três camadas com diferentes funções de ativação entre as camadas, adicionando a não linearidade necessária. Na primeira camada aplica-se a tangente hiperbólica, tanh, o que permite separar as influências dos sinais em direções positivas e negativas. Após a segunda camada, utiliza-se SoftPlus, enfatizando o peso das respostas positivas e suavizando valores baixos. Por fim, a camada de saída é controlada por uma sigmoide, que normaliza o espaço de ações do Ator e garante seu valor no intervalo [0,1], o que é conveniente para o escalonamento dos parâmetros de trading.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.batch = BatchSize;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SoftPlus;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = NActions;
   descr.activation = SIGMOID;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Os modelos do Diretor e do Crítico possuem arquitetura semelhante, apenas o tensor do estado da conta é substituído pelo vetor de ações do Ator, e o tamanho da camada de resultados é alterado. Por isso, não os analisaremos detalhadamente neste artigo, deixando isso para estudo independente. A arquitetura completa de todos os modelos é apresentada no anexo.



Programa de treinamento do Codificador do ambiente

Chegamos, assim, ao treinamento ou aprendizado dos modelos. Esse processo foi dividido em três etapas. Inicialmente, destacamos separadamente o treinamento do Codificador do estado da conta. Com o objetivo de garantir o treinamento ou aprendizado com o maior volume de dados disponível, esse processo é realizado sem a formação explícita de um conjunto de treinamento, todos os dados necessários são recebidos diretamente do terminal de trading em tempo real.

A implementação desta etapa de treinamento ou aprendizado está organizada no EA "…\Experts\TimeFound\StudyEncoder.mq5". Aqui, o método CreateBuffers é responsável pelo gerenciamento dos buffers, em cujos parâmetros são passados o índice do estado inicial e ponteiros para dois buffers, um para os dados brutos analisados e outro para os valores-alvo do movimento previsto. Essa abordagem permite construir o treinamento ou aprendizado Self-Supervised do Codificador sem custos adicionais com a rotulação de séries históricas.

bool CreateBuffers(const int start_bar, CBufferFloat* state, CBufferFloat *time, CBufferFloat* forecast)
  {
   if(!state || !time || start_bar < 0 ||
      (start_bar + HistoryBars + NForecast) >= int(Rates.Size()))
      return false;
//---
   vector<float> vState = vector<float>::Zeros(HistoryBars * BarDescr);
   vector<float> vForecast = vector<float>::Zeros(NForecast * BarDescr);
   time.Clear();
   time.Reserve(HistoryBars);

No corpo do método, verificamos se há informação suficiente nos dados previamente carregados e preparamos os buffers internos para armazenamento dos valores. Em seguida, são organizados os laços de preparação dos dados. As séries temporais brutas, previamente carregadas do terminal, são armazenadas de modo que os pontos mais recentes estejam no início e os mais antigos no final, mantendo ao mesmo tempo a ordem cronológica. Para o ajuste dos parâmetros do Codificador, em cada etapa de treinamento ou aprendizado precisamos obter um intervalo temporal completo, abrangendo a janela de dados históricos e o horizonte de planejamento definido.

Primeiramente, forma-se o vetor do estado analisado: a partir do índice indicado nos parâmetros, avançamos pelo horizonte de planejamento definido e copiamos todos os elementos subsequentes para o buffer de dados brutos. Essa ordem de operações, que repete a cronologia das séries temporais, minimiza os custos adicionais de preparação dos dados e garante o funcionamento eficiente do modelo em tempo real.

int bar = start_bar + NForecast;
for(int b = 0; b < (int)HistoryBars; b++)
  {
   float open = (float)Rates[b + bar].open;
   float rsi = (float)RSI.Main(b + bar);
   float cci = (float)CCI.Main(b + bar);
   float atr = (float)ATR.Main(b + bar);
   float macd = (float)MACD.Main(b + bar);
   float sign = (float)MACD.Signal(b + bar);
   if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE ||
      macd == EMPTY_VALUE || sign == EMPTY_VALUE)
      return false;
   //---
   int shift = b * BarDescr;
   vState[shift] = (float)(Rates[b + bar].close - open);
   vState[shift + 1] = (float)(Rates[b + bar].high - open);
   vState[shift + 2] = (float)(Rates[b + bar].low - open);
   vState[shift + 3] = (float)(Rates[b + bar].tick_volume / 1000.0f);
   vState[shift + 4] = rsi;
   vState[shift + 5] = cci;
   vState[shift + 6] = atr;
   vState[shift + 7] = macd;
   vState[shift + 8] = sign;
   if(!time.Add(float(Rates[b + bar].time)))
      return false;
  }

Em seguida, passamos à preparação dos valores-alvo. Como se tratam de dados previstos, eles se referem ao futuro e, para a formação correta dos buffers-alvo, é necessário inverter a ordem dos valores. Para isso, ao transferir os dados utilizamos indexação reversa: o último elemento do horizonte-alvo torna-se o primeiro no buffer e assim por diante. Essa técnica garante que o modelo, em cada etapa de treinamento ou aprendizado, associe corretamente a entrada atual ao respectivo valor futuro.

bar--;
for(int b = 0; b < (int)NForecast; b++)
  {
   float open = (float)Rates[bar - b].open;
   float rsi = (float)RSI.Main(bar - b);
   float cci = (float)CCI.Main(bar - b);
   float atr = (float)ATR.Main(bar - b);
   float macd = (float)MACD.Main(bar - b);
   float sign = (float)MACD.Signal(bar - b);
   if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE ||
      macd == EMPTY_VALUE || sign == EMPTY_VALUE)
      return false;
   //---
   int shift = (NForecast - b - 1) * BarDescr;
   vForecast[shift] = (float)(Rates[bar - b].close - open);
   vForecast[shift + 1] = (float)(Rates[bar - b].high - open);
   vForecast[shift + 2] = (float)(Rates[bar - b].low - open);
   vForecast[shift + 3] = (float)(Rates[bar - b].tick_volume / 1000.0f);
   vForecast[shift + 4] = rsi;
   vForecast[shift + 5] = cci;
   vForecast[shift + 6] = atr;
   vForecast[shift + 7] = macd;
   vForecast[shift + 8] = sign;
  }

As séries temporais preparadas são transferidas para os buffers de dados e concluímos o método, retornando previamente o resultado lógico da execução das operações ao programa chamador.

   if(!state.AssignArray(vState))
      return false;
   if(!forecast.AssignArray(vForecast))
      return false;
   if(time.GetIndex() >= 0)
      if(!time.BufferWrite())
         return false;
//---
   return true;
  }

O próprio processo de treinamento ou aprendizado é organizado no método Train. Nele, inicialmente determinamos o tamanho do buffer do conjunto de treinamento com base nas datas de início e término do período de treinamento definidas pelo usuário.

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);

Com base nesses limites, calcula-se o número de barras e aloca-se o volume necessário de memória para os buffers de dados.

if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) ||
   !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }

Em seguida, realiza-se o carregamento dinâmico dos dados históricos a partir do terminal para o instrumento e timeframe especificados.

   int count = -1;
   bool load = false;
   do
     {
      RSI.Refresh();
      CCI.Refresh();
      ATR.Refresh();
      MACD.Refresh();
      count++;
      load = (RSI.BarsCalculated() >= bars &&
              CCI.BarsCalculated() >= bars &&
              ATR.BarsCalculated() >= bars &&
              MACD.BarsCalculated() >= bars
             );
      Sleep(100);
      count++;
     }
   while(!load && count < 100);
   if(!load)
     {
      PrintFormat("%s -> %d The training data has not been loaded", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
//---
   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;
     }

Após a preparação dos dados de treinamento, passamos diretamente ao processo de treinamento ou aprendizado, organizando-o no corpo de um sistema de laços. O laço externo é responsável pela contagem das iterações de treinamento. Seu número total é definido pelo usuário nos parâmetros do programa.

   vector<float> result, target, neg_target;
   bool Stop = false;
//---
   uint ticks = GetTickCount();
//---
   for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter ++)
     {
      int posit = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
      if(!CreateBuffers(posit + end, GetPointer(bState), GetPointer(bTime), Result))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         ExpertRemove();
         return;
        }

No corpo do laço externo de treinamento ou aprendizado, amostramos o índice de um estado do conjunto de treinamento, que serve como referência para a formação dos dados analisados e dos dados-alvo. Esse índice, juntamente com os ponteiros para os respectivos buffers, é transmitido ao método CreateBuffers, o que permite que cada iteração receba sequências históricas e previstas corretamente alinhadas para um treinamento Self-Supervised eficiente.

Em seguida, organizamos o laço interno, cujo número de iterações é definido pelo usuário por meio do parâmetro Repeats. Aqui é preciso ser extremamente cuidadoso: dentro desse laço, treinamos o modelo repetidamente com os mesmos dados analisados e dados-alvo. A presença, no Codificador, de uma camada de normalização com adição de ruído fornece a ampliação necessária, o modelo aprende a focar na estrutura interna dos dados, e não em valores específicos. No entanto, com um número excessivo de repetições, quando os valores-alvo permanecem inalterados, o modelo corre o risco de começar a ignorar os dados brutos e produzir sempre o mesmo resultado. Por isso, recomendamos definir o parâmetro Repeats em torno de cinco iterações.

for(int r = 0; r < Repeats; r++)
  {
   //--- Feed Forward
   if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)GetPointer(bTime)))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }
   //--- Study
   if(!cEncoder.backProp(Result, (CBufferFloat*)NULL))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }

No corpo do laço, são realizadas as operações de propagação para frente e propagação reversa do modelo. Em seguida, informamos o usuário sobre o andamento do treinamento ou aprendizado e passamos para a próxima iteração do sistema de laços.

    if(GetTickCount() - ticks > 500)
      {
       double percent = double(iter) * 100.0 / (Iterations);
       string str = StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Encoder",
                                   percent, cEncoder.getRecentAverageError());
       Comment(str);
       ticks = GetTickCount();
      }
   }
}

Após a conclusão de todas as iterações, os resultados do treinamento ou aprendizado são exibidos no log, e encerramos o programa.

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Encoder", cEncoder.getRecentAverageError());
   ExpertRemove();
//---
  }

Os programas de treinamento offline e de ajuste fino online dos modelos Ator, Diretor e Crítico foram totalmente transferidos de projetos anteriores sem qualquer modificação. Além disso, utilizamos soluções já testadas para a coleta do conjunto de treinamento e para o teste dos modelos treinados, o que nos permitiu concentrar nos aprimoramentos arquiteturais, sem gastar recursos na reformulação de componentes auxiliares.

Todo o código-fonte dos programas utilizados na preparação deste artigo está apresentado no anexo.


Teste

Como já mencionado anteriormente, o processo de treinamento ou aprendizado do modelo foi organizado em três etapas consecutivas, o que permitiu estruturar todo o sistema de forma lógica, gradual e com máxima confiabilidade.

O primeiro passo foi o treinamento do Codificador com cinco anos de dados históricos do par EURUSD no timeframe de um minuto. Esse volume e nível de detalhamento permitem formar uma representação latente realmente profunda e informativa do estado atual do mercado. O Codificador aprende a distinguir padrões importantes, identificar regularidades e codificar a situação de mercado em um vetor compacto, porém informativo, que posteriormente é utilizado por todos os demais módulos.

Em seguida vem a segunda etapa, o treinamento offline dos principais elementos da nossa arquitetura: Ator, Diretor e Crítico. Para isso, foi reunido um conjunto de dados de mercado do ano de 2024, mantendo todos os parâmetros aplicados no treinamento do Codificador. Durante o processo de treinamento ou aprendizado, foi utilizada a concepção de trajetória quase ideal: as ações do Agente não foram escolhidas de forma arbitrária, mas com base na análise do movimento posterior dos preços. Em outras palavras, tendo à disposição toda a trajetória de preços, sabíamos previamente quais ações teriam levado aos melhores resultados, e utilizamos exatamente essas para o treinamento ou aprendizado. Essa abordagem permite mostrar ao modelo como se deve operar, em vez de obrigá-lo a buscar cegamente uma estratégia eficaz por tentativa e erro, vagando pelo ambiente sem mapa. Graças a isso, o Agente aprende com exemplos previamente verificados, claros, fundamentados e o mais próximo possível do ideal do ponto de vista do resultado. Isso não apenas simplifica o processo de treinamento ou aprendizado, mas o torna direcionado e economicamente fundamentado.

A etapa final é o ajuste fino online, realizado diretamente no testador de estratégias. Aqui, os modelos se deparam com dados históricos em um regime o mais próximo possível do trading real e adaptam seus parâmetros à dinâmica viva do mercado. Isso é especialmente importante, pois permite refinar o comportamento do Agente levando em conta condições mutáveis, ruído de mercado e flutuações aleatórias que não são visíveis no conjunto de treinamento.

Após a conclusão de toda a cadeia de treinamento ou aprendizado, o modelo foi testado em novos dados — cotações de Janeiro de 2025. Todos os parâmetros e configurações utilizados durante o treinamento ou aprendizado foram mantidos sem alterações, o que garante total objetividade e honestidade na avaliação. Os resultados do teste são apresentados abaixo.

Os resultados do teste podem ser considerados encorajadores, mas com ressalvas. A rentabilidade final foi de +26,5% no mês, no entanto o gráfico de saldo não demonstra uma tendência de alta sustentável. Ao contrário, na maior parte do tempo observa-se um movimento lateral, com períodos de instabilidade e rebaixamentos expressivos. O rebaixamento máximo atingiu 58%, o que indica alta volatilidade e instabilidade das decisões em determinadas situações de mercado.

Esse quadro sugere mais que o modelo ainda está em busca de um estilo de trading sustentável, do que seguindo uma trajetória claramente lucrativa. Ainda assim, o resultado positivo por si só é um sinal encorajador: a base foi construída, agora a tarefa — aumentar a estabilidade.



Considerações finais

Ao longo deste trabalho, percorremos o caminho desde o conceito de previsão universal cross-domain de séries temporais até sua implementação prática no ambiente MetaTrader 5. Analisamos detalhadamente a estrutura do Codificador, dominamos o mecanismo de patching multiescalar e, em seguida, integramos as representações latentes obtidas à arquitetura Actor–Director–Critic. Cada etapa, desde a preparação dos dados e o treinamento Self-Supervised do Codificador até o ajuste offline e online dos módulos principais, demonstrou como a combinação harmoniosa de pesquisas modernas e soluções de engenharia pode elevar a qualidade da análise da dinâmica de mercado.

O teste com as cotações de Janeiro de 2025 demonstrou que nosso sistema é capaz de gerar lucro, embora não esteja isento de rebaixamentos profundos. Isso ressalta a importância de novos aprimoramentos em gerenciamento de risco e mecanismos adaptativos de limitação de perdas. Ao mesmo tempo, o próprio fato de apresentar rentabilidade positiva sem otimização adicional de parâmetros indica que a arquitetura escolhida possui uma estrutura sólida e permite construir previsões reutilizando o conhecimento já adquirido.


Links


Programas utilizados no artigo

# Nome Tipo Descrição
1 Research.mq5 Expert EA coletor de exemplos
2 ResearchRealORL.mq5
Expert
EA de coleta de exemplos pelo método Real-ORL
3 StudyEncoder.mq5 Expert EA de treinamento do Codificador do ambiente
4 Study.mq5 Expert EA de treinamento offline dos modelos
5 StudyOnline.mq5
Expert
EA de treinamento online dos modelos
6 Test.mq5 Expert EA para teste do modelo
7 Trajectory.mqh Biblioteca de classe Estrutura de descrição do estado do sistema e da arquitetura dos modelos
8 NeuroNet.mqh Biblioteca de classe Biblioteca de classes para criação da rede neural
9 NeuroNet.cl Biblioteca Biblioteca de código de programas OpenCL

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

Arquivos anexados |
MQL5.zip (2829.04 KB)
Redes neurais em trading: Pipeline inteligente de previsões (Time-MoE) Redes neurais em trading: Pipeline inteligente de previsões (Time-MoE)
Propomos conhecer o framework moderno Time-MoE, adaptado para tarefas de previsão de séries temporais. No artigo, implementaremos passo a passo os principais componentes da arquitetura, acompanhando-os com explicações e exemplos práticos. Essa abordagem permitirá não apenas compreender os princípios de funcionamento do modelo, mas também aplicá-los em tarefas reais de trading.
Técnicas do MQL5 Wizard que você deve conhecer (Parte 54): Aprendizado por Reforço com SAC híbrido e Tensores Técnicas do MQL5 Wizard que você deve conhecer (Parte 54): Aprendizado por Reforço com SAC híbrido e Tensores
Soft Actor Critic é um algoritmo de Aprendizado por Reforço que analisamos em um artigo anterior, onde também introduzimos Python e ONNX nesta série como abordagens eficientes para treinar redes. Retomamos o algoritmo com o objetivo de explorar tensores, grafos computacionais que frequentemente são utilizados em Python.
Do básico ao intermediário: Sub Janelas (III) Do básico ao intermediário: Sub Janelas (III)
Este texto detalha o uso de sub janelas em indicadores MQL5: criação básica, detecção de instâncias e prevenção de duplicações. Aborda INDICATOR_SHORTNAME, consulta de janelas/indicadores do gráfico e a diferença entre exibição no gráfico principal e em janela separada. Mostra ainda como definir altura e limites da sub janela para padronizar a interface e facilitar a disposição de objetos.
Dominando JSON: Crie Seu Próprio Leitor JSON do Zero em MQL5 Dominando JSON: Crie Seu Próprio Leitor JSON do Zero em MQL5
Experimente um guia passo a passo sobre como criar um parser JSON personalizado em MQL5, completo com manipulação de objetos e arrays, verificação de erros e serialização. Obtenha insights práticos para conectar sua lógica de trading e dados estruturados com esta solução flexível para lidar com JSON no MetaTrader 5.