Русский Español
preview
Redes neurais em trading: Modelos com uso de wavelet transform e atenção multitarefa (Conclusão)

Redes neurais em trading: Modelos com uso de wavelet transform e atenção multitarefa (Conclusão)

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

Introdução

No artigo anterior, aprendemos os aspectos teóricos do framework Multitask-Stockformer e começamos a implementar as abordagens propostas com MQL5. O Multitask-Stockformer une duas ferramentas poderosas: a wavelet transform discreta, que oferece uma análise aprofundada de séries temporais, e os modelos multitarefa de Self-Attention, capazes de identificar dependências complexas nos dados financeiros. Essa combinação permite criar uma ferramenta versátil para análise e previsão de séries temporais.

A estrutura do framework é composta por 3 blocos. No módulo de decomposição de séries temporais, os dados analisados são divididos em componentes de baixa e alta frequência. As componentes de baixa frequência refletem as tendências globais e permitem a análise de padrões de longo prazo. Enquanto isso, a componente de alta frequência capta flutuações de curto prazo, incluindo picos de atividade e anomalias. A decomposição detalhada dos dados melhora a qualidade do processamento e facilita a identificação de características-chave, o que é crucial na análise de séries temporais dos mercados financeiros.

Após a decomposição, os dados seguem para o codificador espaço-temporal de duas frequências. Esse bloco combina vários módulos e tem como objetivo analisar as componentes frequenciais destacadas, assim como suas interdependências. Os sinais de baixa frequência são processados por um mecanismo de atenção temporal. Ele foca nas tendências de longo prazo e suas mudanças. Já os dados de alta frequência passam por camadas convolucionais causais expandidas, permitindo destacar pequenas variações e sua dinâmica. Em seguida, os sinais processados são integrados aos módulos de atenção a grafos, que captam dependências espaço-temporais, refletindo as conexões entre diferentes ativos e intervalos de tempo. Esse processo gera representações em grafo de múltiplos níveis, que são convertidas em incorporações multidimensionais. Essas incorporações são unificadas por mecanismos de soma e atenção a grafos, criando uma representação consolidada dos dados para análise posterior.

A etapa-chave do processamento é o decodificador de fusão de duas frequências, que desempenha papel essencial na formação dos resultados previstos. O decodificador integra os preditores por meio do mecanismo Fusion Attention, o que permite agregar os dados dos sinais de baixa e alta frequência em uma única representação latente, que reflete padrões temporais em diferentes escalas, oferecendo uma abordagem abrangente à análise de dados. Nesse estágio, o modelo cria representações ocultas que depois são processadas por camadas totalmente conectadas especializadas. Essas camadas permitem resolver várias tarefas ao mesmo tempo: prever a rentabilidade dos ativos, identificar as probabilidades de mudança de tendência e encontrar outras características-chave das séries temporais. A abordagem de processamento multitarefa torna o modelo flexível e adaptável a diversas condições de mercado, o que é especialmente importante em ambientes de alta volatilidade nos mercados financeiros.

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


Implementação do framework Multitask-Stockformer

Damos continuidade ao nosso trabalho de implementação das abordagens propostas pelos autores do framework Multitask-Stockformer, utilizando MQL5. Isso envolve a aplicação prática dos componentes principais do sistema, com foco na otimização da análise de séries temporais.

Um dos elementos fundamentais do framework é o módulo de decomposição de séries temporais, que foi implementado por nós na classe CNeuronDecouplingFlow. Esse componente realiza a separação dos dados brutos em componentes de baixa e alta frequência, formando a base para análises futuras. O principal objetivo do módulo é identificar as características estruturais chave das séries temporais, considerando suas especificidades e tendências de mercado potenciais. No artigo anterior, analisamos as características arquitetônicas e soluções algorítmicas por trás da construção da classe CNeuronDecouplingFlow.

A próxima etapa do processamento dos dados é sua análise com o uso do codificador espaço-temporal de duas frequências. Como já mencionado, os autores do framework propuseram uma arquitetura complexa de codificador, composta por dois fluxos de dados independentes. Cada trilha possui sua própria arquitetura.

As componentes de baixa frequência são analisadas com o mecanismo de atenção temporal, baseado na arquitetura Self-Attention. Essa arquitetura oferece amplos recursos para detectar dependências de longo prazo e prever tendências globais do mercado. A aplicação de Self-Attention proporciona um entendimento aprofundado das estruturas complexas dos dados, reduzindo os riscos de negligenciar relações relevantes. Na implementação atual, optamos por usar um dos módulos de atenção já existentes na nossa biblioteca, que utiliza o mecanismo Self-Attention.

As componentes de alta frequência das séries temporais são processadas com o módulo de convolução causal dilatada, implementado na classe CNeuronDilatedCasualConv. Os algoritmos aprimorados permitem identificar com eficácia anomalias locais e picos de atividade. Esse componente é fundamental na análise da dinâmica de curto prazo do mercado, especialmente durante períodos de alta volatilidade. A inclusão desse módulo na arquitetura geral do framework contribui para aumentar sua adaptabilidade e desempenho. As soluções arquitetônicas e modificações locais no framework original, que usamos na construção da classe CNeuronDilatedCasualConv, foram discutidas no artigo anterior.

Após o pré-processamento dos componentes de baixa e alta frequência do sinal analisado, os dados são direcionados para trilhas separadas no slot de atenção a grafos. O módulo em questão é baseado na criação de dois grafos especializados. O primeiro grafo modela as dependências temporais, destacando sua estrutura sequencial. Esse grafo é fundamental para identificar tendências, ciclicidade e outras características temporais. O segundo grafo é construído a partir da matriz de correlação dos preços dos ativos financeiros, permitindo uma integração aprofundada das relações entre os ativos. Isso possibilita considerar a influência de um ativo sobre outro, algo especialmente importante para a modelagem e previsão financeiras. Juntos, esses grafos formam uma estrutura multinível que melhora a precisão da análise e interpretação dos dados.

Para converter as informações dos grafos em representações úteis para análise, utiliza-se o algoritmo Struct2Vec. Esse algoritmo transforma as propriedades topológicas dos grafos em incorporações vetoriais compactas, que são então otimizadas por camadas totalmente conectadas e treináveis. Essas incorporações permitem integrar com eficiência características locais e globais dos dados, aumentando a qualidade da análise de séries temporais. Depois disso, os dados processados são enviados para as trilhas de atenção a grafos, onde passam por nova análise com mecanismos de atenção. Essa etapa permite identificar tanto dependências de curto quanto de longo prazo.

Os autores do framework Multitask-Stockformer propuseram uma arquitetura bastante complexa para o slot de atenção a grafos. E sua implementação exige considerável capacidade computacional e preparação cuidadosa dos dados. Durante a preparação do modelo para este artigo, aplicamos algumas simplificações com o objetivo de tornar o uso do modelo mais prático, mantendo seu alto desempenho. A primeira simplificação foi eliminar a informação temporal sobre o estado do ambiente analisado. Essa decisão se baseou na suposição de que, embora a informação temporal seja útil, ela não exerce influência crítica sobre a eficácia geral do nosso modelo neste estágio. Na versão original do framework, a saída era um portfólio de ações formado, enquanto em nossa implementação o objetivo principal é criar uma representação latente do ambiente. Essa representação é usada pelo modelo Актера para tomar decisões de negociação, recebendo também dados sobre o estado da conta e marcas temporais. Isso garante consciência contextual ao modelo. Portanto, apenas transferimos o ponto de entrega da informação temporal ao modelo.

Contudo, a simplificação aplicada ao grafo de dependências temporais não pode ser replicada no grafo de correlação de ativos, pois isso acarretaria perda de informações essenciais. Como alternativa, propomos substituir a estrutura original por um obrigatório camada de codificação posicional. Essa abordagem permite treinar incorporações de forma eficiente, minimizando a complexidade computacional e preservando as conexões relevantes entre os ativos, que o modelo aprende por conta própria durante o treinamento. Essa melhoria oferece uma arquitetura mais flexível, capaz de se adaptar a diferentes condições de mercado.

Além disso, demos mais um passo à frente ao substituir os slots de atenção a grafos pelos módulos de suavização adaptativa de características dos nós (Node-Adaptive Feature SmoothingNAFS). Uma vantagem importante desse método é que os módulos NAFS não possuem parâmetros treináveis, o que não apenas reduz a complexidade computacional, como também simplifica o ajuste e o treinamento do modelo.

Com o uso de NAFS, o processo de criação das incorporações torna-se mais flexível e robusto, já que a técnica de suavização se adapta à topologia do grafo e às características de seus nós. Isso é particularmente importante em tarefas nas quais a estrutura dos dados pode ser heterogênea ou sofrer alterações dinâmicas. Dessa forma, a aplicação de NAFS garante a criação de representações de dados de alta qualidade, levando em conta ao mesmo tempo relações locais e globais no grafo.

A agregação dos dois fluxos de informação é feita no decodificador de duas frequências, que integra diferentes aspectos dos dados, formando a base para uma análise multidimensional. Isso permite obter uma visão mais completa da dinâmica de variação dos sinais. O mecanismo central do decodificador de duas frequências é o Fusion Attention, que combina dois módulos de atenção paralelos. O primeiro deles, baseado no mecanismo Self-Attention, é especializado no processamento profundo da componente de baixa frequência, destacando dependências de longo prazo, tendências estáveis e padrões globais. Esse módulo permite identificar características fundamentais das séries temporais, essenciais para previsões. O segundo módulo usa o mecanismo Cross-Attention para integrar informações de alta frequência, adicionando componentes de curto prazo e detalhes à análise. Essa integração enriquece consideravelmente os dados de baixa frequência com detalhes, algo crucial para capturar pequenas, porém importantes, flutuações.

Ambos os módulos de atenção operam em sincronia, o que permite criar representações de dados coerentes e complementares. Seus resultados são combinados por soma e depois encaminhados para o processamento em camadas totalmente conectadas (MLP). Essa abordagem garante que tanto as características globais quanto as locais do sinal analisado sejam consideradas, permitindo captar uma ampla gama de relações e influências.

A arquitetura proposta do Fusion Attention pode ser facilmente implementada com os módulos já existentes de Cross- e Self-Attention. Além disso, sua implementação não exige alterações significativas nos algoritmos de base.

Portanto, podemos concluir que já estão presentes todos os módulos-chave para criar uma arquitetura abrangente do framework Multitask-Stockformer. Isso cria a base para avançarmos ao próximo estágio de desenvolvimento: a formação de um objeto de alto nível que una todos os módulos mencionados em um único algoritmo funcional e completo. A principal tarefa dessa etapa é não apenas integrar os componentes, mas também garantir seu funcionamento sincronizado, respeitando as especificidades de cada módulo. A seguir, apresentamos a estrutura do novo objeto CNeuronMultitaskStockformer.

class CNeuronMultitaskStockformer   :  public CNeuronBaseOCL
  {
protected:
   CNeuronDecouplingFlow      cDecouplingFlow;
   CNeuronBaseOCL             cLowFreqSignal;
   CNeuronBaseOCL             cHighFreqSignal;
   CNeuronRMAT                cTemporalAttention;
   CNeuronDilatedCasualConv   cDilatedCasualConvolution;
   CNeuronLearnabledPE        cLowFreqPE;
   CNeuronLearnabledPE        cHighFreqPE;
   CNeuronNAFS                cLowFreqGraphAttention;
   CNeuronNAFS                cHighFreqGraphAttention;
   CNeuronDMHAttention        cLowFreqFusionDecoder;
   CNeuronCrossDMHAttention   cLowHighFreqFusionDecoder;
   CNeuronBaseOCL             cLowHigh;
   CNeuronConvOCL             cProjection;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;

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

A estrutura apresentada inclui diversos objetos internos, que refletem diretamente os módulos descritos do framework Multitask-Stockformer. Esses componentes foram organizados para garantir um alto nível de integração funcional e flexibilidade na implementação. Vamos analisar detalhadamente os algoritmos de interação entre eles, bem como os fluxos de transferência de informações durante a implementação dos métodos do objeto de integração.

Todos os objetos internos são declarados estaticamente, o que nos permite deixar o construtor e o destrutor da classe vazios. A inicialização de todos os objetos recém-declarados e herdados é realizada no método Init.

bool CNeuronMultitaskStockformer::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                       uint window, uint window_key, uint units_count,
                                       uint heads, uint layers, uint neurons_out, uint filters,
                                       ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, neurons_out, optimization_type, batch))
      return false;

Entre os parâmetros desse método, além das constantes já conhecidas, encontramos uma nova, neurons_out. Esse parâmetro define o tamanho do vetor de representação latente do estado do ambiente analisado, que o usuário espera obter como saída deste bloco Multitask-Stockformer. Esse valor é passado na chamada ao método homônimo da classe-pai, onde ocorre a inicialização das interfaces básicas de troca de dados com as camadas neurais externas do modelo.

Após a execução bem-sucedida do método da classe-pai, iniciamos a inicialização dos objetos internos. Esse processo é feito, como de costume, na ordem de utilização dos objetos durante a propagação para frente. Como já mencionado, os dados brutos recebidos são inicialmente divididos em componentes de baixa e alta frequência no módulo de decomposição do sinal CNeuronDecouplingFlow.

   uint index = 0;
   uint wave_window = MathMin(24, units_count);
   if(!cDecouplingFlow.Init(0, index, OpenCL, wave_window, 2, units_count, filters, window, optimization, iBatch))
      return false;
   cDecouplingFlow.SetActivationFunction(None);

Observe que nos parâmetros externos do nosso método de inicialização do objeto de integração não especificamos o tamanho e o passo da janela da wavelet transform discreta. Esses parâmetros serão definidos com valores fixos diretamente dentro do método. Neste artigo, planejamos realizar experimentos com dados históricos do timeframe H1. Com base nisso, limitamos o tamanho da janela da wavelet transform a um único dia, ou seja, 24 passos da sequência analisada. Também adicionamos um controle para que o tamanho da série temporal multimodal analisada não seja excedido. O passo da janela é definido como 2, o que corresponde à omissão de um elemento da sequência.

Na saída do módulo de decomposição de dados, é formado um único tensor contendo as componentes de baixa e alta frequência do sinal analisado. No entanto, o codificador espaço-temporal de duas frequências trabalha com dois fluxos paralelos, nos quais cada componente é analisada separadamente. Para permitir isso, separamos os dados, alocando cada componente em seu próprio objeto. Isso proporciona praticidade e flexibilidade para o processamento posterior.

//--- Dual-Frequency Spatiotemporal Encoder
   uint wave_units_out = cDecouplingFlow.GetUnits();
   index++;
   if(!cLowFreqSignal.Init(0, index, OpenCL, cDecouplingFlow.Neurons() / 2, optimization, iBatch))
      return false;
   cLowFreqSignal.SetActivationFunction(None);
   index++;
   if(!cHighFreqSignal.Init(0, index, OpenCL, cDecouplingFlow.Neurons() / 2, optimization, iBatch))
      return false;
   cHighFreqSignal.SetActivationFunction(None);
   index++;

A componente de baixa frequência é processada no módulo de atenção temporal, baseado no mecanismo Self-Attention. Na versão original do framework Multitask-Stockformer, os autores recomendam adicionar codificação posicional para melhorar o processamento de sequências. Porém, decidimos utilizar o módulo atenção com codificação relativa, que já inclui um mecanismo embutido para determinar as posições relativas dos elementos da sequência. Isso elimina a necessidade de codificação posicional adicional, simplificando a arquitetura e, ao mesmo tempo, aumentando sua eficiência.

   if(!cTemporalAttention.Init(0, index, OpenCL, filters, window_key, wave_units_out * window, heads, layers,
                                                                                       optimization, iBatch))
      return false;
   cTemporalAttention.SetActivationFunction(None);
   index++;

Vale destacar que a dimensionalidade do vetor que descreve um elemento da sequência corresponde ao número de filtros utilizados na wavelet transform. Além disso, a dimensionalidade da sequência analisada abrange todas as séries temporais unitárias. Essa abordagem permite estudar as interdependências de tendências em toda a sequência multimodal, e não apenas em componentes isoladas.

A análise das dependências de alta frequência é realizada no módulo de convolução causal dilatada. Neste caso, utilizamos o tamanho mínimo da janela de convolução: 2 elementos, com o mesmo passo. Aqui, a análise é feita exclusivamente dentro das sequências unitárias, o que possibilita investigar com mais profundidade as dependências locais.

   if(!cDilatedCasualConvolution.Init(0, index, OpenCL, 2, 2, filters, wave_units_out, window, layers,
                                                                                 optimization, iBatch))
      return false;
   index++;

Em seguida, adicionamos a codificação posicional a ambas as componentes.

   if(!cLowFreqPE.Init(0, index, OpenCL, cTemporalAttention.Neurons(), optimization, iBatch))
      return false;
   index++;
   if(!cHighFreqPE.Init(0, index, OpenCL, cDilatedCasualConvolution.Neurons(), optimization, iBatch))
      return false;
   index++;

É importante observar que cada componente recebe sua própria camada treinável de codificação posicional. Essa abordagem permite analisar e compreender melhor a estrutura das componentes de alta e baixa frequência de forma independente.

Finalizando o trabalho com o codificador de duas frequências, inicializamos os módulos de suavização adaptativa dos nós, que são aplicados separadamente às componentes de alta e baixa frequência. Ambos os módulos recebem os mesmos parâmetros, exceto pelo comprimento da sequência. Isso porque se espera uma redução no tamanho da sequência de alta frequência devido à natureza do módulo de convolução causal dilatada.

   if(!cLowFreqGraphAttention.Init(0, index, OpenCL, filters, 3, wave_units_out * window, optimization, iBatch))
      return false;
   index++;
   if(!cHighFreqGraphAttention.Init(0, index, OpenCL, filters, 3, cDilatedCasualConvolution.Neurons()/filters,
                                                                                          optimization, iBatch))
      return false;
   index++;

Na etapa seguinte, passamos à inicialização dos objetos do decodificador de fusão dos fluxos de dados. Aqui, inicializamos dois blocos de atenção: Self-Attention para a componente de baixa frequência e Cross-Attention para incorporar a componente de alta frequência.

//--- Dual-Frequency Fusion Decoder
   if(!cLowFreqFusionDecoder.Init(0, index, OpenCL, filters, window_key, wave_units_out * window, heads,
                                                                           layers, optimization, iBatch))
      return false;
   index++;
   if(!cLowHighFreqFusionDecoder.Init(0, index, OpenCL, filters, window_key, wave_units_out * window, filters,
                             cDilatedCasualConvolution.Neurons()/filters, heads, layers, optimization, iBatch))
      return false;
   index++;

Como mencionado anteriormente, os resultados desses blocos de atenção são somados. Para armazenar o resultado dessa operação, criamos um objeto de camada neural base.

   if(!cLowHigh.Init(0, index, OpenCL, cLowFreqFusionDecoder.Neurons(), optimization, iBatch))
      return false;
   CBufferFloat *grad = cLowFreqFusionDecoder.getGradient();
   if(!grad ||
      !cLowHigh.SetGradient(grad, true) ||
      !cLowHighFreqFusionDecoder.SetGradient(grad, true))
      return false;
   index++;

Para evitar operações desnecessárias de cópia de dados, sincronizamos os ponteiros para o buffer de gradientes de erro dos três últimos objetos. Esse método reduz a sobrecarga de memória e melhora a eficiência geral do treinamento.

Em seguida, inicializamos os objetos MLP para gerar a representação latente do estado do ambiente analisado. Utilizamos aqui uma camada convolucional para reduzir a dimensionalidade e uma camada totalmente conectada para gerar uma representação com o tamanho desejado.

É importante lembrar que, ao criar o objeto de integração, utilizamos uma camada totalmente conectada como classe-pai. Isso nos permite inicializar apenas o objeto interno da camada convolucional, especificando o número necessário de conexões de saída. Para implementar a funcionalidade da camada totalmente conectada, utilizaremos os recursos herdados da classe-pai.

   if(!cProjection.Init(Neurons(), index, OpenCL, filters, filters, 3, wave_units_out, window, optimization, iBatch))
      return false;
//---
   return true;
  }

Após a inicialização bem-sucedida de todos os objetos internos, finalizamos a execução do método, retornando antes o resultado lógico das operações ao programa que realizou a chamada.

Concluído o trabalho no método de inicialização do objeto, passamos à construção do algoritmo de propagação para frente do objeto de integração no método feedForward.

bool CNeuronMultitaskStockformer::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//--- Decoupling Flow
   if(!cDecouplingFlow.FeedForward(NeuronOCL))
      return false;

Nos parâmetros do método, recebemos um ponteiro para o objeto de dados brutos, que é imediatamente repassado ao método homônimo do módulo de decomposição da série temporal analisada.

O tensor consolidado de componentes de alta e baixa frequência, obtido na saída do módulo de decomposição, é então dividido entre dois objetos para posterior análise em trilhas independentes.

   if(!DeConcat(cLowFreqSignal.getOutput(), cHighFreqSignal.getOutput(), cDecouplingFlow.getOutput(),
                                          cDecouplingFlow.GetFilters(), cDecouplingFlow.GetFilters(), 
                                          cDecouplingFlow.GetUnits()*cDecouplingFlow.GetVariables()))
      return false;

Como discutido anteriormente, a componente de baixa frequência é enviada ao módulo de atenção temporal. Em seguida, é adicionada a codificação posicional e os dados são encaminhados ao módulo de representação adaptativa de grafos.

//--- Dual-Frequency Spatiotemporal Encoder
//--- Low Frequency Encoder
   if(!cTemporalAttention.FeedForward(cLowFreqSignal.AsObject()))
      return false;
   if(!cLowFreqPE.FeedForward(cTemporalAttention.AsObject()))
      return false;
   if(!cLowFreqGraphAttention.FeedForward(cLowFreqPE.AsObject()))
      return false;

A componente de alta frequência segue sua própria trilha, iniciando no módulo de convolução causal dilatada.

//--- High Frequency Encoder
   if(!cDilatedCasualConvolution.FeedForward(cHighFreqSignal.AsObject()))
      return false;
   if(!cHighFreqPE.FeedForward(cDilatedCasualConvolution.AsObject()))
      return false;
   if(!cHighFreqGraphAttention.FeedForward(cHighFreqPE.AsObject()))
      return false;

Os resultados das duas trilhas são então alimentados no decodificador de fusão de duas frequências. Nessa etapa, os dados são processados por dois módulos de atenção. Seus resultados são somados e normalizados.

//--- Dual-Frequency Fusion Decoder
   if(!cLowFreqFusionDecoder.FeedForward(cLowFreqGraphAttention.AsObject()))
      return false;
   if(!cLowHighFreqFusionDecoder.FeedForward(cLowFreqGraphAttention.AsObject(), cHighFreqGraphAttention.getOutput()))
      return false;
   if(!SumAndNormilize(cLowFreqFusionDecoder.getOutput(), cLowHighFreqFusionDecoder.getOutput(), cLowHigh.getOutput(),
                                                                 cLowFreqFusionDecoder.GetWindow(), true, 0, 0, 0, 1))
      return false;

Agora resta comprimir os dados com a camada convolucional de projeção.

   if(!cProjection.FeedForward(cLowHigh.AsObject()))
      return false;
//---
   return CNeuronBaseOCL::feedForward(cProjection.AsObject());
  }

E enviar o resultado ao método homônimo da classe-pai para gerar a representação final do estado do ambiente analisado.

A próxima etapa do nosso trabalho é a organização dos processos de propagação reversa, fundamentais para o treinamento do modelo. Tradicionalmente, começamos esse processo com o desenvolvimento do algoritmo de distribuição do gradiente de erro no método calcInputGradients. Nesse método, organizamos o fluxo de informação seguindo exatamente o algoritmo da propagação para frente, mas no sentido inverso.

bool CNeuronMultitaskStockformer::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
   if(!prevLayer)
      return false;

Nos parâmetros do método, recebemos um ponteiro para o objeto de dados brutos, cujo buffer deverá receber o gradiente de erro correspondente à influência dos dados brutos no resultado final do modelo. No corpo do método, verificamos a validade do ponteiro recebido. Pois, caso contrário, a transmissão dos dados se torna inviável.

Primeiro, o gradiente de erro é transmitido ao nível da camada convolucional de projeção, utilizando os recursos da classe-pai. Depois, ele é direcionado à camada de soma dos dados provenientes dos dois módulos de atenção do decodificador de duas frequências.

   if(!CNeuronBaseOCL::calcInputGradients(cProjection.AsObject()))
      return false;
   if(!cLowHigh.calcHiddenGradients(cProjection.AsObject()))
      return false;

Vale lembrar que, ao inicializar o objeto de integração, realizamos a substituição dos ponteiros para os buffers de gradientes de erro utilizados pelos módulos de atenção do decodificador e pela camada de soma de seus resultados. Essa decisão garante que todo o gradiente de erro transmitido à camada de soma seja completamente encaminhado aos respectivos módulos de atenção. Isso nos permite passar diretamente à distribuição dos gradientes pelos módulos de atenção do decodificador.

No entanto, é importante destacar que os dados da componente de baixa frequência são usados simultaneamente por ambos os blocos de atenção. Isso exige que o gradiente de erro seja obtido a partir de dois fluxos de informação. Primeiro, realizamos a operação de distribuição do gradiente de erro pelo módulo Self-Attention.

//--- Dual-Frequency Fusion Decoder
   if(!cLowFreqGraphAttention.calcHiddenGradients(cLowFreqFusionDecoder.AsObject()))
      return false;

Em seguida, fazemos uma substituição temporária do ponteiro para o buffer de gradientes de erro do módulo Self-Attention por um buffer livre de tamanho equivalente e executamos as operações do método de distribuição de gradientes do módulo Cross-Attention.

   CBufferFloat *grad = cLowFreqGraphAttention.getGradient();
   if(!cLowFreqGraphAttention.SetGradient(cLowFreqGraphAttention.getPrevOutput(), false) ||
      !cLowFreqGraphAttention.calcHiddenGradients(cLowHighFreqFusionDecoder.AsObject(),
            cHighFreqGraphAttention.getOutput(),
            cHighFreqGraphAttention.getGradient(),
            (ENUM_ACTIVATION)cHighFreqGraphAttention.Activation()) ||
      !SumAndNormilize(grad, cLowFreqGraphAttention.getGradient(), grad, 1, false, 0, 0, 0, 1) ||
      !cLowFreqGraphAttention.SetGradient(grad, false))
      return false;

Depois, somamos os dados dos dois fluxos de informação e restauramos os ponteiros para os buffers de dados ao estado original.

Neste ponto, já distribuímos o gradiente de erro até os componentes de alta e baixa frequência, no nível dos resultados do codificador espaço-temporal de duas frequências. Em seguida, distribuímos o gradiente de forma sequencial pelos objetos das duas trilhas independentes. Primeiro, pela trilha da componente de baixa frequência.

//--- Dual-Frequency Spatiotemporal Encoder
//--- Low Frequency Encoder
   if(!cLowFreqPE.calcHiddenGradients(cLowFreqGraphAttention.AsObject()))
      return false;
   if(!cTemporalAttention.calcHiddenGradients(cLowFreqPE.AsObject()))
      return false;
   if(!cLowFreqSignal.calcHiddenGradients(cTemporalAttention.AsObject()))
      return false;

Depois, pela de alta frequência.

//--- High Frequency Encoder
   if(!cHighFreqPE.calcHiddenGradients(cHighFreqGraphAttention.AsObject()))
      return false;
   if(!cDilatedCasualConvolution.calcHiddenGradients(cHighFreqPE.AsObject()))
      return false;
   if(!cHighFreqSignal.calcHiddenGradients(cDilatedCasualConvolution.AsObject()))
      return false;

Os gradientes de erro obtidos das duas trilhas são concatenados em um único tensor no buffer de gradientes do módulo de decomposição dos dados analisados.

//--- Decoupling Flow
   if(!Concat(cLowFreqSignal.getGradient(), cHighFreqSignal.getGradient(),
              cDecouplingFlow.getGradient(), cDecouplingFlow.GetFilters(),
              cDecouplingFlow.GetFilters(), cDecouplingFlow.GetUnits()*cDecouplingFlow.GetVariables()))
      return false;
   if(!prevLayer.calcHiddenGradients(cDecouplingFlow.AsObject()))
      return false;
//---
   return true;
  }

E são repassados até o nível dos dados brutos, com a chamada ao método homônimo do módulo de decomposição. Encerramos, então, o método retornando o resultado lógico das operações para o programa que o chamou.

As operações do método de otimização dos parâmetros do modelo updateInputWeights seguem a mesma ordem, mas apenas para objetos que contêm parâmetros treináveis. Por isso, sugiro deixar esse método para estudo individual. O código completo do objeto de integração e de todos os seus métodos está disponível no anexo.

Com isso, encerramos a análise dos algoritmos de implementação do framework Multitask-Stockformer e passamos à próxima etapa do nosso trabalho, que é a integração das abordagens implementadas à arquitetura dos modelos treináveis.


Arquitetura dos modelos

As abordagens implementadas do framework Multitask-Stockformer são integradas ao modelo de codificador do estado do ambiente. Vale dizer que, graças ao uso do objeto complexo que representa o framework Multitask-Stockformer, a arquitetura do modelo ficou bastante compacta e contém apenas 3 camadas. Começamos, como de costume, com uma camada de dados brutos e uma de normalização em lote.

bool CreateEncoderDescriptions(CArrayObj *&encoder)
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
//--- 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;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Essas camadas realizam o pré-processamento dos dados "crus" vindos do ambiente. Logo após, temos a nova camada que implementa as abordagens do framework Multitask-Stockformer.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMultitaskStockformer;
//--- Windows
     {
      int temp[] = {BarDescr, 10, LatentCount}; //Window, Filters, Output
      if(ArrayCopy(descr.windows, temp) < int(temp.Size()))
         return false;
     }
   descr.count = HistoryBars;
   descr.window_out = 32;
   descr.step = 4;                              // Heads
   descr.layers = 3;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

Em nosso experimento, utilizamos 10 filtros wavelet. Cada módulo de atenção trabalha com 4 cabeças e contém 3 camadas internas.

Os resultados do codificador do estado do ambiente são utilizados pelos modelos Актера, para tomar decisões de negociação, e Критика, que avalia as operações geradas pelo Ator. A arquitetura desses modelos foi aproveitada de trabalhos anteriores. Neles também buscamos os programas de interação com o ambiente e de treinamento dos modelos. A descrição completa da arquitetura dos modelos e o código dos programas usados neste artigo estão disponíveis no anexo. Agora passamos à etapa final, verificamos a eficácia das soluções implementadas com dados históricos reais.


Testes

Ao longo dos dois artigos, realizamos um extenso trabalho de implementação das abordagens propostas pelos autores do framework Multitask-Stockformer usando MQL5. E chegou o momento mais aguardado, o de testar a eficácia das abordagens implementadas com dados históricos reais.

Vale destacar que estamos avaliando as abordagens *implementadas*, e não o framework Multitask-Stockformer em sua forma original. Como alterações foram feitas no algoritmo original do framework durante a implementação,

Durante os testes, os modelos foram treinados com dados históricos de todo o ano de 2023 do instrumento financeiro EURUSD, no timeframe H1. Todos os parâmetros dos indicadores analisados foram utilizados conforme os valores padrão.

Na primeira etapa do treinamento, utilizamos um conjunto de dados de treino coletado em pesquisas anteriores. Posteriormente, a amostra de treino foi atualizada periodicamente para se adaptar à política atual do АктераApós vários ciclos de treinamento e atualização do conjunto, foi obtida uma política que demonstrou lucratividade tanto nos dados de treino quanto nos dados de teste.

O teste da política treinada foi realizado com dados históricos de janeiro de 2024, mantendo todos os demais parâmetros. Os resultados dos testes são apresentados a seguir.

Durante o período de teste, o modelo realizou 19 operações de trade, das quais 10 foram encerradas com lucro. O que representa pouco mais de 50%. Entretanto, graças ao valor médio das operações lucrativas ser superior ao das posições perdedoras, o modelo finalizou o período de teste com lucro, registrando um profit factor de 1.45.


O gráfico com os horários de abertura das posições chama atenção. Observamos que quase metade das posições foi aberta durante a sessão americana. E praticamente não houve operações durante os momentos de maior volatilidade do instrumento.


Conclusão

Conhecemos o framework Multitask-Stockformer, que representa um modelo inovador de seleção de ações, combinando wavelet transform discreta com módulos multitarefa de Self-Attention. Essa abordagem abrangente permite identificar características temporais e frequenciais dos dados de mercado, garantindo ao mesmo tempo um modelo preciso das interações complexas entre os fatores analisados.

Na parte prática, implementamos nossa própria visão das abordagens do framework usando MQL5. Integramos essas abordagens aos modelos e os treinamos com dados históricos reais. Os modelos treinados foram testados no testador de estratégias do MetaTrader 5. Os resultados dos nossos experimentos demonstram o potencial das soluções implementadas. No entanto, antes de utilizar essas soluções em negociações reais, é necessário treinar o modelo com um conjunto de dados de treino mais representativo, seguido de testes completos e rigorosos.


Referências


Programas utilizados no artigo

# Nome Tipo Descrição
1 Research.mq5 Expert Advisor EA para coleta de exemplos
2 ResearchRealORL.mq5
Expert Advisor
EA para coleta de exemplos usando o método Real-ORL
3 Study.mq5 Expert Advisor EA para 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 da arquitetura dos modelos
6 NeuroNet.mqh Biblioteca de Classe Biblioteca de classes para criação de redes neurais
7 NeuroNet.cl Biblioteca Biblioteca de código para programa OpenCL


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

Arquivos anexados |
MQL5.zip (2279.09 KB)
Do básico ao intermediário: Ponteiro para função Do básico ao intermediário: Ponteiro para função
Você provavelmente já deve ter ouvido falar em ponteiro. Isto quando o assunto é programação. Mas você sabia que podemos fazer uso deste tipo de dado aqui no MQL5? Isto claro, de uma forma a não perder o controle ou gerar coisas bizarras durante a execução do código. Porém, sendo um recurso com uso muito específico e voltado para certos tipos de atividade. É difícil ver alguém falando sobre o que seria de fato um ponteiro e como usar eles no MQL5.
Indicador de força e direção da tendência em barras 3D Indicador de força e direção da tendência em barras 3D
Vamos considerar uma nova abordagem para analisar tendências de mercado, baseada em visualização tridimensional e análise tensora da microestrutura do mercado.
Simulação de mercado (Parte 24): Iniciando o SQL (VII) Simulação de mercado (Parte 24): Iniciando o SQL (VII)
No artigo anterior terminamos de fazer as devidas apresentações sobre o SQL. Então o que eu havia me proposto a mostrar e explicar, sobre SQL, ao meu ver, foi devidamente explicado. Isto para que todos, que vierem a ver o sistema de replay / simulador, sendo construído. Consigam no mínimo terem alguma noção do que pode estar se passando ali. Devido ao fato, de que não faz sentido, programar diversas coisas, que podem ser perfeitamente cobertas pelo SQL.
Simulação de mercado (Parte 23): Iniciando o SQL (VI) Simulação de mercado (Parte 23): Iniciando o SQL (VI)
Neste artigo exploremos como fazer a visualização, e por consequência entender como um banco de dados está estruturado. Isto foi feito, ao observarmos o diagrama interno do banco de dados. Mesmo que este tipo de coisa, pareça ser algo desnecessário. Pode ser algo bastante valido, se você pretende de fato se tornar um administrador de bancos de dados. E sim, existem pessoas que, vivem de fazer manutenção, e criação de bancos de dados.