Русский
preview
Redes neurais em trading: Extração eficiente de características para classificação precisa (Conclusão)

Redes neurais em trading: Extração eficiente de características para classificação precisa (Conclusão)

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

Introdução

Chegamos à fase final de familiarização com o framework Mantis. Primeiro, destrinchamos detalhadamente sua base teórica, como o modelo percebe séries temporais multicanais, por que são necessários sinais diferenciais, como os tokens locais são formados. Em seguida, nos aprofundamos na arquitetura e analisamos de que forma os patches são estruturados e como o modelo aprende a extrair padrões estáveis do ruído de mercado. Agora chegou o momento de conectar tudo em um todo coerente.

O framework Mantis é construído como uma sequência de módulos independentes, porém logicamente conectados. Cada um deles executa uma tarefa especializada, desde o processamento primário dos dados brutos até a formação de tokens e a agregação de informações. Na base está a ideia de viés mínimo e máximo de regularidades extraídas diretamente dos próprios dados.

Tudo começa com a entrada das características brutas de mercado. Em vez de tratá-las como uma única matriz, o modelo separa cada tipo de dado em um canal individual. Para cada um deles, é criado adicionalmente um canal de primeiras diferenças, o que permite capturar explicitamente a dinâmica de curto prazo. Esses canais diferenciais aumentam a sensibilidade do modelo às mudanças na direção do movimento do preço. Por exemplo, uma aceleração repentina no canal de diferenças pode indicar o início de um impulso muito antes de ser reconhecido por indicadores.

Cada um desses canais é alimentado nos blocos convolucionais. Aqui tem início a análise primária. As convoluções extraem características locais, aprendendo a distinguir micro-padrões estáveis. Na saída de cada canal, obtemos 256 características que representam seu comportamento local.

Após a convolução, inicia-se a próxima etapa: os dados são divididos em 32 patches não sobrepostos. Cada um deles cobre um segmento específico da série temporal. Em cada patch é realizada a média ao longo dos canais (mean-pooling). Dessa forma, obtemos 32 tokens, cada um com comprimento de 256 características. Este é um ponto importante. Os patches são fragmentos localizados do comportamento do mercado, como se fossem mini-gráficos compactados. Eles capturam fases que vão de impulsos e consolidações a correções e divergências. Essa divisão torna o comportamento do modelo mais estável e adaptativo. Ele passa a enxergar o mercado não como um fluxo de números, mas como uma sequência de cenários reconhecíveis.

Nesta etapa, para cada canal dos dados de entrada, é extraído um token do fluxo original e do fluxo diferencial. Esses tokens são levados a uma escala comum, normalizados e combinados. Em seguida, por meio de uma camada linear de projeção, é formado um token agregado que reflete o comportamento desse canal em um determinado intervalo. Esse token contém em si o estado, a velocidade de mudança e a força, tudo aquilo que permite ao trader julgar intuitivamente o sentimento do mercado.

Em seguida, toda a sequência de tokens é alimentada nos blocos de atenção. Este é o nível mais importante, onde os patches começam a se comunicar entre si. O modelo avalia quais segmentos do passado são relevantes para o estado atual. Talvez um sinal que surgiu 20 barras atrás agora se torne crítico para avaliar uma reversão. Essa capacidade de enxergar relações entre segmentos distantes torna o modelo especialmente útil para a análise de estruturas complexas de mercado.

Por trás de tudo isso está uma ideia simples, porém poderosa: preservar a precisão local sem perder o contexto global. No trading, isso é crítico. Por exemplo, uma sequência de pequenos candles em um momento de calmaria pode não carregar informação alguma. Mas, se eles aparecem após um pico brusco de volume e ainda por cima em um nível importante, o contexto muda completamente. O Mantis leva isso em consideração automaticamente.

O framework Mantis não é apenas uma arquitetura. É uma ferramenta flexível, capaz de se adaptar a diferentes tipos de estratégias de trading.

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


Arquitetura do modelo

Nos trabalhos anteriores, já construímos os principais componentes do framework Mantis e agora chegamos à etapa-chave, a criação da arquitetura do modelo treinável, capaz de tomar decisões de trading em tempo real. Assim como um trader experiente, que acompanha o comportamento do preço, os volumes, a estrutura dos candles, o sentimento geral do mercado e só então, levando em conta o contexto, toma a decisão de entrada, nosso modelo deve aprender a perceber a dinâmica do mercado não como um fluxo de números, mas como um conjunto de processos interconectados. Buscamos reproduzir um modelo comportamental que seja capaz de distinguir padrões de mercado, sentir a mudança de fases e se adaptar a novos cenários. É exatamente nesse contexto que se constrói a lógica de todo o framework Mantis.

É importante enfatizar que, originalmente, o framework Mantis foi desenvolvido como um classificador de séries temporais. Sua arquitetura é orientada à segmentação e à identificação de regularidades ocultas em sequências complexas. Essa abordagem mostrou-se especialmente eficaz no contexto financeiro, onde a dinâmica do preço muitas vezes está escondida sob camadas de ruído de mercado. Com base em um processamento multinível, incluindo convoluções, formação de patches locais e agregação por canais, o modelo aprende a enxergar nos dados de mercado justamente a estrutura, e não flutuações aleatórias. O Mantis permite não apenas fixar sinais, mas também identificar regimes de mercado.

No entanto, dentro da nossa tarefa atual, o Mantis já atua não como um classificador independente, mas como um fundamento, a base da camada sensorial do Agente de trading. Utilizamos a arquitetura do Mantis para construir o embedding, uma representação compacta, porém rica, da situação de mercado, que é então alimentada na parte de controle do modelo. Assim, o classificador se transforma nos olhos do agente, conferindo a ele a capacidade de enxergar o tecido comportamental do mercado.

É justamente esse embedding que é transmitido para a próxima parte da arquitetura, construída segundo o princípio Actor–Director–Critic. Essa abordagem permite dividir responsabilidades entre os módulos e garantir a estabilidade do comportamento do agente. O Actor recebe o embedding e forma a decisão de trading. O Director atua como um filtro estrutural e corretor comportamental, classificando as ações propostas pelo Actor em admissíveis e equivocadas, fornecendo sinais fortes de propagação reversa. Sua tarefa é eliminar decisões claramente irracionais ou instáveis, especialmente em zonas de turbulência de mercado. Critic fecha o ciclo avaliando a viabilidade estratégica das ações, com base no estado atual e no histórico de interações. Ele desempenha o papel de um EA interno, pois mesmo que uma ação seja tecnicamente possível, é necessário avaliar se ela realmente vale o risco no contexto atual.

A arquitetura de todos os componentes do modelo é definida por meio do método CreateDescriptions, no qual as camadas de cada módulo são configuradas de forma sequencial. Um sistema flexível de parâmetros permite escalar a arquitetura, adaptá-la a diferentes instrumentos de mercado e adicionar ou desativar elementos experimentais sem a necessidade de reescrever a lógica principal.

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

Nos parâmetros do método, recebemos ponteiros para quatro arrays dinâmicos, cada um destinado a armazenar as descrições arquiteturais dos respectivos componentes do modelo: Codificador, Ator, Diretor e Crítico. Esses arrays atuam como contêineres nos quais são adicionados, um a um, objetos que descrevem a estrutura dos blocos de redes neurais.

Dentro do corpo do método, primeiramente é realizada a verificação da validade dos ponteiros recebidos. Se algum deles não estiver inicializado ou apontar para uma área de memória inválida, um novo exemplar do objeto correspondente é criado automaticamente. Essa abordagem elimina a necessidade de preparar previamente os arrays de forma manual, aumentando a modularidade do código e sua adequação para soluções escaláveis.

Em seguida, passamos diretamente à descrição da arquitetura dos modelos. E o primeiro elemento dessa cadeia, de forma lógica, é o Codificador, o módulo-chave por meio do qual o modelo começa a perceber o mercado. É exatamente aqui que se inicia o processo de transformação das séries temporais brutas em uma representação significativa do estado de mercado.

Na primeira etapa, utilizamos uma camada totalmente conectada que, neste caso, não desempenha uma função computacional, mas exclusivamente uma função de interface. Sua tarefa é fornecer uma interface de buffer conveniente para carregar os dados brutos para dentro do modelo.

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

Os dados brutos, heterogêneos e em diferentes escalas, são direcionados para o bloco de normalização em lote. Aqui, todos os valores são trazidos para uma escala comparável, eliminando distorções de amplitude, o que é especialmente crítico ao trabalhar com séries temporais financeiras multidimensionais. A normalização permite que o modelo perceba os dados não como um conjunto aleatório de números, mas como um fluxo de informação logicamente coerente.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormWithNoise;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Para uma augmentação suave dos dados durante o processo de treinamento, aplicamos uma camada de normalização em lote com adição de ruído controlado, o que permite que o modelo se torne mais resistente ao ruído de mercado e não se sobreajuste a flutuações irrelevantes. Essa abordagem ajuda o sistema a perceber a informação de mercado de forma mais flexível, mantendo o equilíbrio entre adaptação e estabilidade.

O próximo passo importante passa a ser a criação dos canais de primeiras diferenças. Essa camada forma características diferenciais, isto é, variações de valores entre pontos temporais adjacentes, que ajudam o modelo a capturar a dinâmica e a direção do movimento do mercado. No trading, são justamente essas mudanças que muitas vezes fornecem a chave para compreender reversões ou a continuação de uma tendência, pois a alta ou a queda do preço em um momento específico pode ser mais relevante do que os valores absolutos. A criação de canais de diferenças permite ampliar significativamente a informatividade dos dados brutos, aumentando a sensibilidade do modelo a tendências locais e oscilações rápidas.

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

Após a formação dos canais de primeiras diferenças, harmônicas de codificação temporal são adicionadas ao conjunto expandido de dados. Esse passo é extremamente importante para que o modelo compreenda o contexto das dependências temporais e a ciclicidade dos processos de mercado. As harmônicas funcionam como uma espécie de balizadores temporais, permitindo que o modelo reconheça não apenas o instante no tempo, mas também padrões recorrentes, oscilações sazonais e diferentes escalas temporais. A codificação temporal por meio de harmônicas confere ao modelo uma compreensão profunda da estrutura temporal.

//--- 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 = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Para considerar de forma eficaz as conexões intercanal e as interdependências nos dados, aplicamos um bloco de convoluções com diferentes janelas. Essa abordagem multijanela permite que o modelo capture simultaneamente padrões locais de curto prazo e tendências mais prolongadas, que se manifestam na dinâmica de vários canais ao mesmo tempo.

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

A etapa final da preparação dos dados é a camada de patching. Essa camada divide o tensor multicanal processado em patches individuais não sobrepostos, uma espécie de fragmentos da série temporal, cada um contendo informações agrupadas sobre estados locais do mercado.

Esse tipo de divisão permite que o modelo se concentre em segmentos específicos dos dados, reduzindo a complexidade computacional e facilitando a identificação de padrões locais. Dentro de cada patch, é realizada adicionalmente uma agregação, o que garante uma representação compacta, porém informativa.

Como resultado, cada patch se torna um token independente, carregando uma descrição comprimida e estruturada da situação de mercado em um intervalo temporal limitado. Isso melhora significativamente a capacidade do modelo de capturar regularidades locais e forma uma base sólida para as etapas subsequentes de processamento e tomada de decisão.

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMantisPatching;
   descr.count = prev_count;
   descr.layers = prev_out;
   descr.window = EmbeddingSize;
   descr.window_out = NSkills;
   descr.step = Segments;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_count = descr.step;
   prev_out = descr.layers;

O bloco de atenção em nosso Codificador é implementado como um único objeto, o CNeuronMantisAttentionUnit. Esse componente desempenha um papel fundamental na captura de relações importantes entre patches e canais. Nos parâmetros do objeto, definimos o número de módulos internos de atenção cruzada, o que permite ajustar de forma flexível a profundidade e a amplitude da análise das informações.

Cada módulo de atenção cruzada concentra-se em destacar os elementos mais significativos no fluxo de dados, relacionando-os a um class-token treinável. Essa arquitetura garante uma filtragem eficiente do ruído e ajuda o modelo a se concentrar precisamente nos sinais que realmente influenciam a tomada de decisões de trading.

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMantisAttentionUnit;
   descr.step = 4;
   descr.count = prev_count;
     {
      int temp[] = {EmbeddingSize, EmbeddingSize};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.layers = 3;
   descr.window_out = NSkills;
   descr.window = prev_out;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Na saída do módulo de atenção cruzada, forma-se um token, uma representação compacta e informativa do estado analisado do ambiente. No contexto da nossa tarefa, a classificação precisa de estados não é uma prioridade. Para nós, é mais importante obter representações latentes suficientemente claras e distinguíveis, que permitam ao Ator tomar decisões de trading ponderadas e fundamentadas.

É justamente por isso que optamos conscientemente por evitar o excesso de complexidade na arquitetura do Codificador. Essa abordagem mantém o equilíbrio entre a profundidade de percepção do modelo e sua eficiência computacional, permitindo uma adaptação flexível às condições reais de mercado sem perda da qualidade das decisões tomadas.

Em seguida, passamos à descrição da arquitetura do Ator. Assim como no caso do Codificador, na entrada é utilizada a combinação de uma camada totalmente conectada com um bloco de normalização em lote. Esse conjunto funciona como uma interface para a recepção e a padronização primária dos dados de entrada. No entanto, neste caso, pela principal via informacional é fornecido um outro tipo de informação, os dados sobre o estado atual da conta de trading, como o valor do saldo, as posições abertas, o nível de rebaixamento e outros indicadores que refletem o contexto interno de funcionamento do Agente.

//---
   CLayerDescription *latent = encoder.At(7);
//--- 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 = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Paralelamente, por uma via auxiliar, chega ao modelo a representação latente do ambiente de mercado formada pelo Codificador. A combinação desses dois fluxos de dados, o estado interno e o contexto externo, permite que o Ator tome decisões que já incorporam elementos de pensamento estratégico e gestão de risco. O modelo aprende a considerar não apenas a situação atual do mercado, mas também seus próprios recursos, o nível de risco aceitável e a dinâmica da posição em andamento. Essa abordagem torna o comportamento do Agente consciente e estável, mesmo em condições de maior turbulência de mercado.

A consolidação dos dados provenientes dos dois fluxos informacionais, o contexto de mercado e o estado da conta de trading, é realizada na camada de concatenação. Esse objeto desempenha uma função simples, porém importante: ele une o vetor de embedding recebido do Codificador com as características internas da conta de trading em uma representação única. Dessa forma, o modelo passa a dispor de uma visão integral do que está acontecendo.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = AccountDescr;        // Inputs window
   descr.step = latent.windows[0];     // Cross window
   descr.batch = 1e4;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Após a união dos dados, inicia-se o processamento profundo das características, implementado por meio de uma sequência de três camadas totalmente conectadas. A primeira camada é responsável pela extração e generalização das principais características. A segunda reforça as dependências relevantes entre os parâmetros, revelando possíveis regularidades. Na terceira, forma-se a ação de trading. Essa transformação sequencial permite que o modelo não apenas reaja a sinais isolados, mas tome decisões ponderadas e estrategicamente fundamentadas.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.batch = 1e4;
   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 = 1e4;
   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 = 1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

A arquitetura dos modelos Diretor e Crítico praticamente repete por completo a estrutura do Ator. A principal diferença está na dimensionalidade dos dados de entrada. Pela via principal, na entrada desses modelos é fornecido o tensor de ações do Agente, e na saída forma-se a sua avaliação, classificatória no Diretor e estratégica no Crítico. Como a lógica interna desses modelos é análoga, não iremos sobrecarregar o artigo com uma descrição repetida. Para um estudo mais detalhado, o leitor pode consultar o código que acompanha este artigo, onde é apresentada a descrição completa da arquitetura de todos os componentes treináveis do sistema.


Aprendizado contrastivo

Após a construção da arquitetura dos modelos, passamos ao próximo passo importante, o treinamento. Conforme foi detalhado na parte teórica, uma das características centrais do framework Mantis é o aprendizado contrastivo autossupervisionado. Esse mecanismo permite que o modelo forme tokens informativos e bem distinguíveis, que descrevem o estado do ambiente de forma compacta, porém expressiva. Assim como um trader experiente distingue à primeira vista uma fase de consolidação de um movimento impulsivo, nosso modelo deve aprender a identificar estados característicos do mercado e representá-los de forma condensada para uso posterior por outros componentes do sistema.

O aprendizado contrastivo, neste caso, atua como o fundamento para o desenvolvimento do senso de mercado do modelo. Sem um professor rígido, mas com uma compreensão clara das diferenças entre pares de estados, o Codificador aprende a construir representações que permitem distinguir da forma mais eficiente possível cenários de mercado próximos entre si, mas essencialmente distintos.

A implementação do algoritmo correspondente é apresentada no EA "…\MQL5\Experts\Mantis\StudyContrast.mq5". Esse EA gerencia o processo de formação de pares de exemplos positivos e negativos, realiza a iteração de treinamento, acumula estatísticas e salva os parâmetros treinados.

Vale destacar uma diferença importante e, sem exagero, conceitual da nossa implementação de aprendizado contrastivo em relação às abordagens clássicas. Optamos conscientemente por abandonar a etapa trabalhosa de formação prévia do conjunto de treinamento nesta fase do aprendizado. Em vez disso, aproveitando todo o poder e a flexibilidade da plataforma MetaTrader 5, implementamos a formação dinâmica dos dados de treinamento diretamente durante o processo de aprendizado.

Tudo o que é exigido do usuário é indicar, nos parâmetros do EA, as datas de início e término do período de treinamento. Todos os dados de mercado necessários são automaticamente solicitados e extraídos do terminal em tempo real. Essa abordagem não apenas simplifica o procedimento, como também oferece possibilidades muito mais amplas durante o treinamento do modelo.

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input datetime             Start          = D'2020.01.01';
input datetime             End            = D'2025.01.01';
input int                  Iterations     = 100000;
input int                  Batch          = 50;
input group                "---- Indicators ----"
input ENUM_TIMEFRAMES      TimeFrame   =  PERIOD_M1;
//---
input group                "---- RSI ----"
input int                  RSIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   RSIPrice    =  PRICE_CLOSE;   //Applied price
//---
input group                "---- CCI ----"
input int                  CCIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   CCIPrice    =  PRICE_TYPICAL; //Applied price
//---
input group                "---- ATR ----"
input int                  ATRPeriod   =  14;            //Period
//---
input group                "---- MACD ----"
input int                  FastPeriod  =  12;            //Fast
input int                  SlowPeriod  =  26;            //Slow
input int                  SignalPeriod =  9;            //Signal
input ENUM_APPLIED_PRICE   MACDPrice   =  PRICE_CLOSE;   //Applied price

Em seguida, proponho analisar de forma mais detalhada como esse processo é implementado no método Train. Esse método inicia todo o ciclo de aprendizado contrastivo e contém a lógica de preparação dos dados, gerenciamento de buffers, propagação para frente e propagação reversa pela rede.

O processo começa com a definição dos limites dos dados históricos. Com o auxílio da função iBarShift, determina-se o deslocamento em relação à barra atual até o momento de início e de término do treinamento.

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

Depois disso, é realizada a reserva dos buffers de todos os indicadores de acordo com o comprimento do histórico carregado.

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

A etapa seguinte é o carregamento dos dados. Para isso, foi implementado um laço no qual são chamados sequencialmente os métodos Refresh de todos os indicadores e verificada a quantidade de valores calculados. O laço é encerrado quer seja após o carregamento bem-sucedido dos dados, ou após o esgotamento de 100 tentativas.

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

Ao final do processo de preparação dos dados, definimos a direção necessária da indexação do array de cotações (ArraySetAsSeries) e declaramos as variáveis locais necessárias.

   if(!ArraySetAsSeries(Rates, true))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
   bars -= end + HistoryBars;
   if(bars < 0)
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
//---
   vector<float> result, target, neg_target;
   bool Stop = false;

Agora é iniciado o ciclo principal de treinamento. Em cada iteração, a partir do intervalo total, é escolhida aleatoriamente uma posição, o índice da barra a partir do qual será formado o estado de referência.

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

Para esse estado, são gerados os buffers de dados brutos e é executada a propagação para frente do Codificador.

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

O resultado da propagação para frente é salvo como referência, uma espécie de amostra do estado de mercado para a qual o modelo irá convergir. Em seguida, o que é importante, a propagação para frente do Codificador é executada novamente com exatamente os mesmos dados brutos. Porém, aqui existe uma sutileza. A camada de normalização em lote com adição de ruído, incorporada à nossa arquitetura, atua como uma augmentação suave dos dados. Isso significa que, na segunda passagem, o modelo não apenas repete o resultado anterior, mas obtém uma saída ligeiramente modificada e ruidosa.

É exatamente essa propriedade que utilizamos para formar o par positivo. A propagação reversa é realizada usando como alvo a referência salva anteriormente. Dessa forma, ensinamos o modelo a ignorar ruídos aleatórios e a se concentrar em características de contexto relevantes e estáveis. Em termos simples, o modelo aprende a enxergar através do ruído e a manter uma representação estável das principais características do mercado.

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

Em seguida, começa talvez a parte mais interessante, a formação dos pares negativos. A quantidade de pares negativos é definida nos parâmetros do EA pela variável Batch. Com o objetivo de criar a quantidade necessária de pares negativos, criamos um laço aninhado, previamente movendo o token de referência para um vetor.

//--- Negotive
if(!Result.GetData(target))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
for(int b = 0; b < Batch; b++)
  {
   int negot = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
   int count = 0;
   while(negot == posit)
     {
      negot = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
      count++;
      if(count > 100)
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }
     }
   if(Stop)
            break;

No corpo do laço aninhado, escolhemos aleatoriamente mais um estado dentro do intervalo de treinamento, diferente daquele utilizado anteriormente como referência. Aqui vale ressaltar um ponto importante, admitimos qualquer estado, desde que ele não coincida com o estado de referência. Teoricamente, até mesmo interseções de intervalos são possíveis.

É exatamente isso que torna o processo de treinamento mais vivo e realista. O modelo aprende a destacar diferenças em estados muito semelhantes. Se os intervalos se sobrepõem, a tarefa do modelo é enfatizar precisamente aquelas nuances que distinguem um estado do outro. Em condições reais de mercado, são justamente essas diferenças sutis que muitas vezes determinam o sucesso de uma decisão de trading.

Dessa forma, nosso aprendizado contrastivo força o Codificador não apenas a reconhecer estados, mas a extrair características-chave, criando tokens informativos e distinguíveis, necessários para o trabalho subsequente do Ator.

Para o estado selecionado, geramos os buffers de dados brutos e realizamos a propagação para frente do Codificador.

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

Em seguida, chega o momento de formar os alvos do exemplo negativo de treinamento. Para isso, calculamos a diferença entre o token obtido no passo atual e a representação de referência que foi salva anteriormente. Este é um ponto-chave, ao afastar o token atual da referência, criamos o valor-alvo para o exemplo negativo.

Para intensificar esse efeito, dobramos a distância entre a referência e o estado atual, aumentando assim a força de repulsão. Como resultado, estados localizados próximos à referência são repelidos apenas levemente, enquanto aqueles que diferem de forma significativa são afastados com muito mais intensidade. Isso ajuda a ampliar o espaço livre ao redor da representação de referência, tornando os tokens mais distinguíveis e mais resistentes ao ruído.

Esse mecanismo introduz no treinamento uma dinâmica que garante uma separação clara dos espaços latentes. O modelo não apenas aprende a reconhecer estados, mas aprende a criar ao redor da referência uma zona de segurança, da qual outros estados distintos permanecem a uma distância perceptível. Isso eleva significativamente a qualidade e a confiabilidade da tomada de decisões subsequente.

cEncoder.getResults(result);
neg_target = result * 2 - target;
neg_target = neg_target - neg_target.Max();
if(!neg_target.Activation(neg_target, AF_SOFTMAX) ||
   !Result.AssignArray(neg_target))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Resta-nos dar o último passo, não menos importante, executar a propagação reversa do Codificador utilizando o alvo negativo formado. É exatamente essa etapa que permite otimizar os parâmetros do modelo, ajustando-o para repelir de forma eficaz estados distintos entre si no espaço latente.

if(!cEncoder.backProp(Result, (CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Agora, quando todas as operações de treinamento desta etapa são concluídas com sucesso, o sistema não deixa o usuário no escuro, ocorre a atualização do status do processo. Informamos o progresso atual, exibindo o percentual de execução e os indicadores de erro do modelo. Essa abordagem transparente não apenas aumenta a confiança no funcionamento do algoritmo, como também ajuda a acompanhar de forma rápida a eficácia do treinamento.

Depois disso, passamos para a próxima iteração do ciclo de treinamento, na qual todo o processo descrito se repete novamente com novos dados. Esse ciclo garante um aprimoramento profundo e consistente do modelo, permitindo que ele construa gradualmente representações cada vez mais precisas e informativas, necessárias para a tomada de decisões de trading ponderadas.

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

Após a execução bem-sucedida de todas as iterações dos ciclos de treinamento, exibimos no log os resultados do treinamento. Em seguida, ocorre a inicialização do processo de encerramento da execução do programa.

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

Trata-se de um encerramento cuidadoso e controlado da sessão, que garante a preservação de todos os dados importantes e a liberação correta dos recursos.

O código completo do programa de aprendizado contrastivo do Codificador é apresentado no anexo. Nesse mesmo arquivo, você encontrará as implementações dos programas de treinamento offline e online dos modelos Ator, Diretor e Crítico. Esses programas foram transferidos de desenvolvimentos anteriores com ajustes mínimos, em particular, neles foi removido o processo de treinamento do Codificador, que agora é implementado separadamente. Essa abordagem permite estruturar o treinamento, aumentando a flexibilidade e a facilidade de manutenção de todo o framework.  


Teste

O próprio processo de treinamento foi organizado em três etapas sequenciais, o que garante a sistematicidade e a confiabilidade de todo o modelo.

A primeira etapa é o aprendizado contrastivo do Codificador. Ele é realizado com dados históricos dos últimos cinco anos para o par de moedas EURUSD no timeframe M1. Esse volume e nível de detalhamento de dados permitem que o Codificador desenvolva representações latentes de alta qualidade e informatividade do estado do mercado, o que constitui a base para o funcionamento posterior de todo o sistema.

Em seguida, inicia-se a segunda etapa, o treinamento offline dos principais componentes do sistema: Ator, Diretor e Crítico. Para isso, é utilizado um conjunto de treinamento coletado a partir dos dados de 2024, mantendo todos os parâmetros especificados anteriormente. Durante o processo, é aplicada a concepção de uma trajetória quase ideal, permitindo que os modelos aprendam a partir dos exemplos mais confiáveis possíveis de ações e avaliações. Essa etapa é importante para consolidar as estratégias básicas e os critérios de tomada de decisão.

A terceira etapa é o ajuste fino online dos modelos, que já é realizado diretamente no testador de estratégias no mesmo intervalo histórico. Isso permite adaptar os modelos às condições de mercado em mudança e refinar os parâmetros com máxima precisão.

Após a conclusão de todas as etapas de treinamento, é realizado o teste do modelo com dados de janeiro a março de 2025. Ao mesmo tempo, todos os parâmetros utilizados nas etapas de treinamento são mantidos sem alterações. Essa abordagem garante uma avaliação honesta e objetiva da eficácia do modelo em um novo conjunto de dados, não utilizado anteriormente. Os resultados do teste são apresentados abaixo.

Durante o período de teste, o modelo realizou 881 operações, das quais 447 foram encerradas com lucro, o que corresponde a uma taxa de operações bem-sucedidas de 50.74%. Isso indica um equilíbrio neutro entre operações lucrativas e deficitárias. O indicador de fator de lucro (Profit Factor) é igual a 1.25.

De modo geral, a estratégia demonstra um resultado positivo com um nível moderado de risco e um crescimento estável do capital na primeira metade do período testado. No entanto, após meados de fevereiro, observa-se uma redução da rentabilidade e um movimento lateral evidente com queda do equity.

Dessa forma, a estratégia tem direito de existir e apresenta uma dinâmica positiva no intervalo de teste, porém necessita de melhorias em diversos parâmetros.


Conclusão

Como resultado do trabalho realizado, integramos completamente o framework Mantis a um modelo adaptado de trading algorítmico, capaz de extrair características informativas de séries temporais de alta frequência e transformá-las em decisões significativas em tempo real.

Uma atenção especial foi dedicada à etapa de aprendizado contrastivo autossupervisionado do Codificador, organizada no StudyContrast.mq5. Abandonamos o conjunto de dados estático, implementando o carregamento dinâmico de dados diretamente do terminal e o enriquecimento com exemplos reais dos últimos cinco anos para o instrumento EURUSD M1. Os pares positivos e negativos, formados graças à augmentação suave e a ruídos controlados, permitiram que o modelo aprendesse na prática a enxergar através do ruído de mercado.

O teste final do modelo no período de janeiro a março de 2025 demonstrou a rentabilidade das soluções implementadas. A estratégia tem direito de existir e apresenta uma dinâmica positiva no intervalo testado, porém necessita de melhorias em diversos parâmetros.


Links


Programas utilizados no artigo

# Nome Tipo Descrição
1 Research.mq5 Expert Advisor EA de coleta de exemplos
2 ResearchRealORL.mq5
Expert Advisor
EA de coleta de exemplos pelo método Real-ORL
3 StudyContrast.mq5 Expert Advisor EA de aprendizado contrastivo do Codificador
4 Study.mq5 Expert Advisor EA de treinamento offline dos modelos
5 StudyOnline.mq5
Expert Advisor
EA de treinamento online dos modelos
6 Test.mq5 Expert Advisor 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 de rede neural
9 NeuroNet.cl Biblioteca Biblioteca de código OpenCL do programa

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

Arquivos anexados |
MQL5.zip (2794.71 KB)
Últimos Comentários | Ir para discussão (1)
[Excluído] | 30 mai. 2025 em 17:49
É estranho por que ele negocia somente em uma direção. E como são formados os sinais para fechar as negociações?
Negociamos opções sem opções (Parte 1): Fundamentos da teoria e emulação por meio de ativos subjacentes Negociamos opções sem opções (Parte 1): Fundamentos da teoria e emulação por meio de ativos subjacentes
O artigo descreve uma variante de emulação de opções por meio do ativo subjacente, implementada na linguagem de programação MQL5. São comparadas as vantagens e desvantagens da abordagem escolhida em relação às opções reais negociadas em bolsa, usando como exemplo o mercado futuro FORTS da bolsa de Moscou MOEX e a corretora de criptomoedas Bybit.
Indicador de previsão ARIMA em MQL5 Indicador de previsão ARIMA em MQL5
Neste artigo, criamos um indicador de previsão ARIMA em MQL5. É analisado como o modelo ARIMA forma previsões, sua aplicabilidade ao mercado Forex e ao mercado de ações em geral. Também é explicado o que é a autorregressão AR, de que forma os modelos autorregressivos são usados para previsão e como funciona o mecanismo de autorregressão.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Rede neural na prática: Gradiente Descendente Rede neural na prática: Gradiente Descendente
Neste artigo, tentarei apresentar, de forma o mais simplificada e didática, quanto foi possível fazer, uma das questões mais controvérsias quando o assunto é rede neural. Que é justamente como procurar o melhor ponto possível, ou menor custo de uma função. Mostrarei a diferença que existe entre uma regressão linear e um gradiente descendente. Ambos casos bastante simples e voltados para mostrar que nem sempre o que parece obvio, realmente é o melhor caminho.