Русский Español
preview
Redes neurais em trading: Agente multimodal complementado com ferramentas (Conclusão)

Redes neurais em trading: Agente multimodal complementado com ferramentas (Conclusão)

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

Introdução

No artigo anterior, conhecemos o FinAgent, um framework que se apresenta como uma ferramenta avançada para análise de dados e apoio à tomada de decisão nos mercados financeiros. Seu desenvolvimento é voltado para criar um mecanismo eficiente de formulação de estratégias de trading e minimizar riscos em um ambiente de mercado complexo e dinâmico. A arquitetura do FinAgent é composta por cinco módulos interligados, cada um executando funções específicas para garantir a adaptabilidade geral do sistema.

O módulo de análise de mercado é responsável pela extração e pelo processamento de dados de fontes heterogêneas, como gráficos de preços, notícias financeiras e relatórios. A partir daí, são identificados padrões estáveis que podem ser utilizados para prever a dinâmica dos preços.

Os módulos de reflexão desempenham um papel essencial no processo de adaptação e aprendizado do modelo. O módulo de reflexão de baixo nível analisa as interdependências entre sinais atuais do mercado, aumentando a precisão das previsões de curto prazo. Já o módulo de alto nível, por sua vez, trabalha com tendências de longo prazo, considerando dados históricos e resultados de decisões anteriores de trading, com o objetivo de ajustar a estratégia utilizada com base na experiência acumulada.

O módulo de memória garante o armazenamento de longo prazo de grandes volumes de dados de mercado. O uso de tecnologias modernas de similaridade vetorial reduz a influência de ruídos e aumenta a precisão na busca de informações, o que é particularmente importante para a elaboração de estratégias de longo prazo e a identificação de relações complexas.

O elo central do sistema é o módulo de tomada de decisão, que integra os resultados obtidos por todos os outros componentes. Com base em informações atuais e históricas, ele gera recomendações de trading otimizadas. E graças à possibilidade de integrar conhecimento especializado e indicadores tradicionais, o módulo é capaz de produzir recomendações equilibradas e bem fundamentadas.

A seguir está apresentada a visualização autoral do framework FinAgent.

No artigo anterior, iniciamos a implementação das abordagens propostas pelos autores do framework FinAgent, por meio do MQL5. Foram apresentados algoritmos para os módulos de reflexão de baixo e alto nível, implementados como os objetos CNeuronLowLevelReflection e CNeuronHighLevelReflection. Esses módulos analisam os sinais de mercado, o histórico de decisões de trading tomadas e os resultados financeiros efetivamente alcançados, o que permite adaptar a política de comportamento do agente às condições mutáveis do mercado. Eles também tornam possível reagir de maneira flexível às mudanças dinâmicas das tendências e identificar padrões-chave nos dados.

Uma característica da nossa implementação é a integração dos blocos de memória diretamente nos objetos de reflexão. Essa abordagem difere da arquitetura original do framework, na qual a memória para todos os fluxos de informação era alocada em um módulo separado. A integração dos blocos de memória simplifica a construção dos fluxos de informação da interação entre os diferentes componentes do framework.

Dando continuidade ao trabalho iniciado, vamos analisar a implementação de alguns módulos importantes, cada um desempenhando um papel único na arquitetura geral do sistema:

  • Módulo de análise de mercado é destinado ao processamento de dados provenientes de diversas fontes disponíveis, incluindo relatórios financeiros, notícias e cotações de bolsa. Ele converte os dados multimodais em um formato unificado e destaca padrões estáveis que podem ser utilizados para prever a dinâmica futura do mercado.
  • Ferramentas adicionais, baseadas em conhecimento prévio, que oferecem suporte à análise e à tomada de decisão com base em regularidades históricas, dados estatísticos e avaliações de especialistas. Ao mesmo tempo, permitem fornecer uma interpretação lógica das decisões tomadas.
  • Sistema de apoio à tomada de decisão integra os resultados de todos os módulos para a formação de uma estratégia de trading adaptativa e otimizada. Esse sistema fornece recomendações de ações em tempo real, permitindo que traders e analistas reajam prontamente às mudanças na conjuntura de mercado e tomem decisões mais equilibradas.

O módulo de análise de mercado desempenha um papel central no sistema, pois é responsável pelo pré-processamento e unificação dos dados. Isso é particularmente importante para revelar padrões ocultos que dificilmente podem ser identificados pela abordagem tradicional de análise de dados. Os autores do framework FinAgent utilizaram grandes modelos de linguagem (LLM) para extrair os principais aspectos dos dados e realizar sua compressão. Em nossa implementação, optamos por não utilizar LLM e priorizamos modelos especializados na análise de séries temporais, que garantem alta precisão e desempenho. Ao longo desta série de artigos, foram apresentados vários frameworks para análise e previsão de séries temporais multidimensionais. E aqui pode ser aplicado qualquer um deles. Para a preparação deste artigo, nossa escolha recaiu sobre o transformador com atenção segmentada, implementado como a classe CNeuronPSformer.

No entanto, essa não é a única opção. Além disso, o framework FinAgent propõe o uso de dados brutos multimodais. Isso nos permite não apenas experimentar diferentes algoritmos de representação e análise de séries temporais, mas também combiná-los. Essa abordagem amplia significativamente as capacidades do sistema, proporcionando uma compreensão mais detalhada dos processos de mercado e contribuindo para o desenvolvimento de estratégias altamente eficazes e adaptativas.

O módulo de ferramentas adicionais exerce a função de integrar conhecimento prévio sobre o ambiente analisado na arquitetura geral do modelo. Esse componente possibilita a geração de sinais analíticos com base em indicadores clássicos, como médias móveis, osciladores e indicadores de volume, que já se mostraram eficazes na prática do trading algorítmico. No entanto, o módulo não se limita apenas ao uso de ferramentas padrão.

Além disso, a geração de sinais por meio de regras claras, fundamentadas em métricas de indicadores técnicos, melhora a interpretabilidade das decisões tomadas pelo modelo, além de contribuir para o aumento de sua confiabilidade e eficiência. Isso representa um fator essencial para o planejamento estratégico e a gestão de riscos.


O módulo de ferramentas adicionais

A formação do módulo de geração de sinais com base em métricas de indicadores clássicos, dentro de um modelo neural, representa uma tarefa muito mais complexa do que pode parecer à primeira vista. A principal dificuldade não está na interpretação dos sinais, mas na avaliação dos valores que chegam à entrada do objeto.

Nas estratégias clássicas, a descrição dos sinais depende diretamente das leituras efetivas dos indicadores. No entanto, seus valores frequentemente formam distribuições completamente desconexas e incomparáveis, o que cria obstáculos significativos para a construção de modelos. Esse fator reduz seriamente a eficiência do aprendizado, pois os algoritmos precisam se adaptar à análise de dados heterogêneos. Isso leva ao aumento do tempo de processamento, à diminuição da precisão das previsões e a outros efeitos negativos. Por esse motivo, anteriormente tomamos a decisão de utilizar exclusivamente dados normalizados em nossos modelos.

O processo de normalização dos dados brutos permite trazer todas as características analisadas para uma mesma escala comparável, o que, por sua vez, melhora significativamente a qualidade do aprendizado dos modelos. Essa abordagem minimiza os riscos de distorções relacionadas a diferentes unidades de medida dos indicadores ou à sua variabilidade ao longo do tempo. Uma vantagem importante da normalização é a possibilidade de uma análise mais profunda dos dados, já que, nesse formato, eles se tornam mais previsíveis para os algoritmos de aprendizado de máquina.

No entanto, é importante destacar que a normalização complica bastante o processo de geração de sinais em estratégias clássicas. Essas estratégias foram originalmente desenvolvidas para trabalhar com dados "brutos" e pressupõem a utilização de valores limiares fixos para a interpretação dos indicadores. Durante o processo de normalização, os dados são transformados, o que leva a um deslocamento indefinido dos valores de referência. Além disso, a normalização torna inviável a formação de sinais baseados no cruzamento de duas linhas de um indicador clássico, pois não há garantia de um deslocamento síncrono dos valores das linhas analisadas. Como consequência, ocorre distorção dos sinais gerados ou até mesmo sua ausência. Tudo isso nos leva à necessidade de desenvolver novas abordagens para a interpretação dos valores dos indicadores.

E aqui, ao meu ver, foi encontrada uma solução simples e, ao mesmo tempo, conceitualmente fundamentada. A essência dela está no fato de que, durante a normalização, todas as características analisadas são ajustadas para média zero e variância unitária. Como resultado desse processo, cada valor se torna comparável aos demais e pode ser interpretado como uma espécie de oscilador. Essa abordagem oferece um esquema universal de interpretação de sinais: valores acima de "0" são considerados sinais de compra, enquanto valores abaixo de "0" indicam venda. Também é possível a introdução de valores-limite, o que permite formar "corredores" para filtrar sinais fracos ou ambíguos. Isso contribui para minimizar a quantidade de falsos positivos, aumenta a precisão da análise e favorece a tomada de decisões mais fundamentadas.

Também admitimos a possibilidade de sinais inversos para algumas características analisadas. Essa questão pode ser resolvida pelo uso de parâmetros treináveis, que se adaptam aos dados históricos.

A aplicação da abordagem proposta cria a base para a construção de modelos capazes de se adaptar de forma eficiente às condições mutáveis e gerar sinais mais precisos e confiáveis.

A implementação dessa abordagem de geração de sinais começa com a construção do kernel MoreLessEqual no lado do programa OpenCL. Neste caso, foi realizado o algoritmo mais simples, com um valor limiar fixo.

Nos parâmetros desse kernel recebemos ponteiros para 2 buffers de dados de mesmo tamanho. Um deles contém os dados brutos, enquanto no segundo iremos registrar os sinais sob a forma de um dos 3 valores numéricos:

  • -1 — venda;
  • 0 — ausência de sinal;
  • 1 — compra.
O kernel será chamado em um espaço unidimensional de tarefas, de acordo com o tamanho do buffer de dados analisado.

__kernel void MoreLessEqual(__global const float * input,
                            __global float * output)
  {
   const size_t i = get_global_id(0);
   const float value = IsNaNOrInf(input[i], 0);
   float result = 0;

No corpo do kernel identificamos o fluxo atual de operações e imediatamente lemos do buffer de dados brutos o valor correspondente para uma variável local. Um passo obrigatório é a verificação da validade do valor recebido: todos os dados que não passam na validação são automaticamente substituídos por "0" para evitar erros nas etapas subsequentes do processamento.

Em seguida, é declarada uma variável local destinada a armazenar os resultados intermediários. Inicialmente, essa variável recebe o valor que indica ausência de qualquer sinal.

O próximo passo é verificar o valor absoluto da variável analisada. Para que um sinal seja gerado, ele deve ultrapassar o valor-limite definido.

   if(fabs(value) > 1.2e-7)
     {
      if(value > 0)
         result = 1;
      else
         result = -1;
     }
   output[i] = result;
  }

Valores positivos acima do limiar geram sinal de compra, enquanto valores negativos resultam em sinal de venda. O respectivo flag é gravado na variável local. E, antes da finalização do kernel, esse flag obtido é transferido para o buffer de resultados.

O algoritmo descrito acima representa um procedimento sequencial de propagação para frente, no qual os dados são processados sem o uso de parâmetros treináveis. Esse método baseia-se em cálculos estritamente determinísticos, voltados para minimizar o consumo de recursos computacionais e evitar complexidade desnecessária, o que é especialmente importante no contexto do processamento de grandes volumes de informação. É fundamental destacar que a distribuição do gradiente de erro nesse fluxo de informação não é prevista. Afinal, o que nos interessa é identificar sinais consistentes a partir dos indicadores, e não "ajustá-los" ao resultado esperado. Isso torna o algoritmo particularmente atrativo para sistemas que exigem alta velocidade e precisão no processamento.

Após a construção do algoritmo no lado do programa OpenCL, precisamos organizar o gerenciamento e a chamada do kernel apresentado acima no lado do programa principal. Para executar essa funcionalidade, criaremos um novo objeto CNeuronMoreLessEqual, cuja estrutura é apresentada a seguir. 

class CNeuronMoreLessEqual :  public CNeuronBaseOCL
  {
protected:
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override {return true; }

public:
                     CNeuronMoreLessEqual(void) {};
                    ~CNeuronMoreLessEqual(void) {};
  };

A estrutura do novo objeto é extremamente simples. Ela sequer possui um método de inicialização. Na prática, quase toda a funcionalidade é herdada da classe pai. Apenas sobrescrevemos os métodos de propagação para frente e propagação reversa.

No método de propagação para frente é realizada apenas a transferência dos ponteiros para os buffers de dados nos parâmetros do kernel apresentado anteriormente, seguida pelo enfileiramento para execução.

bool CNeuronMoreLessEqual::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!OpenCL || !NeuronOCL)
      return false;
   uint global_work_offset[1] = { 0 };
   uint global_work_size[1] = { Neurons() };
   ResetLastError();
   const int kernel = def_k_MoreLessEqual;
   if(!OpenCL.SetArgumentBuffer(kernel, def_k_mle_inputs, NeuronOCL.getOutputIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(kernel, def_k_mle_outputs, getOutputIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
//---
   if(!OpenCL.Execute(kernel, 1, global_work_offset, global_work_size))
     {
      printf("Error of execution kernel %s: %d; line %d", OpenCL.GetKernelName(kernel), GetLastError(), __LINE__);
      return false;
     }
//---
   return true;
  }

A funcionalidade dos métodos de propagação reversa, à primeira vista, pode parecer pouco evidente, considerando a observação feita anteriormente sobre a ausência de parâmetros treináveis e a rejeição à distribuição de gradientes de erro. No entanto, é importante destacar que, dentro da estrutura de construção de redes neurais, esses métodos são obrigatórios para todas as camadas. Caso contrário, seria chamado o método com o mesmo nome da classe pai, que poderia funcionar incorretamente dentro da nossa arquitetura. Para evitar esse tipo de problema, o método de atualização dos parâmetros treináveis é sobrescrito por uma função vazia, que retorna o valor true.

Quanto à recusa em distribuir os gradientes de erro, logicamente ela é equivalente à passagem de valores nulos. Assim, no método de distribuição de gradientes simplesmente zeramos o buffer correspondente no objeto de dados brutos, garantindo o funcionamento correto do modelo e minimizando o risco de erros.

bool CNeuronMoreLessEqual::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL || !NeuronOCL.getGradient())
      return false;
   return NeuronOCL.getGradient().Fill(0);
  }

Com isso, encerramos o trabalho com o objeto do módulo de ferramentas adicionais. O código completo da classe CNeuronMoreLessEqual e todos os seus métodos pode ser consultado no anexo.

Neste ponto, já analisamos praticamente toda a implementação dos principais módulos do framework FinAgent. Resta abordar o módulo de tomada de decisão, que desempenha o papel de elo central na arquitetura geral. Esse módulo é responsável pela síntese da informação proveniente de múltiplos fluxos de dados, cujo número ultrapassa dois. Decidimos integrar o módulo de tomada de decisão diretamente no objeto principal do framework, sem separá-lo em uma entidade independente. Essa escolha permitiu melhorar a interação entre todos os componentes do sistema.


Construção do framework FinAgent

Chegou, então, o momento de unir os módulos desenvolvidos anteriormente em uma única estrutura integrada do framework FinAgent, garantindo sua interação e sinergia. Módulos com diferentes funcionalidades são combinados para alcançar um objetivo comum — a criação de um sistema eficiente e flexível para análise de dados de mercado complexos e desenvolvimento de estratégias que considerem a dinâmica e as particularidades do mercado financeiro. Essa funcionalidade é realizada por meio do novo objeto CNeuronFinAgent, cuja estrutura é apresentada a seguir.

class CNeuronFinAgent   :  public CNeuronRelativeCrossAttention
  {
protected:
   CNeuronTransposeOCL  cTransposeState;
   CNeuronLowLevelReflection  cLowLevelReflection[2];
   CNeuronHighLevelReflection cHighLevelReflection;
   CNeuronMoreLessEqual cTools;
   CNeuronPSformer      cMarketIntelligence;
   CNeuronMemory        cMarketMemory;
   CNeuronRelativeCrossAttention cCrossLowLevel;
   CNeuronRelativeCrossAttention cMarketToLowLevel;
   CNeuronRelativeCrossAttention cMarketToTools;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput,
                       CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override;

public:
                     CNeuronFinAgent(void) {};
                    ~CNeuronFinAgent(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint units_count, uint heads,
                          uint account_descr, uint nactions, uint segments,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void) override   const   {  return defNeuronFinAgent; }
   //---
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   //---
   virtual bool      Clear(void) override;
  };

Na estrutura apresentada, vemos o conjunto habitual de métodos sobrescritos e uma série de objetos internos, entre os quais é fácil identificar os módulos do framework FinAgent que já implementamos. Sobre a construção dos fluxos de informação e sua interação falaremos no processo de discussão dos algoritmos de implementação dos métodos desta classe.

Todos os objetos internos são declarados de forma estática, o que nos permite deixar vazio tanto o construtor quanto o destrutor da classe criada. A inicialização de todos os objetos declarados e herdados é realizada no método Init. Nos parâmetros desse método recebemos um conjunto de constantes que permitem definir de forma inequívoca a arquitetura do objeto que está sendo criado. 

bool CNeuronFinAgent::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                           uint window, uint window_key, uint units_count, uint heads,
                           uint account_descr, uint nactions, uint segments,
                           ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronRelativeCrossAttention::Init(numOutputs, myIndex, open_cl, 3,
                                           window_key, nactions / 3, heads,
                                           window, units_count,
                                           optimization_type, batch))
      return false;

Adiantando um pouco, nosso bloco de tomada de decisão será composto por vários blocos sequenciais de cross-attention. O último deles implementaremos por meio do objeto pai, no qual utilizamos, não por acaso, a classe de cross-attention relativa CNeuronRelativeCrossAttention.

Na saída da nossa implementação do framework FinAgent esperamos obter a representação do tensor de ações do agente em forma de matriz, em que as linhas são vetores de descrição de ações individuais. Nesse formato, operações de compra e venda são representadas por linhas distintas dessa matriz. Cada operação é descrita por 3 parâmetros: volume da negociação e dois níveis de operação (stop-loss e take-profit). Portanto, nossa matriz de ações do agente conterá 3 colunas.

Assim, ao chamar o método de inicialização da classe pai, especificamos a janela de análise de dados do fluxo principal igual a 3, e o número de elementos da sequência analisada — 3 vezes menor do que o tamanho do vetor de descrição das ações do agente recebido nos parâmetros. Isso permite que o modelo avalie a eficácia de cada operação individual no contexto do segundo fluxo de informação, pelo qual será transmitida a informação processada sobre o estado do ambiente. É nesse ponto que ocorre a transferência dos parâmetros correspondentes.

Após a execução bem-sucedida das operações do método de inicialização da classe pai, passamos à preparação dos objetos internos recém-declarados. E começamos esse trabalho pela inicialização dos componentes do módulo de análise de mercado. No nosso caso, ele é representado por dois objetos: o transformador com atenção segmentada para identificação de padrões estáveis em séries temporais multidimensionais de dados brutos, e o bloco de memória.

   int index = 0;
   if(!cMarketIntelligence.Init(0, index, OpenCL, window, units_count, segments, 0.2f, optimization, iBatch))
      return false;
   index++;
   if(!cMarketMemory.Init(0, index, OpenCL, window, window_key, units_count, heads, optimization, iBatch))
      return false;

Com o objetivo de realizar uma análise abrangente do estado do ambiente, utilizamos em nossa implementação 2 módulos de reflexão de baixo nível, que trabalham em paralelo com o tensor de dados brutos representado em diferentes projeções. Para obter a segunda projeção dos dados analisados, aplicamos o objeto de transposição.

   index++;
   if(!cTransposeState.Init(0, index, OpenCL, units_count, window, optimization, iBatch))
      return false;

Em seguida, inicializamos 2 objetos de reflexão de baixo nível. A análise dos dados em diferentes projeções é evidenciada pela troca das dimensões da janela e do comprimento da sequência do tensor analisado.

   index++;
   if(!cLowLevelReflection[0].Init(0, index, OpenCL, window, window_key, units_count, heads, optimization, iBatch))
      return false;
   index++;
   if(!cLowLevelReflection[1].Init(0, index, OpenCL, units_count, window_key, window, heads, optimization, iBatch))
      return false;

No primeiro caso, analisamos uma série temporal multidimensional, onde cada passo temporal é representado por um vetor de dados, e comparamos esses vetores para identificar as interdependências entre eles. No segundo caso, comparamos sequências unitárias individuais, com o objetivo de encontrar dependências e padrões em sua dinâmica.

Depois, inicializamos o módulo de reflexão de alto nível, no qual observamos as últimas ações do agente no contexto da mudança da situação de mercado e dos resultados financeiros.

   index++;
   if(!cHighLevelReflection.Init(0, index, OpenCL, window, window_key, units_count, heads, account_descr, nactions,
                                                                                             optimization, iBatch))
      return false;

E, na sequência, preparamos o objeto do módulo de ferramentas adicionais.

   index++;
   if(!cTools.Init(0, index, OpenCL, window * units_count, optimization, iBatch))
      return false;
   cTools.SetActivationFunction(None);

Os resultados do trabalho de todos os módulos inicializados anteriormente são reunidos no módulo de tomada de decisão, que, como já mencionado, é composto por vários blocos sequenciais de cross-attention. No primeiro estágio, integramos a informação obtida dos dois módulos de reflexão de baixo nível.

   index++;
   if(!cCrossLowLevel.Init(0, index, OpenCL, window, window_key, units_count, heads, units_count, window,
                                                                                   optimization, iBatch))
      return false;

Depois, enriquecemos os resultados do módulo de análise de mercado com a informação recebida dos módulos de reflexão de baixo nível.

   index++;
   if(!cMarketToLowLevel.Init(0, index, OpenCL, window, window_key, units_count, heads, window, units_count,
                                                                                       optimization, iBatch))
      return false;

E acrescentamos um pouco de conhecimento prévio.

   index++;
   if(!cMarketToTools.Init(0, index, OpenCL, window, window_key, units_count, heads, window, units_count,
                                                                                   optimization, iBatch))
      return false;
//---
   return true;
  }

Lembro que a última camada do módulo de tomada de decisão já foi inicializada anteriormente. Ela é representada pelo objeto pai.

Após a inicialização bem-sucedida de todos os objetos internos, retornamos o resultado lógico da execução das operações para o programa chamador e encerramos o método.

A próxima etapa do nosso trabalho é a construção do algoritmo de propagação para frente da nossa implementação do framework FinAgent dentro do método feedForward. Nos parâmetros do método, recebemos ponteiros para 2 objetos de dados brutos. Prevê-se que o primeiro contenha a informação sobre o estado atual do ambiente, enquanto o segundo fluxo de informação transmite dados sobre o estado da conta e os resultados financeiros atuais.

bool CNeuronFinAgent::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput)
  {
   if(!cMarketIntelligence.FeedForward(NeuronOCL))
      return false;
   if(!cMarketMemory.FeedForward(cMarketIntelligence.AsObject()))
      return false;

A informação sobre o estado analisado do ambiente passa por um pré-processamento no módulo de análise de mercado, incluindo a busca de padrões com o transformador de atenção segmentada e a identificação de suas combinações consistentes no módulo de memória.

Em seguida, a informação sobre os padrões encontrados em duas projeções é transmitida aos módulos de reflexão de baixo nível para uma análise abrangente da dinâmica do mercado.

   if(!cTransposeState.FeedForward(cMarketIntelligence.AsObject()))
      return false;
   if(!cLowLevelReflection[0].FeedForward(cMarketIntelligence.AsObject()))
      return false;
   if(!cLowLevelReflection[1].FeedForward(cTransposeState.AsObject()))
      return false;

Vale destacar que os módulos de reflexão de baixo nível trabalham com os padrões detectados no estado atual do ambiente, sem levar em conta os dados do bloco de memória que atua na linha principal do módulo de análise de mercado. Isso se deve à necessidade de analisar a reação imediata do mercado ao surgimento dos padrões encontrados, o que permite avaliar com maior precisão as mudanças e tendências atuais, sem depender de informações históricas.

A situação é semelhante com o módulo de reflexão de alto nível.

   if(!cHighLevelReflection.FeedForward(cMarketIntelligence.AsObject(), SecondInput))
      return false;

Lembro que na entrada do módulo de reflexão de alto nível alimentamos a informação sobre o estado atual do ambiente na forma dos resultados do módulo de análise de mercado e do vetor de resultados financeiros. O tensor das ações anteriores do agente é usado de forma recursiva a partir do buffer de resultados do módulo de reflexão de alto nível.

Já o módulo de ferramentas adicionais trabalha diretamente com os dados brutos, pois busca sinais com base em conhecimento prévio a partir dos indicadores analisados.

   if(!cTools.FeedForward(NeuronOCL))
      return false;

Em seguida, passamos à organização dos processos do módulo de tomada de decisão. Inicialmente, enriquecemos os resultados da reflexão da série temporal multidimensional, integrando as dependências detectadas na dinâmica das sequências unitárias. Isso aumenta a precisão da análise e melhora a compreensão das interações no sistema, proporcionando uma avaliação mais profunda e completa do estado do ambiente analisado.

   if(!cCrossLowLevel.FeedForward(cLowLevelReflection[0].AsObject(), cLowLevelReflection[1].getOutput()))
      return false; 

Na etapa seguinte, integramos a informação obtida no processo de reflexão de baixo nível à representação de padrões estáveis, obtida na saída do bloco de memória que opera na linha principal do módulo de análise de mercado. Isso permite refinar e fortalecer ainda mais as regularidades já identificadas, oferecendo uma percepção mais precisa e abrangente das dinâmicas atuais e das interações no sistema analisado.

   if(!cMarketToLowLevel.FeedForward(cMarketMemory.AsObject(), cCrossLowLevel.getOutput()))
      return false;

É importante destacar que os módulos de reflexão de baixo nível analisam o estado específico do ambiente, identificando a reação do mercado a padrões individuais. No entanto, entre os padrões analisados, podem existir aqueles que ocorrem raramente, e a avaliação da reação do mercado a eles pode ser não representativa. Nesses casos, armazenamos a informação na memória do módulo de reflexão de baixo nível, pois existe a possibilidade de surgimento de padrões semelhantes no futuro. Isso permitirá reunir mais dados sobre a reação do mercado a eles.

Mas não podemos usar informações não confirmadas para a tomada de decisão. Por isso, no módulo de tomada de decisão nos apoiamos apenas em padrões estáveis, solicitando informações sobre a reação do mercado a eles no bloco de reflexão de baixo nível, para uma avaliação mais precisa e fundamentada.

Depois, complementamos os resultados da análise da situação de mercado com conhecimento prévio.

   if(!cMarketToTools.FeedForward(cMarketToLowLevel.AsObject(), cTools.getOutput()))
      return false;

Note que não adicionamos parâmetros treináveis para a interpretação dos flags formados no módulo de ferramentas adicionais, embora essa necessidade tenha sido discutida anteriormente. Atribuímos essa funcionalidade aos parâmetros de formação das entidades Key e Value no módulo de cross-attention. Dessa forma, a interpretação e o processamento dos flags são integrados diretamente ao processo de cross-attention. Assim, a adição explícita de parâmetros treináveis para interpretar os resultados do módulo de ferramentas adicionais torna-se desnecessária.

Ao final do método de propagação para frente, resta apenas analisar os resultados do módulo de reflexão de alto nível no contexto dos padrões estáveis identificados e da reação do mercado a eles. Essa operação é realizada pelos recursos da classe pai.

   return CNeuronRelativeCrossAttention::feedForward(cHighLevelReflection.AsObject(), cMarketToTools.getOutput());
  }

O resultado lógico da execução das operações é retornado ao programa chamador, e encerramos o método de propagação para frente.

Após a construção do método de propagação para frente, passamos à organização dos processos de propagação reversa. Nesta parte do artigo, proponho analisar em detalhe o algoritmo de construção do método de distribuição dos gradientes de erro calcInputGradients, enquanto o método de otimização dos parâmetros treináveis updateInputWeights pode ser deixado para estudo independente.

bool CNeuronFinAgent::calcInputGradients(CNeuronBaseOCL *NeuronOCL,
                                         CBufferFloat *SecondInput,
                                         CBufferFloat *SecondGradient,
                                         ENUM_ACTIVATION SecondActivation = -1)
  {
   if(!NeuronOCL || !SecondGradient)
      return false;

Nos parâmetros do método recebemos ponteiros para os mesmos objetos de dados brutos, mas desta vez precisamos transmitir a eles o gradiente de erro, de acordo com a influência dos dados brutos sobre o resultado final do modelo. No corpo do método, verificamos imediatamente a validade dos ponteiros recebidos, pois, do contrário, a execução das operações seguintes perderia sentido.

Como vocês sabem, a distribuição dos gradientes de erro repete integralmente o fluxo de informação da propagação para frente, só que no sentido inverso. O método de propagação para frente terminou com a chamada do método homônimo da classe pai. Consequentemente, o método de distribuição dos gradientes de erro começa com a chamada do método da classe pai. Nele, distribuímos o erro do modelo entre o módulo de reflexão de alto nível e o bloco anterior de cross-attention do módulo de tomada de decisão.

   if(!CNeuronRelativeCrossAttention::calcInputGradients(cHighLevelReflection.AsObject(),
                                       cMarketToTools.getOutput(),
                                       cMarketToTools.getGradient(),
                                       (ENUM_ACTIVATION)cMarketToTools.Activation()))
      return false;

Em seguida, conduzimos o gradiente de erro sequencialmente por todos os blocos de cross-attention do módulo de tomada de decisão, distribuindo os erros por todos os fluxos de informação do framework, conforme sua influência no resultado do modelo.

   if(!cMarketToLowLevel.calcHiddenGradients(cMarketToTools.AsObject(),
                                         cTools.getOutput(),
                                         cTools.getGradient(),
                                         (ENUM_ACTIVATION)cTools.Activation()))
      return false;
//---
   if(!cMarketMemory.calcHiddenGradients(cMarketToLowLevel.AsObject(),
                                         cCrossLowLevel.getOutput(),
                                         cCrossLowLevel.getGradient(),
                                         (ENUM_ACTIVATION)cCrossLowLevel.Activation()))
      return false;

Depois, distribuímos o gradiente de erro pelos módulos de reflexão de baixo nível.

   if(!cLowLevelReflection[0].calcHiddenGradients(cCrossLowLevel.AsObject(),
         cLowLevelReflection[1].getOutput(),
         cLowLevelReflection[1].getGradient(),
         (ENUM_ACTIVATION)cLowLevelReflection[1].Activation()))
      return false;
   if(!cTransposeState.calcHiddenGradients(cLowLevelReflection[1].AsObject()))
      return false;

Nesse ponto, já distribuímos o gradiente de erro entre todos os módulos do nosso framework. Agora precisamos reunir os dados de todos os fluxos de informação no nível dos dados brutos. Lembro que todos os módulos de reflexão e o bloco de memória do módulo de análise de mercado trabalham com os resultados do pré-processamento dos dados brutos no transformador com atenção segmentada. Portanto, primeiro reunimos o gradiente de erro no nível dos resultados desse objeto.

O primeiro passo será a transmissão do gradiente de erro a partir do bloco de memória.

   if(!((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cMarketMemory.AsObject()))
      return false;

Em seguida, realizamos a substituição do ponteiro para o buffer de gradientes de erro do nosso objeto de pré-processamento dos dados brutos, o que nos permitirá armazenar os valores obtidos anteriormente.

   CBufferFloat *temp = cMarketIntelligence.getGradient();
   if(!cMarketIntelligence.SetGradient(cMarketIntelligence.getPrevOutput(), false) ||
      !((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cHighLevelReflection.AsObject(),
                                                            SecondInput, SecondGradient, SecondActivation) ||
      !SumAndNormilize(temp, cMarketIntelligence.getGradient(), temp, 1, false, 0, 0, 0, 1))
      return false;

E chamamos o método de distribuição de gradientes de erro do módulo de reflexão de alto nível. Depois disso, obrigatoriamente somamos os valores recebidos pelos dois fluxos de informação.

Vale destacar que o módulo de reflexão de alto nível trabalha com dados provenientes de dois fluxos de informação. Assim, ao distribuir os gradientes de erro, por meio desse módulo transmitimos simultaneamente a discrepância tanto pelo fluxo principal quanto pela linha do resultado financeiro. Isso possibilita levar em conta a influência dos erros em ambas as partes mais importantes da análise, contribuindo para um ajuste mais preciso do sistema.

De forma análoga, é realizado o processo de distribuição do gradiente de erro pelos fluxos de informação dos módulos de reflexão de baixo nível. Porém, diferentemente do módulo de reflexão de alto nível, esses módulos trabalham apenas com uma fonte de dados brutos, o que simplifica o processo de distribuição do gradiente de erro.

   if(!((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cLowLevelReflection[0].AsObject()) ||
      !SumAndNormilize(temp, cMarketIntelligence.getGradient(), temp, 1, false, 0, 0, 0, 1))
      return false;
   if(!((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cTransposeState.AsObject()) ||
      !SumAndNormilize(temp, cMarketIntelligence.getGradient(), temp, 1, false, 0, 0, 0, 1) ||
      !cMarketIntelligence.SetGradient(temp, false))
      return false;

Não devemos esquecer que, após cada iteração, é necessário somar o gradiente de erro obtido aos valores acumulados anteriormente. Isso garante que estamos considerando corretamente todas as discrepâncias do funcionamento do modelo. Após o processamento de todos os fluxos de informação, é importante retornar os ponteiros para os buffers de dados ao estado inicial.

Agora, resta transmitir o gradiente de erro para o nível dos dados brutos da linha do fluxo principal de informação e finalizar o método, retornando o resultado lógico da execução das operações ao programa chamador.

   if(!NeuronOCL.calcHiddenGradients(cMarketIntelligence.AsObject()))
      return false;
//---
   return true;
  }

Note que no algoritmo do método de distribuição dos gradientes de erro não participa o módulo de ferramentas adicionais. Como discutido anteriormente, não planejamos a transmissão de gradientes de erro por esse fluxo de informação. Além disso, a limpeza do buffer de gradientes de erro do objeto que fornece os dados brutos, neste caso, seria até prejudicial. Afinal, esse mesmo objeto recebe o gradiente de erro pela linha do fluxo principal de informação.

Com isso, concluímos a análise dos algoritmos de construção do framework FinAgent por meio do MQL5. O código completo de todos os objetos apresentados e de seus métodos está disponível no anexo para sua consulta e uso posterior. Lá também estão incluídos os códigos de todos os programas e a arquitetura do modelo treinável utilizados na preparação deste artigo. Todos eles foram praticamente transferidos sem alterações a partir do artigo dedicado à construção de agente com memória multinível. As mudanças afetaram apenas a arquitetura do modelo, onde substituímos uma camada neural, integrando o framework aqui apresentado.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronFinAgent;
//--- Windows
     {
      int temp[] = {BarDescr, AccountDescr, 2 * NActions, Segments}; //Window, Account description, N Actions, Segments
      if(ArrayCopy(descr.windows, temp) < int(temp.Size()))
         return false;
     }
   descr.count = HistoryBars;
   descr.window_out = 32;
   descr.step = 4;                              // Heads
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

A arquitetura das demais camadas do modelo foi mantida sem modificações. E seguimos adiante. Chegamos à etapa final do nosso trabalho — a avaliação da eficácia das abordagens implementadas em dados históricos reais.


Testes

Nos dois últimos artigos, examinamos em detalhes o framework FinAgent. Ao mesmo tempo, implementamos nossa própria visão das abordagens propostas por seus autores. Adaptamos os algoritmos do framework de acordo com nossas necessidades. E agora chegamos ao estágio decisivo — a verificação da eficácia das soluções implementadas em dados históricos reais.

É importante destacar que, no processo de desenvolvimento, introduzimos modificações significativas nos algoritmos que servem de base para o framework FinAgent. Essas alterações dizem respeito a aspectos-chave do funcionamento do modelo. Portanto, neste caso, não estamos avaliando a solução original, mas sim a nossa versão adaptada.

O treinamento do modelo foi realizado utilizando dados históricos do par de moedas EURUSD referentes ao ano de 2023, no timeframe H1. Todas as configurações dos indicadores usados pelo modelo foram mantidas em seus valores padrão, o que permitiu concentrar a avaliação no próprio algoritmo e em sua capacidade de trabalhar com os dados brutos sem ajustes adicionais.

Para a etapa inicial de treinamento, foi usada uma amostra de dados preparada em pesquisas anteriores. Aplicamos um algoritmo de aprendizado com a formação de “ações quase ideais” do Agente, que possibilitou treinar o modelo sem a necessidade de atualizações constantes da amostra de treinamento. No entanto, apesar do bom desempenho do algoritmo nesse formato, consideramos que a atualização regular da amostra de treinamento seria um complemento útil para aumentar a precisão e ampliar a cobertura de um espectro maior de estados da conta.

Após alguns ciclos de treinamento, o modelo demonstrou rentabilidade estável tanto nos dados de treino quanto nos de teste. O teste final foi realizado com dados históricos de janeiro de 2024. Todos os parâmetros do modelo e dos indicadores analisados foram mantidos sem alterações. Essa abordagem permite obter uma avaliação objetiva da eficácia do modelo em condições o mais próximas possível do mercado real. Os resultados dos testes são apresentados a seguir.

Durante o período de teste, o modelo realizou 95 operações de trading, o que supera significativamente os resultados das últimas versões de modelos para o mesmo intervalo. Mais de 42% das operações foram encerradas com lucro. Mas, graças ao fato de que a média das operações lucrativas foi 1,5 vez maior que a média das operações com prejuízo, no geral, ao longo do período de teste, o modelo apresentou rentabilidade. O profit factor foi registrado no nível de 1,09.

É interessante notar que a maior parte do lucro foi obtida pelo modelo na primeira metade do mês, quando o preço oscilava em um corredor relativamente estreito. Já durante a formação de uma tendência de baixa, a linha de balanço entrou em movimento lateral. E chegou-se até a observar certa retração.

Gráfico do ativo no período de teste

Na minha opinião, as causas desse comportamento podem estar nos algoritmos dos módulos de análise de mercado e de ferramentas adicionais. Mas isso já é matéria para estudos complementares.


Considerações finais

Exploramos o framework FinAgent, que representa uma solução avançada para a análise integrada da dinâmica do mercado e de dados históricos. Os autores do framework combinam informação textual e visual, o que amplia consideravelmente as possibilidades de tomada de decisões de trading mais fundamentadas. Com a utilização de cinco componentes-chave que compõem a arquitetura do framework, o FinAgent demonstra precisão e alta adaptabilidade, algo especialmente importante no trading em mercados financeiros, onde as condições mudam constantemente.

É importante ressaltar que o framework não se restringe apenas à análise, mas oferece um amplo conjunto de ferramentas capazes de trabalhar de forma eficiente tanto com dados textuais quanto com dados gráficos. Essa abordagem permite levar em conta diversos fatores que influenciam o mercado e proporciona uma compreensão mais profunda dos processos de mercado. Essas características tornam o FinAgent uma ferramenta promissora para o desenvolvimento de estratégias de trading capazes de se adaptar às condições de mercado em constante mudança e considerar até mesmo as menores oscilações.

Na parte prática do nosso trabalho, implementamos nossa própria visão das abordagens propostas por meio do MQL5. Treinamos o modelo integrando nele os métodos discutidos e realizamos seu teste em dados históricos reais. Os resultados do teste demonstraram a capacidade do modelo de gerar lucro. No entanto, a rentabilidade do modelo mostrou-se dependente da situação de mercado. Além disso, há a necessidade de realizar experimentos adicionais para encontrar formas de aumentar a adaptabilidade do modelo às condições dinâmicas e mutáveis do mercado. 


Referências


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 Study.mq5 Expert Advisor EA de treinamento de modelos
4 Test.mq5 Expert Advisor EA para teste do modelo
5 Trajectory.mqh Biblioteca de classe Estrutura de descrição do estado do sistema e arquitetura dos modelos
6 NeuroNet.mqh Biblioteca de classe Biblioteca de classes para criação de rede neural
7 NeuroNet.cl Biblioteca Biblioteca de código do programa OpenCL

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

Arquivos anexados |
MQL5.zip (2327.64 KB)
Negociando com o Calendário Econômico do MQL5 (Parte 1): Dominando as Funções do Calendário Econômico do MQL5 Negociando com o Calendário Econômico do MQL5 (Parte 1): Dominando as Funções do Calendário Econômico do MQL5
Neste artigo, exploramos como usar o Calendário Econômico do MQL5 para negociar, primeiro entendendo suas funcionalidades principais. Em seguida, implementamos funções-chave do Calendário Econômico no MQL5 para extrair dados relevantes de notícias para decisões de negociação. Por fim, concluímos mostrando como utilizar essas informações para aprimorar as estratégias de negociação de forma eficaz.
Desenvolvendo um EA multimoeda (Parte 21): Preparação para um experimento importante e otimização do código Desenvolvendo um EA multimoeda (Parte 21): Preparação para um experimento importante e otimização do código
Para avançar mais, seria interessante verificar se conseguimos melhorar os resultados executando periodicamente uma reotimização automática e a geração de um novo EA. Muitas discussões sobre o uso da otimização de parâmetros giram em torno da questão de por quanto tempo é possível usar os parâmetros obtidos para operar em um período futuro, mantendo os principais indicadores de lucratividade e rebaixamento dentro dos níveis estabelecidos. E será que isso é de fato possível?
Do básico ao intermediário: Navegando na SandBox Do básico ao intermediário: Navegando na SandBox
Neste artigo veremos duas formas de observar e até mesmo ter alguma interação com o conteúdo presente em uma SandBox. Isto usando a plataforma MetaTrader 5 como ponto de apoio. Entender o conteúdo mostrado neste artigo, será primordial para entender o que será visto nos próximos artigos.
Técnicas do MQL5 Wizard que você deve conhecer (Parte 44): Indicador técnico Average True Range (ATR) Técnicas do MQL5 Wizard que você deve conhecer (Parte 44): Indicador técnico Average True Range (ATR)
O oscilador ATR é um indicador muito popular para atuar como um proxy de volatilidade, especialmente nos mercados de forex onde os dados de volume são escassos. Nós o examinamos com base em padrões, assim como fizemos com indicadores anteriores, e compartilhamos estratégias e relatórios de testes graças às classes da biblioteca MQL5 wizard e sua montagem.