Redes neurais em trading: Modelos de refinamento iterativo de previsões (Componentes principais)
Introdução
O mercado financeiro moderno não é apenas um conjunto de cotações e índices. É um sistema complexo e dinâmico, no qual cada variação de preço reflete toda uma rede de causas, interações e expectativas. O fluxo de dados que chega a cada segundo se assemelha a uma corrente contínua, em que cada instante carrega significado. Enxergar esse movimento significa aprender a interpretar o mercado como um processo, e não apenas como um resultado. É justamente esse o objetivo do framework RAFT, Recurrent All-Pairs Field Transforms, cuja arquitetura foi originalmente criada para compreender a dinâmica em sequências de imagens, mas também se adapta perfeitamente à natureza das séries temporais do mercado.
O conceito central do framework RAFT é a capacidade de raciocinar em termos de movimento, continuidade e refinamento.
No trading clássico, o preço é tratado como uma sequência de valores discretos: candles, barras e ticks. O analista observa essa sequência e tenta identificar algum padrão. O mercado real, porém, flui como um rio. Ele não salta de um ponto para outro, mas transita continuamente entre estados, alterando direção e velocidade sob a influência de milhares de fatores diferentes. É justamente nesse ponto que a filosofia do RAFT se manifesta com maior clareza. O modelo não percebe os dados como um conjunto de imagens estáticas, mas como um fluxo. Cada novo estado está ligado ao anterior por inúmeras dependências ocultas. Em visão computacional, isso corresponde à estimativa de fluxo óptico. O mais importante não é aquilo que aparece em cada quadro isoladamente, mas como e para onde tudo se desloca entre um quadro e outro. Em finanças, isso significa não apenas prever o próximo preço, mas compreender a direção e a intensidade do movimento do mercado.
O RAFT se baseia na ideia de refinamento contínuo da previsão. O modelo não chega imediatamente a uma conclusão definitiva. Em vez disso, retorna várias vezes à estimativa, confrontando a cada iteração as novas informações com os resultados já obtidos. Esse caráter recorrente lembra a atuação de um analista experiente, que ajusta suas hipóteses à medida que chegam novos dados, sem perder o contexto geral. Graças a isso, o RAFT mantém elevada robustez mesmo diante de fortes oscilações do mercado, quando algoritmos tradicionais perdem a sincronia com o fluxo dos acontecimentos. No contexto da análise financeira, essa característica é especialmente importante, pois o mercado raramente é previsível. Impulsos dão lugar a correções, enquanto tendências aparentemente estáveis podem desmoronar em questão de minutos. A capacidade do modelo de revisar suas próprias decisões o aproxima de uma forma de raciocínio mais compatível com a realidade: flexível, iterativa e capaz de se autoadaptar.
Do ponto de vista metodológico, a estrutura interna do RAFT se apoia em três ideias interligadas:
- extração de atributos,
- estabelecimento das relações entre eles,
- refinamento iterativo dessas relações.
Os atributos descrevem o estado do sistema. No nosso caso, são características do mercado: volatilidade, impulso, equilíbrio entre oferta e demanda e correlações entre ativos. Em seguida, é construído um campo com todas as interações possíveis, uma espécie de mapa das relações do mercado, no qual cada ponto representa a influência mútua entre os estados do sistema ao longo do tempo. Por fim, o bloco recorrente refina esse campo passo a passo, aproximando o modelo da representação mais fiel possível do movimento do mercado. Assim, o RAFT não se limita a buscar uma resposta: ele mantém um "diálogo" com os dados, refinando suas conclusões a cada etapa.
À primeira vista, pode parecer que visão computacional e dados financeiros não têm nada em comum. Mas, se substituirmos os quadros por janelas temporais e os pixels pelos atributos dos ativos, fica evidente como esses dois universos são próximos. Em ambos os casos, lidamos com um fluxo de informações no qual cada mudança só adquire significado no contexto das anteriores. Cada novo tick não é um evento isolado, mas a continuação da história do mercado, integrada ao movimento como um todo. É justamente esse contexto que o RAFT constrói ao formar um campo de deslocamentos, algo semelhante a um mapa dos fluxos de liquidez, no qual é possível observar a direção e a intensidade das mudanças. Isso abre possibilidades não apenas para a previsão, mas também para a interpretação. O pesquisador passa a contar com uma ferramenta de visualização da dinâmica do mercado, capaz de mostrar onde as tendências ganham velocidade, onde começam a surgir reversões e onde as forças do mercado entram em equilíbrio.
Ao contrário de sistemas de redes neurais mais pesados, o RAFT combina eficiência computacional com profundidade conceitual. Sua estrutura é concisa, e o refinamento iterativo não exige um grande volume de recursos computacionais. Isso é especialmente valioso para uma implementação em MQL5, onde a eficiência do código e a velocidade de processamento dos dados são requisitos fundamentais. O modelo pode ser adaptado às condições reais do mercado sem comprometer a precisão nem a estabilidade. Além disso, sua natureza recorrente permite responder em tempo real ao fluxo de ticks, em vez de se limitar à análise de amostras históricas.
Talvez, porém, a principal vantagem do RAFT seja sua capacidade de adaptação. Ele não fica preso a um único padrão nem depende de um esquema aprendido uma única vez. Cada nova fatia temporal oferece ao modelo uma oportunidade de revisar suas próprias conclusões. É aí que se manifesta sua filosofia: em vez de tentar prever o mercado como um objeto estático, o modelo se adapta ao seu movimento e ajusta continuamente sua interpretação à dinâmica do mercado. Em um ambiente de mudanças constantes, essa capacidade pode ser mais valiosa do que qualquer medida isolada de precisão, pois o mercado raramente se comporta de forma previsível, mas está sempre em movimento.
Neste trabalho, concentraremos nossa atenção na arquitetura e na estrutura do framework RAFT. Analisaremos seus princípios de funcionamento e prepararemos a base para uma implementação passo a passo em MQL5. O objetivo é simples e, ao mesmo tempo, ambicioso: mostrar como um modelo criado para analisar o movimento em fluxos de vídeo pode se tornar uma ferramenta poderosa para a análise da dinâmica dos dados financeiros. Afinal, se aprendermos a enxergar uma estrutura nesse fluxo, poderemos nos aproximar daquilo que sempre foi aquilo que sempre foi o sonho de todo trader: compreender para onde o preço está se movendo e por que está seguindo justamente nessa direção.
A visualização do framework RAFT proposta pelo autor é apresentada abaixo.

Módulo de correlação multiescala
Para que o framework RAFT passe a "falar a linguagem dos mercados", começamos pela construção de um mapa de correlação dos atributos. Como base, é natural aproveitar os desenvolvimentos já realizados durante o trabalho com o STE-FlowNet. Assim como anteriormente, em vez do estado inicial fixo proposto pelos autores do framework, utilizamos uma pilha com os estados mais recentes do sistema. Essa abordagem permite que o modelo observe não apenas o valor atual de um indicador, mas também o contexto de seu movimento: dinâmica, aceleração e inércia. É como se o trader, em vez de avaliar um único candle, analisasse todo o histórico das oscilações de preço, estabelecendo uma relação de causa e efeito para explicar seu comportamento.
No entanto, o mapa de correlações clássico do RAFT se baseia em um espaço de coordenadas, no qual o deslocamento dos objetos possui direção e escala. Nos dados financeiros, essa grade de coordenadas não existe. Os impulsos não se deslocam entre os atributos, mas evoluem ao longo do tempo. Por isso, no nosso caso, o escalonamento passa a ser aplicado ao eixo temporal. Criamos um equivalente da pirâmide de correlações, mas estruturado ao longo da escala de tempo. Cada nível representa uma determinada profundidade histórica, desde oscilações de curto prazo até tendências fundamentais. Isso permite que o RAFT observe o mercado em diferentes escalas temporais, captando tanto micro-oscilações quanto estruturas ondulatórias de maior duração e formando uma representação dos dados mais robusta e adaptável.
Em seguida, construímos o mapa multiescala de atributos. Inicialmente, ele é gerado para cada intervalo temporal, registrando tanto as mudanças rápidas quanto as mais lentas. Todos esses níveis são então reunidos em um tensor comum, que é encaminhado à pilha de estados. Essa hierarquia permite que o modelo considere simultaneamente as reações imediatas do mercado e seus movimentos de caráter inercial, algo que muitas vezes se perde nos métodos clássicos.
Na parte prática do artigo anterior, implementamos um kernel OpenCL responsável por agregar e calcular a média dos atributos em todos os níveis da pirâmide temporal. Essa etapa permitiu formar representações consistentes dos dados em diferentes escalas, criando uma base sólida para os cálculos subsequentes. Agora avançamos para a criação, no programa principal, de um objeto especializado em correlação multiescala de atributos. Ele se tornará o núcleo da nossa implementação do mecanismo RAFT no contexto financeiro, funcionando como uma espécie de visão do sistema capaz de identificar relações entre estados do mercado considerando diferentes profundidades históricas.
class CNeuronMultiScaleStackCorrelation : public CNeuronStackCorrelation { protected: uint iLevels; CNeuronBaseOCL cScales; //--- virtual bool AggregationByTime(CNeuronBaseOCL *NeuronOCL); virtual bool AggregationByTimeGrad(CNeuronBaseOCL *NeuronOCL); //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronMultiScaleStackCorrelation(void) {}; ~CNeuronMultiScaleStackCorrelation(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint stack_size, uint dimension, uint variables, uint levels, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronMultiScaleStackCorrelation; } //--- methods for working with files virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual uint GetLevels(void) const { return iLevels; } virtual uint GetVariables(void) override const { return GetVariables() / iLevels; } };
O novo objeto herda a funcionalidade básica da classe CNeuronStackCorrelation e amplia o mecanismo de correlação entre estados ao adicionar suporte à análise multiescala, ou seja, à capacidade de examinar os dados simultaneamente em vários níveis de profundidade temporal. No contexto das séries temporais financeiras, isso equivale a observar o movimento do preço não apenas no curto prazo, mas também em timeframes maiores.
Na estrutura apresentada, adicionamos a variável iLevels, que define o número de escalas da pirâmide temporal. Em outras palavras, ela determina quantos níveis de análise serão utilizados pelo sistema. Cada nível corresponde a uma determinada janela temporal.
O objeto cScales é uma instância da classe base CNeuronBaseOCL e destina-se ao armazenamento temporário da pirâmide de atributos do estado analisado.
O método Init atua como uma espécie de arquiteto: ele não apenas prepara o objeto para operar, mas também estabelece toda a infraestrutura necessária para a percepção multiescala dos dados. É justamente nesse ponto que a lógica matemática se integra à arquitetura computacional, transformando a ideia abstrata de uma pirâmide temporal em um mecanismo funcional.
bool CNeuronMultiScaleStackCorrelation::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint stack_size, uint dimension, uint variables, uint levels, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronStackCorrelation::Init(numOutputs, myIndex, open_cl, stack_size, dimension, variables * levels, optimization_type, batch)) return false;
A partir da chamada do método, começa a inicialização passo a passo da estrutura que reúne diferentes escalas temporais de análise em um único sistema. Como parâmetros de entrada, são fornecidas as principais configurações: número de saídas, índice do neurônio atual, referência ao objeto de gerenciamento do OpenCL, profundidade da pilha temporal, dimensionalidade dos dados, número de atributos, quantidade de escalas, tipo de otimização e tamanho do lote de dados. Em conjunto, esses parâmetros determinam como o neurônio perceberá o contexto temporal, até que ponto conseguirá "olhar" para o passado e com que nível de detalhamento analisará o mercado.
Na primeira etapa, é chamado o método homônimo da classe pai. Essa chamada pode ser vista como o lançamento dos alicerces da estrutura. Ela cria a estrutura principal da pilha de correlações, aloca memória e prepara os buffers OpenCL. Há, porém, uma diferença fundamental: o número de atributos é multiplicado pela quantidade de níveis da pirâmide temporal. Esse procedimento cria um espaço de atributos unificado, no qual cada nível recebe seu próprio conjunto de dados e representa o mercado com uma profundidade temporal diferente. Se, por qualquer motivo, a inicialização da classe base falhar, o método interrompe imediatamente a execução para evitar erros imprevisíveis.
Após a conclusão bem-sucedida da inicialização da classe base, o número de níveis da pirâmide temporal é armazenado na variável iLevels. Esse valor passa a servir de referência interna para todos os cálculos subsequentes e determina quantas escalas temporais participarão da construção da estrutura de correlação.
iLevels = levels; if(!cScales.Init(0, 0, OpenCL, dimension * levels * variables, optimization, iBatch)) return false; //--- return true; }
Na etapa seguinte, é inicializado o objeto cScales, responsável por armazenar os atributos correspondentes às diferentes escalas. Em essência, é nesse ponto que se forma um equivalente digital de uma percepção em múltiplas camadas: o sistema passa a observar o mercado simultaneamente em vários horizontes temporais, desde oscilações de curto prazo até tendências de longo prazo.
Se o contêiner cScales for criado com sucesso, o método conclui a inicialização e retorna true. Esse encerramento breve, mas importante, indica que o objeto está pronto para funcionar. A partir desse momento, CNeuronMultiScaleStackCorrelation se torna um nó plenamente funcional do grafo computacional, capaz de acumular, representar em diferentes escalas e interpretar a dinâmica dos dados financeiros ao longo do tempo.
Tudo o que acontece dentro desse método pode ser comparado à configuração de um instrumento analítico complexo, no qual cada parâmetro possui significado e finalidade próprios.
O algoritmo de propagação para frente da classe CNeuronMultiScaleStackCorrelation implementa a lógica central do processamento de correlação multiescala dos dados. É nesse ponto que se combinam a agregação temporal, a análise em diferentes escalas e as transformações matriciais que formam a resposta final do bloco neural.
bool CNeuronMultiScaleStackCorrelation::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!AggregationByTime(NeuronOCL)) return false;
A execução do método começa com a chamada de AggregationByTime, que funciona como um wrapper para o kernel OpenCL homônimo implementado na parte prática do artigo anterior. Nessa etapa, os atributos atuais são agregados às informações dos estados anteriores armazenados na pilha, formando uma estrutura logicamente consistente ao longo do eixo temporal. Esse procedimento permite identificar dependências dinâmicas persistentes e, ao mesmo tempo, suavizar oscilações aleatórias características das séries temporais financeiras.
A etapa seguinte consiste em adicionar as informações obtidas à pilha do histórico de estados.
if(!cStack.FeedForward(cScales.AsObject())) return false;
Depois que todos os componentes de escala são calculados, o método realiza a multiplicação matricial entre a pilha do histórico acumulado e a representação detalhada do estado analisado. É nesse ponto que os atributos das diferentes escalas são combinados no vetor de resposta final.
if(!MatMul(cStack.getOutput(), NeuronOCL.getOutput(), Output, GetStackSize() * GetLevels(), GetDimension(), 1, GetVariables(), true)) return false;
Observe que os parâmetros da função definem explicitamente as dimensões das matrizes. Uma matriz de dimensões [(StackSize × Levels), Dimension] é multiplicada por um vetor-coluna [Dimension]. Ao mesmo tempo, são calculados em paralelo tantos produtos independentes quanto o valor de Variables, formando as matrizes de correlação de cada sequência unitária no horizonte definido e para o número especificado de escalas. Em essência, essa etapa sintetiza as informações, reunindo todos os níveis de correlação e todas as escalas em um único resultado consolidado, pronto para o processamento subsequente.
Na etapa final, é aplicada a função de ativação, naturalmente, caso ela tenha sido definida pelo usuário.
if(activation != None) if(!Activation(Output, Output, activation)) return false; //--- return true; }
A lógica do método é clara e bem estruturada: parte da agregação temporal, passa pela análise multiescala, avança para a síntese das informações e termina com a resposta não linear. Essa arquitetura permite extrair com eficiência dependências ocultas de dados dinâmicos e multidimensionais, preservando ao mesmo tempo a estabilidade estrutural e a interpretabilidade do resultado.
Como você deve ter percebido, este módulo não contém parâmetros treináveis. Ainda assim, ele desempenha um papel importante como um dos componentes do nosso modelo neural. E, como sabemos, o modelo como um todo precisa ser treinado. Para que o módulo de correlação multiescala não se torne um obstáculo durante o treinamento, precisamos garantir que os gradientes do erro sejam propagados corretamente até todos os componentes do grafo computacional, de acordo com a contribuição de cada um para o resultado final. Esse mecanismo é implementado no método calcInputGradients.
bool CNeuronMultiScaleStackCorrelation::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false;
A execução do método começa verificando se o ponteiro recebido para o objeto com os dados de origem é válido, evitando assim chamadas inválidas.
O cálculo dos gradientes propriamente dito começa com a chamada da função MatMulGrad, responsável por calcular os gradientes da multiplicação matricial e propagá-los das matrizes de correlação de saída para os dados de origem e para a pilha de atributos multiescala.
if(!MatMulGrad(cStack.getOutput(), cStack.getGradient(), NeuronOCL.getOutput(), cScales.getGradient(), Gradient, GetStackSize()*GetLevels(), GetDimension(), 1, GetVariables(), true)) return false;
Nesse cálculo, são considerados todos os níveis da pirâmide temporal e cada um dos atributos, enquanto a execução paralela garante eficiência no processamento de séries temporais extensas.
Em seguida, é chamada a função AggregationByTimeGrad, responsável pela propagação reversa dos gradientes pela agregação temporal. Essa etapa garante que a contribuição de cada estado temporal seja considerada corretamente durante o ajuste dos parâmetros do modelo.
if(!AggregationByTimeGrad(NeuronOCL)) return false;
É importante observar que os parâmetros do estado analisado participam simultaneamente de duas operações: da multiplicação matricial usada para calcular as correlações e da agregação dos atributos multiescala. Por isso, torna-se necessário combinar cuidadosamente os gradientes provenientes desses dois fluxos de informação. É exatamente essa a função de SumAndNormilize. Ela soma os valores, mantendo o equilíbrio e a consistência dos gradientes. Dessa forma, o modelo atualiza corretamente seus componentes treináveis, sem superestimar a influência de determinados níveis em relação aos demais.
if(!SumAndNormilize(NeuronOCL.getGradient(), cScales.getGradient(), NeuronOCL.getGradient(), GetDimension(), false, 0, 0, 0, 1)) return false; Deactivation(NeuronOCL) //--- return true; }
A etapa final é a chamada de Deactivation, que ajusta os gradientes aplicando a derivada da função de ativação, deixando-os na forma adequada para a propagação subsequente.
Assim, o método calcInputGradients transforma o módulo de correlação multiescala de um bloco passivo em um componente ativo do treinamento, distribuindo cuidadosamente o erro entre todos os componentes relacionados e garantindo um treinamento estável e correto de todo o modelo neural.
Bloco 2D-GRU
Depois de montarmos o módulo de correlação multiescala, é natural voltarmos nossa atenção para o próximo componente crítico: as GRUs convolucionais. Os autores do framework RAFT propõem não uma, mas duas unidades GRU convolucionais executadas em sequência. Cada uma delas opera em uma dimensão diferente do plano de coordenadas. Na tarefa de visão computacional, isso significa que o primeiro conjunto de convoluções pode capturar dependências horizontais, enquanto o segundo captura as dependências verticais. A atuação sequencial de ambos produz uma representação mais profunda dos deslocamentos locais e globais. No contexto financeiro, a mesma ideia é adaptada para diferentes eixos de análise. Um ramo GRU pode modelar a dinâmica temporal dentro de um mesmo conjunto de atributos, enquanto o outro captura as interações entre diferentes conjuntos de atributos. O requisito fundamental aqui não é simplesmente conectar em sequência dois módulos já existentes, mas criar um fluxo de dados único, no qual os estados ocultos e os dados de origem sejam utilizados de forma conjunta e sincronizada.
Se tentarmos simplesmente encadear duas implementações padrão, encontraremos uma limitação. Cada uma operará em seu próprio espaço de estados e não conseguirá compartilhar de maneira eficiente o estado oculto com o ramo adjacente nem acessar simultaneamente os mesmos dados de correlação de origem. Precisamos de um módulo que implemente internamente as duas etapas da GRU convolucional, mas que seja tratado pela arquitetura como um único nó, com interfaces comuns, gestão unificada da memória e um protocolo previsível para a passagem dos estados ocultos. Esse módulo deve receber como entrada os mapas de correlação agregados e os atributos, manter buffers internos para os estados ocultos das duas etapas e permitir que ambas acessem em paralelo os tensores de origem sem cópias redundantes.
Implementamos a ideia de processamento bidimensional proposta pelos autores do framework RAFT na classe CNeuronSpikeConvGRU2D, uma versão ampliada da GRU convolucional, especialmente adaptada para analisar sinais em duas direções. Esse módulo herda a funcionalidade básica de CNeuronSpikeConvGRU, mas acrescenta um segundo nível de cálculos. Isso permite considerar simultaneamente as dependências temporais e as relações entre os atributos. Essa estrutura proporciona uma percepção mais profunda dos dados, em que cada etapa do modelo se apoia não apenas no estado anterior, mas também no contexto espacial, aumentando a precisão do refinamento iterativo da previsão.
class CNeuronSpikeConvGRU2D : public CNeuronSpikeConvGRU { protected: CNeuronConvOCL cProjectionX; CNeuronBaseOCL cConcatenated2[2]; CNeuronConvOCL cZR2; CNeuronBaseOCL cZ2; CNeuronBaseOCL cR2; CNeuronConvOCL cHt2; CNeuronBaseOCL cH2; CNeuronTransposeOCL cTranspose[3]; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronSpikeConvGRU2D(void) {}; ~CNeuronSpikeConvGRU2D(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units, uint chanels_in, uint chanels_out, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronSpikeConvGRU2D; } //--- methods for working with files virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; //--- virtual uint GetChanels(void) const { return cHt.GetFilters(); } virtual uint GetUnits(void) const { return cHt.GetUnits(); } };
Na seção protected da classe estão localizados os principais nós computacionais. O componente cProjectionX projeta os atributos de entrada em um espaço alternativo, formando uma representação independente para a segunda iteração da GRU e compatibilizando a dimensionalidade dos atributos dos dados de origem com a dimensão analisada do estado oculto. Em essência, ele cria uma perspectiva adicional sobre os dados, proporcionando sensibilidade cruzada às dependências temporais e correlacionais. Isso é especialmente importante em tarefas de análise financeira, nas quais o comportamento de um único ativo pode influenciar todo um grupo de instrumentos relacionados.
Em seguida, temos os dois buffers cConcatenated2[2], utilizados para combinar as saídas da primeira etapa com os dados de origem. Esse procedimento cria um contexto enriquecido, reunindo no mesmo tensor tanto os novos atributos quanto os originais. Com isso, a segunda célula GRU recebe uma representação completa do estado do sistema, incluindo tanto a mudança atual quanto sua causa subjacente. Isso se assemelha ao refinamento de uma previsão: primeiro o modelo formula uma estimativa preliminar e, depois, levando em conta o contexto e os dados refinados, ajusta sua decisão.
Os blocos cZR2, cZ2, cR2, cHt2 e cH2 formam a estrutura interna da segunda etapa da GRU. Em conjunto, eles criam um mecanismo fechado de atualização em duas fases, no qual as informações são refinadas de forma gradual e contínua.
Os três objetos cTranspose[3] são responsáveis por reorientar os dados entre as etapas. No contexto financeiro, isso permite alternar entre a dimensão temporal e a dimensão dos fatores, analisando os mesmos atributos ora como dinâmica ao longo do tempo, ora como relações entre os ativos. Isso confere ao modelo uma percepção multidimensional do mercado, fazendo com que a análise deixe de ser linear e passe a considerar a dimensão espacial.
A inicialização dos componentes internos do módulo é uma etapa cuidadosamente estruturada, na qual cada elemento recebe seu lugar e sua função dentro da arquitetura geral. Tudo começa com a chamada do método homônimo da classe pai, que prepara a base da arquitetura com as principais propriedades herdadas da GRU convolucional.
bool CNeuronSpikeConvGRU2D::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units, uint chanels_in, uint chanels_out, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronSpikeConvGRU::Init(numOutputs, myIndex, open_cl, units, chanels_in, chanels_out, optimization_type, batch)) return false;
Essa etapa está longe de ser apenas uma formalidade. Ela garante a configuração correta do ambiente computacional OpenCL, vincula o objeto ao contexto de processamento e cria as estruturas de dados básicas sobre as quais será construída toda a lógica subsequente.
Após a inicialização bem-sucedida do nível base, é ativado o bloco cProjectionX, que funciona como um transformador de projeção. Sua tarefa é adaptar os tensores de entrada às dimensões internas do modelo, ou seja, uniformizar o número de canais e as dimensões dos filtros de acordo com a estrutura interna do modelo. Em essência, essa camada atua como uma ponte de entrada, reduzindo as diferenças entre as escalas dos dados e preparando-os para um processamento mais profundo. Sua função de ativação é deliberadamente desativada, pois, nesta etapa, é necessário preservar a estrutura original dos atributos sem introduzir distorções adicionais.
uint index = 0; if(!cProjectionX.Init(0, index, OpenCL, chanels_in, chanels_in, chanels_out, units, optimization, iBatch)) return false; cProjectionX.SetActivationFunction(None);
Em seguida, são inicializados dois objetos cConcatenated2, cada um responsável por combinar dados provenientes de diferentes fontes. A ausência de ativação também é intencional neste caso: a concatenação deve permanecer linear para não comprometer o equilíbrio dos sinais entre os diferentes ramos do modelo.
index++; for(uint i = 0; i < cConcatenated2.Size(); i++) { if(!cConcatenated2[i].Init(0, index, OpenCL, 2 * units * chanels_in, optimization, iBatch)) return false; cConcatenated[i].SetActivationFunction(None); index++; }
Na etapa seguinte, são criados os blocos de transposição cTranspose. São três, e cada um desempenha uma função bem definida: o primeiro projeta os dados do estado oculto do passo anterior, o segundo realiza uma projeção equivalente dos dados de origem e o terceiro restaura a estrutura original ao encaminhar o novo estado oculto para o nível seguinte. Essas operações podem ser comparadas a virar um tabuleiro de xadrez: a orientação dos dados muda, mas suas relações lógicas permanecem intactas. Esse procedimento permite trabalhar com as informações simultaneamente em duas dimensões, o que constitui uma das características centrais da abordagem RAFT.
if(!cTranspose[0].Init(0, index, OpenCL, units, chanels_out, optimization, iBatch)) return false; index++; if(!cTranspose[1].Init(0, index, OpenCL, units, chanels_out, optimization, iBatch)) return false; index++; if(!cTranspose[2].Init(0, index, OpenCL, chanels_out, units, optimization, iBatch)) return false;
Em seguida, começa a construção do segundo nível da GRU, representada pelos objetos cZR2, cZ2, cR2 e cHt2. O componente cZR2 forma o vetor das portas de controle de atualização e reset, responsáveis por determinar que parte das informações anteriores deve ser preservada e que parte deve ser substituída por novos dados. Sua ativação sigmoide torna esse mecanismo de decisão suave e diferenciável, algo especialmente importante para o treinamento correto do modelo.
index++; if(!cZR2.Init(0, index, OpenCL, 2 * units, 2 * units, 2 * units, chanels_out, 1, optimization, iBatch)) return false; cZR2.SetActivationFunction(SIGMOID);
Os blocos cZ2 e cR2 separam os sinais de controle e os preparam para a etapa seguinte, o cálculo do candidato ao novo estado em cHt2. Aqui é utilizada a função de ativação TANH, que mantém os valores em uma faixa equilibrada e contribui para a estabilidade do treinamento, mesmo em séries temporais longas.
index++; if(!cZ2.Init(0, index, OpenCL, cZR2.Neurons() / 2, optimization, iBatch)) return false; cZ2.SetActivationFunction((ENUM_ACTIVATION)cZR2.Activation()); index++; if(!cR2.Init(0, index, OpenCL, cZR2.Neurons() / 2, optimization, iBatch)) return false; cR2.SetActivationFunction((ENUM_ACTIVATION)cZR2.Activation()); index++; if(!cHt2.Init(0, index, OpenCL, 2 * units, 2 * units, units, chanels_out, 1, optimization, iBatch)) return false; cHt2.SetActivationFunction(TANH);
Toda essa cadeia lembra um mecanismo de precisão, no qual cada engrenagem responde por uma parte específica do movimento, mas todas trabalham em conjunto para produzir um ritmo harmonioso e sincronizado.
O elemento final da inicialização é cH2, responsável por acumular os resultados do processamento. Esse bloco reúne todos os cálculos intermediários em uma única representação coerente do estado atual do sistema. A partir desse ponto, o modelo está pronto para receber o fluxo de dados e realizar uma análise multidimensional, identificando relações que permanecem fora do alcance das arquiteturas unidimensionais clássicas.
index++; if(!cH2.Init(0, index, OpenCL, units * chanels_out, optimization, iBatch)) return false; //--- return true; }
Assim, o método Init estabelece todo um ecossistema computacional, no qual cada componente ocupa uma posição bem definida e interage com os demais por meio de uma troca coordenada de informações.
O algoritmo de propagação para frente implementado no método feedForward consiste em uma sequência de cálculos cuidadosamente estruturada, na qual a lógica recorrente bidimensional se combina com os princípios do processamento convolucional dos dados. Cada etapa não representa apenas uma operação isolada, mas um elo em uma cadeia de transformações destinada a extrair e sintetizar relações profundas entre os componentes espaciais e temporais do fluxo de entrada.
bool CNeuronSpikeConvGRU2D::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cProjectionX.FeedForward(NeuronOCL)) return false; if(!cTranspose[0].FeedForward(cH.AsObject())) return false; if(!cTranspose[1].FeedForward(cProjectionX.AsObject())) return false;
A execução do algoritmo começa com a transformação por projeção dos dados de origem por meio do bloco cProjectionX. Esse componente atua como um adaptador entre os atributos originais e as representações internas do modelo, convertendo os dados para um formato adequado à análise bidimensional. Em seguida, entram em ação as camadas de transposição cTranspose[0] e cTranspose[1], que reorganizam os tensores ao longo dos eixos de coordenadas. Isso permite combinar as dimensões espacial e temporal, criando a base para a interação cruzada dos dados nos dois sentidos.
Após a preparação dos dados, são calculados os principais parâmetros: o número de passos temporais (units), a quantidade de canais ocultos (hidden) e o tamanho da janela de entrada (inputs). Esses parâmetros definem a configuração arquitetural da iteração atual da propagação para frente e garantem a consistência de todas as operações subsequentes.
uint units = cHt.GetUnits(); uint hidden = cHt.GetFilters(); uint inputs = cHt.GetWindow() - hidden;
Em seguida, é executada a primeira etapa de combinação dos fluxos de dados por meio da operação Concat, que forma o contexto comum necessário ao cálculo das portas de controle do segundo bloco GRU. Nesse ponto, são combinadas as saídas das camadas de transposição.
if(!Concat(cTranspose[0].getOutput(), cTranspose[1].getOutput(), cConcatenated2[0].getOutput(), units, units, hidden)) return false;
O tensor resultante é encaminhado ao bloco cZR2, responsável por gerar os vetores das portas de atualização e reset. Esses sinais de controle funcionam como mecanismos reguladores que determinam que parte das informações anteriores será preservada e que parte será substituída por novos dados.
if(!cZR2.FeedForward(cConcatenated2[0].AsObject())) return false; if(!DeConcat(cZ2.getOutput(), cR2.getOutput(), cZR2.getOutput(), units, units, hidden)) return false;
Depois disso, os dados são separados em dois fluxos, cZ2 e cR2, cada um desempenhando sua função no mecanismo de memória adaptativa. O sinal de reset cR2 interage com a saída da primeira camada de transposição, ajustando o grau de influência do estado anterior.
if(!ElementMult(cR2.getOutput(), cTranspose[0].getOutput(), cR2.getPrevOutput())) return false; if(!Concat(cR2.getPrevOutput(), cTranspose[1].getOutput(), cConcatenated2[1].getOutput(), units, units, hidden)) return false; if(!cHt2.FeedForward(cConcatenated2[1].AsObject())) return false;
Os resultados são novamente combinados por uma segunda operação Concat, formando a base para o cálculo do novo candidato a estado no bloco cHt2. Nessa etapa ocorre uma transformação não linear com ativação TANH, que gera uma representação atualizada dos atributos.
Na sequência, é executada a operação GateElementMult, na qual o estado candidato cHt2 é combinado com o estado anterior sob o controle da porta de atualização cZ2. Isso permite que o sistema ajuste dinamicamente o equilíbrio entre memória e atualização, formando uma representação interna estável, mas ao mesmo tempo flexível.
if(!GateElementMult(cHt2.getOutput(), cTranspose[0].getOutput(), cZ2.getOutput(), cH2.getOutput())) return false; if(!cTranspose[2].FeedForward(cH2.AsObject())) return false;
O resultado obtido é então convertido de volta por meio de cTranspose[2], retornando à dimensão original, onde ocorre a segunda etapa do processamento GRU, agora no sentido clássico, de forma análoga a uma GRU convolucional padrão.
if(!Concat(NeuronOCL.getOutput(), cTranspose[2].getOutput(), cConcatenated[0].getOutput(), inputs, hidden, units)) return false; if(!cZR.FeedForward(cConcatenated[0].AsObject())) return false; if(!DeConcat(cZ.getOutput(), cR.getOutput(), cZR.getOutput(), hidden, hidden, units)) return false; if(!ElementMult(cR.getOutput(), cTranspose[2].getOutput(), cR.getPrevOutput())) return false; if(!Concat(NeuronOCL.getOutput(), cR.getPrevOutput(), cConcatenated[1].getOutput(), inputs, hidden, units)) return false; if(!cHt.FeedForward(cConcatenated[1].AsObject())) return false; if(!GateElementMult(cHt.getOutput(), cTranspose[2].getOutput(), cZ.getOutput(), cH.getOutput())) return false;
Depois disso, a saída passa pelo bloco de normalização cNorm, no qual os valores são estabilizados eliminando os desvios acumulados, e segue para a etapa final, a ativação CNeuronSpikeActivation, que confere à saída do modelo uma resposta não linear compatível com a dinâmica neural do tipo spike.
Como resultado, todo o algoritmo de propagação para frente forma um fluxo coordenado de cálculos, no qual cada nível complementa os demais e permite que o modelo identifique dependências multidimensionais nos dados. Não se trata apenas de uma sequência de operações, mas de uma espécie de diálogo entre espaço e tempo, em que os sinais trocam informações, refinam e reforçam uns aos outros. É justamente essa harmonia arquitetural que torna o módulo CNeuronSpikeConvGRU2D um componente fundamental da implementação do RAFT, proporcionando uma percepção profunda da estrutura dos dados e elevada capacidade de adaptação na análise de processos dinâmicos complexos, desde fluxos de imagens até séries temporais financeiras.
Hoje realizamos um trabalho substancial e produtivo: construímos os principais componentes do framework RAFT e estabelecemos a base de sua arquitetura computacional. Agora é um bom momento para fazer uma breve pausa e assimilar o que foi desenvolvido. Ainda temos pela frente a etapa final: reunir todos os módulos criados em uma única estrutura coordenada. É justamente a isso que dedicaremos o próximo artigo.
Conclusão
Neste artigo, avançamos passo a passo em direção à implementação prática das principais ideias do framework RAFT, adaptando-as às particularidades da análise financeira. Criamos o módulo de correlação multiescala de atributos e implementamos um bloco GRU convolucional bidimensional, capaz de considerar tanto as dependências temporais quanto as relações estruturais presentes nos dados. Esses componentes deram continuidade lógica aos desenvolvimentos anteriores e abriram caminho para a construção de um modelo iterativo completo de previsão da dinâmica do mercado. No próximo artigo, reuniremos todos os elementos criados em um único sistema, testaremos seu funcionamento e avaliaremos a eficácia das abordagens implementadas com dados históricos reais.
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 classes | Estrutura de descrição do estado do sistema e das arquiteturas dos modelos |
| 5 | NeuroNet.mqh | Biblioteca de classes | 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/20120
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.
Redes neurais em trading: Percepção adaptativa da dinâmica do mercado (Conclusão)
Modelos matemáticos em estratégias de grade
Redes neurais em trading: modelos de refinamento iterativo de previsões (RAFT)
Tabelas no paradigma MVC em MQL5: colunas configuráveis e ordenáveis
- 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