Redes neurais em trading: Previsão de séries temporais com o auxílio da decomposição modal adaptativa (Conclusão)
Introdução
No artigo anterior, conhecemos o framework ACEFormer, que representa um modelo de previsão de séries temporais especialmente adaptado às particularidades dos mercados financeiros. Foram analisados os princípios básicos de funcionamento da atenção probabilística, os algoritmos de sua implementação no nível de um programa OpenCL e as formas de aumentar a eficiência computacional, mantendo a precisão das previsões. No entanto, ficou fora de cena uma questão não menos importante, que é como todos esses mecanismos são integrados ao programa principal e como garantir uma conexão confiável entre o módulo computacional e a lógica do algoritmo de trading.
Neste artigo, daremos continuidade à implementação das abordagens propostas pelos autores do framework ACEFormer. A atenção principal será dedicada à construção dos algoritmos no lado do programa principal. Mas antes de passar à implementação técnica, vamos relembrar em que consiste a essência e a força do framework ACEFormer. Sua base é o algoritmo ACEEMD (Alias Complete Ensemble Empirical Mode Decomposition with Adaptive Noise), voltado para a eliminação de ruídos em séries temporais financeiras. O ACEEMD resolve o problema do efeito de borda e permite preservar os principais pontos de reversão no gráfico, sem perder informações importantes durante a suavização. Uma atenção especial é dada à primeira moda (IMF), cuja remoção permite eliminar o ruído de alta frequência sem suprimir excessivamente o sinal útil.
Na saída, obtemos uma representação modal adaptativa da série temporal, que se torna os dados brutos para o módulo de destilação, construído com base em uma arquitetura transformer com atenção probabilística. Essa abordagem permite não apenas filtrar o ruído de mercado, mas também concentrar-se nos trechos realmente significativos dos dados históricos, aumentando a precisão das previsões em condições de alta volatilidade e estocasticidade do mercado.
Em seguida, as representações processadas são encaminhadas para o bloco clássico de Self-Attention, o que permite considerar adicionalmente o contexto global da sequência original e aumentar a coerência entre diferentes intervalos de tempo. Essa combinação de atenção localizada e global fornece ao modelo a capacidade de focar simultaneamente tanto em reversões bruscas quanto em tendências estáveis.
O elemento final da arquitetura é a cabeça de previsão totalmente conectada, que transforma a representação de alto nível em um valor numérico específico.
O ACEFormer combina em si os pontos fortes de dois mundos, a robustez da decomposição modal empírica e a flexibilidade da atenção profunda. Ao mesmo tempo, o modelo permanece suficientemente compacto para aplicação em tempo real em terminais de usuários e não exige recursos computacionais excessivos. Graças à arquitetura modular e às possibilidades de processamento paralelo, ACEFormer se adapta facilmente a diferentes estratégias de trading, desde modelos impulsivos de curto prazo até sistemas de análise posicional de médio prazo.
A visualização autoral do framework ACEFormer é apresentada a seguir.

Construção do objeto de atenção probabilística
Após a análise da implementação dos mecanismos de atenção probabilística no lado do programa OpenCL, passamos para a próxima etapa, que é a criação do módulo de alto nível correspondente no lado do programa principal. Para isso, criamos um objeto especializado CNeuronMHProbAttention, no qual é estruturado o algoritmo completo de atenção probabilística.
Durante sua criação, herdamos da classe CResidualConv, cuja estrutura já implementa a arquitetura de duas camadas convolucionais sequenciais com conexões residuais. Isso nos permite concentrar exclusivamente na implementação da lógica da atenção probabilística, sem afetar os mecanismos de propagação para frente (Feed-Forward), que são realizados pelos recursos da classe pai. Dessa forma, alcança-se um alto grau de modularidade, bem como conveniência de integração com outros componentes do modelo.
A estrutura da nova classe é apresentada a seguir.
class CNeuronMHProbAttention : public CResidualConv { protected: uint iWindow; uint iWindowKey; uint iHeads; uint iUnits; uint iTopKQuerys; uint iRandomKeys; int ibScore; //--- CNeuronConvOCL cQKV; CNeuronBaseOCL cQ; CNeuronBaseOCL cKV; CNeuronBaseOCL cRandomK; CNeuronBaseOCL cMHAttentionOut; CNeuronConvOCL cPooling; CNeuronTransposeOCL cTranspose[2]; CNeuronConvOCL cScaling; //--- virtual bool RandomKeys(CBufferFloat* indexes, int random, int units, int heads); virtual bool QueryImportance(void); virtual bool TopKIndexes(void); //--- virtual bool AttentionOut(void); virtual bool AttentionInsideGradients(void); //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *prevLayer) override; public: CNeuronMHProbAttention(void) {}; ~CNeuronMHProbAttention(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint units_count, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) const { return defNeuronMHProbAttention; } //--- 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 da classe CNeuronMHProbAttention reflete a implementação passo a passo do mecanismo de atenção probabilística, começando pela geração de chaves aleatórias e pelo cálculo da relevância das consultas, e finalizando com a propagação reversa do erro. Cada componente é implementado na forma de um nó separado, garantindo uma clara separação de responsabilidades e a possibilidade de ajustes finos.
Do ponto de vista arquitetural, a principal característica da nova classe CNeuronMHProbAttention é a declaração estática de todos os objetos internos. Essa decisão não apenas simplifica o gerenciamento de memória, como também permite evitar a alocação dinâmica de recursos durante a criação da instância da classe. Como resultado, o construtor e o destrutor da classe permanecem vazios, o que aumenta a confiabilidade e a previsibilidade do comportamento do objeto durante sua criação e destruição.
Toda a inicialização necessária dos objetos internos e das variáveis é realizada de forma centralizada no método Init, que desempenha o papel de um tipo de construtor, sendo responsável pela montagem correta de todos os componentes, pela configuração dos parâmetros e pela alocação dos recursos necessários.
bool CNeuronMHProbAttention::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint units_count, ENUM_OPTIMIZATION optimization_type, uint batch ) { if(!CResidualConv::Init(numOutputs, myIndex, open_cl, window, window, units_count, optimization_type, batch)) return false;
Como nosso novo objeto é criado com base em CResidualConv, uma classe que implementa uma arquitetura convolucional com conexões residuais, iniciamos o algoritmo do método de inicialização com a chamada do método homônimo da classe pai. Isso permite obter imediatamente uma infraestrutura pronta: camadas convolucionais, conexões residuais e inicialização de buffers. Dessa forma, não desperdiçamos recursos implementando o que é óbvio e nos concentramos na lógica única da atenção.
Em seguida, passamos à inicialização dos parâmetros-chave que determinam o comportamento do mecanismo de atenção.
iWindow = window; iWindowKey = MathMax(5, window_key); iHeads = MathMax(1, heads); iUnits = units_count; iTopKQuerys = int(MathMin(5 * MathMax(MathLog(iUnits),1), iUnits)); iRandomKeys = int(MathMin(5 * MathMax(MathLog(iUnits),1), iUnits));
Aqui, além dos já conhecidos, observamos 2 novas variáveis relacionadas ao mecanismo de atenção probabilística:
- iTopKQuerys — quantidade das Consultas mais significativas selecionadas para a análise subsequente;
- iRandomKeys — número de Chaves aleatórias utilizadas no processo de seleção das Consultas mais significativas.
Com o objetivo de preservar a escalabilidade, na definição dos valores das variáveis indicadas é utilizada uma função logarítmica da extensão total da sequência analisada.
Após o armazenamento dos parâmetros do objeto, inicia-se a montagem gradual de todos os elementos internos responsáveis pela implementação da atenção. Cada objeto é inicializado separadamente, em uma sequência estritamente definida. O primeiro a ser inicializado é o cQKV, uma camada convolucional que forma simultaneamente três entidades: Q (Query), K (Key) e V (Value). Ela cria uma representação densa dos dados analisados para todas as cabeças de atenção, codificando-os por meio da função de ativação TANH.
int index = 0; if(!cQKV.Init(0, index, OpenCL, iWindow, iWindow, 3 * iWindowKey * iHeads, iUnits, optimization, iBatch)) return false; cQKV.SetActivationFunction(TANH);
Aqui, vale lembrar que, nos kernels criados anteriormente para a implementação do algoritmo de atenção probabilística, é utilizado um buffer separado para as Consultas. Por isso, em seguida criamos 2 objetos adicionais para a separação dessas entidades.
index++; if(!cQ.Init(0, index, OpenCL, cQKV.Neurons() / 3, optimization, iBatch)) return false; index++; if(!cKV.Init(0, index, OpenCL, 2 * cQ.Neurons(), optimization, iBatch)) return false;
Na sequência, criamos o objeto de armazenamento dos índices das Chaves selecionadas aleatoriamente.
index++; if(!cRandomK.Init(0, index, OpenCL, iHeads * MathMax(iRandomKeys, iTopKQuerys), optimization, iBatch)) return false;
Observe que o tamanho da camada é determinado pelo valor máximo entre as Chaves amostradas e as Consultas mais importantes. A ideia consiste em garantir um volume suficiente de memória para todos os índices possíveis que possam ser necessários durante o processo de cálculo. Não sabemos antecipadamente qual das duas grandezas será maior, a quantidade de Chaves aleatórias (iRandomKeys) ou a quantidade de Consultas mais significativas (iTopKQuerys), portanto utilizamos o máximo entre os dois valores. Em seguida, escalamos o resultado pelo número de cabeças de atenção (iHeads), pois cada cabeça opera de forma independente e necessita de seu próprio conjunto de índices.
Dessa forma, um único objeto cRandomK é utilizado para armazenar os índices das Chaves selecionadas aleatoriamente e das Consultas mais importantes. Isso racionaliza a estrutura do módulo, reduz o número de objetos e simplifica o gerenciamento de memória.
Em seguida, vem a camada de armazenamento dos resultados da atenção multi-head das consultas mais significativas.
index++; if(!cMHAttentionOut.Init(0, index, OpenCL, iTopKQuerys * iHeads * iWindowKey, optimization, iBatch)) return false;
Logo após, a camada de agregação adaptativa do trabalho das cabeças de atenção em uma representação unificada.
index++; if(!cPooling.Init(0, index, OpenCL, iHeads * iWindowKey, iHeads * iWindowKey, iWindow, iTopKQuerys, optimization, iBatch)) return false; cPooling.SetActivationFunction(TANH);
Nesse estágio, o algoritmo já formou as saídas do mecanismo de atenção probabilística. No entanto, é importante enfatizar uma característica crítica: estamos operando com resultados correspondentes apenas às Top-K Queries. Isso significa que o tensor final possui uma dimensionalidade temporal significativamente menor do que a dos dados originais.
Como resultado, surge uma incompatibilidade arquitetural: o bloco de atenção fornece uma representação comprimida, enquanto a estrutura clássica de Self-Attention pressupõe a preservação da dimensionalidade dos dados de entrada e dos resultados, o que é necessário, em particular, para a correta adição das conexões residual, que garantem a estabilidade do treinamento e o suporte ao fluxo de gradientes.
Para eliminar essa contradição, aplicamos a seguinte estratégia:
- Primeiro, rotacionamos a matriz dos resultados da atenção de modo que a estrutura dos dados permita aplicar uma transformação convolucional ao longo do eixo das características.
index++; if(!cTranspose[0].Init(0, index, OpenCL, iTopKQuerys, iWindow, optimization, iBatch)) return false;
- Em seguida, é utilizada a camada convolucional cScaling, cujo objetivo é restaurar a dimensionalidade dos resultados até o comprimento original da sequência. Dessa forma, obtemos um tensor comparável em dimensão à entrada original. É importante notar que o escalonamento ocorre no recorte de características individuais, isto é, cada ponto temporal é restaurado levando em conta a estrutura global da atenção.
index++; if(!cScaling.Init(0, index, OpenCL, iTopKQuerys, iTopKQuerys, iUnits, iWindow, optimization, iBatch)) return false; cScaling.SetActivationFunction(None);
- Após o escalonamento, retornamos os dados à representação original, aplicando novamente a operação de transposição.
if(!cTranspose[1].Init(0, index, OpenCL, iWindow, iUnits, optimization, iBatch)) return false;
Graças a essa sequência de etapas, alcançamos a máxima compatibilidade entre o mecanismo estocástico de atenção e as particularidades arquiteturais do Self-Attention clássico. O modelo preserva tanto a integridade estrutural quanto a alta flexibilidade, assegurando simultaneamente eficiência computacional e estabilidade durante o processo de treinamento.
Uma atenção especial é dedicada à criação do buffer ibScore, no qual são armazenadas as estimativas intermediárias de importância (pesos de atenção) para a propagação reversa do erro. Ele é criado exclusivamente no lado do contexto OpenCL.
ibScore = OpenCL.AddBuffer(sizeof(float) * iTopKQuerys * iUnits * iHeads, CL_MEM_READ_WRITE); if(ibScore < 0) return false; //--- return true; }
Após a inicialização de todos os objetos internos, concluímos a execução do método, retornando previamente o resultado lógico da execução das operações.
Em seguida, passamos para a próxima etapa importante, que é a organização do algoritmo de propagação para frente. No entanto, antes de iniciar o trabalho com o fluxo principal de dados, é necessário realizar uma pequena etapa preparatória.
Como na maioria dos módulos que implementamos, aqui são utilizados métodos nos quais ocorre o enfileiramento para execução dos kernels do programa OpenCL. Os algoritmos empregados neles já são conhecidos do leitor a partir de artigos anteriores, e não faz sentido duplicá-los. Em vez disso, concentraremos a atenção no algoritmo de amostragem dos índices das Chaves, que é implementado no método RandomKeys.
A essência do mecanismo de atenção probabilística consiste em reduzir o número de Chaves para a busca das Consultas mais significativas. Isso diminui a carga computacional e, ao mesmo tempo, adiciona um componente estocástico, o que ajuda a evitar o sobreajuste e melhora a capacidade de generalização do modelo.
O método RandomKeys recebe um ponteiro para o buffer indexes, que deve ser preenchido com os índices de Chaves amostradas. Os parâmetros random, units e heads definem, respectivamente, a quantidade de valores aleatórios, o número total de Chaves disponíveis e o número de cabeças de atenção.
bool CNeuronMHProbAttention::RandomKeys(CBufferFloat *indexes, int random, int units, int heads) { if(!indexes || random > units || indexes.Total() < (random * heads) ) return false;
Inicialmente, realizamos a verificação dos valores recebidos. Se o buffer não estiver definido, se a quantidade de amostras aleatórias exceder o número de Chaves disponíveis ou se o buffer for de tamanho insuficiente, o método retorna imediatamente false.
Após a passagem bem-sucedida pelo bloco de controles, criamos uma matriz random × heads, em que cada coluna corresponde a uma cabeça de atenção e as linhas correspondem às posições de Chaves amostradas.
matrix<float> ind = matrix<float>::Zeros(random, heads);
Em seguida, são possíveis dois cenários. Se não for necessário realizar a amostragem (random == units), preenchemos a matriz com números sequenciais, isto é, utilizamos todas as Chaves disponíveis.
if(random == units) { for(int r = 0; r < random; r++) { for(int c = 0; c < heads; c++) ind[r, c] = (float)r; } }
Quando o número de Chaves selecionadas (random) é menor que o número total disponível (units), surge o risco de obter uma amostra não representativa. A forma mais simples consiste em simplesmente amostrar valores aleatórios de todo o intervalo, pode levar a repetições, agrupamentos locais ou, ao contrário, a lacunas em determinadas zonas do intervalo. Como resultado, o modelo pode não observar partes importantes dos dados, e isso afetará negativamente o seu treinamento.
Para evitar esse problema, na nossa implementação é utilizada a amostragem uniformemente estratificada. Primeiro, dividimos todo o intervalo em segmentos iguais.
else { double step = double(units) / random;
Esse passo pode ser fracionário, o que é perfeitamente aceitável, pois o tamanho da amostra raramente é múltiplo do comprimento de toda a sequência.
Para cada estrato (segmento do intervalo), um valor é selecionado aleatoriamente dentro de seus limites.
for(int r = 0; r < random; r++) { for(int c = 0; c < heads; c++) ind[r, c] = float(int((r + MathRand() / 32767.0) * step)); } }
Dessa forma, para cada cabeça de atenção criamos o seu próprio conjunto de índices, mas todos eles cobrem o intervalo completo, sem deslocamentos ou concentrações excessivas em zonas específicas.
Após a geração da amostra, ela é gravada no buffer indexes, de onde será posteriormente utilizada no programa OpenCL.
if(!indexes.AssignArray(ind) || !indexes.BufferWrite()) return false; //--- return true; }
A abordagem implementada possui uma série de vantagens evidentes. Em primeiro lugar, garante-se uma cobertura uniforme de todo o espaço de características. Em segundo lugar, elimina-se a possibilidade de valores fora do intervalo permitido e minimiza-se a probabilidade de repetições. Como resultado, aumenta a representatividade geral da amostra e, consequentemente, o modelo recebe uma visão mais completa e equilibrada dos dados de entrada. Isso é especialmente importante ao trabalhar com séries temporais financeiras, onde tanto os períodos calmos quanto os trechos com mudanças bruscas são igualmente relevantes.
Após a conclusão do trabalho preparatório, passamos para a etapa principal, isto é, a organização do método de propagação para frente feedForward, no qual é implementado o algoritmo completo de atenção probabilística.
bool CNeuronMHProbAttention::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cQKV.FeedForward(NeuronOCL)) return false;
O primeiro passo consiste na chamada do método FeedForward da camada convolucional cQKV, que, como o próprio nome indica, é responsável pela formação do tensor concatenado de Consultas, Chaves e Valores. Na saída, forma-se um único tensor, no qual as três entidades seguem uma após a outra.
No entanto, para o trabalho subsequente, precisamos decompor esse tensor em dois: um para Consultas (Q) e outro para Chaves e Valores (K e V). Para isso, utiliza-se o método DeConcat, que extrai do agrupamento QKV comum as partes correspondentes e as encaminha para cQ e cKV.
if(!DeConcat(cQ.getOutput(), cKV.getOutput(), cQKV.getOutput(), iWindowKey * iHeads, 2 * iWindowKey * iHeads, iUnits)) return false;
Em seguida, é executado o algoritmo de amostragem dos índices das Chaves no método RandomKeys, analisado anteriormente.
if(!RandomKeys(cRandomK.getOutput(), iRandomKeys, iUnits, iHeads)) return false;
O próximo passo é a chamada de dois métodos: QueryImportance e TopKIndexes. Eles realizam o enfileiramento para execução dos kernels de ranqueamento da importância das Consultas e a seleção das mais informativas.
if(!QueryImportance() || !TopKIndexes()) return false;
Depois disso, é executado o núcleo principal do algoritmo, o método AttentionOut. Nele ocorre o enfileiramento para execução do kernel de atenção para as Consultas selecionadas.
if(!AttentionOut()) return false;
Os resultados da atenção multi-head são agregados pela camada convolucional cPooling. Ela também utiliza a função de ativação TANH, que aumenta a expressividade do sinal de saída.
if(!cPooling.FeedForward(cMHAttentionOut.AsObject())) return false;
Os resultados obtidos são escalonados com o objetivo de retornar o tensor a uma dimensionalidade compatível com a sequência original, para que possamos utilizar corretamente as conexões residuais.
if(!cTranspose[0].FeedForward(cPooling.AsObject())) return false; if(!cScaling.FeedForward(cTranspose[0].AsObject())) return false; if(!cTranspose[1].FeedForward(cScaling.AsObject())) return false;
A ação final do bloco de atenção é a chamada do método SumAndNormilize, que combina os dados escalonados com os dados originais, adicionando a conexão residual e realizando a normalização. Graças a isso, é garantida a estabilidade do fluxo de gradientes e acelera-se a convergência do treinamento.
if(!SumAndNormilize(cTranspose[1].getOutput(), NeuronOCL.getOutput(), cTranspose[1].getOutput(), iWindow, true, 0, 0, 0, 1)) return false;
Por fim, o resultado obtido é encaminhado ao método homônimo da classe pai, que conclui o propagação para frente, retornando o tensor de saída pronto para ser passado ao próximo módulo da rede neural.
return CResidualConv::feedForward(cTranspose[1].AsObject()); }
Dessa forma, toda a lógica da atenção probabilística é implementada dentro do objeto CNeuronMHProbAttention. Essa abordagem garante modularidade, reutilização e facilidade de escalonamento do modelo.
Após a conclusão do propagação para frente, inicia-se a etapa criticamente importante do treinamento, a propagação reversa do gradiente do erro para otimização dos parâmetros do modelo visando sua redução. É exatamente nesse estágio que se calcula a contribuição de cada elemento para o erro final e, com base nessa contribuição, os pesos da rede são ajustados. No caso da atenção probabilística implementada na classe CNeuronMHProbAttention, esse processo adquire uma complexidade especial, pois apenas os gradientes referentes às Consultas mais significativas selecionadas são propagados. Isso exige uma organização rigorosa do processo e o estrito cumprimento da lógica de cálculo.
bool CNeuronMHProbAttention::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(!prevLayer) return false;
O método calcInputGradients inicia-se com uma verificação básica da validade do ponteiro para o objeto da camada anterior, recebido nos parâmetros do método. Sem a existência de um ponteiro válido, é impossível continuar a propagação do gradiente do erro para baixo na rede.
Em seguida, é realizada uma operação preparatória importante, a zeragem forçada do buffer de gradientes das Consultas.
if(!cQ.getGradient().Fill(0)) return false;
Esse passo é obrigatório, pois o próprio algoritmo de atenção atua apenas sobre um subconjunto das Consultas, aquelas mais relevantes, selecionadas durante o propagação para frente. Os demais elementos na matriz de gradientes podem conter valores antigos ou aleatórios, que agora se tornam irrelevantes e podem distorcer a atualização dos parâmetros do modelo. Por isso, toda a matriz é previamente limpa e somente depois disso é possível iniciar o acúmulo cuidadoso dos valores corretos.
Na sequência, é chamado o método homônimo da classe pai. Essa operação propaga o erro através do bloco FeedForward. Assim, delegamos parte do propagação reversa a uma lógica já testada e validada.
if(!CResidualConv::calcInputGradients(cTranspose[1].AsObject())) return false;
Os próximos passos são direcionados à propagação sequencial do gradiente do erro por todos os blocos internos da atenção probabilística, reproduzindo exatamente a estrutura do propagação para frente, porém na ordem inversa. Primeiramente, os gradientes passam pelo bloco de escalonamento.
if(!cScaling.calcHiddenGradients(cTranspose[1].AsObject())) return false; if(!cTranspose[0].calcHiddenGradients(cScaling.AsObject())) return false;
Depois, pela camada de agregação dos resultados da atenção multi-head.
if(!cPooling.calcHiddenGradients(cTranspose[0].AsObject())) return false; if(!cMHAttentionOut.calcHiddenGradients(cPooling.AsObject())) return false;
Todas essas etapas são necessárias para a reconstrução cuidadosa dos gradientes, pois no propagação para frente foram realizadas transformações na forma e na estrutura dos dados. Agora, é preciso desfazer essas ações no sentido inverso.
Uma atenção especial é dedicada ao método AttentionInsideGradients, no qual ocorre a propagação dos gradientes dentro do próprio mecanismo de atenção probabilística. É exatamente aqui que o erro é cuidadosamente transmitido pelas Consultas selecionadas. Este é o elemento mais sensível de todo o propagação reversa: ele permite preservar a precisão e a correção na atualização dos pesos, mesmo considerando que apenas uma parte da informação participa do processo.
if(!AttentionInsideGradients()) return false;
Após isso, segue-se o procedimento de unificação dos gradientes, é realizada a fusão dos gradientes Consultas, Chaves e Valores em um único tensor QKV, que reflete a estrutura original formada na etapa de propagação para frente. Caso uma função de ativação tenha sido aplicada no bloco cQKV, é feita a correção dos gradientes por meio do cálculo da derivada dessa função. Isso permite levar em conta sua influência sobre os sinais transmitidos e garante a precisão matemática de todo o processo.
if(!Concat(cQ.getGradient(), cKV.getGradient(), cQKV.getGradient(), iWindowKey * iHeads, 2 * iWindowKey * iHeads, iUnits)) return false; if(cQKV.Activation() != None) if(!DeActivation(cQKV.getOutput(), cQKV.getGradient(), cQKV.getGradient(), cQKV.Activation())) return false;
Os gradientes obtidos nessa etapa são transmitidos para a camada precedente.
if(!prevLayer.calcHiddenGradients(cQKV.AsObject())) return false;
Agora resta o gradiente do erro no fluxo informacional das conexões residuais. Aqui, primeiro ajustamos o gradiente do erro desse fluxo pela derivada da função de ativação da camada dos dados originais e, em seguida, somamos os valores das duas ramificações principais.
if(prevLayer.Activation() != None) if(!DeActivation(prevLayer.getOutput(), cTranspose[1].getGradient(), cTranspose[1].getGradient(), prevLayer.Activation())) return false; if(!SumAndNormilize(cTranspose[1].getGradient(), prevLayer.getGradient(), prevLayer.getGradient(), iWindow, false, 0, 0, 0, 1)) return false; //--- return true; }
Dessa forma, o método calcInputGradients implementa um algoritmo completo e consistente de propagação reversa do gradiente do erro para o módulo de atenção probabilística. Ele preserva alta precisão e coerência em todas as transformações de gradiente, o que permite utilizar o módulo de atenção probabilística como parte de modelos complexos, sem risco de perda de informação ou distorção do sinal de treinamento.
A etapa final é a atualização dos parâmetros do modelo, é implementada no método updateInputWeights. Como todos os parâmetros treináveis estão localizados nos objetos internos, toda a lógica se resume à chamada sequencial dos métodos homônimos desses componentes. Devido à simplicidade da implementação, não analisamos esse método em detalhes no âmbito deste artigo. O código completo do objeto de atenção probabilística, incluindo o método updateInputWeights, é apresentado no anexo.
Arquitetura do modelo
Encerrando a análise da implementação dos componentes básicos, vale a pena nos determos na arquitetura geral do sistema treinável. Assim como em diversos trabalhos anteriores, seguimos um esquema hierárquico de treinamento baseado no framework Actor–Director–Critic. Essa abordagem permite separar de forma flexível as funções de processamento do ambiente, tomada de decisões e avaliação das ações, o que é especialmente importante em condições de uma dinâmica de mercado complexa e instável.
Dentro desse esquema, treinamos quatro modelos. O primeiro deles (Codificador do estado do ambiente) desempenha um papel fundamental. É ele que responde pela análise profunda da situação de mercado e pela construção de uma representação latente compacta, porém o mais informativa possível. Nesse modelo, são utilizadas as abordagens do framework ACEFormer, no contexto da tarefa de previsão dos estados futuros do ambiente em um horizonte de planejamento definido. Isso permite formar uma representação de mercado estável e dinamicamente significativa, sobre a qual as ações do Agente se baseiam posteriormente.
O segundo modelo (Ator) opera simultaneamente com duas fontes de informação: o estado atual da conta e as posições abertas, por um lado, e a representação latente do ambiente obtida a partir do Codificador, por outro. Ator é treinado para selecionar ações que correspondam ao máximo à situação atual de trading, e é justamente o seu comportamento que se torna objeto de avaliação por parte dos outros dois modelos.
Os modelos Diretor e Crítico desempenham a função de avaliar as ações escolhidas pelo Ator. Nessa abordagem, Diretor introduz elementos de filtragem binária rígida, ajudando a evitar decisões estrategicamente desfavoráveis. Já Crítico fornece um retorno mais suave e quantitativo, determinando o grau de utilidade das ações realizadas. O trabalho conjunto desses dois componentes permite formar uma estratégia de comportamento do agente no mercado que seja estável e adaptativa.
No âmbito deste artigo, concentraremos a atenção na arquitetura do modelo Codificador do estado do ambiente, pois é justamente nela que são implementados os principais componentes do ACEFormer. A estrutura dos demais modelos, de modo geral, preserva os princípios arquiteturais apresentados em materiais anteriores.
Como camada de dados de entrada, como de costume, utilizamos uma camada totalmente conectada de tamanho suficiente.
//--- 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; }
Aqui, vale observar que não adicionamos valores nulos para os elementos a serem previstos, como foi sugerido pelos autores do framework ACEFormer.
Como etapa inicial do processamento dos dados de entrada, o modelo utiliza um módulo especializado de pré-processamento de características. Ele é construído segundo um esquema que combina normalização com a adição de ruído estocástico e uma transformação convolucional da dimensionalidade das características.
O primeiro passo consiste na normalização dos dados de entrada. Isso permite eliminar distorções de escala entre diferentes características e estabilizar o treinamento do modelo. No entanto, para reforçar a capacidade de generalização da rede e aumentar sua robustez a flutuações locais dos dados, durante a etapa de normalização é introduzido adicionalmente um nível controlado de ruído aleatório. Esse elemento desempenha o papel de regularizador e simula a variabilidade do ambiente de mercado, ajudando o modelo a se adaptar melhor a situações instáveis ou anteriormente não observadas.
Em seguida, os dados normalizados e com ruído são encaminhados para uma camada convolucional, cuja tarefa é ajustar a dimensionalidade das características de entrada ao formato exigido, em conformidade com a arquitetura dos componentes subsequentes. Tal operação permite, por um lado, reduzir dados redundantes e, por outro, destacar as dependências espaciais mais relevantes entre as características dentro de uma janela temporal definida.
//--- 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; } //--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; prev_count = descr.count = HistoryBars; descr.window = BarDescr; descr.step = BarDescr; int prev_out = descr.window_out = NSkills; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Após o pré-processamento, os dados são transpostos para que se passe da análise temporal para uma análise independente das características. Isso permite que o módulo de atenção destaque dependências significativas dentro de cada característica, melhorando a qualidade da representação e o treinamento do modelo.
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; prev_count = descr.window = prev_out; prev_out = descr.count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Em seguida, avançamos para o bloco de destilação. Na arquitetura do Codificador ele implementa a ideia central do framework ACEFormer, a extração das características mais informativas com a subsequente agregação da informação relevante. Sua base é o módulo de atenção probabilística, que realiza o primeiro processamento dos dados, concentrando a atenção nas características mais importantes. Isso permite que o modelo se foque nos aspectos mais significativos do sinal de entrada.
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMHProbAttention; descr.count = prev_count; descr.window = prev_out; descr.step = 4; descr.window_out = 32; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Na sequência do bloco de atenção, vem uma camada convolucional que reduz a dimensão temporal das características. Ela é utilizada para comprimir a informação e formar uma representação compacta, sem detalhes excessivos.
//--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; prev_out = descr.count = (prev_out + 1) / 2; descr.window = 2; descr.step = 2; int filt=descr.window_out = 5; descr.layers = prev_count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Em seguida, é aplicado o módulo de agregação, que atua como um max-pooling: ele seleciona os sinais mais pronunciados de cada janela, garantindo a preservação das características dominantes.
//--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronProofOCL; descr.count = prev_count*prev_out; descr.window = filt; descr.step = filt; descr.layers = prev_count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
A camada final de normalização estabiliza a distribuição dos dados e acelera o processo de treinamento.
//--- layer 7 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; }
No nosso modelo, são utilizados 3 blocos de destilação sequenciais, com redução independente e gradual da dimensionalidade das séries temporais unitárias de cada característica.
//--- layer 8 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMHProbAttention; descr.count = prev_count; descr.window = prev_out; descr.step = 4; descr.window_out = 32; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 9 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; prev_out = descr.count = (prev_out + 1) / 2; descr.window = 2; descr.step = 2 ; filt=descr.window_out = 3; descr.layers = prev_count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 10 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronProofOCL; descr.count = prev_count*prev_out; descr.window = filt; descr.step = filt; descr.layers = prev_count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 11 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; }
//--- layer 12 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMHProbAttention; descr.count = prev_count; descr.window = prev_out; descr.step = 4; descr.window_out = 32; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 13 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; prev_out = descr.count = (prev_out + 1) / 2; descr.window = 2; descr.step = 2 ; filt=descr.window_out = 3; descr.layers = prev_count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 14 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronProofOCL; descr.count = prev_count*prev_out; descr.window = filt; descr.step = filt; descr.layers = prev_count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 15 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; }
Dessa forma, o bloco de destilação não apenas reduz a dimensionalidade, pois ele seleciona e concentra a essência dos dados originais, formando uma representação latente rica e resistente a ruídos.
Em seguida, é adicionado à arquitetura do Codificador um bloco Self-Attention de duas camadas, cuja tarefa é identificar dependências internas entre as características na representação compactada dos dados. Isso ajuda a capturar conexões ocultas e a sincronizar a informação entre as características espaciais.
//--- layer 16 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronDMHAttention; descr.count = prev_count; descr.window = prev_out; descr.step = 4; descr.layers = 2; descr.window_out = 32; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Graças a esse bloco, o modelo passa a enxergar o quadro de forma mais ampla, não apenas como um conjunto de observações isoladas, mas como um sistema integral de elementos interagindo entre si.
Para a previsão independente de sequências unitárias, a arquitetura do Codificador utiliza dois camadas convolucionais sequenciais. Essa construção permite que o modelo transforme de forma eficiente a representação latente generalizada em previsões numéricas específicas para cada característica-alvo.
A primeira camada convolucional possui um número ampliado de filtros, quatro vezes maior que o número de parâmetros a serem previstos, e aplica a ativação SoftPlus. Isso assegura uma transformação suavizada e restrita ao domínio positivo, contribuindo para a estabilidade do treinamento e a filtragem de ruídos.
//--- layer 17 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = 1; descr.window = prev_out; descr.step = prev_out; prev_out = descr.window_out = 4 * NForecast; descr.layers = prev_count; descr.activation = SoftPlus; if(!encoder.Add(descr)) { delete descr; return false; }
A segunda camada finaliza o processo de decodificação, ajustando o número de saídas exatamente ao número de parâmetros previstos (NForecast) e aplicando a função de ativação TANH. Isso permite obter resultados dentro de um intervalo de valores controlado, o que é especialmente importante ao trabalhar com dados normalizados.
//--- layer 18 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = 1; descr.window = prev_out; descr.step = prev_out; prev_out = descr.window_out = NForecast; descr.layers = prev_count; descr.activation = TANH; if(!encoder.Add(descr)) { delete descr; return false; }
Uma estrutura desse tipo é simples, porém eficaz: ela fornece ao modelo a flexibilidade necessária na formação da previsão, ao mesmo tempo em que preserva a independência da avaliação de cada característica, o que é crítico ao lidar com séries temporais financeiras, nas quais cada parâmetro pode carregar informações únicas.
Na etapa final do funcionamento do Codificador, passamos à interpretação das previsões geradas pelo modelo, isto é, retornamo-las ao espaço dos dados originais, onde elas assumem uma forma significativa.
Primeiro, realiza-se a transposição do tensor. Esse passo permite recolocar o eixo temporal em sua posição habitual e garantir a correspondência entre os passos de tempo e os valores previstos.
//--- layer 19 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; prev_count=descr.window = prev_out; prev_out=descr.count; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Em seguida, é aplicada uma camada convolucional cuja tarefa é ajustar a dimensionalidade das características ao formato correspondente aos dados originais. Essencialmente, trata-se de uma forma de projeção: o modelo comprime ou expande a representação para que ela se torne compatível com o espaço no qual os dados de entrada foram originalmente formados.
//--- layer 20 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = prev_count; descr.window = prev_out; descr.step = prev_out; prev_out = descr.window_out = BarDescr; descr.layers = 1; descr.activation = TANH; if(!encoder.Add(descr)) { delete descr; return false; }
E, por fim, é realizada a normalização reversa, isto é, a conversão dos valores do escala interna normalizada do modelo de volta para grandezas reais, compreensíveis ao ser humano e adequadas para análise ou uso em decisões de trading.
//--- layer 21 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronRevInDenormOCL; descr.count = prev_count * prev_out; descr.layers = 1; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Em conjunto, essas operações garantem a ligação entre a lógica interna do modelo e o mercado real, o passo final que transforma previsões abstratas em resultados práticos.
A arquitetura completa de todos os modelos treináveis é apresentada no anexo.
Teste
Realizamos um trabalho em larga escala de adaptação e implementação do framework ACEFormer no ambiente MQL5. Os principais componentes do framework foram integrados à arquitetura dos modelos treináveis. Agora chegou a etapa mais importante, a verificação da eficácia das soluções em dados históricos reais.
Como amostra de treinamento, foram coletadas passagens utilizando políticas aleatórias de comportamento do agente no testador de estratégias MetaTrader 5, em cotações de um minuto do EURUSD ao longo de 2024. Essa abordagem cobre um amplo espectro de cenários de mercado e aumenta a universalidade do comportamento do modelo.
O treinamento foi realizado em duas etapas. Primeiro, offline, sem atualização da amostra até a estabilização dos erros. Para isso, anexamos ao gráfico o EA Study.mq5. Em seguida, online, no testador de estratégias, utilizando o EA StudyOnline.mq5. Nesta etapa, é realizada uma calibração fina dos modelos em condições o mais próximas possível da realidade.
Para uma avaliação objetiva dos resultados, o teste dos modelos treinados foi realizado em dados históricos fora do período de treinamento (janeiro–março de 2025). Isso exclui o sobreajuste e destaca o valor prático dos resultados obtidos.
Todos os demais parâmetros do ambiente e dos indicadores analisados durante o processo de treinamento e teste permaneceram inalterados, o que permite avaliar exatamente a qualidade da estratégia aprendida.
Os resultados dos testes são apresentados a seguir.

De modo geral, durante o período de teste o modelo obteve lucro, realizando 13 operações de trading. Pouco mais da metade delas foi encerrada com lucro. No entanto, é preciso observar que 13 operações de trading ao longo de 3 meses do período de teste, extremamente pouco.
Uma possível explicação para uma atividade de trading tão baixa pode estar na especificidade do uso da atenção probabilística. Esse mecanismo seleciona apenas as características e consultas mais significativas, o que aumenta a qualidade da generalização, mas pode limitar a sensibilidade do modelo a sinais de trading menos pronunciados.
Além disso, isso pode indicar uma representatividade insuficiente da amostra de treinamento: o modelo simplesmente não se deparou com um número suficiente de situações diversas para agir com confiança em condições semelhantes durante o período de teste. O aumento do volume e da diversidade dos dados pode melhorar a flexibilidade comportamental do agente.
Considerações finais
Conhecemos o framework ACEFormer, que oferece ferramentas eficazes para a extração de características-chave de séries temporais e sua representação compacta. Sua arquitetura combina mecanismos de atenção probabilística, destilação de características e transformação profunda das sequências analisadas, o que o torna especialmente atraente para tarefas de análise de dados financeiros com alto nível de ruído e instabilidade.
Na parte prática, foi implementada uma visão própria dos principais componentes ACEFormer utilizando MQL5. Nós os incorporamos à arquitetura dos modelos treináveis dentro da abordagem Ator–Diretor–Crítico. A atenção principal foi dedicada à construção do modelo Codificador do ambiente, no qual é aplicado o mecanismo de processamento de características proposto pelo framework.
Os resultados dos testes mostraram um desfecho positivo: o modelo obteve lucro no período de teste, confirmando a funcionalidade geral da arquitetura. No entanto, foi identificada uma baixa atividade de trading, o que pode estar relacionado às particularidades da atenção probabilística e a uma amostra de treinamento limitada. Isso abre perspectivas para pesquisas futuras.
Referências
- An End-to-End Structure with Novel Position Mechanism and Improved EMD for Stock Forecasting
- Outros artigos da série
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 offline de modelos |
| 4 | StudyOnline.mq5 | Expert Advisor | EA de treinamento online de modelos |
| 4 | Test.mq5 | Expert Advisor | EA para teste de 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 de programa OpenCL |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/18041
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Replay e Simulação de mercado: Gran Finale
Análise quantitativa de tendências: coletando estatísticas em Python
Do básico ao intermediário: Sobrecarga de operadores (V)
Componente View para tabelas no paradigma MVC em MQL5: elemento gráfico básico
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
Neural Networks in Trading: Time Series Forecasting with Adaptive Modal Decomposition (Redes neurais no comércio: previsão de séries temporais com decomposição modal adaptativa):
Autor: Dmitriy Gizlyk