Redes neurais em trading: treinamento de metaparâmetros com base na heterogeneidade (Componentes principais)
Introdução
No artigo anterior, analisamos o framework HimNet, uma ferramenta prática que não apenas analisa os dados, mas também compreende seu contexto. No trading, isso significa que o modelo distingue períodos de alta liquidez e de baixa atividade. O HimNet se adapta automaticamente a essas mudanças.
A arquitetura do HimNet é simples e lógica: segmentos de séries temporais de comprimento T por N nós espaciais são fornecidos ao modelo. Primeiro, o modelo forma dois tipos de embeddings treináveis: temporais e espaciais. Os embeddings temporais, como hora do dia e dia da semana, capturam ciclos e regimes: picos matinais na abertura, quedas durante o dia e calmaria noturna. Os embeddings espaciais, um vetor separado para cada nó ou ticker, codificam o perfil da série temporal. Todos esses vetores ficam armazenados em dicionários e são refinados durante o treinamento. Eles atuam como vetores de consulta para os pools de metaparâmetros e permitem que o modelo selecione pesos diferentes para diferentes contextos de mercado, definidos pelo comportamento da convolução em grafos. Essa geração dinâmica oferece a flexibilidade do meta-aprendizado sem consumo explosivo de memória e recursos computacionais.
O coração do HimNet são os blocos recorrentes em grafos (GCRU), reforçados pela base de Chebyshev. Esse recurso arquitetural torna o modelo ao mesmo tempo profundo e pragmático. A GCRU lê a rede de plataformas de negociação como um grafo: cada nó é um ticker ou uma plataforma, e as arestas refletem correlação, spread cruzado ou relação empírica. Os polinômios de Chebyshev fornecem uma implementação compacta da agregação K-hop: o modelo considera a influência de vizinhos a até K saltos de distância, mas faz isso sem uma diagonalização espectral pesada, de forma rápida, local e numericamente estável. Em termos simples, em vez de calcular o espectro completo do grafo, os autores do framework constroem passo a passo um conjunto de matrizes que capturam a influência dos vizinhos mais próximos, dos vizinhos desses vizinhos e assim por diante, combinando depois esses efeitos conforme o contexto.
A etapa de codificação no HimNet ocorre em dois fluxos de informação paralelos. O primeiro, o codificador espacial, examina o perfil das plataformas: profundidade do livro de ofertas, volume típico, reação histórica a notícias. O segundo, o codificador temporal, registra regimes temporais: abertura do pregão, fluxo de notícias, calmaria noturna ou período de anúncios relevantes. Ambos os codificadores geram representações ocultas, que são somadas em um tensor latente unificado. Não se trata de uma soma mecânica, mas de uma composição cuidadosa de sentidos: o tempo determina o grau de sensibilidade, enquanto o espaço define o método de agregação. Como resultado, obtemos uma visão unificada do estado atual do mercado.
O decodificador recebe essa representação unificada e a projeta em um embedding espaço-temporal, que serve como vetor de consulta para o pool ST de metaparâmetros. O pool retorna um conjunto de parâmetros específicos para essa combinação espaço-temporal específica, e esses parâmetros formam diretamente os pesos do bloco GCRU no decodificador. Em outras palavras, os autores do framework reconfiguram dinamicamente a própria estrutura do processamento recorrente para o regime atual: na abertura da sessão, certos pesos e certa sensibilidade; no período noturno, outros. Em seguida, o decodificador gera iterativamente, passo a passo, previsões para o horizonte de previsão definido.
Do ponto de vista da álgebra e dos aspectos numéricos, para cada estado modelamos K níveis de Chebyshev, concatenamos os resultados e os passamos por uma matriz gerada pelo metapool a partir do vetor de consulta representado pelo embedding. Os gradientes retornam tanto para os parâmetros dos pools quanto para os embeddings. Por isso, o modelo aprende não apenas a prever, mas também a identificar contextos espaço-temporais úteis. Na prática, isso proporciona uma adaptação que permite ao modelo selecionar o modo de operação mais relevante naquele momento.
No trading real, isso se traduz em vantagens concretas. Na abertura da sessão principal, o HimNet reforça automaticamente os parâmetros voltados a uma reação rápida a picos de volume, reduzindo o slippage. Em períodos de baixa liquidez, o modelo torna as previsões mais conservadoras e reduz entradas falsas, protegendo contra movimentos bruscos pontuais provocados por negócios pontuais. Isso significa ampliar adequadamente o spread quando a liquidez diminui e filtrar com mais rigor falsas rupturas. Diante de um choque repentino, o modelo ST alterna para um modo defensivo, aumentando a robustez das previsões e reduzindo o risco de armadilhas de mercado.
Tecnicamente, a arquitetura continua pragmática e adequada para uso em produção: os pools de metaparâmetros são compactos e não provocam consumo explosivo de memória e recursos computacionais, enquanto os cálculos pelas potências de Chebyshev escalam facilmente na GPU.
Os hiperparâmetros, na prática, refletem um compromisso razoável entre a flexibilidade do modelo e o tempo de resposta. Sua seleção ocorre por validação em cenários de mercado representativos. A interpretabilidade aqui não é decorativa: pela ativação de candidatos específicos do pool e pelos mapas de calor dos grafos adaptativos, é possível ver rapidamente quais regimes o modelo identifica no momento, por que determinadas previsões foram feitas e quais regras de execução convém aplicar.
Na prática, isso se transforma em um instrumento concreto de controle, desde gatilhos automáticos de alternância para o modo conservador até relatórios transparentes para a gestão de risco, tornando o HimNet controlável e compreensível para o trader e o engenheiro.
Assim, o HimNet combina agregação local poderosa, meta-aprendizado adaptativo e uma implementação prática. Não é apenas um modelo de previsão aprimorado, mas uma ferramenta que compreende os regimes de mercado e consegue mudar seu comportamento junto com eles, permanecendo previsível e controlável para o trader e o gestor de risco.
A visualização do framework HimNet apresentada pelos autores está abaixo.

A parte prática do primeiro artigo confirmou que a ideia funciona em um ambiente real de engenharia. Transferimos para a GPU as operações pesadas de construção dos polinômios de Chebyshev e de retropropagação do erro, implementando em OpenCL os kernels ChebStep e ChebStepGrad.
No programa principal, foi implementado o objeto CChebPolinom, que encapsula os kernels e fornece uma interface unificada para inferência e cálculo de gradientes. Como resultado, obtivemos um pipeline híbrido: treinamento rápido e inferência confiável.
Neste artigo, continuamos a desenvolver as abordagens do framework HimNet com MQL5.
Objeto de convolução em grafos
Na próxima etapa, passamos à criação do objeto de convolução em grafos, que usará a base de Chebyshev como um mecanismo computacional para a agregação K-hop. Esse módulo é o componente principal de execução: ele recebe janelas de atributos preparadas, solicita ao CChebPolinom as matrizes Tk prontas e, com base nos metaparâmetros atuais, constrói as sequências de saída, prontas para serem processadas pelas etapas seguintes do modelo. É importante que ele seja, ao mesmo tempo, flexível, com suporte a diferentes modos de metaparametrização, rápido e previsível, com gestão rigorosa da memória e verificação dos valores numéricos.
A arquitetura do novo objeto CNeuronHimNetGrapConv foi concebida como um adaptador pragmático e modular entre janelas temporais de atributos e a convolução em grafos, combinando a preparação dos dados, a chamada de operações matriciais e a integração com kernels OpenCL, sem poluir a lógica de alto nível do modelo com detalhes de implementação.
class CNeuronHimNetGrapConv : public CNeuronTransposeRCDOCL { protected: CNeuronBaseOCL cX_G; public: CNeuronHimNetGrapConv(void) {}; ~CNeuronHimNetGrapConv(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint count, uint window, uint cheb_k, ENUM_OPTIMIZATION optimization_type, uint batch) override; //--- virtual bool FeedForward(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support); virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support); //--- virtual bool Load(const int file_handle) override; //--- virtual int Type(void) const { return defNeuronHimNetGrapConv; } virtual void SetOpenCL(COpenCLMy *obj); virtual uint GetCount(void) override const { return iWindow; } virtual uint GetWindow(void) override const { return GetDimension(); } virtual uint GetChebK(void) const { return iCount; } };
Dentro da classe, está localizado o objeto auxiliar compacto, mas essencial, cX_G. Ele desempenha o papel de buffer de trabalho e garante a troca rápida de dados entre as etapas dos cálculos. Como o objeto é declarado estaticamente, o construtor e o destrutor da classe podem permanecer vazios: a inicialização e a limpeza da memória ocorrem uma única vez ao longo de todo o ciclo de vida, sem overhead desnecessário na criação e remoção de instâncias.
A inicialização de uma nova instância da classe é implementada no método Init. Nele, primeiro chamamos o método homônimo da classe pai, que define as interfaces gerais do neurônio.
bool CNeuronHimNetGrapConv::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint count, uint window, uint cheb_k, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronTransposeRCDOCL::Init(numOutputs, myIndex, open_cl, cheb_k, count, window, optimization_type, batch)) return false; if(!cX_G.Init(0, 0, OpenCL, Neurons(), optimization, iBatch)) return false; //--- return true; }
Em seguida, é inicializado o buffer auxiliar cX_G, que serve como área de trabalho para os dados já concatenados e armazena o resultado da multiplicação de matrizes. Se qualquer uma dessas operações falhar, Init retorna false, e o objeto permanece em um estado seguro.
O método de propagação para frente FeedForward atua como um maestro. Primeiro, o método verifica rigorosamente a compatibilidade dos objetos recebidos como parâmetros: NeuronOCL e Support devem existir. Além disso, Support deve ter Dimension e Steps esperados iguais às configurações do módulo.
bool CNeuronHimNetGrapConv::FeedForward(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support) { if(!NeuronOCL || !Support) return false; if(Support.GetDimension() != iWindow || Support.GetSteps() != iCount) return false; if(NeuronOCL.Neurons() != (Neurons() / iCount)) return false;
Também é verificado se o número de neurônios no objeto dos dados de entrada corresponde ao formato esperado. Essas verificações não são mera formalidade, mas uma garantia de que as operações matriciais seguintes não se transformarão em uma catástrofe silenciosa na GPU.
Em seguida, vem a operação central de multiplicação de matrizes, na qual os polinômios de Chebyshev obtidos são multiplicados pelo tensor da série temporal analisada. O resultado da operação é colocado no nosso buffer cX_G.
if(!MatMul(Support.getOutput(), NeuronOCL.getOutput(), cX_G.getOutput(), iWindow, iWindow, GetDimension(), iCount, false)) return false;
Em termos conceituais, essa é exatamente a operação que constrói X_G como a concatenação das representações transformadas T0 X, T1 X, …, Tk X. Na analogia com trading: Support é o mapa de inter-relações entre sequências unitárias, representado pelos polinômios de Chebyshev; NeuronOCL é a janela de atributos da sequência temporal multimodal; e cX_G é a versão já analisada dos dados de entrada, em que, para cada sequência unitária, a influência dos vizinhos foi considerada. É justamente essa matriz ajustada que será encaminhada para a etapa seguinte.
Mas observe que, como resultado da multiplicação de matrizes, obtemos uma sequência de vários conjuntos de dados de entrada ajustados em diferentes níveis de detalhamento {K-hop, N, C}. E essa não é a representação dos dados esperada na saída do objeto. Por isso, na última etapa do método de passagem direta, passamos cX_G para o método homônimo da classe pai, que transpõe o tensor para a representação esperada. Essa abordagem mantém o código modular, facilitando testes e otimização.
Se algo der errado em qualquer etapa, o método retorna false de forma limpa, sem deixar efeitos colaterais.
Depois que a propagação para frente calcula a saída da camada, vem uma etapa igualmente importante: a retropropagação. É nessa etapa que a arquitetura adquire a capacidade de aprender com dados históricos e se adaptar a cenários reais de mercado.
O método calcInputGradients começa com um bloco de verificação dos parâmetros recebidos: se os ponteiros passados para a camada anterior e para a matriz dos polinômios de Chebyshev são válidos. Essa verificação funciona como uma espécie de cabo de segurança, evitando cálculos incorretos caso a estrutura do modelo fique dessincronizada.
bool CNeuronHimNetGrapConv::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support) { if(!NeuronOCL || !Support) return false; if(Support.GetDimension() != iWindow || Support.GetSteps() != iCount) return false; if(NeuronOCL.Neurons() != (Neurons() / iCount)) return false;
Em seguida, o método confere as dimensões: o número de passos, isto é, K-hop, e o tamanho da janela devem corresponder ao esperado. Essa etapa é importante, pois convoluções em grafos são sensíveis a incompatibilidades dimensionais. Um erro aqui pode desencadear uma sequência de gradientes incorretos e comprometer todo o treinamento.
Se a verificação for aprovada, passamos diretamente à distribuição do gradiente do erro. Primeiro, os gradientes recebidos da camada seguinte do modelo são transpostos para a representação dos resultados da nossa convolução.
if(!CNeuronTransposeRCDOCL::calcInputGradients(cX_G.AsObject())) return false; if(!MatMulGrad(Support.getOutput(), Support.getGradient(), NeuronOCL.getOutput(), NeuronOCL.getGradient(), cX_G.getGradient(), iWindow, iWindow, GetDimension(), iCount, false)) return false; //--- return true; }
A etapa final é a distribuição dos gradientes do erro entre os dados de entrada e os polinômios de Chebyshev. Neste passo, o método distribui a influência acumulada do erro pelos mesmos canais por onde, na passagem direta, a informação se propagou, mas agora no sentido inverso. Usaremos o método MatMulGrad, herdado do objeto pai. Como resultado, obtemos dois tipos de gradientes. O primeiro é calculado em relação aos próprios dados de entrada da camada, que depois serão usados para ajustar os pesos das camadas anteriores. O segundo é calculado em relação aos coeficientes da decomposição de Chebyshev, permitindo que o framework reajuste a estrutura em grafo quando as dependências de mercado se deslocam ao longo do tempo.
Assim, é exatamente nesta etapa que ocorre a análise final dos resultados: o sistema determina quais conexões foram úteis e quais se mostraram redundantes, preparando o terreno para o próximo ciclo de otimização.
Do ponto de vista prático, CNeuronHimNetGrapConv é o ponto em que o modelo torna os sinais compreensíveis para o trader. Ele transforma janelas brutas de cotações em sinais localmente ajustados. A arquitetura da classe foi concebida para que essas transformações sejam rápidas, reproduzíveis e explicáveis. O código completo desta classe e de todos os seus métodos está disponível no anexo.
Bloco recorrente
Depois de analisarmos a estrutura interna do bloco básico de convolução em grafos com polinômios de Chebyshev e vermos como os fluxos de dados de entrada e gradientes do erro são distribuídos dentro desse elemento relativamente compacto, chegou a hora de subir um nível, em direção ao nó que conecta o processamento local do grafo à dinâmica dos estados ocultos. É aqui que se forma a verdadeira memória do modelo, e é justamente esse componente que impede que a informação se perca entre os passos no tempo, integrando-a de forma orgânica à estrutura em grafo. Trata-se da classe CNeuronHimNetGCRU, o elo central que atua como uma espécie de despachante de sinais entre os componentes temporal e espacial do modelo. A estrutura do objeto é apresentada abaixo.
class CNeuronHimNetGCRU : public CNeuronBaseOCL { protected: CNeuronBaseOCL cInpAndHidden; CNeuronHimNetGrapConv cZ_R; CNeuronConvOCL cZ_R_emb; CNeuronBaseOCL cZe_Re; CNeuronBaseOCL cZ; CNeuronBaseOCL cR; CNeuronBaseOCL cCandidate; CNeuronHimNetGrapConv cHC; CNeuronConvOCL cHC_emb; CNeuronBaseOCL cHCe; public: CNeuronHimNetGCRU(void) {}; ~CNeuronHimNetGCRU(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units, uint window, uint window_out, uint cheb_k, uint embed_dim, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support, CNeuronBaseOCL *Embedding); virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support, CNeuronBaseOCL *Embedding); virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support, CNeuronBaseOCL *Embedding); //--- virtual int Type(void) const { return defNeuronHimNetGCRU; } //--- methods for working with files virtual bool Save(int const file_handle); virtual bool Load(int const file_handle); //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetOpenCL(COpenCLMy *obj) override; virtual void SetActivationFunction(ENUM_ACTIVATION value) override { }; };
Aqui, já não basta simplesmente aplicar uma convolução pelos vizinhos do nó ou passar os dados por uma célula recorrente. Como sabemos, os mercados financeiros raramente se comportam de forma linear: pares de moedas formam clusters interconectados, ações se movem por setores, e commodities muitas vezes ressoam com a moeda dos países exportadores. Por isso, um modelo puramente temporal não é suficiente por si só: ele precisa de um mapa de conexões, e a convolução em grafos fornece esse mapa. Mas, por si só, ela é estática. Para decidir, no momento, se deve reforçar o sinal em EURUSD quando os pares vizinhos GBPUSD e EURGBP mostram dinâmicas opostas, ou se é melhor atenuá-lo, é necessária uma lógica de controle. É exatamente isso que esta classe implementa.
Dentro deste módulo, não está apenas a mecânica GRU familiar aos desenvolvedores de modelos temporais, mas uma versão adaptada à estrutura em grafo. E, se na classe anterior de convolução em grafos lidávamos com um conjunto bastante compacto de elementos, aqui temos uma configuração significativamente mais complexa. A lista de componentes internos é extensa, mas cada um ocupa um lugar bem definido na lógica geral de funcionamento. Vamos revelar seu papel gradualmente, à medida que construirmos os métodos deste objeto, para não sobrecarregar a compreensão com detalhes antes da hora. Ao mesmo tempo, é importante destacar: todos esses objetos internos são declarados estaticamente, o que torna a classe mais simples de gerenciar e elimina a necessidade de construtores e destrutores volumosos. Eles permanecem vazios, como portas bem ajustadas, que não rangem nem exigem movimentos desnecessários a cada execução do algoritmo.
Ao passar da estrutura geral da classe para os detalhes concretos de seu funcionamento, chegamos naturalmente ao método de inicialização Init. É aqui que ocorre o posicionamento das peças no tabuleiro: a criação e a preparação de todos os componentes internos dos quais dependerá, posteriormente, a dinâmica dos cálculos. O método Init é uma espécie de batuta de maestro nos bastidores: enquanto o código externo apenas o chama, internamente se desdobra uma sequência complexa, porém bem estruturada, de preparação de cada módulo.
bool CNeuronHimNetGCRU::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units, uint window, uint window_out, uint cheb_k, uint embed_dim, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units * window_out, optimization_type, batch)) return false; activation = None;
Tudo começa pela inicialização da classe pai. Aqui são criadas todas as interfaces básicas herdadas da camada neural. Observe que o número de neurônios é definido como o produto entre a quantidade de elementos da sequência e o tamanho da janela de atributos na saída do objeto. Com isso, reservamos antecipadamente o espaço computacional para todo o contexto temporal.
Logo depois, a função de ativação é zerada. Isso é feito de propósito: nesta etapa, é importante preservar a neutralidade, para que as camadas seguintes recebam uma tela limpa para sua ativação específica.
Em seguida, desdobra-se a cadeia de inicialização dos componentes internos. O primeiro, cInpAndHidden, funciona como uma espécie de gateway que conecta os dados de entrada ao estado oculto. Sua dimensão é definida pela soma da janela de atributos de entrada com a de saída, multiplicada pela quantidade de elementos da sequência, o que lhe permite acumular todas as informações necessárias para o processamento posterior.
int index = 0; if(!cInpAndHidden.Init(0, index, OpenCL, (window + window_out)*units, optimization, iBatch)) return false; cInpAndHidden.SetActivationFunction(None);
Depois entra em cena cZ_R, responsável pela formação das matrizes combinadas Z e R, as principais portas da GRU. Aqui entra em ação o mecanismo de operação com estruturas em grafo: além das dimensões do tensor dos dados analisados, o método recebe o número de termos do polinômio de Chebyshev, o que influencia diretamente a profundidade da aproximação do grafo.
index++; if(!cZ_R.Init(0, index, OpenCL, units, window + window_out, cheb_k, optimization, iBatch)) return false; cZ_R.SetActivationFunction(None);
O próximo na cadeia de inicialização é o objeto cZ_R_emb, uma espécie de ponte entre o embedding e a dinâmica convolucional. Sua tarefa é formar a matriz de parâmetros da convolução, não de forma mecânica, mas com base no embedding obtido, para que depois esses parâmetros possam ser aplicados no pipeline computacional.
Não se trata apenas de uma variável auxiliar, mas de um intermediário essencial: é justamente aqui que ocorre a reembalagem inteligente das conexões entre sequências unitárias, transformando-as de um conjunto de observações dispersas em um mapa coerente de inter-relações. Em paralelo com os mercados financeiros, esse passo lembra a atuação de um analista experiente, que toma os dados brutos da atividade de negociação e os reorganiza em um formato no qual tendências, correlações e dependências defasadas começam a ganhar contornos capazes de ajudar a prever movimentos futuros de preço.
index++; if(!cZ_R_emb.Init(0, index, OpenCL, embed_dim, embed_dim, 2 * cheb_k * (window + window_out) * window_out, 1, units, optimization, iBatch)) return false; cZ_R_emb.SetActivationFunction(None); index++; if(!cZe_Re.Init(0, index, OpenCL, 2 * window_out * units, optimization, iBatch)) return false; cZe_Re.SetActivationFunction(None); index++; if(!cZ.Init(0, index, OpenCL, window_out * units, optimization, iBatch)) return false; cZ.SetActivationFunction(None); index++; if(!cR.Init(0, index, OpenCL, window_out * units, optimization, iBatch)) return false; cR.SetActivationFunction(None);
Depois disso, o método inicializa vários blocos mais leves, mas não menos importantes: cZe_Re, cZ, cR. Esses módulos formam os vetores finais Z e R, garantindo controle flexível dos mecanismos de esquecimento e atualização. Sua dimensão depende diretamente da janela de atributos e da quantidade de elementos da sequência no tensor de resultados. Na prática, isso reflete o quanto o modelo consegue manter em memória o contexto de mercado atual.
Em seguida, é criado cCandidate, o componente responsável pelo cálculo do candidato ao novo estado. Sua dimensão é herdada do cInpAndHidden já inicializado, o que reforça a conexão estreita entre o sinal de entrada e a representação proposta do estado oculto.
index++; if(!cCandidate.Init(0, index, OpenCL, cInpAndHidden.Neurons(), optimization, iBatch)) return false; cCandidate.SetActivationFunction(None);
O segundo grande bloco, cHC, é outra convolução em grafos, agora operando no nível do estado atualizado. A ele correspondem seus próprios parâmetros de convolução, cHC_emb, construídos com base no embedding obtido, mas com coeficientes diferentes. O elemento final é cHCe, que forma a representação final do estado oculto atualizado.
index++; if(!cHC.Init(0, index, OpenCL, units, window + window_out, cheb_k, optimization, iBatch)) return false; cHC.SetActivationFunction(None); index++; if(!cHC_emb.Init(0, index, OpenCL, embed_dim, embed_dim, (window + window_out) * window_out * cheb_k, 1, units, optimization, iBatch)) return false; cHC_emb.SetActivationFunction(None); index++; if(!cHCe.Init(0, index, OpenCL, window_out * units, optimization, iBatch)) return false; cHCe.SetActivationFunction(None); //--- if(!Output.Fill(0)) return false; //--- return true; }
Depois que todos os módulos são inicializados, ocorre a preparação final: o buffer de resultados é zerado. Não podemos esquecer que estamos trabalhando com um bloco recorrente. Zerar o buffer de resultados garante que o modelo comece a operar a partir de um estado limpo, sem distorções causadas por ruído do passado.
Tudo isso parece volumoso apenas à primeira vista. Na prática, é uma estrutura bem articulada, na qual cada bloco desempenha seu papel: desde o empacotamento preliminar dos dados até o ajuste fino dos mecanismos de esquecimento e atualização das informações na estrutura GRU em grafos. Para tarefas de mercados financeiros, isso é criticamente importante: o modelo precisa considerar tanto as oscilações locais de preço dentro da janela quanto seu contexto global por meio da aproximação em grafo, reagindo com flexibilidade a novos eventos sem perder a inércia histórica.
Depois da análise detalhada da inicialização, é hora de mergulhar no funcionamento do bloco recorrente em grafos e ver como o método de propagação para frente feedForward dá vida à arquitetura. Primeiro, verificamos cuidadosamente se todos os componentes necessários estão devidamente atualizados: o buffer principal dos dados de entrada, o tensor dos polinômios de Chebyshev e os embeddings. Se pelo menos um deles estiver ausente, a execução não começa. É como um trader que, antes de abrir uma posição, verifica todos os indicadores e dados: sem uma visão completa, não é possível fazer uma previsão.
bool CNeuronHimNetGCRU::feedForward(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support, CNeuronBaseOCL *Embedding) { if(!NeuronOCL || !Support || !Embedding) return false; if(!SwapOutputs()) return false;
Em seguida, ocorre a troca de ponteiros para os buffers de dados dos resultados atuais e anteriores, o que cria a memória recorrente. Esse passo é importante, pois o passado influencia o futuro, de forma semelhante a como as oscilações de mercado de ontem moldam as expectativas de hoje.
Depois disso, vale observar que, durante a inicialização, não salvamos os parâmetros de configuração do bloco em variáveis separadas. Isso foi feito de propósito. Afinal, todos eles já estão duplicados nos componentes internos. e podemos extraí-los.
uint cheb_k = cZ_R.GetChebK(); uint units = cZ_R.GetCount(); uint window_out = Neurons() / units; uint window = cZ_R.GetWindow() - window_out;
Só depois de concluída a preparação passamos à execução do processamento. Os dados de entrada e o estado anterior são concatenados no objeto cInpAndHidden, formando um fluxo unificado de informações. Esse fluxo, como uma combinação de notícias, indicadores e sinais, está pronto para o processamento posterior.
if(!Concat(NeuronOCL.getOutput(), PrevOutput, cInpAndHidden.getOutput(), window, window_out, units)) return false;
Em seguida, entra em ação cZ_R, onde os polinômios de Chebyshev executam a convolução em grafos: os nós vizinhos influenciam uns aos outros, mas essa influência é limitada a K-hop, o que garante uso eficiente dos recursos e estabilidade dos cálculos.
if(!cZ_R.FeedForward(cInpAndHidden.AsObject(), Support)) return false; if(!cZ_R_emb.FeedForward(Embedding)) return false;
Em paralelo, cZ_R_emb gera a matriz de parâmetros da convolução com base nos embeddings, como um cozinheiro que adiciona temperos a um prato para realçar nuances de sabor: os atributos temporais e espaciais são corretamente empacotados para aplicação posterior.
Os resultados da convolução são multiplicados e passam pela ativação sigmoide; em seguida, são divididos nos gates Z e R. Eles controlam o fluxo de informações, determinando o que deve ser preservado do passado e o que deve ser atualizado.
if(!MatMul(cZ_R.getOutput(), cZ_R_emb.getOutput(), cZe_Re.getOutput(), 1, (window + window_out)*cheb_k, 2 * window_out, units, true)) return false; if(!Activation(cZe_Re.getOutput(), cZe_Re.getOutput(), SIGMOID)) return false; if(!DeConcat(cZ.getOutput(), cR.getOutput(), cZe_Re.getOutput(), window_out, window_out, units)) return false;
Em seguida, é formado o estado candidato cCandidate, que combina a entrada atual com o estado anterior modificado. Esse estado é passado para o segundo bloco de convolução em grafos, cHC, onde novos parâmetros são gerados de forma semelhante por meio de cHC_emb, e o resultado passa pela tangente hiperbólica, formando o estado oculto atualizado.
if(!ElementMult(cZ.getOutput(), PrevOutput, cZ.getPrevOutput())) return false; if(!Concat(NeuronOCL.getOutput(), cZ.getPrevOutput(), cCandidate.getOutput(), window, window_out, units)) return false; if(!cHC.FeedForward(cCandidate.AsObject(), Support)) return false; if(!cHC_emb.FeedForward(Embedding)) return false; if(!MatMul(cHC.getOutput(), cHC_emb.getOutput(), cHCe.getOutput(), 1, (window + window_out)*cheb_k, window_out, units, true)) return false; if(!Activation(cHCe.getOutput(), cHCe.getOutput(), TANH)) return false; if(!GateElementMult(PrevOutput, cHCe.getOutput(), cR.getOutput(), Output)) return false; //--- return true; }
No final, ocorre a combinação elemento a elemento do estado anterior com o candidato por meio da porta R, e na saída obtemos uma previsão pronta para análise ou para envio posterior ao decodificador. Em termos de mercados financeiros, cada passo é uma filtragem multicamada do sinal: movimentos passados, ativos vizinhos, atributos temporais e embeddings internos são combinados para formar uma previsão precisa e estável. Aqui, o método feedForward funciona como um maestro que coordena uma orquestra de múltiplos instrumentos, transformando dados de mercado caóticos em uma sinfonia harmoniosa de previsões.
Depois de analisarmos em detalhes o método de passagem direta, é hora de passar a uma etapa igualmente importante: a retropropagação. Se a propagação para frente pode ser comparada a um trader que constrói uma previsão com base nos dados atuais, a retropropagação é o momento em que ele se senta após o pregão, abre o diário de operações e analisa onde os sinais funcionaram e onde a previsão falhou. Com base nessa análise, os parâmetros internos do modelo são ajustados para que, no dia seguinte, ele atue com mais precisão.
O método calcInputGradients começa com uma verificação cuidadosa de todos os componentes: o tensor dos dados de entrada, o objeto dos polinômios de Chebyshev e os embeddings.
bool CNeuronHimNetGCRU::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support, CNeuronBaseOCL *Embedding) { if(!NeuronOCL || !Support || !Embedding) return false; //--- uint cheb_k = cZ_R.GetChebK(); uint units = cZ_R.GetCount(); uint window_out = Neurons() / units; uint window = cZ_R.GetWindow() - window_out;
Só depois de confirmar que tudo está no lugar, passamos ao cálculo dos parâmetros principais: a ordem do polinômio de Chebyshev, a quantidade de elementos da sequência e a dimensão dos atributos na entrada e na saída. Esses valores definem a base para a propagação dos gradientes, determinando como o sinal de erro se deslocará pela arquitetura.
Na primeira etapa, é processado o gradiente da operação de combinação elemento a elemento do estado oculto com os candidatos por meio da porta R. A operação considera a influência de todos os componentes usados. Imagine que avaliamos a contribuição de cada indicador para o resultado da estratégia: alguns sinais são ajustados com mais intensidade, outros com menos, dependendo de sua influência sobre a previsão.
if(!GateElementMultGrad(PrevOutput, cR.getPrevOutput(), cHCe.getOutput(), cHCe.getGradient(), cR.getOutput(), cR.getGradient(), Gradient, None, TANH, cR.Activation())) return false;
Em seguida, passamos o gradiente do erro pela operação de multiplicação matricial para atualizar os blocos cHC e cHC_emb. Isso se parece com a filtragem de sinais de mercado: o erro atravessa todas as camadas e é distribuído cuidadosamente, para que cada componente da rede receba sua parcela de ajuste.
if(!MatMulGrad(cHC.getOutput(), cHC.getGradient(), cHC_emb.getOutput(), cHC_emb.getGradient(), cHCe.getGradient(), 1, (window + window_out)*cheb_k, window_out, units, true)) return false;
A chamada de calcInputGradients para cHC garante que os gradientes sejam propagados corretamente até o candidato do estado oculto, como se verificássemos não apenas os resultados de operações individuais, mas também sua inter-relação na estratégia de trading como um todo.
if(!cHC.calcInputGradients(cCandidate.AsObject(), Support)) return false; if(!DeConcat(NeuronOCL.getGradient(), cZ.getPrevOutput(), cCandidate.getGradient(), window, window_out, units)) return false;
Depois disso, é realizada a desconcatenação, ou seja, a distribuição dos gradientes do erro entre os dados de entrada e os candidatos do estado oculto.
A passagem dos gradientes do erro pela operação de multiplicação elemento a elemento e a concatenação posterior formam os gradientes corretos para as portas Z e R. Já o ajuste dos valores obtidos pela derivada da função de ativação garante o ajuste correto da magnitude do erro, evitando a sobrecarga do modelo.
if(!ElementMultGrad(cZ.getOutput(), cZ.getGradient(), PrevOutput, cCandidate.getPrevOutput(), cZ.getPrevOutput(), None, None)) return false; if(!Concat(cZ.getGradient(), cR.getGradient(), cZe_Re.getGradient(), window_out, window_out, units)) return false; if(!DeActivation(cZe_Re.getOutput(), cZe_Re.getGradient(), cZe_Re.getGradient(), SIGMOID)) return false;
Os polinômios de Chebyshev merecem atenção especial. Primeiro, é preservado o gradiente anterior recebido do objeto cHC. Em seguida, é chamado calcInputGradients para cZ_R, depois os gradientes acumulados são cuidadosamente somados. Isso permite que o modelo considere a influência direta e indireta dos nós vizinhos do grafo, garantindo uma atualização estável e correta dos pesos da convolução em grafos.
if(!MatMulGrad(cZ_R.getOutput(), cZ_R.getGradient(), cZ_R_emb.getOutput(), cZ_R_emb.getGradient(), cZe_Re.getGradient(), 1, (window + window_out)*cheb_k, 2 * window_out, units, true)) return false; CBufferFloat* temp = Support.getGradient(); if(!Support.SetGradient(Support.getPrevOutput(), false) || !cZ_R.calcInputGradients(cInpAndHidden.AsObject(), Support) || !SumAndNormilize(temp, Support.getGradient(), temp, Support.GetDimension(), false, 0, 0, 0, 1) || !Support.SetGradient(temp, false)) return false;
A passagem dos gradientes para o nível dos dados de entrada conclui o fluxo principal de informação, combinando as informações provenientes dos dois fluxos principais, com posterior ajuste pela derivada da função de ativação.
if(!DeConcat(cInpAndHidden.getPrevOutput(), cCandidate.getPrevOutput(), cInpAndHidden.getGradient(), window, window_out, units)) return false; if(!SumAndNormilize(NeuronOCL.getGradient(), cInpAndHidden.getPrevOutput(), NeuronOCL.getGradient(), window, false, 0, 0, 0, 1)) return false; if(NeuronOCL.Activation() != None) if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(), NeuronOCL.getGradient(), NeuronOCL.Activation())) return false;
Depois, os embeddings passam por um procedimento análogo: seus gradientes são ajustados por meio de cHC_emb e cZ_R_emb. Em seguida, são somados, garantindo a atualização coordenada de todos os parâmetros.
if(!Embedding.CalcHiddenGradients(cHC_emb.AsObject())) return false; temp = Embedding.getGradient(); if(!Embedding.SetGradient(Embedding.getPrevOutput(), false) || !Embedding.CalcHiddenGradients(cZ_R_emb.AsObject()) || !SumAndNormilize(temp, Embedding.getGradient(), temp, cZ_R_emb.GetWindow(), false, 0, 0, 0, 1) || !Embedding.SetGradient(temp, false)) return false; //--- return true; }
Como resultado, calcInputGradients se transforma em um sistema sofisticado de redistribuição dos erros por toda a arquitetura GCRU. Na prática, é como um trader que não apenas registra os erros da estratégia, mas analisa detalhadamente cada componente: padrões temporais e espaciais, conexões entre ativos, reação a eventos de mercado, e usa essas informações para melhorar a precisão das previsões futuras. Essa abordagem torna o treinamento do modelo vivo, dinâmico e o mais próximo possível da lógica real do mercado.
Aqui chegamos à parte aparentemente mais modesta do mecanismo: a atualização dos pesos. No entanto, como costuma ocorrer em arquiteturas com uma lógica interna cuidadosamente pensada, por trás dessa concisão existe uma hierarquia bem estruturada de gerenciamento dos parâmetros. Todo o sistema complexo, com portas, polinômios de Chebyshev, multiplicações matriciais e ativações, no fim das contas se reduz a ajustar corretamente apenas dois objetos principais: cZ_R_emb e cHC_emb. É como se, em um mecanismo de relojoaria complexo com dezenas de engrenagens, apenas dois eixos de regulagem definissem a precisão de todo o funcionamento.
O método updateInputWeights começa com uma verificação simples, mas essencial: a existência do objeto de embeddings. Sem ele, a atualização não faz sentido, pois é por meio dele que o modelo absorve as conexões espaciais e semânticas do grafo. Se o objeto estiver ausente, o método interrompe imediatamente a execução, retornando false.
bool CNeuronHimNetGCRU::updateInputWeights(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support, CNeuronBaseOCL *Embedding) { if(!Embedding) return false; //--- if(!cZ_R_emb.UpdateInputWeights(Embedding)) return false; if(!cHC_emb.UpdateInputWeights(Embedding)) return false; //--- return true; }
Em seguida, o controle é passado aos dois principais conjuntos de parâmetros treináveis: primeiro, são atualizados os pesos responsáveis pelo bloco Z-R (cZ_R_emb); depois, os pesos do bloco candidato H-C (cHC_emb). As duas chamadas de UpdateInputWeights são executadas sequencialmente, como dois parafusos de regulagem precisos, cada um influenciando sua própria parte do nó computacional.
Vale destacar que essa concisão é resultado de uma arquitetura bem estruturada. Todas as demais camadas, buffers intermediários e portas já receberam seus gradientes durante a propagação reversa do erro. Aqui, na prática, aplicamos efetivamente essas mudanças aos parâmetros que realmente determinam o comportamento do modelo no longo prazo. Em outras palavras, se o método anterior (calcInputGradients) pode ser comparado à análise das causas dos erros passados, updateInputWeights é o ato de ajustar o sistema de trading: não apenas tiramos conclusões, mas as incorporamos às configurações, para que amanhã a previsão seja mais precisa e robusta.
Essa abordagem torna a arquitetura não apenas eficiente, mas também previsível: alterar apenas dois pontos permite controlar uma enorme cadeia computacional, preservando sua integridade e consistência.
Hoje avançamos bastante, e agora é hora de fazer uma breve pausa. No próximo artigo, continuaremos esse caminho. Concluiremos o que foi iniciado, levando a implementação ao seu ponto lógico final, e faremos sua validação com dados históricos reais, para ver como a teoria se manifesta na prática.
Conclusão
Neste artigo, concentramos nossa análise em um dos componentes centrais de toda a arquitetura e analisamos passo a passo seu funcionamento, desde a formação das conexões internas até o mecanismo de retropropagação e o procedimento de atualização dos parâmetros. Vimos como módulos separados, à primeira vista autônomos, começam a interagir de forma coordenada, criando uma estrutura consistente, pronta para treinamento e operação em condições reais de mercado. É especialmente importante destacar que um sistema tão bem articulado se apoia em apenas dois objetos com parâmetros treináveis, o que o torna não só compacto, mas também previsível em sua gestão.
Nesta etapa, obtivemos um componente quase totalmente concluído da futura cadeia computacional, pronto para receber a carga de dados reais de trading. No próximo artigo, daremos o passo decisivo: integraremos todos os componentes em uma estrutura unificada, realizaremos testes completos em séries temporais históricas e veremos até que ponto o algoritmo desenvolvido é capaz não apenas de reproduzir padrões do passado, mas também de se orientar com confiança no fluxo instável do mercado.
Referências
- Heterogeneity-Informed Meta-Parameter Learning for Spatiotemporal Time Series Forecasting
- Outros artigos da série
Programas usados 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 da rede neural |
| 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/19250
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.
Caminhe em novos trilhos: Personalize indicadores no MQL5
Análise da variação por hora dos símbolos de negociação e de seus spreads no MetaTrader 5
Está chegando o novo MetaTrader 5 e MQL5
Redes neurais em trading: treinamento de metaparâmetros com base na heterogeneidade (HimNet)
- 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