Русский
preview
Redes neurais em trading: Pipeline inteligente de previsões (Conclusão)

Redes neurais em trading: Pipeline inteligente de previsões (Conclusão)

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

Introdução

O framework Time-MoE oferece uma visão verdadeiramente nova sobre o trabalho com séries temporais. Diferentemente dos modelos clássicos, ele preserva a informação completa de cada tick ou candle graças à tokenização pontual e, em seguida, enriquece esses tokens atômicos com o embedding SwiGLU, capaz de capturar tanto movimentos de tendência suaves quanto picos bruscos de volatilidade.

A ideia central dos autores consiste em utilizar uma mistura esparsa de especialistas dentro do Decoder-Only Transformer, na qual, para cada token, são dinamicamente selecionados os modelos mais relevantes e, juntamente com um especialista comum permanente, formam uma arquitetura verdadeiramente adaptativa e escalável. As saídas de previsão multi-cabeça complementam o conjunto, permitindo simultaneamente construir estimativas em diferentes horizontes, do tick mais próximo até a tendência semanal.

No primeiro artigo, dedicado ao framework Time-MoE, analisamos passo a passo como transformar as ideias teóricas do trabalho original "Time-MoE: Billion-Scale Time Series Foundation Models with Mixture of Experts" em código funcional em MQL5. Criamos o módulo CNeuronSwiGLUOCL, que transforma dados brutos em vetores ocultos ao combinar duas projeções.

Na segunda parte, concentramos nossa atenção no mecanismo de mistura esparsa de especialistas e o reunimos em um módulo único CNeuronTimeMoESparseExperts, no qual combinamos caminhos individuais e comum de processamento de dados, o roteador Top-K e o gate sigmoidal do especialista comum.

Agora chegou o momento de conectar todos esses elementos em um modelo completo e organizar o seu treinamento. Ao final, realizaremos o teste do modelo treinado em dados históricos reais.



Módulo de atenção

Antes de iniciar a construção da arquitetura completa do modelo, precisamos introduzir um pequeno, mas extremamente importante ajuste: substituir o bloco FeedForward na arquitetura Transformer pelo módulo de mistura esparsa de especialistas que criamos. Recordemos que a base do Time-MoE é uma arquitetura Decoder-Only, na qual o papel fundamental é desempenhado pela atenção cruzada (Cross-Attention), que permite ao decodificador considerar informações da camada de contexto.

Para isso, como classe base, é escolhido o componente de atenção cruzada já pronto CNeuronCrossDMHAttention, que fornece todos os mecanismos necessários. Nossa tarefa é apenas substituir o bloco FeedForward por uma chamada ao CNeuronTimeMoESparseExperts. A estrutura do novo objeto CNeuronTimeMoEAttention é apresentada a seguir.

class CNeuronTimeMoEAttention   :  public CNeuronCrossDMHAttention
  {
protected:
   //---
   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:
                     CNeuronTimeMoEAttention(void)  {};
                    ~CNeuronTimeMoEAttention(void)  {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint units_count,
                          uint window_cross, uint units_cross,
                          uint heads, uint layers,
                          uint experts, uint experts_dimension, uint topK,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronTimeMoEAttention; }
  }; 

Vale lembrar que o objeto CNeuronCrossDMHAttention é construído segundo o princípio de arquitetura interna dinâmica. Isso significa que os componentes-chave do módulo não são declarados rigidamente na estrutura da classe, mas são criados dinamicamente na quantidade necessária durante a inicialização. Na estrutura da classe é criado apenas um array dinâmico para armazenar ponteiros para esses objetos, o que permite, se necessário, literalmente remodelar o esquema interno sem alterar todo o código, bastando ajustar a lógica no método Init.

Os demais métodos simplesmente são herdados da classe pai ou exigem alterações mínimas. Essa abordagem garante máxima flexibilidade e reutilização, permitindo criar facilmente novas variações do módulo, alterando apenas os parâmetros de inicialização. É exatamente por isso que concentramos toda a atenção no método de inicialização, pois é aqui que nasce a estrutura interna do nosso módulo de atenção com o bloco MoE incluído.

bool CNeuronTimeMoEAttention::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                   uint window, uint window_key, uint units_count,
                                   uint window_cross, uint units_cross, uint heads,
                                   uint layers, uint experts, uint experts_dimension,
                                   uint topK, ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count,
                                                     optimization_type, batch))
      return false;

No corpo do método, a primeira ação é chamar o método de mesmo nome da camada neural base. Isso permite criar o esqueleto do módulo. E então começa a parte mais interessante. Preparamos o array de camadas internas cLayers e o vinculamos à nossa interface OpenCL.

cLayers.Clear();
cLayers.SetOpenCL(OpenCL);
CNeuronRelativeSelfAttention *attention = NULL;
CNeuronRelativeCrossAttention *cross = NULL;
CNeuronTimeMoESparseExperts *MoE = NULL;
bool use_self = units_count > 0;
int layer = 0;

Em seguida, também preparamos as variáveis locais necessárias. Com isso, conclui-se a etapa de trabalho preparatório.

Depois, em um loop conforme o número definido de camadas internas (layers), construímos gradualmente uma cadeia unificada. Se no fluxo principal de informação houver mais de um token, primeiro criamos e inicializamos o módulo Self-Attention, adicionando-o ao cLayers. Isso permite que o decodificador analise por conta própria os tokens já gerados antes de alternar para fontes externas de contexto.

for(uint i = 0; i < layers; i++)
  {
   if(use_self)
     {
      attention = new CNeuronRelativeSelfAttention();
      if(!attention ||
         !attention.Init(0, layer, OpenCL, window, window_key, units_count, heads,
                                                          optimization, iBatch) ||
         !cLayers.Add(attention)
        )
        {
         delete attention;
         return false;
        }
      layer++;
     }

Em seguida, sem rodeios, é criado o módulo de atenção cruzada, que combina as informações da camada anterior com o contexto. Passamos a ele, dinamicamente, os parâmetros de janela e o número de cabeças. Ele imediatamente entra em operação, pronto para calcular queries, keys e values. Dentro do loop, o ponteiro para cada um desses objetos é armazenado no array de camadas, como se estivéssemos montando, elo por elo, uma única cadeia de processamento.

cross = new CNeuronRelativeCrossAttention();
if(!cross ||
   !cross.Init(0, layer, OpenCL, window, window_key, units_count, heads,
                     window_cross, units_cross, optimization, iBatch) ||
   !cLayers.Add(cross)
  )
  {
   delete cross;
   return false;
  }
layer++;

Mas o principal destaque vem logo após o Cross-Attention: criamos uma instância de CNeuronTimeMoESparseExperts. É exatamente aqui que configuramos o nosso bloco MoE: especificamos o número de especialistas, o tamanho de suas projeções, isto é, quantas características cada especialista processa, e o parâmetro topK, que determina quantos especialistas são ativados em cada passagem. E, naturalmente, não esquecemos da integração com o contexto OpenCL.

 MoE = new CNeuronTimeMoESparseExperts();
 if(!MoE ||
    !MoE.Init(0, layer, OpenCL, window, experts_dimension, units_count, 1,
                                   experts, topK, optimization, iBatch) ||
    !cLayers.Add(MoE)
   )
   {
    delete MoE;
    return false;
   }
 layer++;
}

Após concluir o loop de inicialização de todas as camadas, conectamos os buffers de resultados da última camada às interfaces externas do objeto.

   SetOutput(MoE.getOutput(), true);
   SetGradient(MoE.getGradient(), true);
//---
   return true;
  }

Como resultado, o método Init transforma um contêiner vazio em um organismo vivo composto por três tipos de camadas, organizadas em sequência rigorosa, e faz isso sem qualquer modificação nos mecanismos básicos de atenção. Toda a dinâmica é criada apenas pela adição de novos elementos e por sua configuração paramétrica, um exemplo clássico de arquitetura flexível com esforço mínimo de manutenção.

Mas há um detalhe. O módulo de atenção cruzada exige dois fluxos de informação: o principal e o de contexto. E no framework Time-MoE vemos apenas um. Essa questão é resolvida com intervenção mínima nos mecanismos principais. Apenas sobrescrevemos os métodos de propagação para frente e de propagação reversa que, ao receberem um único fluxo de informação como entrada, o redirecionam em duas direções.

bool CNeuronTimeMoEAttention::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
   return CNeuronCrossDMHAttention::feedForward(NeuronOCL, NeuronOCL.getOutput());
  }

À primeira vista, tal operação transforma o módulo Cross-Attention em Self-Attention. No entanto, nossa ideia consiste em, pelo fluxo principal de informação, transmitir apenas os tokens do último passo temporal. Isso é resolvido por meio da redução da quantidade de elementos analisados no fluxo principal de informação. Já pelo fluxo de informação de contexto, transmitimos o conjunto completo de dados, permitindo enriquecer os tokens do fluxo principal com todo o contexto histórico. Graças a isso, são preservadas todas as vantagens da arquitetura Decoder-Only do Time-MoE: cada novo token consulta os especialistas sobre seu subespaço específico<5> e, ao mesmo tempo, apoia-se em toda a dinâmica acumulada do mercado.

O código-fonte completo dessa classe e de todos os seus métodos está apresentado no anexo.



Arquitetura dos modelos

Depois que todos os componentes necessários foram implementados, finalmente passamos à construção da arquitetura completa dos modelos treináveis. Assim como em trabalhos anteriores, o projeto continua baseado no framework Actor-Director-Critic, que já se mostrou eficaz em tarefas de aprendizado por reforço. É dentro dessa estrutura que integramos todas as soluções relacionadas ao Time-MoE, incorporando-as ao Encoder do estado do ambiente, a parte do modelo responsável por formar uma representação semanticamente rica dos dados de entrada.

No entanto, nesta etapa surge um ponto importante. O pipeline geral que utilizamos prevê apenas uma única saída para o modelo, um vetor universal de características, utilizado por todos os componentes: Actor, Director e Critic. Ao mesmo tempo, a arquitetura original do Time-MoE pressupõe a existência de múltiplas cabeças de previsão, cada uma operando com um horizonte de planejamento diferente. Isso cria um potencial conflito. Misturar todas as saídas em um único buffer significa complicar o treinamento e perder interpretabilidade.

Optamos por não seguir o caminho de unificar tudo em uma única estrutura. Em vez disso, foi implementada uma separação lógica clara entre o Encoder e as cabeças de previsão. O Encoder, baseado no Time-MoE, atua como um mecanismo universal de extração de características, funcionando de forma única para todos. Em seguida, para cada horizonte de planejamento, é criada sua própria modelagem, que recebe como entrada os resultados do Encoder e constrói a previsão dentro de sua respectiva tarefa.

Essa abordagem oferece vantagens significativas: mantemos um bloco treinável unificado para extração de informações e, ao mesmo tempo, garantimos independência e flexibilidade no nível das previsões. Cada cabeça responde por sua própria escala temporal e, portanto, pode se adaptar ao caráter específico das oscilações do mercado. Como resultado, a arquitetura permanece modular, escalável e, ao mesmo tempo, profundamente adaptativa à estrutura do contexto temporal.

Após definir a lógica geral do framework, passamos à construção da arquitetura dos modelos treináveis, na qual cada subsistema é montado segundo um princípio unificado. Isso garante clareza estrutural e possibilita escalar o modelo de forma flexível conforme as necessidades do núcleo de trading.

Toda a inicialização está concentrada no método CreateDescriptions, que monta as arquiteturas camada por camada, começando pelo Encoder.

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

Na entrada do Encoder são fornecidos dados históricos brutos do mercado. A primeira camada é simplesmente um objeto base que, essencialmente, atua como interface de recebimento de dados. Em seguida, vem uma camada de normalização em lote com adição de ruído, que estabiliza a distribuição dos valores iniciais e introduz certa augmentação de dados, ajudando a evitar o overfitting do modelo.

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

Depois é utilizada a camada ConcatDiff, que adiciona canais de valores diferenciais, decompondo cada barra em seus componentes e preparando a estrutura de dados para um processamento mais complexo.

//--- 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, os dados entram no módulo Mamba4CastEmbedding. É aqui que a série de entrada é enriquecida com marcações simultâneas de múltiplas escalas temporais. Esse mecanismo permite obter uma representação multifrequencial do sinal original. Essa camada produz uma janela latente de comprimento fixo (NSkills), que então passa por uma camada de transposição para ajuste da dimensionalidade do tensor, tornando-a conveniente para o processamento posterior de sequências unitárias independentes de canais individuais.

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

Na sequência, vem uma camada do tipo embedding SwiGLU. Ela aumenta a profundidade das características e forma a base inicial para uma representação abstrata do estado.

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

Em seguida, aplica-se a camada de transposição TransposeRCDOCL, que permite reorientar as dimensões dos passos temporais e dos canais analisados. Essa operação é crítica para o funcionamento correto do módulo de atenção que criamos. É graças a ela que, no início do buffer, são reunidos os tokens do último passo temporal de todas as variáveis analisadas.

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

Depois, segue uma camada de normalização em lote, estabilizando a distribuição dos dados antes de enviá-los ao módulo de atenção.

E chegamos ao bloco principal, o TimeMoEAttention. Trata-se de um módulo de atenção multi-head com múltiplos especialistas, cada um focado em seu próprio aspecto da informação temporal. Na entrada, ele recebe tokens compactos do estado atual e o contexto de todo o histórico, assim, os tokens literalmente extraem significado dos dados. As dimensões de entrada e o número de especialistas (NExperts) são definidos por parâmetros, e cada cabeça seleciona os Top-K melhores, formando uma representação latente compacta, porém expressiva. Essa camada é a última no encoder e define o espaço de saída ao qual todas as demais modelos de previsão e controle irão recorrer.

//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTimeMoEAttention;
   descr.window_out = EmbeddingSize / 4;
     {
      uint temp[] = {prev_out, prev_out, 8, TopK};               //Window Main, Window Cross, Experts dimension, TopK
      if(ArrayCopy(descr.windows, temp) < ArraySize(temp))
         return false;
     }
     {
      uint temp[] = {prev_var, prev_var * prev_count, NExperts}; //Units Main, Units Cross, Experts
      if(ArrayCopy(descr.units, temp) < ArraySize(temp))
         return false;
     }
   descr.layers = 6;
   descr.step = 4;                                                // Attention heads
   descr.batch = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Na saída do encoder é formado um array latente comum, acessível a todos os modelos. O primeiro modelo de previsão forecast1 o recebe diretamente. A camada base de entrada assume o tensor. Em seguida, aplica-se um processamento convolucional simples, que gera a previsão para o horizonte de planejamento especificado em cada canal analisado. Neste caso, é realizada a previsão de apenas um valor subsequente.

//---
   CLayerDescription *latent = descr;
//--- Forecast 1
   forecast1.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = int(latent.windows[0] * latent.units[0]);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!forecast1.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = latent.units[0];
   descr.window = latent.windows[0];
   descr.step = descr.window;
   descr.layers = 1;
   descr.window_out = 1;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!forecast1.Add(descr))
     {
      delete descr;
      return false;
     }

Aqui vale prestar atenção especial a um importante detalhe técnico. No processo de pré-processamento dos dados, realizado pelo Encoder do estado do ambiente, o espaço original de características foi deliberadamente expandido. Essa expansão é necessária para extrair características de alto nível e formar uma representação latente mais expressiva. No entanto, tal transformação leva a uma incompatibilidade de dimensionalidades entre os valores previstos e os dados reais com os quais trabalhamos na etapa de treinamento.

Para garantir a correspondência correta dos resultados previstos com os valores reais, é necessário trazer as previsões de volta à escala e à estrutura do espaço original. Neste caso, isso é alcançado com o uso de uma camada totalmente conectada simples, que reduz a dimensionalidade dos dados ao valor especificado BarDescr.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = BarDescr;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!forecast1.Add(descr))
     {
      delete descr;
      return false;
     }

O segundo ponto que merece análise separada é a restauração da distribuição original dos dados, perdida durante a normalização. Anteriormente, para esses fins, utilizávamos a camada de normalização reversa RevIN, que extrai automaticamente os parâmetros de normalização da camada correspondente no Encoder. No entanto, na implementação atual surgiu uma limitação arquitetural: a camada de normalização permaneceu dentro do Encoder do estado do ambiente e não está diretamente acessível pelas modelos de previsão. Em outras palavras, o RevIN simplesmente não funcionará aqui, pois não há acesso às estatísticas de lote armazenadas.

A solução, à primeira vista, pode parecer paradoxal: utilizamos novamente uma camada de normalização em lote, mas agora não para normalizar e sim para restaurar a escala original. Sim, soa como um jogo de palavras, mas na prática é uma técnica plenamente válida.

O ponto é que a arquitetura do BatchNorm inclui, na saída, dois parâmetros: scale (scale) e bias (bias), que podem ser treináveis. E embora tradicionalmente sejam aplicados para estabilizar e acelerar o treinamento, no nosso caso assumem outro papel, o de aprender a transformação inversa que aproxima os valores previstos da distribuição original. Assim, em vez de armazenar as estatísticas de normalização, permitimos que o próprio modelo aprenda como desnormalizar os dados, ajustando-se às condições reais.

Essa abordagem não carece de elegância: ela mantém a estrutura do modelo compacta, evita a complexidade adicional no buffer de estados e, o que é importante, não rompe o grafo computacional, garantindo total compatibilidade com a lógica atual de propagação reversa do erro.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNorm;
   descr.count = BarDescr;
   descr.batch = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!forecast1.Add(descr))
     {
      delete descr;
      return false;
     }

Tudo é construído com máxima eficiência: janela constante, uma camada, comprimento fixo.

A segunda e a terceira modelos de previsão (forecast2 e forecast3) utilizam os mesmos dados de entrada que a primeira. E praticamente repetem integralmente sua arquitetura, mas diferem por uma janela de previsão ampliada. Os parâmetros da camada convolucional de previsão são copiados da primeira cabeça, porém o número de filtros é expandido até o horizonte de planejamento necessário.

//--- Forecast 2
   forecast2.Clear();
//--- Input layer
   if(!forecast2.Add(forecast1.At(0)))
      return false;
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   if(!descr.Copy(forecast1.At(1)))
     {
      delete descr;
      return false;
     }
   prev_out = descr.window_out = NForecast / 2;
   if(!forecast2.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_count = descr.count; 

Outro ponto tecnicamente importante diz respeito à arquitetura do bloco de previsão. Na saída da camada convolucional, o modelo gera valores previstos para sequências temporais unitárias individuais, cada uma refletindo a dinâmica local de um indicador ou característica específica. Isso torna a estrutura da previsão espacialmente esparsa, mas informacionalmente rica.

Surge a questão: como generalizar essas previsões e trazê-las a uma forma comparável aos dados originais? Formalmente, poderíamos utilizar uma camada totalmente conectada clássica, pois ela é plenamente capaz de agregar informações. No entanto, à medida que o horizonte de planejamento aumenta e, consequentemente, o comprimento da sequência de saída, tal implementação perde eficiência. O número de parâmetros cresce de forma explosiva, e as dependências temporais locais começam a se perder.

Para evitar isso, primeiro transponemos o tensor, reorganizando os eixos de modo que os passos temporais se tornem canais independentes. Isso permite aplicar a próxima camada convolucional separadamente a cada recorte temporal. Em outras palavras, em vez de processar toda a sequência como um único todo, damos ao modelo a possibilidade de analisar cada instante temporal sob a perspectiva de suas próprias características. Essa operação traz ganhos em duas frentes: reduz a dimensionalidade da representação de saída, atuando como um gargalo, e ao mesmo tempo preserva a coerência temporal entre os passos.

//--- layer 2
   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(!forecast2.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.window = prev_count;
   descr.step = prev_count;
   prev_count = descr.count = prev_out;
   descr.layers = 1;
   prev_out = descr.window_out = BarDescr;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!forecast2.Add(descr))
     {
      delete descr;
      return false;
     }

Dessa forma, implementa-se um mecanismo eficiente de agregação sem perda da estrutura detalhada da previsão. Isso é especialmente valioso ao treinar modelos com dados de alta granularidade e ao utilizar características multigráficas, nas quais cada recorte temporal carrega uma informação específica que não pode ser reduzida a um padrão comum.

Tudo é concluído com uma normalização em lote final, que restaabelece a distribuição original dos dados.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out;
   descr.activation = None;
   if(!forecast2.Add(descr))
     {
      delete descr;
      return false;
     }

O modelo funciona como uma previsão de maior alcance, utilizando as mesmas características, mas interpretando-as por meio de uma lente ampliada.

Quanto à terceira modelo de previsão, sua arquitetura é totalmente idêntica à da segunda. A única diferença é o horizonte de planejamento ampliado, que permite olhar mais longe no futuro. No restante, são as mesmas camadas convolucionais e de transposição, a mesma lógica de compatibilização de dimensionalidades e normalização. Por isso, para não sobrecarregar a exposição com repetições, omitimos sua descrição detalhada e concentramos a atenção nas modelos de tomada de decisão.

Em seguida vem a modelo Actor, isto é, o executor da estratégia. Na entrada, ela recebe os descritores do estado atual da conta. Os dados recebidos passam por normalização padrão.

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

Depois disso, são enviados ao mecanismo CrossDMHAttention. Trata-se de atenção cruzada, em que o fluxo principal é a informação atual da conta, e o contexto são as características latentes provenientes do Encoder. Essa abordagem permite que o modelo de atenção destaque as características mais relevantes do estado do mercado considerando a situação atual do usuário. Dentro do bloco é utilizada uma pilha de três camadas de atenção, cada uma empregando múltiplas cabeças.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronCrossDMHAttention;
     {
      uint temp[] = {AccountDescr,         // Inputs window
                    latent.windows[0]     // Cross window
                   };
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
     {
      uint temp[] = {1,                 // Inputs units
                    latent.units[0]    // 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;
     }

Após a atenção, segue uma sequência de três camadas totalmente conectadas, que transformam os dados obtidos em probabilidades de ações (NActions). É exatamente essa saída que é utilizada na geração das decisões 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;
     }

Assim, toda a arquitetura é organizada de forma sequencial, lógica e modular: desde o pré-processamento dos dados históricos, passando pelos mecanismos de transformação e pela atenção multi-head, até a previsão e a tomada de decisões. Essa abordagem garante transparência, confiabilidade e flexibilidade suficiente, o que é especialmente importante para aplicações reais de trading, nas quais a arquitetura não tolera fragmentação. Tudo está conectado a um espaço latente comum, e cada modelo opera em seu próprio contexto, sem perder a coerência global.

As arquiteturas dos modelos Critic e Director são construídas por analogia ao Actor. A diferença está apenas no fato de que, na entrada, não é fornecido o estado da conta, mas sim o vetor de ações gerado pelo próprio Actor. E na saída é formada uma avaliação numérica dessas ações, no caso do Critic, trata-se do valor da função value, e no caso do Director, do gradiente de sinal de uma classificação binária, ação boa ou ruim. A estrutura interna inclui os mesmos blocos de normalização, o mecanismo de atenção cruzada e a cascata de camadas de saída. Para não sobrecarregar o texto com a repetição de detalhes já descritos, não nos deteremos neles em profundidade. A arquitetura completa de todos os componentes está apresentada no anexo.



Treinamento dos modelos

A etapa seguinte do nosso trabalho é o treinamento dos modelos. E aqui nos deparamos com um dos principais desafios. O ponto é que os autores do framework original Time-MoE treinaram suas redes neurais em um conjunto de dados massivo, para não dizer titânico, o Time-300B. Esse conjunto abrange mais de 300 bilhões de pontos temporais provenientes de nove diferentes áreas, incluindo economia, energia, transporte, saúde e outros domínios. Tal volume de dados garante uma capacidade impressionante de generalização do modelo, mas também exige recursos inacessíveis no contexto de treinamento local ou semi-automatizado.

Reproduzir um conjunto dessa magnitude em ambiente doméstico é uma tarefa irrealista. Por outro lado, limitar-se apenas ao treinamento online com dados correntes significa criar um sistema incapaz de aprender padrões estáveis, especialmente em condições de alta volatilidade do mercado e janela de observação limitada.

Por isso, adotamos uma solução de compromisso: abandonar a etapa de coleta prévia de um conjunto estático de treinamento, mas preservar o controle do processo de aprendizado por meio do acúmulo passo a passo e da utilização de buffers internos de estados. Aqui recorremos a um mecanismo já testado anteriormente no modelo TimeFound, que se mostrou suficientemente eficaz para ser reutilizado, agora não apenas no treinamento do modelo de previsão, mas também dos modelos de tomada de decisão.

A essência do mecanismo consiste em treinar o agente com dados pseudo-reais. A história real das cotações é obtida a partir do terminal, enquanto a avaliação das ações é realizada por um ambiente simulado. Todo o processo é estruturado dentro do método Train do EA "…\Experts\TimeMoE\Study.mq5". É a partir dele que se inicia o processamento passo a passo dos dados históricos, a formação de lotes de treinamento, a coleta do contexto da conta de trading e o treinamento sequencial do modelo com base na propagação reversa do erro.

Na primeira etapa, o método define os limites temporais do treinamento, calculando os índices de início e término do segmento histórico no qual o treinamento será realizado.

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

Em seguida, são carregadas as cotações e inicializados os principais indicadores técnicos. Cada um deles é verificado quanto à prontidão, se os cálculos ainda não foram concluídos, o sistema aguarda com pequenas pausas. Isso é necessário para a inicialização estável dos buffers, caso contrário o processamento posterior simplesmente não fará sentido.

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

Assim que os indicadores estão prontos, inicia-se a coleta de padrões para treinamento. Organizamos um loop de treinamento em que, a cada iteração, é selecionada uma posição aleatória dentro do intervalo permitido de barras. Esse é o ponto de entrada para a próxima cena de treinamento. Com base nessa posição, são formados três vetores principais:

  • bState descreve o estado do mercado na forma de um tensor composto por indicadores e parâmetros de preço;
  • bTime reflete o contexto temporal;
  • Result contém a previsão de referência com base no comportamento futuro do preço.
Dessa forma, o modelo recebe uma situação histórica concreta e sabe qual deve ser o resultado desejado de sua previsão.

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

Além do estado de mercado, é formado um pseudoestado da conta, uma simulação do contexto de trading do trader naquele momento. Isso ocorre na função SampleAccount, que cria um vetor de características incluindo saldo, equity, tamanho da posição, lucro acumulado, relação risco com take-profit e stop-loss, bem como sinais senoidais que modelam padrões sazonais ocultos.

const vector<float> account = SampleAccount(GetPointer(bState), datetime(bTime[0]));
if(!bAccount.AssignArray(account))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }

Esse vetor é fornecido como entrada ao Actor, ampliando sua compreensão da situação atual, o agente toma decisões não apenas com base no mercado, mas também considerando sua própria posição na negociação.

Após a formação dos dados iniciais, começa a propagação para frente do modelo: Encoder codifica o mercado, após o que três blocos Forecast[i] constroem previsões para diferentes horizontes temporais.

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

Ao mesmo tempo, o Actor gera uma decisão de trading, que é imediatamente avaliada por dois modelos independentes, Critic e Director. Um analisa a decisão sob a perspectiva clássica da abordagem value, o outro atua como um classificador binário, fornecendo um forte sinal de retorno, separando boas ações de ações claramente equivocadas.

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

Em seguida, passamos à otimização dos parâmetros dos modelos por meio da chamada do método de propagação reversa. Para o treinamento dos modelos de previsão é utilizado um tensor previamente preparado de estados futuros do ambiente.

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

Para obter uma avaliação objetiva da ação, é utilizada a função CheckAction. Ela simula a abertura de uma posição virtual e calcula o lucro esperado considerando o coeficiente de desconto com base em dados históricos reais. Com base nesses dados é formada a recompensa (reward), que retorna ao modelo e se torna a base para o recálculo dos parâmetros de todos os componentes, Actor, Critic, Director.

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

A correção por gradiente ocorre rapidamente, em uma única atualização. Os erros obtidos são propagados de volta pela rede, e os pesos são atualizados de acordo com o sinal de treinamento. Isso permite que o modelo acumule gradualmente padrões úteis e ajuste seu comportamento com novos exemplos, aprendendo a evitar erros e reforçar ações benéficas.

Para controle visual do processo em tempo real, o método periodicamente exibe nos comentários do gráfico os principais parâmetros de treinamento, erros das previsões e a precisão do Critic.

 if(GetTickCount() - ticks > 500)
   {
    double percent = double(iter) * 100.0 / (Iterations);
    string str = "";
    for(uint f = 0; f < caForecast.Size(); f++)
       str += StringFormat("%-12s%d %6.2f%% -> Error %15.8f\n", "Forecast", f, percent,
                                                caForecast[f].getRecentAverageError());
    str += StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Critic", percent, 
                                                      cCritic.getRecentAverageError());
    str += StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Director", percent, 
                                                    cDirector.getRecentAverageError());
    Comment(str);
    ticks = GetTickCount();
   }
}

Após a conclusão do número especificado de iterações, o método registra no log do terminal os resultados do treinamento e encerra corretamente a execução do programa por meio de ExpertRemove.

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

Dessa forma, cada treinamento é uma série de pequenas simulações, cada uma reproduzindo um fragmento da história, desde o estado do mercado até o resultado da operação. O agente aprende de forma dinâmica, por meio de erros e decisões bem-sucedidas, adaptando-se gradualmente a diferentes condições de mercado e aprimorando sua própria estratégia graças a um sistema equilibrado de avaliação e correção pontual.

Após a etapa principal de treinamento, o modelo passa para o ajuste fino, realizado no modo de treinamento online. Para isso, é utilizado o testador de estratégias integrado do MetaTrader 5, que permite executar o algoritmo em condições o mais próximas possível do mercado real, mas com total controle da simulação. Essa abordagem permite não apenas verificar a qualidade do modelo, mas também adaptar seu comportamento ao ambiente de mercado atual, ajustando gradualmente os parâmetros à volatilidade corrente e à natureza do movimento de preços.

O mecanismo de treinamento online foi implementado com base em um programa transferido de trabalhos anteriores, sem alterações. O modelo continua aprendendo com novos dados, ajustando suas ações em tempo real, o que é especialmente importante em condições de frequente mudança de regimes de mercado.



Teste

O processo de treinamento do modelo foi dividido em duas etapas. Essa abordagem permitiu estruturar o sistema de forma sequencial, confiável e sem pressa.

Primeiro, o treinamento offline. Utilizamos quinze anos de histórico do par EURUSD, no timeframe M1. Isso forneceu ao modelo um enorme volume de situações de mercado variadas. O Encoder aprendeu a reconhecer regularidades, identificar padrões significativos e codificar o estado do mercado em um vetor de características compacto e informacionalmente rico. Esse vetor torna-se a base para todas as decisões tomadas pelo agente. O Actor, durante o treinamento, desenvolve a estratégia de comportamento, recebendo sinais do Critic e do Director.

Em seguida, o ajuste online. Ele é realizado no testador de estratégias do MetaTrader 5. Aqui o modelo interage com o histórico já em modo realista, vela por vela, com ruído de mercado, oscilações aleatórias e instabilidade. Isso ajuda a adaptar o comportamento do agente à dinâmica real e a ajustar a estratégia em condições próximas às reais.

Após o treinamento, o modelo foi testado em novos dados, cotações de janeiro de 2025. Todas as configurações foram previamente fixadas e não foram alteradas. Isso garante objetividade e transparência na avaliação. Os resultados do teste são apresentados a seguir.

Os resultados do teste apresentam ambiguidades e devem ser avaliados com cautela. Por um lado, a rentabilidade acumulada é tradicionalmente importante: com um depósito inicial de $100, o sistema encerrou o período com lucro líquido de $1 209. Isso representa um valor 12 vezes superior ao capital inicial. O gráfico de saldo demonstra crescimento consistente até meados do mês e, em seguida, relativa estabilização na faixa de $1 350 a $1 400.

No entanto, chama imediatamente a atenção o tamanho extremo do drawdown. O drawdown máximo do saldo ultrapassou 72 %, e o da equity chegou a 87 %. Isso significa que, nos momentos mais críticos da operação, o sistema perdeu a maior parte do seu capital. Esse comportamento caracteriza uma política de alto risco, mesmo considerando a recuperação posterior.

O indicador Profit Factor foi de 1.49, para muitos isso é considerado um nível aceitável, no entanto, com tais drawdowns, dificilmente compensa possíveis períodos de perdas. O Recovery Factor, a relação entre o lucro líquido e o drawdown máximo, é praticamente igual a 1, o que indica uma recuperação muito lenta após as perdas.

A estatística das operações mostra que, em um mês, foram abertas quase 2.5K operações, das quais 53.98 % foram lucrativas. O ganho médio supera apenas ligeiramente a perda média.

De modo geral, esses resultados mostram que o modelo é capaz de encontrar sinais lucrativos e proporcionar crescimento do depósito, mas faz isso ao custo de drawdowns muito profundos e longas sequências de operações negativas. Para trading real, essa estratégia exige proteção adicional para reduzir o risco de uma queda acentuada do capital.


Considerações finais

Neste trabalho, transferimos passo a passo as principais ideias do framework Time-MoE do artigo acadêmico para código real em MQL5 e OpenCL. Implementamos o embedding SwiGLU, construímos a mistura esparsa de especialistas e a incorporamos ao mecanismo de atenção cruzada. Organizamos um pipeline completo de treinamento dentro da arquitetura Actor-Director-Critic. Graças a uma estrutura modular clara, todos os componentes ficaram interconectados, mas ao mesmo tempo facilmente configuráveis e expansíveis.

A primeira etapa de treinamento offline, com quinze anos de dados, permitiu que o Encoder formasse uma representação latente rica do mercado, e que o Actor assimilasse uma estratégia básica sob a supervisão do Critic e do Director. A segunda etapa de ajuste online, no testador do MetaTrader 5, levou o modelo ao nível de prontidão para trading, refletindo nos parâmetros a dinâmica e o ruído do mercado contemporâneo.

O teste nas cotações de janeiro de 2025 mostrou que o Agent é capaz de gerar lucro consistente, porém acompanhado de drawdowns profundos. Isso indica a necessidade de otimização adicional do gerenciamento de risco e de ajuste fino dos critérios das operações de trading.


Links


Programas utilizados no artigo

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

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

Arquivos anexados |
MQL5.zip (2856.61 KB)
Últimos Comentários | Ir para discussão (1)
[Excluído] | 19 jun. 2025 em 08:11
Por algum motivo, as negociações são novamente unilaterais, como se o agente não aprendesse nada. Seria interessante ver o equilíbrio dos dados de tendência. Sem opencl, não posso verificar :)
Identificação e classificação de padrões fractais por meio de aprendizado de máquina Identificação e classificação de padrões fractais por meio de aprendizado de máquina
Neste artigo abordaremos o tema intrigante da análise fractal e da previsão de mercados por meio de aprendizado de máquina. Estes são apenas os primeiros passos no caminho para o estudo das diversas estruturas fractais que se formam nos gráficos de cotações financeiras. Utilizaremos a correlação para a busca de padrões e o algoritmo CatBoost para a classificação desses padrões.
Automatizando Estratégias de Trading em MQL5 (Parte 7): Construindo um EA de Grid Trading com Escalonamento Dinâmico de Lote Automatizando Estratégias de Trading em MQL5 (Parte 7): Construindo um EA de Grid Trading com Escalonamento Dinâmico de Lote
Neste artigo, construímos um expert advisor de grid trading em MQL5 que utiliza escalonamento dinâmico de lote. Cobrimos o design da estratégia, a implementação do código e o processo de backtesting. Por fim, compartilhamos insights principais e boas práticas para otimizar o sistema de negociação automatizado.
Aplicação do modelo Grey na análise técnica de séries temporais financeiras Aplicação do modelo Grey na análise técnica de séries temporais financeiras
Este artigo é dedicado ao estudo do modelo Grey, uma ferramenta promissora, capaz de ampliar as possibilidades do trader. Vamos considerar algumas formas de aplicar esse modelo na análise técnica e na construção de estratégias de negociação.
Desenvolvimento do Toolkit de Análise de Price Action (Parte 13): Ferramenta RSI Sentinel Desenvolvimento do Toolkit de Análise de Price Action (Parte 13): Ferramenta RSI Sentinel
A análise de price action pode ser realizada de forma eficaz por meio da identificação de divergências, utilizando indicadores técnicos como o RSI para fornecer sinais cruciais de confirmação. Neste conteúdo, é explicado como a análise automatizada de divergência do RSI pode identificar continuações de tendência e reversões, oferecendo percepções valiosas sobre o sentimento do mercado.