Redes neurais em trading: Agregação temporal do movimento (Componentes principais)
Introdução
O mercado financeiro, como um organismo vivo, respira, muda e se movimenta. Ele não produz fotografias estáticas, mas ondas. Para compreendê-lo, não basta registrar pontos isolados: é preciso enxergar a trajetória. Não importa apenas saber onde o preço esteve, mas também perceber para onde ele se dirige e como o movimento evoluiu ao longo do caminho. Os métodos modernos de análise ainda reduzem o tempo, com frequência excessiva, a marcas discretas, transformando o fluxo vivo do mercado em uma sequência de quadros imóveis. Mas o preço não salta simplesmente de um ponto para outro: ele flui, acelera, perde força, faz pausas e depois volta a ganhar impulso. Por isso, para prever seu movimento, precisamos trabalhar não com um conjunto de observações estáticas, mas com a própria dinâmica. Com os micromovimentos internos que, juntos, dão origem a uma tendência mais ampla.
No artigo anterior, conhecemos o framework Temporal Motion Aggregation (TMA), criado originalmente para analisar dados de eventos relacionados ao fluxo óptico. Curiosamente, sua proposta se mostrou muito próxima dos problemas encontrados na previsão financeira. A ideia central é tratar o fluxo de informações como uma sequência de eventos conectados por dependências temporais. Cada evento carrega não apenas um valor, mas também um impulso de movimento. Para os autores do estudo original, esses eventos correspondiam a variações de luminosidade registradas por um sensor. Para nós, correspondem a microvariações de preço, picos de volume e mudanças de liquidez. Em ambos os casos, o objetivo é o mesmo: captar a própria dinâmica do movimento e preservar no modelo a continuidade temporal que normalmente se perde no processamento tradicional dos dados.
Os métodos clássicos de análise de séries financeiras tratam o mercado como uma sequência de barras ou candles, medindo preço e volume em intervalos regulares. Mas a atividade do mercado não é uniforme: durante a divulgação de notícias ou em momentos de pânico, episódios inteiros podem se desenrolar em poucos minutos, enquanto, em períodos de baixa atividade, o movimento pode parecer praticamente paralisado. Assim, um modelo baseado em intervalos fixos inevitavelmente perde fases importantes do movimento, como se assistisse a um filme do qual metade dos quadros tivesse sido removida. O TMA propõe outra perspectiva: enxergar o mercado como um fluxo de eventos, no qual o mais importante não é simplesmente o fato de o preço ter mudado, mas a natureza dessa mudança, sua forma, seu ritmo e sua cadência interna.
A base do TMA está na disposição sequencial dos eventos ao longo do tempo. O fluxo é dividido em intervalos curtos e parcialmente sobrepostos, permitindo que o modelo analise a dinâmica em diferentes escalas. Não se trata apenas de dividir os dados em intervalos regulares, mas de uma forma de captar o pulso natural do mercado, identificando onde a atividade se concentra e onde perde intensidade. Em cada fragmento, são extraídos atributos que representam a estrutura interna do movimento: velocidade da variação do preço, força do impulso, volume negociado e direção da microtendência. Como cada segmento, porém, segue seu próprio ritmo temporal, a etapa seguinte consiste em alinhá-los e colocá-los em uma escala comum, para que o modelo consiga comparar e integrar as informações. Essa etapa, chamada de alinhamento linear, atua como uma espécie de tradutor temporal. Ela sincroniza fragmentos com diferentes escalas e densidades, formando uma visão integrada do movimento.
Quando os fragmentos estão alinhados entre si, chega o momento decisivo: a agregação de padrões. Nessa etapa, o TMA identifica formas de movimento persistentes que se manifestam em várias camadas temporais. Em essência, o modelo procura atributos coerentes entre si, isto é, microestruturas recorrentes que, em conjunto, dão origem à tendência efetiva. As oscilações provocadas por ruído são descartadas, os picos aleatórios são atenuados e permanece apenas o essencial: o movimento dotado de uma lógica interna. Se, em tarefas visuais, esse mecanismo permite distinguir a trajetória de um objeto em meio à agitação dos pixels, no contexto financeiro ele revela a estrutura do fluxo de capital, aquela linha invisível ao longo da qual se desloca o interesse dos participantes do mercado.
Essa abordagem oferece vantagens evidentes. Ela se adapta naturalmente à irregularidade do tempo de mercado, sem impor limites artificiais à sua dinâmica. Permite ir além da simples análise barra a barra, examinando os micromovimentos e a coerência entre eles. Também reduz o ruído do sinal, reforçando aquilo que realmente importa, e ajuda o modelo não apenas a estimar a direção, mas a compreender a própria lógica de evolução da tendência. Como resultado, o TMA transforma o fluxo caótico de dados de mercado em uma estrutura dinâmica e ritmada, na qual cada micro-onda tem seu significado e o conjunto revela padrões passíveis de análise e previsão.
A visualização do framework TMA proposta pelos autores é apresentada a seguir.

No artigo anterior, estabelecemos a base teórica e demos início à implementação prática da abordagem em MQL5. O código passou a contar com as primeiras estruturas responsáveis pelo processamento do fluxo de eventos, pelo cálculo das correlações entre as camadas temporais e pelos mecanismos de memória que permitem ao modelo preservar informações sobre o passado recente. Esse foi um passo importante rumo à construção de um sistema capaz de aprender a interpretar o mercado da mesma forma que uma pessoa percebe o movimento de uma onda: não apenas pelos números, mas também por sua forma e seu ritmo.
Hoje avançamos mais uma etapa. O objetivo agora é passar da teoria para uma implementação completa da arquitetura TMA e mostrar como seus princípios fundamentais podem ser incorporados aos algoritmos. Não se trata de forçar um modelo desenvolvido para tarefas visuais a se encaixar nos dados financeiros, mas de adaptar de forma consciente a um novo ambiente ideias que já demonstraram sua eficácia em outro domínio.
Codificador de atributos de movimento
Começamos pela construção do módulo Codificador de atributos de movimento (Motion Feature Encoder, MFE). Na arquitetura original, esse componente é formado por duas ramificações convolucionais paralelas. Uma delas processa o fluxo principal de dados, enquanto a outra processa os coeficientes de correlação. Em seguida, os dois fluxos são combinados e passam por uma camada convolucional adicional, na qual é formada a representação final dos atributos de movimento.
No entanto, ao examinarmos a implementação com mais atenção, fica evidente que esse paralelismo é apenas formal. Os cálculos são executados de forma sequencial. Os fluxos de dados apenas se alternam, em vez de serem processados simultaneamente. No contexto de machine learning, isso é perfeitamente aceitável, mas essa abordagem reduz a eficiência e aumenta a latência, sobretudo ao lidar com grandes volumes de dados.
Nosso objetivo, porém, não é apenas reproduzir a ideia original, mas torná-la o mais eficiente possível. Queremos obter paralelismo real, reduzir o tempo de execução das operações e aproveitar todo o potencial do contexto OpenCL. Por isso, vamos nos concentrar em encontrar uma forma de estruturar os cálculos para que ambos os fluxos sejam efetivamente processados ao mesmo tempo. Assim, o módulo deixará de ser apenas uma solução lógica e passará a ser também uma ferramenta otimizada do ponto de vista computacional, capaz de processar com eficiência dados de alta frequência do mercado financeiro.
Para resolver essa tarefa, o Objeto de camada convolucional multijanela é especialmente adequado. Sua estrutura foi projetada desde o início para o processamento paralelo de vários fluxos de dados de entrada, o que o torna uma solução ideal para implementar o conceito do MFE. Ao contrário de um bloco convolucional convencional, no qual as entradas são processadas sequencialmente, a camada multijanela permite distribuir os cálculos e executar simultaneamente as operações de convolução.
Essa abordagem reduz a latência e proporciona maior densidade computacional. Cada canal de janela da camada pode ser configurado para um tipo específico de atributo analisado. Como resultado, obtemos uma representação rica e multicamada do movimento, na qual cada elemento contribui para a compreensão do quadro geral.
Para implementar na prática a solução arquitetural proposta, criamos um novo objeto CNeuronTMAMFE, que herdará a funcionalidade básica da classe de camada convolucional CNeuronConvOCL. Essa escolha não é por acaso.
Vale lembrar que, na saída do módulo MFE, é formada uma representação generalizada dos dados por meio de uma camada convolucional final, cuja função é combinar os resultados do processamento preliminar e destacar os principais atributos dinâmicos. Pretendemos implementar essa funcionalidade utilizando os recursos do objeto pai, pois a classe base já reúne todas as ferramentas necessárias para executar esse tipo de operação no ambiente OpenCL. Assim, precisamos apenas acrescentar ao módulo um bloco próprio: um sistema de processamento preliminar independente dos dois canais de informação. Esse bloco permitirá tratar separadamente os dados de origem e os coeficientes de correlação, preparando-os para a fusão posterior. Dessa forma, preservamos a clareza da arquitetura e, ao mesmo tempo, aproveitamos com máxima eficiência os recursos de paralelismo computacional.
class CNeuronTMAMFE : public CNeuronConvOCL { protected: CLayer cEncoder; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronTMAMFE(void) {}; ~CNeuronTMAMFE(void) {}; //--- //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units, uint &windows[], uint window_out, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronTMAMFE; } //--- methods for working with files 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 void TrainMode(bool flag) override; };
A arquitetura do novo neurônio foi projetada com precisão de engenharia. Na área protegida da classe está o objeto cEncoder, um array dinâmico destinado a armazenar ponteiros para os componentes do módulo. Essa solução permite configurar com flexibilidade a estrutura interna do codificador, adicionando ou removendo elementos individuais sem precisar reconstruir toda a arquitetura. O array é preenchido no método de inicialização. Nele é implementada a lógica que cria, em etapas, todos os componentes responsáveis por extrair, normalizar e combinar os atributos de movimento.
bool CNeuronTMAMFE::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units, uint &windows[], uint window_out, ENUM_OPTIMIZATION optimization_type, uint batch) { if(windows.Size() != 2) return false; //--- if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, 2 * window_out, 2 * window_out, window_out, units, 1, optimization_type, batch)) return false;
Tudo começa pela inicialização e pela validação dos parâmetros recebidos. Antes de iniciar a montagem da arquitetura interna, o método verifica se os dados fornecidos estão corretos. A primeira verificação diz respeito ao tamanho do array de janelas windows. Ele deve conter exatamente duas janelas: um canal é destinado ao processamento dos dados principais, enquanto o outro analisa as correlações. Se o número de janelas for diferente do esperado, o método encerra imediatamente a execução. É como um capitão que não parte para o mar antes de confirmar que toda a tripulação está a bordo.
A etapa seguinte é a chamada do construtor da classe pai. É aqui que a infraestrutura básica é configurada: a memória é alocada, os parâmetros são vinculados e o contexto OpenCL é inicializado. A dimensionalidade dos dados de entrada é duplicada, pois teremos de processar dois fluxos simultaneamente. Se essa etapa falhar, não faz sentido prosseguir: a base precisa ser sólida antes de construirmos o restante da estrutura.
Após a inicialização bem-sucedida, preparamos cEncoder, a estrutura dinâmica que armazenará todas as camadas internas do módulo. Ela é limpa de quaisquer dados anteriores e vinculada ao contexto OpenCL atual. A partir desse momento, cada camada criada deixa de ser apenas um objeto isolado e passa a atuar como parte ativa de toda a cadeia computacional.
uint index = 0; cEncoder.Clear(); cEncoder.SetOpenCL(OpenCL); CNeuronMultiWindowsConvOCL* mw_conv = NULL; CNeuronBatchNormOCL* normal = NULL;
O primeiro componente adicionado ao sistema é o bloco convolucional multijanela CNeuronMultiWindowsConvOCL. Ele atua como analisador primário dos sinais, processando os dados de entrada dos dois canais e identificando padrões locais característicos. Esse bloco pode ser comparado à lente de uma câmera, que direciona o foco para os detalhes mais importantes da cena e permite distinguir a estrutura do movimento. Após a inicialização bem-sucedida, é atribuída a ele a função de ativação SoftPlus. Suave e sem limiares abruptos, ela ajuda o modelo a manter a sensibilidade às menores variações dos dados analisados, sem eliminar partes úteis da faixa de valores.
mw_conv = new CNeuronMultiWindowsConvOCL(); if(!mw_conv || !mw_conv.Init(0, index, OpenCL, windows, 2 * window_out, units, 1, optimization, iBatch) || !cEncoder.Add(mw_conv)) { DeleteObj(mw_conv) return false; } mw_conv.SetActivationFunction(SoftPlus); index++;
No entanto, apenas extrair os atributos não é suficiente: também é necessário estabilizá-los. Por isso, logo após a convolução, adicionamos a camada de normalização em lote CNeuronBatchNormOCL. Ela uniformiza as estatísticas dos dados de saída, reduzindo a dispersão dos valores e aumentando a robustez do modelo ao ruído. É como um filtro de áudio que remove o ruído de fundo e preserva apenas o sinal limpo.
normal = new CNeuronBatchNormOCL(); if(!normal || !normal.Init(0, index, OpenCL, mw_conv.Neurons(), iBatch, optimization) || !cEncoder.Add(normal)) { DeleteObj(normal) return false; } index++;
Em seguida, entra em cena o segundo bloco convolucional. Nesse ponto, os dois fluxos de informação já passaram pelo processamento inicial, mas continuam independentes. Não os combinamos ainda: o processamento paralelo prossegue, mantendo os canais separados. O array temporário temp, que contém os tamanhos ampliados das janelas, desempenha aqui apenas uma função auxiliar, definindo os parâmetros correspondentes à dimensionalidade de saída da camada convolucional anterior. Assim, o novo bloco atua como o próximo nível de filtragem local, refinando e reforçando os atributos já extraídos dentro de cada canal.
uint temp[] = {2 * window_out, 2 * window_out}; mw_conv = new CNeuronMultiWindowsConvOCL(); if(!mw_conv || !mw_conv.Init(0, index, OpenCL, temp, window_out, units, 1, optimization, iBatch) || !cEncoder.Add(mw_conv)) { DeleteObj(mw_conv) return false; } mw_conv.SetActivationFunction(SoftPlus); index++; normal = new CNeuronBatchNormOCL(); if(!normal || !normal.Init(0, index, OpenCL, mw_conv.Neurons(), iBatch, optimization) || !cEncoder.Add(normal)) { DeleteObj(normal) return false; } //--- return true; }
Este é um ponto importante. Nesta etapa, a arquitetura do codificador atua como dois processadores sincronizados que trabalham com tipos diferentes de informação, mas sob um controle comum. Cada fluxo convolucional percorre seu próprio laço de ativação e normalização, formando uma representação espacial própria do movimento. Essa abordagem permite manter os dois fluxos de dados separados, evitar a combinação prematura dos atributos e, assim, preparar a base para sua posterior fusão no nível da classe pai.
Se nenhum erro ocorrer durante a execução do método, ele retorna true, indicando que o módulo foi montado com sucesso e está pronto para funcionar.
Em essência, o método Init estabelece a lógica de interação entre os elementos, transformando um conjunto de operações isoladas em um mecanismo coeso. Como resultado, o codificador passa a ser capaz de extrair atributos e se adaptar à dinâmica variável dos dados.
A propagação para frente foi implementada de forma simples, mas elegante. Não há nada supérfluo: apenas uma sequência bem definida de cálculos e uma passagem correta dos dados entre as camadas. É esse método que dá vida ao codificador, transformando um conjunto de componentes inicializados em um mecanismo funcional, capaz de processar e transformar sequencialmente os atributos analisados.
bool CNeuronTMAMFE::feedForward(CNeuronBaseOCL *NeuronOCL) { CNeuronBaseOCL* prev = NeuronOCL; CNeuronBaseOCL* curr = NULL;
A execução do método começa com a preparação dos ponteiros locais. A variável prev representa a camada anterior, da qual vêm os dados que serão processados pelo bloco atual, enquanto curr será usada para referenciar a camada que está sendo processada no codificador.
Em seguida, inicia-se o laço principal. Ele percorre todos os componentes do array cEncoder, no qual, como vimos, estão armazenadas as camadas do módulo: os blocos convolucionais e de normalização.
for(int i = 0; i < cEncoder.Total(); i++) { curr = cEncoder[i]; if(!curr || !curr.FeedForward(prev)) return false; prev = curr; }
A cada iteração, o método obtém do array o objeto seguinte. Se, por algum motivo, a referência à camada estiver ausente ou a propagação para frente do objeto interno retornar um erro, a execução é interrompida imediatamente. Essa solução torna o algoritmo confiável e tolerante a falhas: nenhum problema interno do codificador se propaga a ponto de comprometer todo o modelo.
Após a conclusão bem-sucedida da operação, a camada atual passa a ser a camada anterior para o elemento seguinte. Dessa forma, cria-se uma cadeia contínua de passagem de dados, na qual cada bloco recebe como entrada o resultado produzido pelo bloco precedente.
Quando todo o codificador interno conclui o processamento, o controle é transferido para a camada convolucional da classe pai.
if(!CNeuronConvOCL::feedForward(prev)) return false; //--- return true; }
É nesse ponto que os dados dos dois canais, já transformados e normalizados nas etapas intermediárias, são encaminhados para o nível seguinte da hierarquia. O objeto pai executa a convolução final, responsável por formar os atributos de movimento generalizados e prepará-los para as camadas seguintes do modelo.
O último passo é sinalizar a conclusão bem-sucedida da propagação para frente.
Visualmente, esse fluxo pode ser imaginado como o movimento suave de uma onda: o sinal recebido na entrada atravessa uma sequência de filtros e, aos poucos, é depurado, estruturado e enriquecido de significado. Cada camada funciona como um instrumento de uma orquestra, acrescentando seu próprio timbre à melodia formada pelos dados.
Assim, o método feedForward transforma a arquitetura do módulo CNeuronTMAMFE em um sistema computacional plenamente funcional. Ele integra todas as camadas do codificador, garantindo a passagem correta e sequencial das informações entre elas e, por meio da classe base, também com o restante do modelo. É esse mecanismo que torna possível concretizar a ideia central do framework TMA: o processamento paralelo, porém coordenado, do movimento, no qual cada fragmento de dados percorre seu próprio caminho até contribuir para a compreensão da dinâmica geral do processo.
Desse modo, passo a passo, linhas de código dão origem a uma ferramenta inteligente: um módulo capaz de captar o ritmo do movimento e transformar o caos das oscilações do mercado em padrões significativos. O CNeuronTMAMFE deixa de ser apenas mais um componente do framework e passa a atuar como elo entre teoria e prática. Ele materializa a ideia de processamento paralelo de múltiplos fluxos de dados e estabelece a base para ampliar o TMA em direção a uma análise mais profunda do movimento, seja para prever impulsos do mercado, identificar fases de tendência ou sincronizar atributos temporais em séries financeiras. O código completo da classe e de todos os seus métodos está disponível no anexo.
Módulo de agregação de padrões de movimento
A próxima etapa da construção do modelo é o módulo de agregação de padrões de movimento (Motion Pattern Aggregation, MPA). É nele que todos os atributos previamente processados convergem, como se estivessem no foco de uma lente, para formar uma representação integrada da dinâmica do processo. Nesse ponto, os autores do framework adotam uma solução pouco convencional e até ousada. Eles introduzem um mecanismo de atenção cruzada, mas o utilizam de maneira diferente daquela encontrada nas arquiteturas clássicas.
Em geral, uma abordagem baseada em atenção compara o estado atual com o contexto histórico. Em termos simples, olhamos para trás para determinar quais fragmentos de dados anteriores foram mais relevantes para formar o quadro atual. Assim, o contexto funciona como uma fonte de memória: ele confere continuidade temporal ao modelo e ajuda a extrair padrões da sequência de observações.
Os autores do framework TMA, porém, decidiram seguir o caminho inverso. Em vez de situar o estado atual dentro do contexto do passado, eles propõem analisar todo o conjunto de atributos tomando o estado mais recente como contexto. Agora, não é o passado que explica o presente; o presente passa a funcionar como uma espécie de lente pela qual se avalia a relevância de cada estado anterior.
Essa inversão de perspectiva altera a própria filosofia da análise. Se um modelo clássico de atenção procura responder à pergunta "quais elementos do passado são importantes para compreender o estado atual?", o módulo TMA busca responder a outra: "quais estados exerceram influência efetiva sobre o ponto em que nos encontramos agora?"
Com isso, o mecanismo deixa de ser apenas uma ferramenta de ponderação do histórico e se transforma em um filtro analítico capaz de identificar relações de causa e efeito entre padrões dinâmicos. Não se trata apenas de direcionar a atenção ao passado, mas de procurar os vestígios que ele deixou no presente.
Essa solução confere ao modelo uma sensibilidade especial às mudanças na dinâmica do mercado: em vez de simplesmente fazer uma média do histórico, passamos a identificar com precisão quais padrões realmente conduziram o sistema ao estado atual. É justamente isso que torna o módulo de agregação de padrões de movimento o coração do framework TMA. Ele transforma um conjunto de atributos em uma estrutura temporal significativa, na qual cada estado é avaliado pela influência que exerceu, e não apenas por sua posição na sequência temporal.
Do ponto de vista técnico, para implementar essa lógica basta trocar entre si os ponteiros para os objetos que contêm os dados de origem. No nosso caso, porém, a questão não está no mecanismo de atenção em si, que permanece o mesmo, mas na forma como os dados são utilizados. Na abordagem clássica, o bloco de atenção cruzada recebe duas fontes de dados como entrada: o tensor principal de atributos e um contexto separado. Na arquitetura proposta pelos autores do framework TMA, esse contexto corresponde ao último estado do sistema. Para isso, a fatia temporal final é extraída do array geral de dados e, em seguida, passada como um objeto independente.
Nós optamos por outro caminho, mais econômico e, por assim dizer, mais elegante do ponto de vista da engenharia. Na nossa pilha de dados, o último estado já está armazenado nos primeiros elementos do tensor. Portanto, extrair o contexto separadamente seria não apenas redundante, mas também dispendioso em termos de recursos. Criar um novo objeto de atenção cruzada apenas para inverter a posição dos tensores simplesmente não faz sentido. Na prática, estamos lidando com o mesmo Self-Attention, mas em uma forma reduzida: o contexto fica limitado a um único estado, em vez de abranger toda a sequência.
Por isso, em vez de duplicar os dados, criamos uma extensão do módulo de atenção cruzada já implementado. A ideia é simples: o mesmo tensor é utilizado simultaneamente como fonte de atributos e como contexto, enquanto o comprimento da janela de contexto é definido como a dimensionalidade de um único estado do sistema. Essa solução não apenas preserva o significado conceitual da proposta dos autores, como também aumenta significativamente a eficiência computacional.
Com isso, eliminamos cópias desnecessárias na memória, reduzimos o uso de recursos no contexto OpenCL e aceleramos os cálculos. Na prática, não alteramos a estrutura; apenas reinterpretamos a lógica de interação entre seus componentes.
Para incorporar essa lógica à arquitetura de forma organizada, criamos um novo objeto especializado, o CNeuronTMAMPA. Ele preserva a estrutura existente e acrescenta um componente responsável pela comparação contextual dos atributos.
class CNeuronTMAMPA : public CNeuronBaseOCL { protected: CNeuronRelativeCrossAttention cCrossAttention; CLayer cProjection; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronTMAMPA(void) {}; ~CNeuronTMAMPA(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units, uint segments, uint heads, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronTMAMPA; } //--- 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 void SetActivationFunction(ENUM_ACTIVATION value) override { }; };
O objeto se baseia em dois componentes principais. O primeiro é cCrossAttention, uma instância da classe CNeuronRelativeCrossAttention. É ele que implementa o mecanismo de atenção cruzada: calcula as matrizes de chaves, consultas e valores, normaliza os pesos de atenção e forma o vetor ponderado final de atributos. Na prática, esse bloco atua como um filtro inteligente, determinando quais elementos da série temporal são mais relevantes no contexto do estado atual.
O segundo componente é o array dinâmico cProjection. Ele atua como um contêiner de objetos destinados ao processamento dos atributos analisados após a atenção cruzada, convertendo-os em uma representação compacta e adequada aos cálculos posteriores. Podemos compará-lo a um acabamento final: depois que o mecanismo de atenção destaca os aspectos mais relevantes, a camada de projeção organiza os resultados para encaminhá-los às próximas etapas do modelo.
O método de inicialização do objeto funciona como uma ponte entre teoria e prática: é justamente aqui que as ideias por trás da adaptação da arquitetura do bloco de atenção cruzada ganham forma no código. Toda a lógica segue uma sequência bem definida, sem operações desnecessárias, refletindo o princípio de otimização mencionado anteriormente: nenhuma cópia redundante dos dados e máxima eficiência computacional.
bool CNeuronTMAMPA::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units, uint segments, uint heads, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units * segments, optimization_type, batch)) return false; activation = None;
A primeira etapa consiste em chamar o inicializador da classe pai. Essa chamada define os parâmetros básicos do neurônio. Na prática, é aqui que se estabelece a base sobre a qual serão construídos todos os demais blocos computacionais. Se a inicialização básica falhar, o método é encerrado, evitando o uso incorreto da memória ou o acesso a buffers ainda não inicializados.
Em seguida, é ativado o componente central, o bloco de atenção cruzada relativa cCrossAttention. É ele que implementa o mecanismo de comparação contextual entre os estados com base na observação mais recente. Durante a inicialização, são passados os tamanhos das janelas de dados e de contexto, o número de elementos da sequência e a quantidade de segmentos em cada uma delas. A particularidade está no fato de que ambas as janelas utilizam o mesmo tensor, enquanto o comprimento do contexto é limitado artificialmente a um único estado. Dessa forma, o bloco tem acesso a toda a sequência, mas a analisa exclusivamente sob a perspectiva do estado mais recente.
uint index = 0; if(!cCrossAttention.Init(0, index, OpenCL, window, window_key, units * segments, heads, window, segments, optimization, iBatch)) return false; index++;
Em seguida, é montado o módulo de projeção cProjection, responsável pela estruturação final dos dados após o processamento pela atenção. Ele é limpo e vinculado ao contexto OpenCL atual, sendo então preenchido sequencialmente com várias camadas.
cProjection.Clear(); cProjection.SetOpenCL(OpenCL); //--- CNeuronBaseOCL* neuron = NULL; CNeuronConvOCL* conv = NULL; CNeuronBatchNormOCL* norm = NULL;
O primeiro componente adicionado é o objeto básico CNeuronBaseOCL, que atua como um buffer de concatenação. Sua função é combinar os resultados da atenção com os dados de origem.
//--- Concatenated neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(0, index, OpenCL, 2 * Neurons(), optimization, iBatch) || !cProjection.Add(neuron)) { DeleteObj(neuron) return false; } neuron.SetActivationFunction(None); index++;
Aqui também podemos observar uma diferença em relação à implementação clássica dos Transformers. Em vez de somar os dados ao fluxo das conexões residuais, os autores do TMA propõem concatenar os dois fluxos de informação e, em seguida, agregá-los por meio de uma camada convolucional. Essa camada reduz a dimensionalidade do tensor de entrada, destaca as principais dependências espaciais e prepara os atributos para a normalização.
//--- Projection conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, index, OpenCL, 2 * window, 2 * window, window, units * segments, 1, optimization, iBatch) || !cProjection.Add(conv)) { DeleteObj(conv) return false; } conv.SetActivationFunction(SoftPlus); index++;
A escolha da função de ativação SoftPlus também não é casual: ela introduz uma não linearidade suave e, ao mesmo tempo, mantém a estabilidade dos gradientes durante o treinamento.
Após o bloco convolucional, vem a camada CNeuronBatchNormOCL, responsável pela normalização em mini-batches. Essa etapa uniformiza a distribuição dos valores, acelera a convergência e estabiliza o treinamento. Depois dela, a função de ativação é desativada (None), preservando o sinal sem transformações adicionais antes da próxima etapa de convoluções.
norm = new CNeuronBatchNormOCL(); if(!norm || !norm.Init(0, index, OpenCL, conv.Neurons(), iBatch, optimization) || !cProjection.Add(norm)) { DeleteObj(norm) return false; } norm.SetActivationFunction(None); index++;
A etapa final é uma projeção convolucional dupla, na qual são criados mais dois objetos CNeuronConvOCL. O primeiro realiza o processamento direto dos atributos mantendo a ativação SoftPlus, enquanto o segundo executa uma convolução linear adicional, sem ativação, atuando como a transformação final que encaminha o resultado para as camadas seguintes da rede. Esse par refina a representação dos dados em duas etapas: a primeira camada aplica uma transformação não linear com SoftPlus, enquanto a segunda executa a projeção linear final, sem ativação.
//--- Feed Forward conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, index, OpenCL, window, window, 2 * window, units * segments, 1, optimization, iBatch) || !cProjection.Add(conv)) { DeleteObj(conv) return false; } conv.SetActivationFunction(SoftPlus); index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, index, OpenCL, 2 * window, 2 * window, window, units * segments, 1, optimization, iBatch) || !cProjection.Add(conv)) { DeleteObj(conv) return false; } conv.SetActivationFunction(None); //--- return true; }
Assim, o método de inicialização monta um pipeline computacional completo. Toda a estrutura é lógica e compacta, sem conexões redundantes nem cópias desnecessárias dos dados. A arquitetura permanece rigorosa, mas ao mesmo tempo flexível, podendo ser facilmente dimensionada e adaptada a diferentes comprimentos das sequências analisadas.
A propagação para frente implementada no método feedForward é, em essência, o coração do módulo de agregação de padrões de movimento. É aqui que a ideia abstrata de comparar os estados sob a perspectiva da observação mais recente se concretiza em uma sequência precisa e bem definida de operações. Cada etapa foi concebida para preservar a consistência dos dados, minimizar a sobrecarga computacional e, ao mesmo tempo, aproveitar plenamente o potencial da atenção cruzada relativa.
bool CNeuronTMAMPA::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false; if(!cCrossAttention.FeedForward(NeuronOCL, NeuronOCL.getOutput())) return false;
A execução do método começa pela validação do ponteiro recebido para o objeto que contém os dados de origem. Pode parecer uma formalidade, mas é uma verificação indispensável: qualquer acesso a um neurônio não inicializado pode comprometer toda a cadeia computacional. Se a referência estiver vazia, o método encerra imediatamente a execução, evitando erros posteriores.
Na etapa seguinte, é ativado o componente central, o bloco de atenção cruzada. É aqui que ocorre a principal operação de todo o mecanismo: a análise dos atributos atuais no contexto do estado mais recente. Na prática, como vimos anteriormente, o mesmo tensor é usado simultaneamente como dados analisados e como contexto. Dessa forma, o sistema evita cópias desnecessárias e, ainda assim, calcula corretamente as relações entre os estados da sequência. Se a operação for concluída com sucesso, é formado o tensor de saída da atenção, contendo uma representação dos atributos ponderada de acordo com sua relevância.
Em seguida, começa a etapa de agregação. O método acessa o primeiro elemento de cProjection, cProjection[0], e o utiliza para combinar os dois fluxos de dados: o original e o processado pelo mecanismo de atenção. Por meio da função Concat, os dois fluxos são concatenados, combinando o sinal original com o resultado da atenção em uma representação ampliada dos atributos.
CNeuronBaseOCL* neuron = cProjection[0]; uint window = cCrossAttention.GetWindow(); uint units = cCrossAttention.GetUnits(); uint segments = cCrossAttention.GetUnitsKV(); if(!neuron || !Concat(NeuronOCL.getOutput(), cCrossAttention.getOutput(), neuron.getOutput(), window, window, units)) return false;
Depois da concatenação, os dados seguem por uma sequência de camadas que compõem cProjection. É nesse ponto que os dados percorrem todas as etapas do módulo de projeção, com cada camada aplicando sucessivamente sua própria transformação. O laço percorre sequencialmente todos os componentes e chama o método de propagação para frente de cada um deles. Assim, o módulo de projeção atua como um filtro que transforma e refina gradualmente a estrutura dos atributos.
for(int i = 1; i < cProjection.Total(); i++) { neuron = cProjection[i]; if(!neuron || !neuron.FeedForward(cProjection[i - 1])) return false; }
A etapa final é a operação SumAndNormilize. Ela soma e normaliza os dados de saída: o sinal original é combinado com os resultados da projeção e, em seguida, passa por uma normalização que uniformiza a escala dos atributos.
if(!SumAndNormilize(NeuronOCL.getOutput(), neuron.getOutput(), Output, window * segments, true, 0, 0, 0, 1)) return false; //--- return true; }
O resultado é uma saída limpa e equilibrada, pronta para ser encaminhada à próxima camada do modelo.
Assim, o método feedForward implementa um ciclo completo de processamento dos dados: desde a extração das interdependências entre os estados até a formação de um resultado devidamente normalizado. Nenhuma linha do código está ali por acaso: tudo segue os princípios de precisão e eficiência.
Ao contrário da propagação para frente, a retropropagação do erro no método calcInputGradients exige um ajuste arquitetural muito mais cuidadoso. A eliminação da duplicação de dados, tão vantajosa para economizar memória e acelerar os cálculos, transforma-se aqui em um desafio. Agora, cada gradiente precisa ser distribuído com extrema precisão para não comprometer a consistência da cadeia computacional. Em essência, esse método funciona como o equivalente matemático de um restaurador que recupera o significado original de cada traço em uma complexa trama de interações entre neurônios.
bool CNeuronTMAMPA::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false; //--- CNeuronBaseOCL* neuron = cProjection[-1]; if(!neuron || !DeActivation(neuron.getOutput(), neuron.getGradient(), Gradient, neuron.Activation())) return false;
Tudo começa pela validação básica do ponteiro recebido para o objeto que contém os dados de origem. Se a referência ao neurônio estiver ausente, a função encerra imediatamente a execução, pois um erro nessa etapa pode comprometer todo o procedimento de treinamento.
Em seguida, é selecionado o último elemento do bloco de projeção cProjection[-1], correspondente à ativação final da propagação para frente. Por meio da chamada ao método DeActivation, os gradientes recebidos das camadas seguintes são ajustados de acordo com a derivada da função de ativação aplicada durante a propagação para frente. Esse é o primeiro passo para reconstruir a cadeia de dependências.
Depois disso, inicia-se um laço reverso que percorre todas as camadas da projeção. Nele, cada neurônio recupera sua própria contribuição para o erro por meio do método CalcHiddenGradients. O processo lembra o desenrolar de uma trama complexa: camada por camada, do fim para o início, o fluxo de dependências entre as representações internas dos atributos é reconstruído.
for(int i = cProjection.Total() - 2; i >= 0; i--) { neuron = cProjection[0]; if(!neuron || !neuron.CalcHiddenGradients(cProjection[i + 1])) return false; }
Concluído o processamento do bloco de projeção, chega a vez da desconcatenação, operação inversa à combinação dos fluxos realizada durante a propagação para frente. O método DeConcat separa os gradientes em duas partes: uma associada à saída do bloco de atenção cCrossAttention e outra aos atributos de origem. Dessa forma, o erro é direcionado corretamente para cada uma das fontes de dados.
uint window = cCrossAttention.GetWindow(); uint units = cCrossAttention.GetUnits(); if(!DeConcat(PrevOutput, cCrossAttention.getGradient(), neuron.getGradient(), window, window, units)) return false; Deactivation(cCrossAttention)
Em seguida, é executada a ativação reversa dentro do bloco de atenção. No código, a chamada simbólica Deactivation(cCrossAttention) indica a necessidade de calcular os gradientes próprios do mecanismo de atenção, garantindo a consistência do fluxo de informações durante a retropropagação.
Depois, é chamada a operação SumAndNormilize, responsável por somar, no nível dos dados de origem, os gradientes provenientes dos dois fluxos de informação. A esse resultado também são acrescentados os gradientes provenientes das conexões residuais.
if(!SumAndNormilize(PrevOutput, Gradient, PrevOutput, window, false, 0, 0, 0, 1)) return false; if(NeuronOCL.Activation() != None) if(!DeActivation(NeuronOCL.getOutput(), PrevOutput, PrevOutput, NeuronOCL.Activation())) return false;
A etapa seguinte consiste em aplicar uma ativação reversa adicional no neurônio de entrada, caso tenha sido definida uma função de ativação para ele. Isso garante a continuidade da retropropagação entre a entrada externa e os estados internos do modelo.
Por fim, o método CalcHiddenGradients é chamado novamente, desta vez para propagar os gradientes do erro do bloco de atenção até o nível dos dados de origem.
if(!NeuronOCL.CalcHiddenGradients(cCrossAttention.AsObject(), NeuronOCL.getOutput(), cProjection[-1].getPrevOutput(), (ENUM_ACTIVATION)NeuronOCL.Activation())) return false; if(!SumAndNormilize(NeuronOCL.getGradient(), cProjection[-1].getPrevOutput(), NeuronOCL.getGradient(), window, false, 0, 0, 0, 1) || !SumAndNormilize(NeuronOCL.getGradient(), PrevOutput, NeuronOCL.getGradient(), window, false, 0, 0, 0, 1)) return false; //--- return true; }
Aqui vale observar que o objeto com os dados de origem foi utilizado tanto como fluxo principal quanto como contexto da atenção cruzada. Por isso, a etapa final consiste em somar os gradientes provenientes de todos os fluxos de informação.
Se qualquer uma dessas etapas falhar, o método retorna false imediatamente, evitando o acúmulo de valores incorretos.
Assim, calcInputGradients é um algoritmo cuidadosamente estruturado. Cada linha de código contribui para percorrer em sentido inverso as dependências estabelecidas durante a propagação para frente e distribuir corretamente os gradientes necessários ao aprendizado. O algoritmo não se limita a percorrer a propagação para frente no sentido inverso: ele reconstrói a estrutura lógica dos cálculos em ordem reversa, distribuindo o erro por todas as camadas do modelo sem perda de informação.
Em termos metafóricos, podemos imaginar a propagação para frente como uma inspiração e a propagação reversa como uma expiração. A primeira alimenta o sistema com conhecimento; a segunda permite que esse conhecimento seja assimilado, processado e fortaleça as conexões internas. É nessa interdependência que reside a respiração de toda a arquitetura de aprendizado.
Como resultado, o CNeuronTMAMPA se torna o elo que combina a capacidade de ponderação contextual da atenção com o rigor computacional do framework. Ele preserva a arquitetura e amplia sua capacidade de modelar dependências contextuais, permitindo avaliar não apenas relações temporais, mas também a influência relativa dos estados anteriores sob a perspectiva do estado mais recente.
Hoje realizamos uma etapa importante do trabalho e podemos nos permitir uma breve pausa. É um bom momento para assimilar o que foi feito e recuperar as energias. No próximo artigo, voltaremos ao projeto para levar o que iniciamos até sua conclusão lógica.
Conclusão
Neste artigo, implementamos os módulos de processamento preliminar e de agregação de padrões de movimento, que formam o núcleo da arquitetura do framework TMA. Aqui, as ideias dos autores ganharam forma prática. Eliminamos cópias redundantes, otimizamos o funcionamento do mecanismo de atenção cruzada e estabelecemos uma lógica clara para a distribuição dos gradientes. Tudo isso torna o sistema, ao mesmo tempo, elegante e eficiente.
A próxima etapa será a fase final: o treinamento e o teste do modelo. É nela que verificaremos até que ponto as soluções implementadas se mantêm robustas em condições reais de mercado e correspondem às premissas conceituais do framework.
Referências
Programas utilizados no artigo
| # | Nome | Tipo | Descrição |
|---|---|---|---|
| 1 | Study.mq5 | Expert Advisor | EA para treinamento offline de modelos |
| 2 | StudyOnline.mq5 | Expert Advisor | EA para treinamento online de modelos |
| 3 | Test.mq5 | Expert Advisor | EA para teste do modelo |
| 4 | Trajectory.mqh | Biblioteca de classe | Estrutura para descrição do estado do sistema e da arquitetura dos modelos |
| 5 | NeuroNet.mqh | Biblioteca de classe | Biblioteca de classes para criação de redes neurais |
| 6 | NeuroNet.cl | Biblioteca | Biblioteca de código do programa OpenCL |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/20208
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.
Integração rápida de um grande modelo de linguagem com o MetaTrader 5 (Parte I): Criando o modelo
Equipe de agentes de IA com rotação baseada no lucro: a evolução de um sistema de trading vivo em MQL5
Redes neurais no trading: agregação do movimento ao longo do tempo (Conclusão)
Redes neurais em trading: Agregação Temporal do Movimento (TMA)
- 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