Redes neurais no trading: uma visão unificada sobre espaço e tempo (Global-Local Attention)
Introdução
Os mercados financeiros formam um sistema complexo e dinâmico, no qual espaço e tempo estão intimamente entrelaçados. Cada movimento de cotação reflete não apenas o equilíbrio instantâneo entre oferta e demanda, mas também os efeitos de eventos anteriores, bem como a influência de instrumentos, setores e até economias inteiras interligadas. Prever o comportamento de um sistema desse tipo com métodos tradicionais sempre foi uma tarefa de alta complexidade. Modelos estatísticos e redes neurais clássicas lidavam razoavelmente bem com previsões de curto prazo, mas perdiam robustez e precisão ao tentar ir além de algumas horas. O principal problema era a análise separada dos fatores temporais e espaciais. As tentativas de combiná-los levavam a um crescimento explosivo dos custos computacionais.
O framework Extralonger, que começamos a conhecer no artigo anterior, propôs uma abordagem fundamentalmente diferente. Seus autores partiram da ideia de que espaço e tempo devem ser considerados como um todo único. Essa filosofia, que dialoga com a teoria da relatividade de Einstein, materializou-se na Unified Spatial-Temporal Representation, uma representação na qual séries temporais e relações espaciais são integradas sem separar artificialmente os dados. Essa solução permitiu reduzir drasticamente a complexidade computacional. Antes, a complexidade das operações crescia rapidamente; o Extralonger as simplificou para dependências quadráticas. O efeito prático foi impressionante: treinamento acelerado em centenas de vezes, redução expressiva do consumo de memória e, principalmente, a possibilidade de construir previsões com duração não de horas, mas de dias inteiros e até semanas.
Para os mercados financeiros, isso abre novas perspectivas. Antes, traders e analistas tradicionalmente se limitavam a avaliações de curto prazo; agora surge a possibilidade de olhar vários pregões à frente ou de prever o movimento dos mercados nos períodos próximos à divulgação de dados macroeconômicos e às decisões de bancos centrais. O Extralonger transforma a previsão semanal, antes uma meta praticamente inalcançável, em uma ferramenta aplicável na prática.
No entanto, a verdadeira força do framework se revela em sua arquitetura. A base dessa arquitetura é um Transformer de três rotas, no qual cada rota fica responsável por um aspecto específico da análise.
A rota temporal pode ser comparada a um analista macroeconômico. Ela estuda os ritmos do mercado, identifica ciclos de alta e queda, captura a sazonalidade e padrões recorrentes. Usando o mecanismo de Self-Attention, o modelo conecta trechos distantes da série temporal. O movimento atual de um par de moedas pode ser a continuação de uma tendência iniciada há um mês.
A rota espacial atua como especialista em relações intermercados. Nos mercados financeiros, nenhum ativo existe isoladamente. A dinâmica do ouro se reflete nos índices acionários. O câmbio do dólar influencia os mercados de commodities. Já os rendimentos dos títulos ditam o tom das ações. O Extralonger considera isso por meio do Global-Local Spatial Transformer, que combina duas perspectivas: a global, na qual cada papel está conectado a todos os demais, e a local, na qual o foco recai sobre clusters de ativos por setor ou região.
A rota mista se aproxima mais do papel de um gestor de portfólio. Sua função é unir a dinâmica temporal e as inter-relações espaciais em uma visão unificada. Com isso, o modelo passa a perceber o mercado de forma integral. Para ele, o aumento da volatilidade no setor de tecnologia, acompanhado por mudanças nos preços do petróleo e por oscilações nas taxas de juros, não é um conjunto de fatos aleatórios, mas um sinal de que um grande cenário de mercado está se formando.
A previsão final surge no ponto de interseção das três rotas. Assim como em um comitê de investimento, no qual cada especialista contribui com sua análise, o Extralonger combina as conclusões de todos os participantes em uma decisão ponderada. Como resultado, o modelo obtém não apenas profundidade local de análise e ampla cobertura das conexões globais, mas também uma percepção integral dos processos de mercado ao longo do tempo.
Essa arquitetura torna o framework especialmente valioso para aplicações financeiras. Ele combina profundidade na análise de séries temporais, amplitude na cobertura das relações intermercados e percepção integral do mercado. Com isso, o Extralonger consegue construir previsões robustas em situações nas quais outros modelos perdem precisão ou passam a exigir recursos excessivos.
A visualização do framework Extralonger proposta pelos autores é apresentada abaixo.

Na parte prática do artigo anterior, demos os primeiros passos na implementação das abordagens propostas em MQL5. Desenvolvemos um módulo de codificação espacial, que permite modelar as inter-relações entre instrumentos, e também apresentamos blocos individuais do programa OpenCL para codificação temporal. Essas implementações formaram a base sobre a qual podemos construir uma versão de engenharia completa do framework.
Hoje, continuamos esse trabalho, avançando na implementação prática das ideias do Extralonger. Nosso objetivo é implementar, passo a passo, no ambiente MQL5 a arquitetura proposta pelos autores, buscando torná-la viável em condições de dados financeiros reais.
Embedding temporal
Continuamos o desenvolvimento dos algoritmos do framework Extralonger em MQL5. No artigo anterior, examinamos em detalhes os kernels do programa OpenCL que adicionam embeddings temporais aos dados originais. Agora passamos para a implementação dessa lógica no programa principal. Para isso, criamos um novo objeto CNeuronTempEmbedding, que herda a funcionalidade básica da classe CNeuronBaseOCL.
class CNeuronTempEmbedding : public CNeuronBaseOCL { protected: uint iWindow; uint iUnits; uint aiEmbeddingDim[2]; uint aiFrames[2]; uint aiPeriod[2]; CParams caEmbeddings[2]; //--- virtual bool ConcatByLabel(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput); virtual bool ConcatByLabelGrad(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput); //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override {return false;} virtual bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override {return false;} virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None ) override; public: CNeuronTempEmbedding(void) {}; ~CNeuronTempEmbedding(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units, uint window, uint embed_dim1, uint period1, uint frame1, uint embed_dim2, uint period2, uint frame2, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) const { return defNeuronTempEmbedding; } //--- methods for working with files virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetOpenCL(COpenCLMy *obj) override; virtual void SetActivationFunction(ENUM_ACTIVATION value) override { }; };
Os principais parâmetros do objeto incluem a quantidade de atributos analisados iWindow, o comprimento da sequência iUnits, as dimensões de embedding aiEmbeddingDim, o tamanho de cada passo iFrames e os períodos de repetição aiPeriod. Definimos tudo isso por meio de arrays, o que torna o objeto flexível e configurável para diferentes escalas temporais dos dados financeiros.
O método Init define a arquitetura do objeto e fica responsável pela inicialização de todos os principais parâmetros da camada de codificação temporal e de seus componentes internos.
bool CNeuronTempEmbedding::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units, uint window, uint embed_dim1, uint period1, uint frame1, uint embed_dim2, uint period2, uint frame2, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, (window + embed_dim1 + embed_dim2)*units, optimization_type, batch)) return false;
Na primeira etapa, chamamos a inicialização da classe pai CNeuronBaseOCL. Nessa etapa, definimos as principais características da camada e inicializamos as interfaces herdadas. Se a inicialização da classe base falhar, a função retorna false, impedindo a criação de um objeto inválido.
Em seguida, inicializamos dois componentes caEmbeddings, que geram os embeddings temporais dos dados. Criamos cada embedding considerando sua própria dimensionalidade (embed_dim1 e embed_dim2) e seu período de repetição (period1 e period2). Isso permite que o modelo capture padrões temporais em diferentes escalas e profundidades de análise, algo especialmente importante ao analisar a dinâmica dos mercados financeiros.
if(!caEmbeddings[0].Init(0, 0, OpenCL, embed_dim1 * period1, optimization, iBatch)) return false; if(!caEmbeddings[1].Init(0, 1, OpenCL, embed_dim2 * period2, optimization, iBatch)) return false;
Depois que os componentes de embedding são inicializados com sucesso, salvamos os parâmetros do objeto em variáveis internas. Esses parâmetros definem a estrutura da camada temporal e controlam como o neurônio percebe o mercado ao longo do tempo.
iWindow = window; iUnits = units; aiEmbeddingDim[0] = embed_dim1; aiEmbeddingDim[1] = embed_dim2; aiFrames[0] = MathMax(1, frame1); aiFrames[1] = MathMax(1, frame2); aiPeriod[0] = period1; aiPeriod[1] = period2; //--- return true; }
Ao final, o método retorna true, confirmando a criação bem-sucedida de um objeto totalmente pronto para uso.
O método feedForward implementa a propagação para frente do sinal na camada. Primeiro, verificamos se o objeto está em modo de treinamento (bTrain==true). Se estiver, a propagação para frente é executada para cada componente de embedding. Essa etapa permite formar um tensor de atributos temporais para capturar padrões em diferentes escalas de tempo. Se pelo menos um embedding não puder ser processado, a função retorna false, impedindo a propagação de dados inválidos.
bool CNeuronTempEmbedding::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { if(bTrain) for(uint i = 0; i < caEmbeddings.Size(); i++) if(!caEmbeddings[i].FeedForward()) return false; //--- return ConcatByLabel(NeuronOCL, SecondInput); }
Depois que a propagação para frente dos componentes de embedding é concluída com sucesso, combinamos os dados pelo método ConcatByLabel, que atua como método wrapper do kernel correspondente do programa OpenCL. Nessa etapa, os atributos temporais são integrados de forma controlada aos dados originais. Como resultado, o objeto gera um sinal pronto para processamento, no qual dados históricos e observações atuais de mercado são combinados, favorecendo a precisão e a sensibilidade do modelo à dinâmica do mercado financeiro.
Como se pode observar, o método de propagação para frente tem uma implementação bastante simples e linear. Da mesma forma, os métodos da propagação reversa e atualização dos gradientes não apresentam dificuldades e seguem a lógica padrão do treinamento de redes neurais. Por isso, para uma compreensão mais aprofundada, esses métodos podem ser examinados de forma independente. O código-fonte completo da classe CNeuronTempEmbedding e de todos os seus métodos está disponível em anexo, para que seja possível examinar em detalhes como esse objeto é implementado e integrado à cadeia computacional do modelo.
Objeto de atenção
Após a preparação dos embeddings temporais e sua integração aos dados originais, avançamos para a próxima etapa: implementar os algoritmos do módulo Global-Local Spatial Attention. Antes de começar a codificar, é importante discutir a abordagem proposta pelos autores do framework.
A ideia central consiste em combinar a atenção global, característica do Self-Attention clássico, com um foco local implementado por meio da matriz de adjacência. Essa abordagem lembra métodos de convolução em grafos, que já usamos anteriormente, mas aqui ela é adaptada à estrutura espaço-temporal dos dados.
Os autores do artigo não fornecem informações detalhadas sobre a construção da matriz de adjacência. Para preencher essa lacuna, usamos o algoritmo Significant Neighbors Sampling do framework SAGDFN. Com ele, criamos uma matriz de adjacência esparsa, o que permite economizar recursos computacionais de forma significativa e, ao mesmo tempo, preservar a capacidade do modelo de considerar dependências locais relevantes. Vale observar que essa abordagem impõe cuidados específicos à implementação, pois manipular uma matriz esparsa exige cuidado no tratamento dos índices e das threads de execução.
Implementação no contexto OpenCL
Ao portar os algoritmos para o programa OpenCL, nos deparamos com dois desafios importantes, que influenciam diretamente a velocidade e a precisão da análise dos dados de mercado. O primeiro é a execução sequencial do Global-Attention e do Local-Attention, que reduz o desempenho geral do modelo e desacelera o processamento de grandes volumes de cotações históricas. O segundo é ainda mais delicado: como coordenar as threads do Global-Attention completo com a matriz de adjacência esparsa sem perder informações importantes sobre as conexões locais entre elementos de preço. A qualidade desse ajuste determina diretamente a capacidade do modelo de reconhecer padrões complexos de mercado com eficiência.
A solução para os dois desafios acabou sendo surpreendentemente direta. Seguindo a lógica da atenção multicabeça, distribuímos os cálculos entre threads separadas, permitindo que o modelo analise simultaneamente diferentes aspectos dos dados de mercado. Naturalmente, isso exigiu introduzir ramificações no algoritmo de acordo com a cabeça de atenção específica. Ainda assim, essa abordagem permitiu preservar a velocidade e a precisão do processamento de grandes séries históricas, algo crítico para identificar padrões de mercado em tempo hábil.
Depois de definirmos a arquitetura de execução do Global-Attention e do Local-Attention em threads paralelas dentro de um único kernel, chegou o momento de implementar na prática o algoritmo de propagação para frente. Implementamos essa lógica no kernel GlobalLocalAttention. Nesse kernel, cada linha representa uma etapa do modelo na análise dos dados históricos e na identificação de sinais relevantes.
__kernel void GlobalLocalAttention(__global const float *q, __global const float2* kv, __global float *scores, __global const float* mask, __global const float* label, __global float *out, const int dimension, const int total_kv, const int total_mask ) { //--- init const int q_id = get_global_id(0); const int local_id = get_local_id(1); const int h_id = get_global_id(2); const int total_q = get_global_size(0); const int total_local = get_local_size(1); const int total_heads = get_global_size(2); //--- __local float temp[LOCAL_ARRAY_SIZE];
O kernel recebe todos os dados essenciais:
- vetores de consulta q, que representam o estado atual do mercado consultado pelo modelo;
- pares chave-valor kv, correspondentes aos padrões históricos e às representações associadas;
- máscara e rótulos para as restrições locais;
- parâmetros referentes às dimensões do espaço de atributos e aos volumes de dados.
No corpo do kernel, o modelo primeiro identifica quem é quem no espaço tridimensional de execução:
- q_id é a consulta específica;
- local_id é a thread dentro do grupo e, dependendo da cabeça de atenção, aponta para o par consulta-chave ou para uma posição na matriz de adjacência esparsa;
- h_id é a cabeça de atenção que analisa um aspecto específico do mercado.
O array local serve como buffer temporário para cálculos intermediários, como a mesa de trabalho de um trader, na qual todos os valores intermediários atuais são reunidos antes da avaliação final.
Em seguida, determinamos o deslocamento da consulta analisada na memória. É como localizar a barra correta no gráfico para relacioná-la com movimentos históricos.
int shift_q = RCtoFlat(h_id, 0, total_heads, dimension, q_id);
Então implementamos a ramificação do algoritmo: as cabeças pares representam a visão global do mercado, na qual os principais padrões são avaliados sobre toda a base histórica, enquanto as ímpares ficam responsáveis pela visão local.
Para as cabeças de atenção pares, calculamos as posições das chaves e dos pesos de atenção correspondentes. É como marcar no gráfico todos os níveis anteriores de suporte e resistência relevantes para a consulta atual.
if(h_id % 2 == 0) { const int shift_kv = RCtoFlat(h_id, 0, total_heads, dimension, local_id); const int shift_s = RCtoFlat(h_id / 2, local_id, total_heads / 2, total_kv + total_mask, q_id); float score = 0; if(local_id < total_kv) { //--- for(int d = 0; d < dimension; d++) score += IsNaNOrInf(q[shift_q + d] * kv[shift_kv + d].s0, 0); } else score = MIN_VALUE; //--- norm score score = LocalSoftMax(score, 1, temp); if(local_id < total_kv) scores[shift_s] = score;
Calculamos a correlação da consulta atual com os dados históricos. Se uma thread não participa do cálculo, atribuímos a ela o valor mínimo para que não distorça o quadro geral, como traders inativos no mercado. Depois disso, normalizamos o peso de atenção de cada padrão. É como ponderar a importância dos sinais: alguns níveis de preço exercem influência maior, enquanto outros praticamente não entram no cálculo.
A composição final da saída consiste em multiplicar os valores históricos pelos respectivos pesos e somá-los.
//--- out for(int d = 0; d < dimension; d++) { float val = (local_id < total_kv ? kv[shift_kv + d].s1 * score : 0); val = LocalSum(val, 1, temp); if(local_id == 0) out[shift_q + d] = val; } }
Na saída, obtemos um sinal para a consulta atual, pronto para análise ou para uso em uma estratégia de trading.
A segunda ramificação do algoritmo processa as conexões locais com a máscara correspondente. Aqui, na prática, operamos sobre uma matriz de adjacência esparsa, na qual muitos elementos estão ausentes ou não são relevantes para a análise atual.
O algoritmo começa verificando se existe uma conexão para o elemento local atual. A variável local kv_id aponta para a chave efetiva com a qual o peso de atenção deve ser calculado. Se kv_id for menor que 0, significa que não há elemento correspondente na matriz esparsa, e a thread não participa do cálculo. A máscara m filtra adicionalmente as conexões inválidas.
else { int kv_id = -1; float score = 0; int shift_kv = -1; float m = 0; if(local_id < total_mask) { const int shift_s = RCtoFlat(h_id / 2, total_kv + local_id, total_heads / 2, total_kv + total_mask, q_id); const int l = RCtoFlat(q_id, local_id, total_q, total_mask, 0); kv_id = IsNaNOrInf(label[l], -1); m = IsNaNOrInf(mask[l], 0); shift_kv = RCtoFlat(h_id, 0, total_heads, dimension, kv_id); if(kv_id >= 0) for(int d = 0; d < dimension; d++) score += IsNaNOrInf(q[shift_q + d] * kv[shift_kv + d].s0, 0); else score = MIN_VALUE; } else score = MIN_VALUE; //--- norm score score = LocalSoftMax(score * m, 1, temp); if(local_id < total_mask) scores[shift_s] = score;
Somente as conexões locais existentes participam do cálculo dos pesos de atenção; as demais recebem o valor mínimo. Isso evita que a estrutura esparsa contamine o resultado e que o modelo se concentre exclusivamente nos padrões locais relevantes.
A normalização via SoftMax leva a máscara em conta para que a probabilidade total seja distribuída apenas entre os elementos locais reais.
Por fim, formamos a saída para a consulta considerando somente os padrões locais que de fato estão presentes na matriz esparsa.
//--- out for(int d = 0; d < dimension; d++) { float val = (kv_id >= 0 ? IsNaNOrInf(kv[shift_kv + d].s1, 0) * score : 0); val = LocalSum(val, 1, temp); if(local_id == 0) out[shift_q + d] = val; } } }
O uso de uma matriz esparsa torna impossível aplicar um algoritmo universal simples para todas as cabeças de atenção. A cabeça global opera sobre uma matriz densa: todos os elementos estão presentes, e os cálculos são diretos. A cabeça local opera sobre uma estrutura esparsa: muitas conexões estão ausentes, sendo necessárias verificações de máscara e rótulos, filtragem de NaN/Inf e uma lógica separada de soma. Por isso, dentro de um único kernel foi necessário implementar dois algoritmos distintos, cada um otimizado para seu tipo de estrutura de dados: um para a matriz global densa e outro para a matriz local esparsa.
Depois de implementarmos a propagação para frente, nos deparamos com um desafio muito mais sutil: a distribuição dos gradientes do erro. Aqui, o kernel GlobalLocalAttentionGrad desempenha o papel de um trader experiente, que acompanha simultaneamente a visão global e os padrões locais. Cada thread dentro do kernel é como um analista independente, que recebe seu próprio trecho do gráfico. Ao mesmo tempo, essa frente de trabalho está longe de ser restrita.
A interpretação de global_id aqui muda conforme a etapa. Ele pode apontar para Query, Key ou Value. Sua função muda em cada etapa e aponta para o objeto de destino do gradiente do erro. De modo semelhante, a função de local_id também muda, que define a thread dentro do grupo, como um trader analisando vários timeframes. Já h_id determina a cabeça de atenção, ou seja, o aspecto do mercado no qual o analista se concentra.
Todos esses índices formam uma rede de observação, permitindo distribuir os gradientes com precisão, como se estivéssemos distribuindo a atenção entre diferentes séries de cotações para que nenhum detalhe passasse despercebido.
__kernel void GlobalLocalAttentionGrad(__global const float *q, __global float *q_gr, __global const float *kv, __global float *kv_gr, __global float *scores, __global const float *mask, __global const float *mask_gr, __global const float *label, __global float *out_gr, const int dimension, const int total_q, const int total_kv, const int total_mask ) { //--- init const int global_id = get_global_id(0); const int local_id = get_local_id(1); const int h_id = get_global_id(2); const int total_global = get_global_size(0); const int total_local = get_local_size(1); const int total_heads = get_global_size(2); //--- __local float temp[LOCAL_ARRAY_SIZE];
Para as cabeças pares, que operam sobre a matriz global densa, a lógica lembra a análise do mercado com base em dados históricos completos. Primeiro, calculamos os gradientes de Value: cada thread acumula cuidadosamente o impacto de sua posição sobre o resultado geral, somando esse impacto via LocalSum. É semelhante ao modo como um trader soma sinais de todas as velas históricas para entender a tendência geral.
if(h_id % 2 == 0) { //--- Value Gradient global_id -> v_id, local_id -> q_id for(int d = 0; d < dimension; d++) { const int shift_v = RCtoFlat(h_id, 2 * d + 1, total_heads, 2 * dimension, global_id); float grad = 0; for(int q_id = local_id; q_id < total_q; q_id += total_local) { int shift_s = RCtoFlat(h_id / 2, global_id, total_heads / 2, total_kv + total_mask, q_id); int shift_q = RCtoFlat(h_id, d, total_heads, dimension, q_id); grad += IsNaNOrInf(scores[shift_s] * out_gr[shift_q], 0); } grad = LocalSum(grad, 1, temp); kv_gr[shift_v] = grad; }
Em seguida, formamos os gradientes de Query. Aqui é importante considerar a normalização via SoftMax, de modo semelhante a um analista que avalia a relevância de cada sinal em relação a todos os demais, para que nenhum ruído distorça o quadro geral. Os gradientes de Query são somados de forma controlada pelas threads locais e gravados em q_gr.
//--- Query Gradient global_id -> q_id, local_id -> k_id/v_id if(global_id < total_q) { //--- 1. Score grad float grad_s = 0; const int shift_v = RCtoFlat(h_id, 1, total_heads, 2 * dimension, local_id); const int shift_s = RCtoFlat(h_id / 2, local_id, total_heads / 2, total_kv + total_mask, global_id); int shift_q = RCtoFlat(h_id, 0, total_heads, dimension, global_id); if(local_id < total_kv) for(int d = 0; d < dimension; d++) grad_s += IsNaNOrInf(kv[shift_v + 2 * d] * out_gr[shift_q + d], 0); //--- 2. SoftMax grad grad_s = LocalSoftMaxGrad(scores[shift_s], grad_s, 1, temp); //--- 3. Query grad const int shift_k = shift_v - 1; for(int d = 0; d < dimension; d++) { float grad = 0; if(local_id < total_kv) grad = kv[shift_k + 2 * d] * grad_s; grad = LocalSum(grad, 1, temp); if(local_id == 0) q_gr[shift_q + d] = grad; } }
Já a Key recebe os gradientes finais pela soma das contribuições de todas as consultas.
//--- Key Gradient global_id -> k_id, local_id -> score_id/v_id/dimension if(global_id < total_kv) { float grad = 0; for(int q_id = 0; q_id < total_q; q_id++) { //--- 1. Score grad local_id -> score_id/v_id float grad_s = 0; const int shift_v = RCtoFlat(h_id, 1, total_heads, 2 * dimension, local_id); const int shift_s = RCtoFlat(h_id / 2, local_id, total_heads / 2, total_kv + total_mask, q_id); int shift_q = RCtoFlat(h_id, 0, total_heads, dimension, q_id); if(local_id < total_kv) for(int d = 0; d < dimension; d++) grad_s += IsNaNOrInf(kv[shift_v + 2 * d] * out_gr[shift_q + d], 0); //--- 2. SoftMax grad grad_s = LocalSoftMaxGrad(scores[shift_s], grad_s, 1, temp); BarrierLoc; if(global_id == local_id) temp[0] = grad_s; BarrierLoc; grad_s = temp[0]; //--- 3. Key grad local_id -> dimension shift_q = RCtoFlat(h_id, local_id, total_heads, dimension, q_id); if(local_id < dimension) grad += IsNaNOrInf(q[shift_q] * grad_s, 0); } const int shift_k = RCtoFlat(h_id, 2 * local_id, total_heads, 2 * dimension, global_id); if(local_id < dimension) kv_gr[shift_k] = IsNaNOrInf(grad); } }
Para a matriz global, o cálculo segue de forma direta, pois todos os dados estão presentes, e o algoritmo consegue distribuir os gradientes em paralelo.
As cabeças ímpares, que operam sobre a matriz local esparsa, exigem do kernel um nível de cuidado digno de um trader experiente. De modo semelhante ao algoritmo apresentado acima, primeiro somamos em um laço os gradientes de Value a partir de todas as consultas Query. Aqui, porém, cada thread primeiro verifica, por meio da máscara e dos rótulos, se a conexão existe. Se a conexão não existir ou se a máscara proibir o uso do elemento, a thread de Query não participa do cálculo.
else { //--- Value Gradient global_id -> v_id, local_id -> mask_index/dimension if(global_id < total_kv) { float grad = 0; for(int q_id = 0; q_id < total_q; q_id++) { //--- 1. kv_id int kv_id = -1; float m = 0; const int l = RCtoFlat(q_id, local_id, total_q, total_mask, 0); const int shift_s = RCtoFlat(h_id / 2, total_kv + local_id, total_heads / 2, total_kv + total_mask, q_id); //--- Check for use current Value if(local_id < total_mask) kv_id = (int)label[l]; if(local_id == 0) temp[0] = 0; BarrierLoc; if(kv_id == global_id) temp[0] = scores[shift_s]; BarrierLoc; if(temp[0] == 0) continue;
O gradiente de Value é calculado apenas para os elementos existentes. O peso de atenção é multiplicado pelo gradiente de dsaída correspondente e acumulado.
//--- Value grad int shift_q = RCtoFlat(h_id, local_id, total_heads, dimension, q_id); if(local_id < dimension) grad += IsNaNOrInf(temp[0] * out_gr[shift_q], 0); } const int shift_v = RCtoFlat(h_id, 2 * local_id + 1, total_heads, 2 * dimension, global_id); if(local_id < dimension) kv_gr[shift_v] = IsNaNOrInf(grad, 0); }
Os gradientes de Query são formados levando em conta a normalização via SoftMax e a máscara, o que permite filtrar sinais irrelevantes, como um trader que ignora barras ruidosas e padrões enganosos.
//--- Query Gradient global_id -> q_id, local_id -> mask label if(global_id < total_q) { //--- 1. kv_id; int kv_id = -1; float m = 0; const int l = RCtoFlat(global_id, local_id, total_q, total_mask, 0); if(local_id < total_mask) { kv_id = (int)IsNaNOrInf(label[l], -1); m = IsNaNOrInf(mask[l], 0); } //--- 2. Score grad float grad_s = 0; const int shift_v = RCtoFlat(h_id, 1, total_heads, 2 * dimension, kv_id); const int shift_s = RCtoFlat(h_id / 2, total_kv + local_id, total_heads / 2, total_kv + total_mask, global_id); int shift_q = RCtoFlat(h_id, 0, total_heads, dimension, global_id); if(local_id < total_mask) for(int d = 0; d < dimension; d++) grad_s += IsNaNOrInf(kv[shift_v + 2 * d] * out_gr[shift_q + d], 0); //--- 3. SoftMax grad float score = IsNaNOrInf(scores[shift_s], 0); grad_s = LocalSoftMaxGrad(scores[shift_s], grad_s, 1, temp); mask_gr[l] = IsNaNOrInf(grad_s * score, 0); grad_s *= m; //--- 4. Query grad const int shift_k = shift_v - 1; for(int d = 0; d < dimension; d++) { float grad = 0; if(local_id < total_mask) grad = kv[shift_k + 2 * d] * grad_s; grad = LocalSum(grad, 1, temp); if(local_id == 0) q_gr[shift_q + d] = grad; } }
Os gradientes de Key são acumulados apenas pelas consultas que realmente têm conexão com a chave, e a sincronização das threads via BarrierLoc evita que qualquer sinal se perca. É como se o analista conferisse os resultados dos colegas antes do registro final da negociação. Cada cálculo reflete com precisão a distribuição real da influência na matriz local esparsa e evita que dados irrelevantes contaminem os gradientes.
//--- Key Gradient global_id -> k_id, local_id -> score_id/v_id/dimension if(global_id < total_kv) { float grad = 0; for(int q_id = 0; q_id < total_q; q_id++) { //--- 1. kv_id; int kv_id = -1; float m = 0; const int l = RCtoFlat(global_id, local_id, total_q, total_mask, 0); if(local_id < total_mask) { kv_id = (int)label[l]; if(kv_id == global_id) m = mask[l]; } m = LocalSum(m, 1, temp); if(m == 0) continue; //--- 2. Score grad local_id -> score_id/v_id float grad_s = 0; const int shift_v = RCtoFlat(h_id, 1, total_heads, 2 * dimension, kv_id); const int shift_s = RCtoFlat(h_id / 2, total_kv + local_id, total_heads / 2, total_kv + total_mask, q_id); int shift_q = RCtoFlat(h_id, 0, total_heads, dimension, q_id); if(local_id < total_mask) for(int d = 0; d < dimension; d++) grad_s += IsNaNOrInf(kv[shift_v + 2 * d] * out_gr[shift_q + d], 0); //--- 3. SoftMax grad grad_s = LocalSoftMaxGrad(scores[shift_s], grad_s, 1, temp); BarrierLoc; if(global_id == local_id) temp[0] = grad_s * m; BarrierLoc; grad_s = temp[0]; //--- 4. Key grad local_id -> dimension shift_q = RCtoFlat(h_id, local_id, total_heads, dimension, q_id); if(local_id < dimension) grad += IsNaNOrInf(q[shift_q] * grad_s, 0); } const int shift_k = RCtoFlat(h_id, 2 * local_id, total_heads, 2 * dimension, global_id); if(local_id < dimension) kv_gr[shift_k] = IsNaNOrInf(grad); } } }
Dentro de um único kernel, implementamos dois algoritmos logicamente distintos, porque as matrizes global e local exigem abordagens fundamentalmente diferentes. A matriz global densa permite calcular diretamente os gradientes de todos os elementos, como se o trader enxergasse o mercado inteiro. Já a matriz local esparsa exige filtragem cuidadosa, sincronização e somatório apenas das conexões existentes, de modo semelhante a um trader que verifica com atenção cada sinal importante em um trecho limitado do gráfico. Essa separação permite distribuir os gradientes de forma correta e eficiente durante o treinamento do modelo em grandes séries históricas, nas quais coexistem dependências globais densas e correlações locais esparsas.
Cada ação do kernel é como um passo individual do trader. A verificação da máscara e dos rótulos lembra a seleção dos níveis de preço realmente relevantes. Filtrar NaN e Inf corresponde a ignorar dados ruidosos. A soma via LocalSum e a sincronização das threads lembram a coordenação entre analistas para que nenhum sinal importante se perca. Como resultado, o modelo recebe gradientes precisos para Value, Query e Key, o que permite aprender com dados históricos, reconhecer padrões de mercado relevantes e minimizar o erro de previsão. O kernel transforma cálculos abstratos em um fluxo dinâmico de análise de mercado, no qual cada operação de gradiente reflete a atenção real e as ações do analista, dando suporte à precisão e à estabilidade do treinamento.
O código completo dos kernels é apresentado em anexo.
Implementação no programa principal
No programa principal, o algoritmo Global-Local Spatial Attention tem uma estrutura baseada em um novo objeto CNeuronGlobalLocalAttention, que herda a funcionalidade da estrutura multicabeça feed-forward CNeuronMHFeedForward. Esse objeto reúne vários componentes-chave, cada um com uma função estritamente definida na construção da atenção.
class CNeuronGlobalLocalAttention : public CNeuronMHFeedForward { protected: CNeuronSNSMHAttention cMask; CNeuronConvOCL cQ; CNeuronConvOCL cKV; CNeuronBaseOCL cScore; CNeuronBaseOCL cMHAttention; CNeuronConvOCL cW0; CNeuronBaseOCL cResidual; //--- virtual bool GlobalLocalAttention(void); virtual bool GlobalLocalAttentionGrad(void); //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronGlobalLocalAttention(void) {}; ~CNeuronGlobalLocalAttention(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units, uint window, uint dimension_k, uint heads, uint m_units, float sparse, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) const { return defNeuronGlobalLocalAttention; } //--- methods for working with files virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetOpenCL(COpenCLMy *obj) override; virtual void SetActivationFunction(ENUM_ACTIVATION value) override { }; };
Dentro da classe, há o objeto cMask, responsável pelo controle das máscaras de conexões locais, o que permite operar corretamente sobre a matriz esparsa e filtrar padrões locais irrelevantes. Dois objetos de convolução, cQ e cKV, implementam respectivamente as transformações das consultas e dos pares chave-valor, preparando os dados para o cálculo dos pesos de atenção. O elemento cScore acumula os scores calculados, enquanto cMHAttention reúne os resultados da atenção multicabeça, combinando componentes globais e locais. Por fim, cW0 e cResidual respondem pela transformação linear e pela adição da conexão de salto, favorecendo a estabilidade e a correção da atualização dos sinais de saída. A classe pai implementa a funcionalidade do bloco FeedForward.
O método Init fica responsável pela preparação completa do neurônio para operação e define todos os principais parâmetros do algoritmo Global-Local Spatial Attention. Primeiro, chamamos a inicialização da classe base CNeuronMHFeedForward. Nessa etapa, definimos todos os principais parâmetros do objeto.
bool CNeuronGlobalLocalAttention::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units, uint window, uint dimension_k, uint heads, uint m_units, float sparse, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronMHFeedForward::Init(numOutputs, myIndex, open_cl, window, 2 * window, units, 1, heads, optimization_type, batch)) return false; activation = None;
Se algo der errado nessa fase, a sessão de trading nem começa: o objeto não é inicializado, evitando quaisquer cálculos inválidos.
Em seguida, desativamos a função de ativação, como se o trader decidisse operar sem filtros emocionais, confiando totalmente nos sinais diretos do mercado.
O primeiro componente a ser acionado é cMask. Ele gerencia as máscaras locais, filtrando padrões esparsos para que o trader não se distraia com sinais irrelevantes. Com ele, o modelo identifica quais conexões locais são importantes e quais podem ser ignoradas.
int index = 0; if(!cMask.Init(0, index, OpenCL, units, window, heads, m_units, sparse, optimization, iBatch)) return false;
Em seguida, formamos cQ e cKV, que transformam dados brutos em sinais analíticos interpretáveis. cQ processa as consultas, como um trader que avalia as posições atuais no mercado, enquanto cKV acumula chaves e valores, de modo semelhante a um trader que reúne informações sobre candles anteriores, níveis de suporte e resistência. Em ambos os blocos, desativamos a função de ativação para que as linhas de análise permaneçam limpas e lineares, sem distorções.
index++; if(!cQ.Init(0, index, OpenCL, window, window, 2 * dimension_k * heads, units, 1, optimization, iBatch)) return false; cQ.SetActivationFunction(None); index++; if(!cKV.Init(0, index, OpenCL, window, window, 4 * dimension_k * heads, units, 1, optimization, iBatch)) return false; cKV.SetActivationFunction(None);
Depois entra em operação cScore, que acumula os scores calculados, de modo análogo a um trader que soma os sinais de todos os indicadores para entender onde vale a pena concentrar a análise.
index++; if(!cScore.Init(0, index, OpenCL, (units + m_units)*units * heads, optimization, iBatch)) return false; cScore.SetActivationFunction(None); index++; if(!cMHAttention.Init(0, index, OpenCL, 2 * dimension_k * heads * units, optimization, iBatch)) return false; cMHAttention.SetActivationFunction(None);
Com base nesses scores, entra em operação cMHAttention, a atenção multicabeça, que distribui recursos entre diferentes instrumentos e intervalos de tempo. Exatamente como um analista experiente decide onde concentrar mais atenção na situação atual do mercado.
cW0 e cResidual encerram a cadeia. O primeiro executa a transformação linear dos sinais, de modo semelhante a um trader que ajusta os cálculos considerando o volume atual e a liquidez do mercado.
index++; if(!cW0.Init(0, index, OpenCL, 2 * dimension_k * heads, 2 * dimension_k * heads, window, units, 1, optimization, iBatch)) return false; cW0.SetActivationFunction(None); index++; if(!cResidual.Init(0, index, OpenCL, Neurons(), optimization, iBatch)) return false; cResidual.SetActivationFunction(None); //--- return true; }
O segundo adiciona a conexão de salto, evitando que a informação sobre etapas anteriores do treinamento se perca e mantendo o sinal estável, como uma verificação das posições em negociações anteriores antes da abertura de uma nova operação.
Cada bloco recebe seu índice único e seus parâmetros, vincula-se ao contexto OpenCL e fica pronto para operar em conjunto. Tudo isso lembra uma equipe bem coordenada de traders e analistas: um filtra os sinais, outro avalia tendências, o terceiro acumula os resultados e o quarto distribui a atenção entre os instrumentos. Como resultado, quando a inicialização é concluída, o objeto está totalmente pronto para uso e é capaz de analisar simultaneamente padrões globais e locais do mercado, filtrar ruído e gerar decisões mais fundamentadas com base em dados históricos.
Depois que inicializamos o objeto e configuramos todos os blocos, começa a etapa de propagação para frente, o momento em que o modelo efetivamente observa o mercado e constrói suas previsões. O método feedForward define uma sequência clara de ações, na qual cada componente executa uma função estritamente definida, enquanto os dados percorrem toda a cadeia de transformações, lembrando a operação de uma equipe bem coordenada de traders.
bool CNeuronGlobalLocalAttention::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cMask.FeedForward(NeuronOCL)) return false;
Primeiro, acionamos cMask para verificar e filtrar as conexões locais. É como um trader que seleciona com atenção apenas os sinais realmente relevantes, ignorando sinais ruidosos ou conexões ausentes. Se algo der errado nessa etapa, a análise posterior se torna impossível: o modelo não construirá uma previsão com base em dados incorretos.
Em seguida, são executados sequencialmente cQ e cKV. Primeiro, acionamos cMask para verificar e filtrar as conexões locais. É como um trader que seleciona com atenção apenas os sinais realmente relevantes, ignorando sinais ruidosos ou conexões ausentes.
if(!cQ.FeedForward(NeuronOCL)) return false; if(!cKV.FeedForward(NeuronOCL)) return false;
O segundo acumula chaves e valores, reunindo, por analogia, o histórico de cotações e os volumes de negociação de cada instrumento, criando a base para avaliar a influência de cada sinal.
Após a preparação ser concluída com sucesso, chamamos o método wrapper GlobalLocalAttention, que coloca na fila de execução o kernel de mesmo nome, que executa o algoritmo principal no contexto OpenCL.
if(!GlobalLocalAttention()) return false;
Aqui, combinamos consultas e chaves na atenção global e local. É nesse ponto que a magia do modelo acontece: cada padrão é avaliado considerando todas as conexões relevantes, esparsas e densas, enquanto as contribuições são somadas de forma controlada ao longo de todas as cabeças de atenção.
Essa etapa é análoga ao momento em que o trader compara os sinais atuais com os dados históricos e decide quais posições merecem mais atenção.
Em seguida, executamos cW0, que aplica a transformação linear aos resultados da atenção multicabeça. Isso pode ser visto como um ajuste dos sinais levando em conta a escala do mercado, a liquidez e o peso de atenção de cada padrão.
if(!cW0.FeedForward(cMHAttention.AsObject())) return false; if(!SumAndNormilize(NeuronOCL.getOutput(), cW0.getOutput(), cResidual.getOutput(), cW0.GetFilters(), true, 0, 0, 0, cW0.GetUnits())) return false; //--- return CNeuronMHFeedForward::feedForward(cResidual.AsObject()); }
Depois disso, os dados passam por SumAndNormalize. Nesse método, somamos as saídas do bloco linear e da conexão de salto e, em seguida, normalizamos o resultado. Essa etapa contribui para manter o sinal estável e escalável, como se o trader reunisse os resultados de vários analistas, verificasse sua consistência e avaliasse a força geral do sinal antes de tomar uma decisão.
Por fim, repassamos o sinal atualizado ao método de mesmo nome da classe pai CNeuronMHFeedForward, que conclui a propagação para frente executando a funcionalidade do bloco FeedForward e integrando os resultados à estrutura geral do modelo.
Assim, todo o fluxo da propagação para frente pode ser visto como uma análise sequencial do mercado, na qual cada bloco executa sua função: filtra, avalia, acumula, ajusta e soma os dados, dando suporte à precisão e à consistência das previsões.
Depois que o modelo conclui com sucesso a propagação para frente e gera a previsão, começa a etapa de treinamento, o momento em que o neurônio avalia o grau de precisão do seu resultado e ajusta seus parâmetros internos. O método calcInputGradients fica responsável por essa avaliação e pela distribuição da contribuição numérica de cada componente do modelo sobre o resultado final.
bool CNeuronGlobalLocalAttention::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false; if(!CNeuronMHFeedForward::calcInputGradients(cResidual.AsObject());) return false;
Primeiro, verificamos a validade do ponteiro recebido para o objeto de dados de entrada NeuronOCL. Se o objeto estiver ausente, os cálculos posteriores se tornam impossíveis, como se o trader tentasse analisar os dados sem gráfico nem cotações.
Em seguida, chamamos o método de mesmo nome da classe pai, que inicia a retropropagação dos gradientes até o nível do objeto de conexões de salto.
Depois, chamamos o método DeActivation para o bloco cW0, no qual os gradientes recebidos são ajustados considerando a função de ativação da camada linear. Isso é análogo ao modo como um analista considera a influência de cada indicador sobre a decisão final, filtrando e normalizando os sinais.
if(!DeActivation(cW0.getOutput(), cW0.getGradient(), cResidual.getGradient(), cW0.Activation())) return false;
Depois disso, passamos os gradientes para cMHAttention, que acumula a influência de cada padrão da atenção multicabeça.
if(!cMHAttention.CalcHiddenGradients(cW0.AsObject())) return false; if(!GlobalLocalAttentionGrad()) return false;
Nessa etapa, chamamos GlobalLocalAttentionGrad, que realiza a distribuição precisa dos gradientes entre as matrizes global e local, levando em conta conexões densas e esparsas, como se o trader avaliasse a contribuição de cada candle e de cada nível de suporte para a tendência geral. Em seguida, propagamos os gradientes de forma controlada para os blocos cQ e cKV.
Agora precisamos agregar, no nível dos dados de entrada, os gradientes do erro provenientes de todos os fluxos de informação. São quatro fluxos. Primeiro, propagamos os gradientes de cQ.
if(!NeuronOCL.CalcHiddenGradients(cQ.AsObject())) return false;
Somamos os valores obtidos aos gradientes do ramo principal das conexões de salto, mas antes ajustamos estes últimos considerando a função de ativação da camada dos dados de entrada.
if(!DeActivation(cResidual.getOutput(), cResidual.getGradient(), cResidual.getGradient(), NeuronOCL.Activation())) return false; if(!SumAndNormilize(NeuronOCL.getGradient(), cResidual.getGradient(), cResidual.getGradient(), cW0.GetFilters(), false, 0, 0, 0, cW0.GetUnits())) return false;
Na etapa seguinte, processamos os gradientes de cKV e os adicionamos aos dados já acumulados.
if(!NeuronOCL.CalcHiddenGradients(cKV.AsObject())) return false; if(!SumAndNormilize(NeuronOCL.getGradient(), cResidual.getGradient(), cResidual.getGradient(), cW0.GetFilters(), false, 0, 0, 0, cW0.GetUnits())) return false;
Por fim, sem perder importância, propagamos os gradientes das máscaras locais cMask. Também somamos sua influência aos dados acumulados anteriormente.
if(!NeuronOCL.CalcHiddenGradients(cMask.AsObject())) return false; if(!SumAndNormilize(NeuronOCL.getGradient(), cResidual.getGradient(), NeuronOCL.getGradient(), cW0.GetFilters(), false, 0, 0, 0, cW0.GetUnits())) return false; //--- return true; }
Como resultado, o método calcInputGradients realiza uma avaliação abrangente e detalhada da influência de todos os componentes do modelo, desde as cabeças de atenção globais e locais até consultas, chaves, valores e máscaras. Cada bloco recebe um retorno quantitativo, permitindo que o modelo atualize corretamente os pesos, minimize o erro e busque aumentar a precisão das previsões. Essa etapa transforma os cálculos abstratos de gradientes em uma avaliação real da importância de cada elemento do sistema, tornando o treinamento o mais transparente e controlável possível.
Depois que o modelo avalia a influência de cada componente por meio da retropropagação dos gradientes, chega o momento de agir: ajustar os parâmetros internos e adaptar o modelo aos dados reais de mercado. O método updateInputWeights executa exatamente essa tarefa.
bool CNeuronGlobalLocalAttention::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!cMask.UpdateInputWeights(NeuronOCL)) return false; if(!cQ.UpdateInputWeights(NeuronOCL)) return false; if(!cKV.UpdateInputWeights(NeuronOCL)) return false; if(!cW0.UpdateInputWeights(cMHAttention.AsObject())) return false; //--- return CNeuronMHFeedForward::updateInputWeights(cResidual.AsObject()); }
O algoritmo do método é bastante direto: ele transfere sequencialmente o controle para os componentes internos que contêm parâmetros treináveis. Primeiro, atualizamos os pesos das máscaras locais cMask; em seguida, ajustamos os parâmetros das consultas cQ e das chaves/valores cKV. Depois disso, atualizamos os pesos da transformação linear cW0. Por fim, ajustamos os parâmetros da classe pai.
Cada passo é simples, mas importante: o modelo ajusta seus parâmetros para avaliar o mercado com mais precisão e gerar previsões mais precisas, enquanto todos os blocos atuam de forma coordenada, como uma equipe de traders seguindo uma estratégia comum.
Realizamos um volume significativo de trabalho e superamos diversos desafios. Agora é o momento ideal para fazer uma breve pausa, recuperar o fôlego e assimilar os resultados. No próximo artigo, com energia renovada e um novo olhar, continuaremos avançando e levaremos esta implementação ao seu fechamento natural.
Conclusão
Percorremos um caminho significativo: cada bloco do algoritmo Global-Local Spatial Attention foi integrado com cuidado e encadeado em uma cadeia computacional unificada. A propagação para frente, a distribuição dos gradientes e a atualização dos parâmetros do modelo lembraram a atuação de um trader experiente, que filtra sinais, avalia a influência de cada instrumento e ajusta posições em busca do melhor resultado.
A abordagem implementada em MQL5 mostrou como algoritmos complexos de atenção podem ser adaptados para uso prático, transformando cálculos abstratos em previsões mais precisas e controláveis. Cada bloco do modelo, como um analista independente, contribuiu para a decisão final, dando ao sistema flexibilidade, robustez e escalabilidade.
A próxima etapa será a verificação final do nosso trabalho: levaremos o modelo ao seu fechamento natural, testaremos o modelo com dados reais de mercado e avaliaremos a eficácia prática do framework proposto.
Referências
- Extralonger: Toward a Unified Perspective of Spatial-Temporal Factors for Extra-Long-Term Traffic Forecasting
- Outros artigos da série
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 classe | Estrutura de descrição do estado do sistema e da arquitetura dos modelos |
| 5 | NeuroNet.mqh | Biblioteca de classe | Biblioteca de classes para criação de redes neurais |
| 6 | NeuroNet.cl | Biblioteca | Biblioteca do código do programa OpenCL |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/19538
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
Algoritmo de otimização do dingo - Dingo Optimization Algorithm (DOA)
Está chegando o novo MetaTrader 5 e MQL5
Introdução ao MQL5 (Parte 15): Criando Indicadores Personalizados com MQL5 — Guia para Iniciantes (IV)
- 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