Redes neurais em trading: Treinamento de modelos spiking profundos (Conclusão)
Introdução
O mercado financeiro, como qualquer sistema não linear complexo, segue suas próprias leis. Ele respira, oscila, acumula energia e a libera na forma de movimentos bruscos de preço. E, quanto mais atentamente estudamos sua dinâmica, mais evidente se torna que os métodos clássicos de análise nem sempre conseguem acompanhar a constante mudança dos cenários de trading atuais. Em seu lugar surge uma nova geração de algoritmos, mais sensíveis, adaptativos e capazes de enxergar, além da superfície do gráfico, o impulso interno do mercado.
Nos trabalhos anteriores, avançamos desde os princípios básicos da computação spiking até a implementação das principais soluções arquiteturais que viabilizam o uso prático desses modelos no ambiente MQL5. Agora chegamos à etapa final desse percurso: aplicar às tarefas de análise e previsão de séries temporais financeiras as abordagens propostas pelos autores do framework SEW ResNet (Spiking Elementary Weights Residual Network).
O SEW ResNet surgiu da busca por combinar a eficiência computacional dos modelos spiking com a robustez e a profundidade das arquiteturas ResNet clássicas. A abordagem se baseia na ideia de treinamento com equilíbrio energético, em que as informações são transmitidas por uma sequência de impulsos discretos. Cada spike carrega uma informação ponderada, enquanto cada atraso reflete a resposta do sistema ao sinal. Como resultado, o modelo não apenas é mais eficiente do ponto de vista computacional, mas também identifica com maior precisão as relações de causa e efeito entre eventos ao longo do tempo, algo especialmente importante na análise de impulsos de preço e de picos de atividade do mercado.
No contexto dos mercados financeiros, isso abre novas possibilidades. Afinal, o sinal de mercado tem, por natureza, um comportamento spiking. Ele é irregular e alterna períodos de calmaria com picos de atividade, nos quais um instante separa o caos de um padrão identificável. Nessas condições, as redes neurais clássicas muitas vezes acabam se prendendo ao ruído ou perdendo sensibilidade durante oscilações prolongadas. O SEW ResNet, por outro lado, foi projetado para preservar sua capacidade de resposta: ele reage apenas a mudanças realmente significativas no fluxo de dados e filtra naturalmente as informações irrelevantes.
Nas etapas anteriores, construímos passo a passo a base da arquitetura que seria desenvolvida. Cada módulo implementado passou a integrar a base do framework, em que o rigor técnico se combinava com a lógica intuitiva dos processos de mercado.
O módulo de ativação spiking com limiares adaptativos tornou-se o primeiro elemento fundamental desse sistema. Sua função é controlar a resposta do neurônio ao fluxo de sinais de entrada, diferenciando as oscilações relevantes do ruído natural. Nos modelos spiking clássicos, a ativação é fixa e muitas vezes reage da mesma forma a qualquer alteração. Optamos por seguir o princípio do protótipo biológico: o limiar adaptativo varia de acordo com a intensidade dos impulsos de entrada e a frequência dos spikes. Como resultado, a rede aprende a reagir apenas quando isso é realmente necessário, evitando o sobreajuste a oscilações aleatórias dos preços.
No mercado, esse mecanismo tem uma aplicação direta. Ele permite que o algoritmo opere de forma robusta em condições de alta volatilidade, nas quais os modelos clássicos frequentemente perdem precisão. Quando o gráfico é dominado pelo caos, o limiar adaptativo atua como um filtro integrado do ruído de mercado, ajudando o sistema a manter uma percepção clara e a reagir apenas às mudanças mais relevantes da tendência.
A etapa seguinte foi a implementação de um mecanismo de gating responsável por regular a sensibilidade do neurônio à novidade do sinal recebido. Em essência, o gate determina em que medida o sinal atual deve influenciar o acúmulo do potencial de membrana. Em outras palavras, regula quanto o novo impulso contribui para o estado interno do neurônio. Isso torna a resposta do sistema mais adaptativa. Ele passa a reagir de maneira diferente a padrões repetitivos e a padrões novos, desenvolvendo gradualmente uma capacidade própria de se habituar a eventos recorrentes do mesmo tipo.
Em termos de mercado financeiro, isso lembra o comportamento de um trader experiente, que não reage a cada movimento de preço, mas concentra a atenção apenas em sinais realmente novos, capazes de romper o equilíbrio estabelecido. O mecanismo de gating atua como um regulador da sensibilidade do modelo: determina quando a resposta deve ser intensificada e quando, ao contrário, deve ser reduzida para preservar a estabilidade da percepção. Esse controle da sensibilidade permite filtrar impulsos falsos sem comprometer a capacidade de aprendizado a partir de mudanças reais na dinâmica do mercado.
Em nosso trabalho, a implementação das abordagens propostas pelos autores do framework SEW ResNet se baseia na arquitetura ResNeXt, modificada para incorporar dinâmica spiking e sensibilidade adaptativa. A abordagem parte da ideia de percepção multicanal, em que cada ramo paralelo do modelo processa os dados sob uma perspectiva própria e, em seguida, os resultados são cuidadosamente combinados em um único vetor de representação do mercado. Essa estrutura permite que o modelo não apenas identifique a ocorrência de um movimento de preço, mas também diferencie sua natureza, intensidade e direção, aspectos que um trader normalmente reconhece pela experiência.
O elemento central do modelo é o módulo Spike ResNeXt Bottleneck, que atua como uma espécie de filtro semântico. Ele comprime o fluxo de sinais recebido, faz o fluxo passar por um canal estreito e depois o expande novamente, agora em uma forma mais limpa e estruturada. Aqui, a ideia clássica de Bottleneck ganha uma dimensão spiking. Cada elemento do bloco é capaz de decidir quando deve reagir e quando deve permanecer inativo.
A implementação do Bottleneck usa um array dinâmico de componentes cBottleneck, que forma o caminho computacional do bloco. O código cria as camadas programaticamente de acordo com os parâmetros definidos pelo usuário. O fluxo passa por uma sequência de transformações: operações de convolução, normalização, ativação spiking e convoluções em grupo. Essa cadeia equilibra a precisão e a velocidade de resposta.
Igualmente importante é o módulo Spike ResNeXt Residual, responsável por transmitir a experiência acumulada. Sua estrutura garante a compatibilidade adequada das dimensões entre o caminho principal e o caminho de atalho, permitindo preservar sem distorções as informações sobre os estados anteriores. No contexto financeiro, isso equivale à memória de um trader, isto é, à capacidade de lembrar o que ocorreu antes do impulso atual e ajustar a resposta levando em conta o histórico.
O bloco Residual segue o mesmo princípio dinâmico: quando as dimensões de entrada e saída não coincidem, o código adiciona camadas intermediárias de convolução e normalização. Essa solução torna a arquitetura robusta a alterações nos parâmetros do modelo e permite aumentar livremente a profundidade da rede sem comprometer a lógica interna dos sinais.
Outra vantagem importante da abordagem é a eficiência energética. A verificação implementada para entradas nulas elimina cálculos desnecessários nos momentos em que o mercado está inativo. Durante períodos de baixa volatilidade, muitos neurônios simplesmente não são ativados, reduzindo a carga sobre o sistema e acelerando os cálculos sem perda de precisão. Esse princípio corresponde a uma economia natural da atenção: o modelo se concentra apenas nos períodos em que ocorre atividade de mercado realmente relevante.
A combinação da arquitetura multicanal ResNeXt com a lógica spiking adaptativa do SEW forma um híbrido singular, no qual a precisão matemática se alia à intuição de mercado. Cada ramo do modelo aprende uma perspectiva própria sobre os dados, enquanto as conexões residuais permitem integrar essas diferentes perspectivas em uma única decisão. Com isso, o modelo apresenta robustez ao ruído e capacidade de identificar padrões sutis, justamente aqueles sinais fracos que podem antecipar o surgimento de uma tendência.
O valor prático dessa abordagem é evidente: o modelo não se limita a analisar o passado, mas aprende a reconhecer os momentos em que o estado do mercado muda. Isso oferece vantagens na criação de estratégias de previsão, na filtragem de sinais falsos e no gerenciamento automatizado de decisões de trading.
A visualização do framework elaborada pelos autores é apresentada abaixo.


Princípios básicos da implementação
Depois de implementar sucessivamente os diferentes módulos, surgiu uma questão natural: como reunir esses elementos em uma arquitetura única e coerente, na qual cada parte complemente as demais e todo o sistema funcione como um organismo integrado?
No entanto, essa etapa exige cuidado. A arquitetura não deve apenas conectar os blocos, mas também transformar gradualmente atributos de baixo nível em representações de mercado mais abstratas por meio da interação harmoniosa entre os diferentes níveis de processamento. Por isso, faz sentido começar pela própria base, o arcabouço sobre o qual será construída nossa inteligência spiking. Como já mencionado anteriormente, esse arcabouço é o framework ResNeXt, conhecido por sua flexibilidade modular e alto desempenho.
Tomamos o ResNeXt como base, mas incorporamos à arquitetura a natureza spiking dos sinais. Na versão clássica, o ResNeXt analisa fluxos de dados por meio de ramos paralelos de processamento, cada um responsável por extrair um aspecto específico das informações de entrada. Em nossa implementação, transformamos esses ramos em canais spiking capazes de ajustar de forma independente sua sensibilidade e a frequência de resposta às mudanças do mercado.
No módulo Spike Bottleneck, já implementamos o ciclo básico de processamento da sequência: o sinal é comprimido, transformado e depois expandido novamente, preservando as informações relevantes dos dados. Essa estrutura funciona muito bem na análise de séries temporais unitárias, nas quais a dependência entre os elementos da sequência é homogênea. O mercado, porém, é mais complexo. Lidamos com uma estrutura temporal multimodal, na qual várias variáveis de mercado inter-relacionadas evoluem em paralelo: preço, volume, volatilidade, liquidez, interesse em aberto e muitos outros atributos.
Analisar cada um desses canais de forma independente, como uma sequência unitária isolada, leva à perda de informações críticas. Existe uma forte relação dinâmica entre os atributos: alterações no volume influenciam a amplitude das oscilações de preço, a volatilidade determina o ritmo do mercado e a liquidez define a intensidade da reação a fatores externos. Quando o modelo processa esses sinais separadamente, essa interdependência se perde, e o modelo deixa de perceber a coerência estrutural do comportamento do mercado.
Para suprir essa lacuna, introduzimos em nossa implementação um segundo Bottleneck, que opera sobre uma representação transposta do tensor original. Diferentemente da análise padrão de sequências unitárias, esse bloco examina os dados por etapas temporais individuais, analisando simultaneamente todos os canais em cada instante. Isso permite que o modelo identifique dependências entre canais, capture sua dinâmica e considere a influência conjunta que os atributos exercem uns sobre os outros.
Ao mesmo tempo, mantemos apenas um bloco Residual para evitar que as conexões residuais exerçam influência excessiva. Essa solução equilibra a preservação das informações sobre estados anteriores e o processamento eficiente de novas combinações de sinais. Como resultado, o modelo passa a ser capaz de:
- extrair padrões locais dentro de cada sequência unitária;
- identificar dependências entre canais e a dinâmica coordenada dos atributos;
- manter robustez ao ruído e ao excesso de sinais por meio da limitação das conexões residuais.
Essa abordagem híbrida transforma a arquitetura SEW ResNeXt em uma ferramenta capaz de enxergar o mercado como um sistema dinâmico complexo, no qual cada atributo está relacionado aos demais e a alteração de um elemento repercute em toda a estrutura do modelo.
Outro aspecto importante que precisamos discutir é a escolha da função usada para combinar, elemento a elemento, os spikes dos três fluxos de informação. Do ponto de vista de uma abordagem orientada ao risco e considerando a natureza binária dos spikes, a multiplicação elemento a elemento surge como uma opção atraente. Nesse caso, o modelo só propaga o sinal quando todos os fluxos o confirmam simultaneamente. Esse princípio lembra o comportamento de um trader cauteloso, que só abre uma posição quando todos os indicadores confirmam o sinal.
À primeira vista, a solução parece lógica e rigorosa: o modelo reage apenas quando há concordância entre as informações, reduzindo o risco de falsas ativações. No entanto, é importante analisar com atenção como o gradiente do erro se distribui nesse caso.
Se todos os fluxos de informação confirmarem o sinal, o algoritmo distribuirá uniformemente o gradiente do erro. Porém, se pelo menos um dos fluxos não gerar um spike, somente ele receberá o gradiente, enquanto os demais não receberão correção. Do ponto de vista lógico, isso faz sentido. Quando pelo menos um fluxo retorna zero, o modelo deixa de propagar o sinal, e é justamente esse fluxo que precisa de correção. O gradiente do erro indica que a rede precisa ativá-lo.
O problema surge no caso crítico em que todos os fluxos de informação permanecem bloqueados. Nesse cenário, cada termo da multiplicação elemento a elemento é igual a zero, e o gradiente do erro, ao ser multiplicado por esses zeros, fica totalmente bloqueado. Como resultado, o neurônio deixa de receber feedback: ele se torna um neurônio morto, e seus parâmetros se degradam. Na prática, isso pode interromper o treinamento de parte da rede ou até mesmo de todo o modelo, especialmente quando situações desse tipo se repetem com frequência.
Em outras palavras, a multiplicação estrita elemento a elemento acaba se tornando uma armadilha para o treinamento. Ela protege contra sinais falsos, mas, ao mesmo tempo, cria o risco de desativar completamente os neurônios.
Uma forma de permitir a propagação dos gradientes do erro sem bloqueios é usar a soma simples dos valores, como ocorre no ResNet clássico. Essa abordagem garante que, mesmo quando todos os fluxos de informação estiverem temporariamente bloqueados, o gradiente do erro ainda possa corrigir os parâmetros do neurônio e o treinamento continue sem interrupções.
No entanto, isso entra em conflito com o conceito de spikes. Diferentemente do sinal binário "0" ou "1", a soma de vários fluxos ativos pode produzir valores 2 ou 3. Isso descaracteriza a natureza spiking da ativação e pode provocar uma resposta excessiva do modelo, fazendo com que o modelo reaja de forma intensa demais. Em termos de mercado, seria como um trader que abre uma posição com confiança excessiva sem que os dados realmente justifiquem essa convicção.
Depois de ponderar esses riscos, chegamos a uma solução mais equilibrada: normalizamos a soma dos valores dos fluxos de informação e, em seguida, aplicamos a ativação spiking. Isso permite preservar simultaneamente a interpretação binária do sinal e a propagação adequada dos gradientes do erro. A normalização mantém o valor resultante dentro de uma faixa admissível, evitando respostas excessivas, e também permite gerar um sinal reverso. Esse sinal surge quando vários fluxos bloqueiam simultaneamente a ativação e indica à rede que ela precisa corrigir os parâmetros no sentido oposto. É um comportamento semelhante ao de um trader cauteloso, que reduz a posição quando surgem sinais de mercado fortemente contrários.
Módulo de nível superior
Para implementar na prática o conceito proposto, criamos um novo objeto CNeuronSpikeResNeXtBlock, que reúne de forma coerente todos os principais elementos da nossa arquitetura.
class CNeuronSpikeResNeXtBlock : public CNeuronSpikeActivation { protected: CNeuronTransposeOCL caTranspose[2]; CNeuronSpikeResNeXtBottleneck caBottleneck[2]; CNeuronSpikeResNeXtResidual cResidual; CLayer cG; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronSpikeResNeXtBlock(void) {}; ~CNeuronSpikeResNeXtBlock(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint chanels_in, uint chanels_out, uint units_in, uint units_out, uint group_size, uint groups, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronSpikeResNeXtBlock; } //--- 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 void TrainMode(bool flag) override; virtual bool Clear(void) override; };
Sua estrutura reúne dois blocos Bottleneck, um bloco Residual e representações transpostas dos dados originais, o que permite considerar tanto as sequências unitárias individuais quanto as complexas relações entre canais. Ao herdar de CNeuronSpikeActivation, essa classe preserva a natureza spiking dos sinais e integra gates com limiares adaptativos, proporcionando uma resposta flexível dos neurônios às mudanças na dinâmica do mercado.
Um dos elementos centrais do bloco é o array dinâmico cG, usado para armazenar ponteiros para objetos que implementam a função de combinação dos spikes. Esse recurso amplia significativamente as possibilidades do bloco e abre espaço para experimentação. O array permite conectar e utilizar diferentes formas de combinar os sinais elemento a elemento, incluindo tanto a soma simples quanto a combinação normalizada seguida de ativação spiking. Graças a essa estrutura, o bloco deixa de ser apenas uma configuração fixa e passa a funcionar como uma plataforma flexível para testar diferentes estratégias de integração de sinais, adaptando-se às características específicas dos dados de mercado.
Toda a configuração do novo bloco fica a cargo do método Init, responsável por inicializar seus principais componentes e definir a interação entre eles.
bool CNeuronSpikeResNeXtBlock::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint chanels_in, uint chanels_out, uint units_in, uint units_out, uint group_size, uint groups, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronSpikeActivation::Init(numOutputs, myIndex, open_cl, chanels_out * units_out, optimization_type, batch)) return false;
O algoritmo começa chamando o método de mesmo nome da classe pai para preparar o processamento spiking e integrar os gates com limiares adaptativos.
Em seguida, o método inicializa as representações transpostas do tensor original. O primeiro objeto prepara os dados para a análise das sequências unitárias, enquanto o segundo realiza a transposição inversa. Essa abordagem permite que o modelo considere simultaneamente a estrutura dos fluxos individuais e as interações entre eles, além de propagar corretamente os sinais e os gradientes.
uint index = 0; if(!caTranspose[0].Init(0, index, OpenCL, units_in, chanels_in, optimization, iBatch)) return false; index++; if(!caTranspose[1].Init(0, index, OpenCL, chanels_out, units_out, optimization, iBatch)) return false;
Na etapa seguinte, o método inicializa os blocos Bottleneck. O primeiro Bottleneck processa as sequências unitárias convencionais, identificando padrões locais nos sinais, enquanto o segundo trabalha com a representação transposta, capturando dependências entre canais e a dinâmica coordenada dos atributos. Cada bloco recebe os parâmetros dos canais, o número de grupos e o tamanho de cada grupo, além dos parâmetros de otimização necessários e do tamanho do batch.
index++; if(!caBottleneck[0].Init(0, index, OpenCL, chanels_in, chanels_out, units_in, units_out, group_size, groups, optimization, iBatch)) return false; index++; if(!caBottleneck[1].Init(0, index, OpenCL, units_in, units_out, chanels_in, chanels_out, group_size, groups, optimization, iBatch)) return false;
Depois disso, o método inicializa o bloco Residual, responsável por preservar as conexões residuais essenciais e manter o equilíbrio entre a memória do neurônio e sua capacidade de adaptação.
index++; if(!cResidual.Init(0, index, OpenCL, chanels_in, chanels_out, units_in, units_out, optimization, iBatch)) return false;
Por fim, o método cria cG, um array dinâmico de objetos usados na função de combinação dos spikes, e adiciona sequencialmente a ele um neurônio básico sem ativação e um objeto de normalização em lote. Essa sequência permite somar e normalizar corretamente os sinais provenientes dos diferentes fluxos de informação.
CNeuronBaseOCL* neuron = NULL; CNeuronBatchNormOCL* norm = NULL; cG.Clear(); cG.SetOpenCL(OpenCL); index++; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(0, index, OpenCL, Neurons(), optimization, iBatch) || !cG.Add(neuron)) { DeleteObj(neuron) return false; } neuron.SetActivationFunction(None); //--- if(!cResidual.SetGradient(neuron.getGradient(), true) || !caTranspose[1].SetGradient(neuron.getGradient(), true) || !caBottleneck[0].SetGradient(neuron.getGradient(), true)) return false; //--- index++; norm = new CNeuronBatchNormOCL(); if(!norm || !norm.Init(0, index, OpenCL, Neurons(), iBatch, optimization) || !cG.Add(norm)) { DeleteObj(norm) return false; } //--- return true; }
O bloco também sincroniza os ponteiros para os buffers dos gradientes do erro entre seus principais componentes. Isso garante a propagação correta do erro pela rede, evita cópias desnecessárias de dados e evita a degradação dos sinais.
Como resultado, o método Init cria um bloco totalmente pronto para uso, no qual cada componente está inicializado, configurado e conectado aos demais.
O método feedForward propaga os sinais sequencialmente por todos os principais componentes do bloco SEW ResNeXt.
bool CNeuronSpikeResNeXtBlock::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!caTranspose[0].FeedForward(NeuronOCL)) return false;
Primeiro, os dados passam pelo primeiro bloco de transposição, formando uma representação dos passos de tempo para análise posterior. Em seguida, o primeiro módulo Bottleneck processa o sinal e identifica padrões locais nas sequências unitárias.
if(!caBottleneck[0].FeedForward(NeuronOCL)) return false;
O segundo Bottleneck, por sua vez, trabalha com a saída do bloco de transposição, capturando dependências entre canais e a dinâmica coordenada dos atributos.
if(!caBottleneck[1].FeedForward(caTranspose[0].AsObject())) return false; if(!caTranspose[1].FeedForward(caBottleneck[1].AsObject())) return false;
Depois disso, as informações passam pelo segundo bloco de transposição, que as prepara para integração com as conexões residuais implementadas pelo bloco Residual*.
if(!cResidual.FeedForward(NeuronOCL)) return false;
Na etapa seguinte, o algoritmo combina os spikes somando os sinais provenientes dos dois blocos Bottleneck e do bloco Residual.
CNeuronBaseOCL* current = cG[0]; if(!current) return false; if(!SumAndNormilize(caBottleneck[0].getOutput(), caTranspose[1].getOutput(), current.getOutput(), caTranspose[1].GetCount(), false, 0, 0, 0, 1) || !SumAndNormilize(current.getOutput(), cResidual.getOutput(), current.getOutput(), caTranspose[1].GetCount(), false, 0, 0, 0, 1)) return false;
Essa abordagem permite integrar corretamente tanto os sinais das sequências unitárias quanto os sinais entre canais, preservando a natureza binária dos spikes e evitando a degradação dos neurônios.
Em seguida, os elementos restantes do array cG executam seu próprio ciclo de propagação para frente. Cada objeto recebe a saída do neurônio anterior e passa seu próprio resultado ao objeto seguinte, formando uma sequência entre todas as funções de combinação.
for(int i = 1; i < cG.Total(); i++) { current = cG[i]; if(!current || !current.FeedForward(cG[i - 1])) return false; } //--- return CNeuronSpikeActivation::feedforward(cG[-1]); }
O processamento termina com a chamada do método de mesmo nome da classe pai, que processa os sinais spiking e transmite os resultados para o próximo nível do modelo.
Dessa forma, o algoritmo conduz os sinais de maneira fluida e estruturada por todos os componentes do bloco, integra dependências locais e entre canais, normaliza os spikes combinados e mantém o fluxo adequado dos gradientes do erro durante o treinamento. No contexto do mercado, isso permite que o modelo reaja simultaneamente às mudanças locais de cada fluxo e à dinâmica coordenada dos atributos, formando um sistema estável e adaptativo para a análise de dados financeiros.
O método calcInputGradients distribui adequadamente os gradientes e garante a atualização sequencial e coordenada dos parâmetros de todos os componentes do bloco SEW ResNeXt.
bool CNeuronSpikeResNeXtBlock::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false;
O método começa verificando se o ponteiro recebido para o objeto de dados de origem é válido e chamando o método de mesmo nome da classe pai, o que garante a propagação correta dos gradientes até a última camada do módulo de combinação de spikes cG.
if(!CNeuronSpikeActivation::calcInputGradients(cG[-1])) return false;
Em seguida, percorremos todos os elementos do array dinâmico cG em ordem inversa, do último para o primeiro, calculando os gradientes ocultos de cada neurônio com base na saída do elemento seguinte na cadeia.
CNeuronBaseOCL* current = NULL; for(int i = cG.Total() - 2; i >= 0; i--) { current = cG[i]; if(!current || !current.CalcHiddenGradients(cG[i + 1])) return false; }
Essa abordagem permite distribuir corretamente o erro entre todas as funções de combinação e normalização, garantindo um treinamento coordenado no nível dos spikes.
Aqui, é importante observar que, como substituímos os ponteiros para os buffers dos gradientes do erro dos principais componentes, o sistema distribui automaticamente o erro a todos os fluxos de informação do bloco assim que ele chega ao nível do primeiro elemento do array cG. Esse recurso garante que cada fluxo receba o feedback adequado sem a necessidade de sincronizar manualmente os gradientes de cada componente. Como resultado, o sistema distribui o erro de forma natural e contínua, mantém o treinamento coordenado em toda a estrutura e evita a degradação dos neurônios mesmo diante de sinais complexos e interdependentes.
Em seguida, o método propaga os gradientes obtidos para o segundo Bottleneck e para o bloco de transposição, fazendo com que o erro do modelo reflita a influência das dependências entre canais.
if(!caBottleneck[1].CalcHiddenGradients(caTranspose[1].AsObject())) return false; if(!caTranspose[0].CalcHiddenGradients(caBottleneck[1].AsObject())) return false;
Depois disso, o bloco Residual retropropaga o erro, permitindo ajustar as conexões residuais sem amplificar excessivamente a influência dos sinais anteriores.
if(!NeuronOCL.CalcHiddenGradients(cResidual.AsObject())) return false;
O método também preserva adequadamente os dados de todos os fluxos de informação. Para isso, utiliza um buffer temporário que acumula as informações sobre o erro e as propaga corretamente para as entradas. Isso evita o surgimento de neurônios mortos e garante um treinamento estável e contínuo, mesmo quando alguns fluxos ficam temporariamente sem sinais.
CBufferFloat* temp = NeuronOCL.getGradient(); if(!NeuronOCL.SetGradient(caTranspose[0].getPrevOutput(), false)) return false; if(!NeuronOCL.CalcHiddenGradients(caBottleneck[0].AsObject())) return false; if(!SumAndNormilize(temp, NeuronOCL.getGradient(), temp, caTranspose[0].GetWindow(), false, 0, 0, 0, 1)) return false; if(!NeuronOCL.CalcHiddenGradients(caTranspose[0].AsObject())) return false; if(!SumAndNormilize(temp, NeuronOCL.getGradient(), temp, caTranspose[0].GetWindow(), false, 0, 0, 0, 1)) return false; if(!NeuronOCL.SetGradient(temp, false)) return false; //--- return true; }
Como resultado, o método calcInputGradients distribui os gradientes do erro de forma coordenada e segura por todos os componentes do bloco, desde as sequências transpostas e os módulos Bottleneck até o Residual e o bloco de combinação dos spikes. Esse mecanismo garante que cada neurônio receba feedback suficiente, ajuste seus parâmetros e preserve a capacidade de adaptação do modelo, permitindo analisar sequências de mercado multimodais complexas.
O código-fonte completo da classe CNeuronSpikeResNeXtBlock, com todos os métodos implementados, está disponível no anexo, permitindo estudar em detalhes o funcionamento interno do bloco e acompanhar a lógica de processamento dos sinais em cada etapa.
Arquitetura do modelo
Na etapa seguinte do nosso trabalho, construímos a arquitetura dos modelos treináveis. Como anteriormente, continuamos trabalhando no paradigma Actor-Critic, criando um modelo capaz de tomar decisões de trading de forma autônoma e reagir às mudanças do mercado. Integramos todas as abordagens SEW ResNeXt implementadas anteriormente ao codificador do estado do ambiente, que funciona como o componente responsável por representar o estado percebido pelo modelo, acumulando e processando todas as informações recebidas sobre o mercado.
bool CreateDescriptions(CArrayObj *&encoder, CArrayObj *&actor, CArrayObj *&critic ) { //--- CLayerDescription *descr; //--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) return false; } if(!actor) { actor = new CArrayObj(); if(!actor) return false; } if(!critic) { critic = new CArrayObj(); if(!critic) return false; }
Para construir o codificador, primeiro inicializamos o array de camadas e depois criamos a camada de entrada, que recebe os dados brutos do histórico de preços e dos atributos das barras.
//--- Encoder encoder.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; uint prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; } iLatentLayer = 0;
Depois dela vem uma camada de normalização em lote com adição de ruído, que aumenta a robustez e torna o treinamento mais estável.
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormWithNoise; descr.count = prev_count; descr.batch = BatchSize; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; } iLatentLayer++;
Na sequência, a camada ConcatDiff agrega as diferenças temporais para formar uma representação oculta inicial.
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConcatDiff; prev_count = descr.count = HistoryBars; descr.layers = BarDescr; descr.step = 1; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } iLatentLayer++; uint prev_out = descr.layers * 2 ;
Em seguida, a camada Mamba4CastEmbedding codifica as informações sobre os intervalos temporais e prepara os dados para um processamento mais complexo.
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defMamba4CastEmbeding; prev_count = descr.count = HistoryBars; descr.window = prev_out; prev_out = descr.window_out = NSkills; { uint temp[] = {PeriodSeconds(PERIOD_D1), PeriodSeconds(PERIOD_MN1)}; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } iLatentLayer++;
Na etapa seguinte, entram em ação as camadas NeuronSpikeResNeXtBlock, repetidas várias vezes para formar uma representação profunda e multicanal do estado do mercado.
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSpikeResNeXtBlock; { uint temp[] = {prev_out, // Chanels In prev_out, // Chanels Out EmbeddingSize/2 // Group Size }; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } { uint temp[] = {prev_count, // Units In prev_count, // Units Out NScenarios // Groups Count }; if(ArrayCopy(descr.units, temp) < (int)temp.Size()) return false; } descr.optimization = ADAM; descr.batch = BatchSize; descr.activation = None; for(int i = 0; i < 2; i++) if(!encoder.Add(descr)) { delete descr; return false; } else iLatentLayer++; //--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSpikeResNeXtBlock; { uint temp[] = {prev_out, // Chanels In prev_out * 2, // Chanels Out EmbeddingSize/2 // Group Size }; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } { uint temp[] = {prev_count, // Units In prev_count / 2, // Units Out NScenarios // Groups Count }; if(ArrayCopy(descr.units, temp) < (int)temp.Size()) return false; } descr.optimization = ADAM; descr.batch = BatchSize; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } iLatentLayer++; prev_count = descr.units[1]; prev_out = descr.windows[1]; //--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSpikeResNeXtBlock; { uint temp[] = {prev_out, // Chanels In prev_out, // Chanels Out EmbeddingSize/2 // Group Size }; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } { uint temp[] = {prev_count, // Units In prev_count, // Units Out NScenarios // Groups Count }; if(ArrayCopy(descr.units, temp) < (int)temp.Size()) return false; } descr.optimization = ADAM; descr.batch = BatchSize; descr.activation = None; for(int i = 0; i < 2; i++) if(!encoder.Add(descr)) { delete descr; return false; } else iLatentLayer++; //--- layer 7 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSpikeResNeXtBlock; { uint temp[] = {prev_out, // Chanels In prev_out * 2, // Chanels Out EmbeddingSize/2 // Group Size }; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } { uint temp[] = {prev_count, // Units In prev_count / 2, // Units Out NScenarios // Groups Count }; if(ArrayCopy(descr.units, temp) < (int)temp.Size()) return false; } descr.optimization = ADAM; descr.batch = BatchSize; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } iLatentLayer++; prev_count = descr.units[1]; prev_out = descr.windows[1]; //--- layer 8 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSpikeResNeXtBlock; { uint temp[] = {prev_out, // Chanels In prev_out, // Chanels Out EmbeddingSize/2 // Group Size }; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } { uint temp[] = {prev_count, // Units In prev_count, // Units Out NScenarios // Groups Count }; if(ArrayCopy(descr.units, temp) < (int)temp.Size()) return false; } descr.optimization = ADAM; descr.batch = BatchSize; descr.activation = None; for(int i = 0; i < 2; i++) if(!encoder.Add(descr)) { delete descr; return false; } else iLatentLayer++; //--- layer 9 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSpikeResNeXtBlock; { uint temp[] = {prev_out, // Chanels In prev_out * 2, // Chanels Out EmbeddingSize/2 // Group Size }; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } { uint temp[] = {prev_count, // Units In prev_count / 2, // Units Out NScenarios // Groups Count }; if(ArrayCopy(descr.units, temp) < (int)temp.Size()) return false; } descr.optimization = ADAM; descr.batch = BatchSize; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } iLatentLayer++; prev_count = descr.units[1]; prev_out = descr.windows[1]; //--- layer 10 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSpikeResNeXtBlock; { uint temp[] = {prev_out, // Chanels In prev_out, // Chanels Out EmbeddingSize/2 // Group Size }; if(ArrayCopy(descr.windows, temp) < (int)temp.Size()) return false; } { uint temp[] = {prev_count, // Units In prev_count, // Units Out NScenarios // Groups Count }; if(ArrayCopy(descr.units, temp) < (int)temp.Size()) return false; } descr.optimization = ADAM; descr.batch = BatchSize; descr.activation = None; for(int i = 0; i < 2; i++) if(!encoder.Add(descr)) { delete descr; return false; } else iLatentLayer++; uint window = prev_out; uint count = prev_count;
Cada bloco recebe como entrada as saídas do anterior, considera as dependências entre canais e normaliza os spikes somados, garantindo a propagação correta dos sinais por toda a rede. Selecionamos os parâmetros dos canais, o número de grupos, o tamanho do batch e os parâmetros de otimização de modo que os blocos possam codificar com eficiência sequências multimodais e preservar sua capacidade de adaptação mesmo diante de oscilações bruscas do mercado.
Após a sequência de blocos spiking, o codificador termina com camadas de convolução e transposição, que consolidam a representação de saída e formam o vetor oculto final do estado, usado para prever o próximo movimento dos atributos de mercado analisados. Esse tensor acumula informações sobre dependências temporais e entre canais, proporcionando uma representação precisa e coerente do estado do mercado.
//--- layer 11 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = prev_count; descr.window = prev_out; descr.step = prev_out; prev_out = descr.window_out = BarDescr; descr.activation = TANH; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 12 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; prev_count = descr.window = prev_out; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } prev_out = descr.count; //--- layer 13 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = prev_count; descr.window = prev_out; descr.step = prev_out; prev_out = descr.window_out = NForecast; descr.activation = TANH; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 14 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; prev_count = descr.window = prev_out; descr.batch = BatchSize; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } prev_out = descr.count;
A última camada, NeuronRevInDenormOCL, realiza o escalonamento e a desnormalização adequados dos sinais.
//--- layer 15 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronRevInDenormOCL; descr.count = prev_count * prev_out; descr.layers = 1; if(!encoder.Add(descr)) { delete descr; return false; }
Reaproveitamos integralmente as arquiteturas Actor e Critic dos trabalhos anteriores, sem alterações, preservando uma estrutura e uma funcionalidade já consolidadas. Elas podem ser consultadas no anexo do artigo. Com essa arquitetura, nosso modelo passa a contar com um codificador robusto, capaz de representar as complexas relações temporais e entre canais do mercado, enquanto o Actor e o Critic utilizam essa representação para realizar previsões precisas e tomar decisões de trading.
Testes
Concluímos a integração dos principais componentes do framework SEW ResNeXt, e chegou o momento de avaliar a eficácia das soluções implementadas. Antes de confiar recursos reais ao modelo, é necessário testar cuidadosamente a estratégia com dados históricos e simulados, como se estivéssemos treinando as habilidades de um trader experiente e avaliando sua capacidade de atuar nas mais diversas condições de mercado. Cada etapa reproduz condições próximas às do mercado real, permitindo que o modelo acumule experiência gradualmente, aprenda a distinguir sinais de ruído e desenvolva padrões de comportamento robustos, necessários para operar em um ambiente real de trading.
A primeira etapa, o treinamento offline, é realizada com dados históricos do par de moedas EURUSD no timeframe H1, abrangendo o período de janeiro de 2024 a junho de 2025. Esse intervalo serviu como base para o treinamento do modelo. O modelo aprendeu a reproduzir padrões históricos, compreender a dinâmica dos preços e dos volumes de negociação, identificar regularidades e relações entre os principais atributos. Pode-se dizer que ele desenvolveu uma espécie de intuição de trader combinada com uma avaliação estratégica precisa. Gradualmente, foi adquirindo a capacidade de antecipar movimentos do mercado e avaliar o risco de cada operação potencial.
A etapa seguinte, o ajuste fino online no Testador de Estratégias do MetaTrader 5, permitirá que o modelo trabalhe com fluxos de dados em tempo real, candle a candle. Nessa fase, ele assimila a dinâmica do mercado real, aprende a manter a estabilidade em meio ao ruído do mercado, ajustar suas ações em condições de baixa liquidez e reagir imediatamente a picos bruscos de preço. Essa etapa funciona como um refinamento da estratégia. A estrutura básica formada a partir dos dados históricos permanecia inalterada, enquanto o modelo aprendia a responder de maneira flexível às condições atuais do mercado, minimizando o risco de sobreajuste e aumentando a precisão das previsões mesmo em ambientes instáveis.
A verificação final foi realizada com dados de julho a setembro de 2025, totalmente novos e não utilizados anteriormente. Todos os parâmetros obtidos nas etapas anteriores foram carregados sem alterações, garantindo uma avaliação imparcial da capacidade de generalização do modelo. Os resultados dos testes confirmam que a abordagem por etapas de treinamento e adaptação permite ao modelo analisar o mercado e gerar uma previsão do próximo movimento dos principais atributos de mercado, que pode servir de base para decisões reais de trading.


O gráfico de balance e equity mostra um crescimento estável do capital durante praticamente todo o período de testes. Apesar de alguns drawdowns pontuais, a equity permanece próxima do balance. Isso indica uma estratégia de gerenciamento de risco moderada e controlada. O drawdown máximo da equity foi de 47,73%, um valor bastante elevado para condições voláteis, mas compatível com o cenário de teste em um período marcado por oscilações bruscas de preço.
O lucro total foi de 21,61 USD, partindo de um depósito inicial de 100,0 USD, o que corresponde a um crescimento do capital superior a 20% no período analisado. O Profit Factor foi de 1,15. Isso indica que o lucro total superou a soma das perdas, enquanto o coeficiente de Sharpe de 9,61 aponta para uma alta eficiência da estratégia em relação ao risco. Ao mesmo tempo, o Recovery Factor foi de 0,42, indicando a necessidade de atenção ao gerenciamento dos drawdowns ao aplicar a estratégia a volumes maiores de capital.
O modelo realizou 130 operações, das quais pouco mais da metade foi lucrativa, 53,08%. O lucro médio por operação foi de 2,34 USD, enquanto a perda média ficou em 2,29 USD. A maior sequência de operações vencedoras foi de 7, com lucro acumulado de 11,86 USD, enquanto a maior sequência de perdas foi de 9 operações, totalizando uma perda de 21,36 USD. Esses indicadores mostram uma agressividade moderada da estratégia e a capacidade do modelo de se adaptar às oscilações de curto prazo do mercado.
Também merece destaque a distribuição equilibrada das operações entre posições short e long: 69 shorts e 61 longs, com taxas de acerto de 53,62% e 52,46%, respectivamente. Isso indica um comportamento equilibrado do modelo em diferentes cenários de mercado e sua capacidade de reagir tanto à alta quanto à queda dos preços.
De modo geral, os resultados dos testes confirmam o funcionamento do framework desenvolvido com base no SEW ResNeXt e em mecanismos de ativação spiking. O modelo demonstra robustez, capacidade de adaptação à dinâmica do mercado e um gerenciamento de risco razoável, o que o torna adequado para dar continuidade ao trabalho em condições reais dos mercados financeiros.
Conclusão
Concluímos a implementação prática das abordagens propostas pelos autores do framework SEW ResNet e construímos uma plataforma funcional em que cada componente tem uma finalidade prática. As ativações spiking com limiares adaptativos ensinaram o modelo a reagir de forma mais seletiva. Os gates acrescentaram sensibilidade a novos sinais. Os dois blocos Bottleneck proporcionaram um equilíbrio entre a análise local e a captura das dependências entre canais. O bloco Residual preservou a memória sem permitir que ela comprometesse a capacidade de adaptação.
Os resultados dos testes confirmaram o funcionamento do conceito. O crescimento do capital de 21,6% é um indicador objetivo de eficiência. O Profit Factor de 1,15 e pouco mais de 50% de operações vencedoras mostram que a lógica dos sinais é funcional. O lucro médio e a perda média têm magnitudes próximas, o que indica que o modelo não depende de raras operações favorecidas pela sorte. Ao mesmo tempo, o drawdown máximo de 47% representa um desafio significativo que não pode ser ignorado.
Os pontos fortes da implementação são evidentes. O modelo filtra o ruído do mercado com eficiência. Ele identifica relações entre canais que se perdem quando cada um é analisado separadamente. A arquitetura preserva o fluxo de gradientes e continua treinável mesmo em cenários adversos. O código é modular. A integração com OpenCL oferece recursos para acelerar os cálculos e ampliar a escala de processamento.
Ao mesmo tempo, ainda há aspectos que precisam ser aprimorados. O drawdown máximo elevado exige um gerenciamento de risco mais rigoroso. Testes em simulador ainda não equivalem ao trading real: slippage, spreads e liquidez de mercado podem alterar os perfis de lucros e perdas. O tamanho da amostra e as características específicas do período também influenciam os resultados, por isso é necessária uma validação criteriosa com dados novos e em diferentes regimes de mercado.
A conclusão prática é simples: a arquitetura demonstrou seu valor como plataforma de pesquisa e uso prático. Ela oferece ao trader uma ferramenta que pode ser ajustada, interpretada e aprimorada. Mas esse ainda não é o fim do caminho.
Referências
Programas usados no artigo
| # | Nome | Tipo | Descrição |
|---|---|---|---|
| 1 | Study.mq5 | Expert Advisor | EA de treinamento offline de modelos |
| 2 | StudyOnline.mq5 | Expert Advisor | EA de 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 da arquitetura dos modelos |
| 5 | NeuroNet.mqh | Biblioteca de classes | Biblioteca de classes para criação de 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/19895
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
Desenvolvemos um gerenciador de terminais (Parte 1): Definição do problema
Está chegando o novo MetaTrader 5 e MQL5
Redes neurais em trading: Treinamento de modelos spiking profundos (Integração de spikes)
- 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