Русский
preview
Redes neurais no trading: percepção adaptativa da dinâmica de mercado (Codificador)

Redes neurais no trading: percepção adaptativa da dinâmica de mercado (Codificador)

MetaTrader 5Sistemas de negociação |
30 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Introdução

O comportamento dos mercados financeiros modernos se assemelha cada vez mais a um fluxo contínuo de eventos, no qual importam não apenas a magnitude da variação do preço e o momento exato em que ela ocorre, mas também os intervalos de tempo entre os impulsos e a própria trama interna dos micromovimentos do mercado. Foi justamente essa ideia que serviu de base para a transição de uma visão estática para uma lógica de processamento de dados orientada a eventos, capaz de captar, em tempo real, o pulso vivo do mercado.

Ao contrário dos modelos clássicos, que trabalham com velas discretizadas e acabam suavizando a dinâmica, a abordagem event-driven considera o mercado um sistema reativo contínuo. Cada evento, seja um tick, um pico de volume ou uma mudança instantânea de liquidez, possui seu próprio peso e significado. Isso permite evitar a perda de informação que ocorre inevitavelmente quando o tempo é submetido a uma quantização grosseira. Além disso, essa abordagem está mais próxima da natureza da dinâmica real do mercado. Ela permite que os modelos aprendam não apenas com os valores, mas também com a estrutura das relações temporais entre eles.

No artigo anterior, conhecemos os aspectos teóricos do framework STE-FlowNet. No entanto, aquele primeiro passo foi apenas a base. Agora é hora de avançar, passando do conceito ao desenvolvimento das abordagens propostas pelos autores do framework e adaptando-as às tarefas de análise e previsão de séries temporais dos mercados financeiros.

Não apenas abandonamos a visão tradicional do mercado como uma sequência de barras com valores agregados, mas também reajustamos a própria forma de percebê-lo. Em vez de registrar apenas o resultado, passamos a captar o próprio processo. Não o resultado final da vela, mas o impulso que a precedeu. Não uma estrutura já pronta, mas a trama de uma evolução contínua. Construímos uma representação do tempo de mercado orientada a eventos. Cada tick passa a ser percebido não como apenas mais um número, mas como um ato de interação entre oferta e demanda, um instante de tomada de decisão no limiar de um microequilíbrio.

Isso criou a base para o passo seguinte: modelos capazes não apenas de reconhecer a ocorrência de um evento, mas também de captar a direção interna do fluxo. Saber distinguir exatamente como essa dinâmica se desenrola neste exato momento é uma condição fundamental para construir sistemas capazes de prever não a partir dos vestígios deixados pelo evento, mas com base na própria trajetória de seu desenvolvimento. É justamente aqui que entramos no domínio do framework STE-FlowNet, com sua visão do mercado orientada a eventos e à dinâmica.

Aprendemos a captar o próprio surgimento de um evento de mercado. Agora, precisamos aprender a distinguir para onde ele tende a conduzir o mercado em seguida. Não basta registrar a ocorrência de um impulso; é preciso interpretar a direção do fluxo oculto de forças antes mesmo que ele se torne evidente no preço. É justamente essa capacidade de antecipação que separa a modelagem passiva da verdadeira antecipação da dinâmica de mercado. Nos modelos clássicos de ML, essa tarefa é resolvida a posteriori: o fluxo já se formou, a vela já fechou e o movimento já ocorreu. O STE-FlowNet propõe uma abordagem fundamentalmente diferente: aprender a enxergar o movimento dentro do próprio movimento, reconstruindo a direção da trajetória enquanto o evento ainda está se formando.

O framework STE-FlowNet foi originalmente desenvolvido não para o mercado financeiro, mas para a análise de fluxos de eventos em ambientes dinâmicos, mais especificamente em tarefas de rastreamento óptico, nas quais o importante não é aquilo que já aconteceu no quadro, mas como a própria mudança da imagem evolui ao longo do tempo. Trata-se de uma arquitetura híbrida orientada a eventos e à dinâmica temporal, capaz de reconstruir, a partir das diferenças entre eventos, a direção oculta do fluxo, de forma semelhante à maneira como o cérebro humano percebe movimento onde os olhos registram apenas flashes discretos de luz. Adaptamos esse mecanismo ao mercado, transformando o tempo financeiro em um fluxo de microdeslocamentos ópticos, do qual é possível extrair uma estrutura vetorial associada à provável evolução futura.

Nos mercados financeiros, isso é particularmente importante. O preço não se move por si só: ele é impulsionado por agrupamentos de microimpulsos de liquidez, e a maioria deles permanece invisível nas representações OHLC tradicionais. Mesmo uma vela de alta frequência registra apenas o resultado dessa disputa, sem revelar sua dinâmica interna. O que nos interessa é compreender para onde o sistema tende neste exato momento, antes que toda a força do movimento potencial se manifeste. É justamente isso que diferencia o STE-FlowNet dos modelos estatísticos de previsão: em vez de inferir o futuro apenas com base em dados passados, ele reconstrói a direção de uma dinâmica ainda em desenvolvimento.

É com esse objetivo que levamos o STE-FlowNet para o contexto da análise de séries temporais de mercado, não como mais uma rede neural destinada a prever preços, mas como uma ferramenta para reconstruir o campo interno de movimento do mercado oculto na microestrutura dos eventos. Na prática, ensinamos o modelo a interpretar o fluxo de ticks de forma semelhante à maneira como uma pessoa percebe intuitivamente a aceleração da tensão do mercado antes de um movimento explícito do preço, mas não de forma subjetiva nem probabilística, e sim por meio de uma estrutura formalizável e computável.

Para tornar essa ideia mais intuitiva, podemos imaginar o mercado como um campo de forças no qual cada evento não é simplesmente um ponto, mas uma perturbação que gera uma tensão local. É justamente essa tensão, não a mudança de preço em si, mas a direção de seu potencial, que queremos captar. O Spatio-Temporal Event embedding no STE-FlowNet atua como um transformador desse campo. Em vez de registrar o preço apenas como um valor, ele codifica o vetor de mudança de estado ao longo do tempo e no espaço dos eventos. Cada evento é interpretado não como um fato isolado, mas como uma transferência de força, uma micropressão que pode se dissipar ou se intensificar em determinada direção.

O passo seguinte é o Learnable Flow Reconstruction. Esse é o núcleo da arquitetura. O modelo não se limita a encadear os eventos em sequência, mas procura reconstruir uma trajetória coerente do fluxo, como se reconstruísse um vento invisível a partir da oscilação da grama. Da sequência de eventos, ele extrai não apenas um histórico, mas uma forma em movimento, um vetor de evolução. Por isso, o STE-FlowNet não se limita a prever um estado do mercado; ele aprende a interpretar seu direcionamento interno em tempo real, enquanto essa tendência ainda está começando a surgir na microestrutura das negociações.

É justamente aqui que se torna evidente a diferença fundamental entre o STE-FlowNet e as abordagens clássicas. Ele funciona como um sistema capaz de captar a intenção do mercado antes mesmo que ela se manifeste nas oscilações de preço. Não se trata de uma previsão estatística nem da identificação de um padrão. Mas de uma tentativa de apreender a arquitetura causal do movimento do mercado enquanto ela ainda está se formando.

E é nesse ponto que ocorre a transição fundamental: da análise do efeito para a percepção da causa.

Na parte prática do artigo anterior, implementamos com sucesso a camada CNeuronSpikeConvGRU, estabelecendo a base para as etapas seguintes. Hoje, damos continuidade a esse trabalho, aprofundando o desenvolvimento dos algoritmos do framework STE-FlowNet. O foco deixa de estar na compreensão teórica do mercado e passa para a implementação prática de uma arquitetura capaz de interpretar o fluxo de eventos e prever a direção do movimento do mercado em tempo real.


Módulo de correlação

A próxima etapa do nosso trabalho consiste na criação de um módulo para o aprendizado das correlações entre estados. É importante observar que, no framework original, os autores analisavam as correlações entre o quadro atual e versões deslocadas do primeiro quadro da sequência analisada, determinando assim o impulso dinâmico de toda a sequência precedente. Essa abordagem é bastante adequada à análise de sequências fixas e limitadas, nas quais o contexto histórico permanece disponível.

Nossa tarefa, porém, é fundamentalmente diferente: buscamos analisar os dados em tempo real. Na prática, isso significa lidar com sequências potencialmente infinitas. Além disso, a influência dos eventos diminui à medida que nos afastamos deles no tempo, tornando pouco eficiente o uso prolongado de um estado de referência estático.

A primeira solução natural seria utilizar um ponto de referência móvel para a análise. Essa abordagem é simples, preserva o restante da arquitetura sem alterações e permite controlar a magnitude do deslocamento como um hiperparâmetro do modelo, estabelecendo um compromisso entre desempenho e precisão da análise. No entanto, há uma limitação conceitual: cada estado passa a ter sua própria referência, o que torna incomparáveis as correlações entre estados consecutivos, sem falar nas correlações com eventos mais distantes.

Precisamos, portanto, de uma solução mais adequada, e uma alternativa é utilizar uma pilha de estados. Nesse caso, deixamos de gerar variações do estado de referência e passamos a calcular as correlações de cada novo estado com todos os estados armazenados na pilha. Essa abordagem permite preservar a estrutura histórica da influência dos eventos e, ao mesmo tempo, manter a comparabilidade das correlações, algo essencial para a análise e a previsão de processos dinâmicos no mercado.

Objeto da pilha

A implementação da pilha é uma tarefa de caráter geral, que exige uma abordagem cuidadosa e flexível. Para garantir modularidade e reutilização, implementamos a pilha como uma classe independente, responsável pelo armazenamento dinâmico dos estados e pela integração com os demais componentes.

class CNeuronAddToStack :  public CNeuronBaseOCL
  {
protected:
   uint              iStackSize;
   uint              iDimension;
   uint              iVariables;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return true; }
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronAddToStack(void) : iStackSize(0), iDimension(0), iVariables(0) {};
                    ~CNeuronAddToStack(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint stack_size, uint dimension, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronAddToStack;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual void      SetActivationFunction(ENUM_ACTIVATION value) override { };
   //---
   virtual uint      GetStackSize(void) const { return iStackSize; }
   virtual uint      GetDimension(void) const { return iDimension; }
   virtual uint      GetVariables(void) const { return iVariables; }
  };

Na estrutura da classe, encontramos apenas 3 variáveis que definem os parâmetros da pilha:

  • iStackSize - armazena o número de estados na pilha;
  • iVariables - quantidade de sequências unitárias em cada estado;
  • iDimension - define a dimensionalidade da representação de uma sequência unitária em cada estado.

Para o armazenamento propriamente dito dos dados, são utilizados os buffers herdados da classe pai.

A inicialização de uma nova instância da classe é realizada no método Init, que configura cuidadosamente todos os parâmetros necessários.

bool CNeuronAddToStack::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                             uint stack_size, uint dimension, uint variables,
                             ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, stack_size * dimension * variables,
                                                                   optimization_type, batch))
      return false;
//---
   activation = None;
   iStackSize = stack_size;
   iDimension = dimension;
   iVariables = variables;
//---
   return true;
  }

O algoritmo do método é bastante simples e direto. Primeiro, transferimos o controle para o método homônimo da classe pai, inicializando todas as interfaces herdadas. Nesse ponto, especificamos um tamanho para o buffer de resultados suficiente para armazenar toda a pilha. Após a execução bem-sucedida desse método, salvamos os parâmetros da pilha nas variáveis internas. Ao final, o método retorna true, confirmando que a configuração foi concluída com sucesso.

Após a inicialização bem-sucedida do objeto, a etapa seguinte consiste na implementação do algoritmo de propagação para frente. Assim como nos demais módulos, todos os cálculos foram transferidos para o contexto OpenCL, permitindo acelerar e paralelizar o processamento dos dados. Para isso, adicionamos um novo kernel AddToStack ao programa OpenCL. Ele realiza o deslocamento sequencial dos elementos da pilha, possibilitando a inclusão de um novo estado sem perder os dados históricos já armazenados.

__kernel void AddToStack(__global const float* inputs,
                         __global float* stack,
                         const int stack_size)
  {
   const size_t id = get_global_id(0);
   const size_t loc_id = get_local_id(1);
   const size_t var = get_global_id(2);
   const size_t dimension = get_global_size(0);
   const size_t total_loc = get_local_size(1);
   const size_t variables = get_global_size(2);

Como parâmetros, o kernel recebe ponteiros para 2 buffers de dados: o buffer de novos estados (inputs) e o buffer da pilha atual (stack). Além disso, o usuário informa o tamanho da pilha por meio do parâmetro stack_size.

No corpo do kernel, primeiro inicializamos os identificadores e as dimensões do espaço de execução:

  • id - índice global do elemento no vetor que descreve o estado de uma sequência unitária;
  • loc_id - índice local dentro do grupo de trabalho;
  • var - índice da sequência unitária.

Em seguida, determinamos o número de iterações do laço interno necessárias para processar toda a sequência da pilha distribuindo as operações entre os elementos do grupo de trabalho, distribuindo corretamente os cálculos e controlando o paralelismo.

   const int total = (stack_size - 1) / total_loc;

Em condições normais, esperamos que uma única iteração do laço seja suficiente. No entanto, a inclusão desse laço torna o algoritmo mais universal, evitando que o tamanho da pilha fique limitado pelos parâmetros do dispositivo utilizado.

Depois, iniciamos o laço principal do fim para o início, deslocando sequencialmente os elementos da pilha. O processamento começa pelo estado mais distante e termina naquele mais próximo do estado atual.

   for(int i = total; i >= 0; i--)
     {
      int inp = 0;
      if(i == 0 && loc_id == 0)
         inp = IsNaNOrInf(inputs[RCtoFlat(var, id, variables, dimension, 1)], 0);
      else
         if((i * total_loc + loc_id) < stack_size)
           {
            int shift = RCtoFlat(i * total_loc + loc_id - 1, id, stack_size, dimension, var);
            inp = IsNaNOrInf(stack[shift], 0);
           }
      BarrierLoc

Dentro do laço, primeiro lemos os dados dos buffers globais. Para o primeiro estado, utilizamos os dados do buffer de novos estados. Todos os demais elementos leem os valores anteriormente armazenados na pilha, com um deslocamento de um estado. Ao mesmo tempo, verificamos obrigatoriamente se os valores obtidos estão corretos e sincronizamos a execução dos work-items do grupo de trabalho para que todos os cálculos de deslocamento sejam concluídos antes da gravação dos novos valores. Isso evita conflitos durante a atualização simultânea da pilha.

Por fim, gravamos o valor preparado na posição atual da pilha.

      if((i * total_loc + loc_id) < stack_size)
        {
         int shift = RCtoFlat(i * total_loc + loc_id, id, stack_size, dimension, var);
         stack[shift] = inp;
        }
     }
  }

Dessa forma, o novo elemento é adicionado, enquanto os anteriores são deslocados corretamente, preservando a sequência dos estados históricos.

Esse algoritmo permite atualizar dinamicamente a pilha com processamento paralelo, possibilitando que o STE-FlowNet analise a sequência de estados em tempo real sem comprometer o desempenho nem a correção dos dados.

O uso combinado de identificadores globais e locais permite processar os arrays de dados com eficiência e gerenciar a dimensionalidade da pilha. Assim, a implementação em OpenCL oferece alto desempenho e boa escalabilidade no processamento de séries temporais dinâmicas do mercado financeiro.

Na nossa classe, criaremos um método wrapper para esse kernel seguindo o esquema que já conhecemos.

bool CNeuronAddToStack::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL || !OpenCL)
      return false;
//---
   if(NeuronOCL.Neurons() < int(iDimension * iVariables))
      return false;
//---
   uint global_work_offset[3] = {0};
   uint global_work_size[3] = { iDimension,
                                MathMin((uint)OpenCL.GetMaxLocalSize(1), iStackSize),
                                iVariables
                              };
   uint local_work_size[3] = { 1, global_work_size[1], 1 };
   uint kernel = def_k_AddToStack;
   setBuffer(kernel, def_k_ats_inputs, NeuronOCL.getOutputIndex())
   setBuffer(kernel, def_k_ats_stack, getOutputIndex())
   setArgument(kernel, def_k_ats_stack_size, (int)iStackSize)
   kernelExecuteLoc(kernel, global_work_offset, global_work_size, local_work_size)
//---
   return true;
  }

Observe que, para definir o tamanho do grupo de trabalho, utilizamos o menor valor entre o tamanho da pilha especificado pelo usuário e o tamanho máximo permitido para o grupo de trabalho no dispositivo utilizado. Isso permite otimizar o uso da memória e do paralelismo.

A inclusão de dados na pilha resolve apenas a propagação para frente. Para que o modelo seja plenamente treinável, isso não é suficiente: também precisamos propagar corretamente os gradientes do erro até os dados de origem. No nosso caso, a própria classe da pilha não contém parâmetros treináveis; ela apenas armazena e desloca os estados. Ainda assim, é importante garantir que o sinal de erro chegue ao elemento que deu origem ao estado atual.

Na prática, implementamos isso da seguinte forma. Durante o treinamento, os gradientes são propagados apenas para o elemento atual, no topo da pilha. Para isso, não é necessário criar um novo kernel OpenCL. Basta utilizar a operação de desconcatenação já existente sobre o buffer geral de erro da saída do objeto. Extraímos o segmento de gradientes correspondente à posição do estado atual e o encaminhamos para a fonte dos dados originais.

bool CNeuronAddToStack::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL || !OpenCL)
      return false;
   if(NeuronOCL.Neurons() < int(iDimension * iVariables))
      return false;
   if(!DeConcat(NeuronOCL.getGradient(), PrevOutput, Gradient, iDimension,
                               iDimension * (iStackSize - 1), iVariables))
      return false;
   Deactivation(NeuronOCL)
//---
   return true;
  }

Depois disso, resta apenas multiplicar os valores obtidos pela derivada da função de ativação do objeto que forneceu os dados de origem.

Essa abordagem é simples, determinística e eficiente em termos de recursos. Não são necessários kernels computacionais adicionais, e a lógica da retropropagação permanece localizada e fácil de compreender.

Objeto de correlação

Agora que dispomos de um objeto confiável para gerenciar a pilha de estados, o passo seguinte é naturalmente a implementação do módulo de correlação. Seguindo a abordagem proposta no trabalho original, avaliaremos a similaridade entre os estados por meio do produto escalar (dot-product) entre os respectivos vetores de representação. No entanto, é importante esclarecer o contexto. Em vez de trabalhar com quadros fixos, aplicamos essa métrica a cada sequência unitária, ou seja, blocos curtos e semanticamente coerentes de eventos, adequados ao processamento em tempo real.

A abordagem baseada no produto escalar é simples, rápida e interpretável. Para dois vetores, ela fornece uma estimativa direta do grau de alinhamento entre os vetores no espaço de atributos, o que, na nossa interpretação, equivale a avaliar o grau de concordância entre as direções das forças no campo. O uso de sequências unitárias oferece ainda uma vantagem adicional: preservamos a coerência local do contexto e, ao mesmo tempo, limitamos o volume de cálculos, algo essencial para a análise online de fluxos de dados potencialmente infinitos.

Implementaremos a abordagem proposta em uma nova classe CNeuronStackCorrelation.

class CNeuronStackCorrelation :  public CNeuronBaseOCL
  {
protected:
   CNeuronAddToStack cStack;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return true; }
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronStackCorrelation(void);
                    ~CNeuronStackCorrelation(void);
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint stack_size, uint dimension, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   override const   {  return defNeuronStackCorrelation;   }
   //--- 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      Clear(void) override;
  };


Na estrutura apresentada da nova classe, o elemento central é o objeto interno cStack, responsável pelo gerenciamento do histórico de estados. Como ele é criado estaticamente, o construtor e o destrutor da classe podem permanecer vazios. Não há necessidade de alocar nem liberar recursos manualmente. Toda a inicialização do novo objeto fica concentrada no método Init.

bool CNeuronStackCorrelation::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                   uint stack_size, uint dimension, uint variables,
                                   ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, variables * stack_size, optimization_type, batch))
      return false;

Como de costume, começamos chamando o método homônimo da classe pai, no qual já está implementada a inicialização das interfaces herdadas.

Aqui, vale observar que a multiplicação de dois vetores retorna um valor escalar. Por isso, definimos o tamanho do buffer de resultados como o produto entre o número de sequências unitárias e a profundidade da pilha.

Se essa etapa for concluída com sucesso, inicializamos imediatamente o objeto interno cStack, passando a ele uma referência ao contexto OpenCL atual e os parâmetros que definem a estrutura da pilha.

   if(!cStack.Init(0, 0, OpenCL, stack_size, dimension, variables, optimization, iBatch))
      return false;
//---
   return true;
  }

Dessa forma, os dois componentes são conectados de maneira sincronizada ao mesmo ambiente computacional, e a pilha já está pronta para receber os primeiros dados assim que o método de inicialização é concluído, sem chamadas adicionais nem operações postergadas de forma implícita. Na prática, tudo é inicializado de imediato e de forma explícita, sem lógica de inicialização tardia e sem efeitos colaterais, o que é fundamental para garantir um comportamento reproduzível durante o treinamento e a depuração.

A etapa seguinte é implementar o algoritmo de propagação para frente, que neste módulo segue diretamente a lógica da análise em fluxo contínuo.

bool CNeuronStackCorrelation::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cStack.FeedForward(NeuronOCL))
      return false;

Primeiro, atualizamos a pilha de estados, passando a ela o estado recebido do objeto que fornece os dados de origem. Se, por qualquer motivo, não for possível atualizar a pilha, a execução é interrompida imediatamente. A propagação para frente não admite estados intermediários ambíguos.

Em seguida, chegamos à etapa principal: o cálculo da correlação entre o estado atual e o histórico. Para isso, utilizamos a operação de multiplicação matricial MatMul. A pilha formada atua como matriz à esquerda da operação, e o estado atual, como matriz à direita. O resultado é gravado no buffer Output.

   if(!MatMul(cStack.getOutput(), NeuronOCL.getOutput(), Output, cStack.GetStackSize(),
              cStack.GetDimension(), 1, cStack.GetVariables(), true))
      return false;

No contexto da análise de sequências unitárias, multiplicamos a matriz que representa o histórico armazenado pelo vetor do estado atual. Como resultado, obtemos um vetor que expressa a similaridade entre a observação atual e cada elemento da pilha, formando assim um mapa completo do contexto causal-temporal local.

Se necessário, uma função de ativação pode ser aplicada imediatamente aos valores obtidos.

   if(activation != None)
      if(!Activation(Output, Output, activation))
         return false;
//---
   return true;
  }

Assim, a propagação para frente segue a lógica Memória -> Comparação -> Interpretação. A pilha é atualizada. As correlações são calculadas. Opcionalmente, o resultado é transformado para o formato necessário. Tudo é conciso, estritamente sequencial e totalmente compatível com o processamento contínuo de dados em tempo real.

Embora esse algoritmo não contenha parâmetros treináveis próprios, ele participa integralmente da cadeia computacional e, por isso, deve oferecer suporte correto à retropropagação do erro. Caso contrário, o modelo perde a capacidade de adaptação, e todo o treinamento se transforma em um esquema estático, sem realimentação. É justamente por isso que o método calcInputGradients implementa a distribuição e o processamento dos gradientes entre todos os elementos da estrutura.

bool CNeuronStackCorrelation::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;

Na primeira etapa, verificamos se o ponteiro recebido para o objeto que fornece os dados de origem é válido. Sem um ponteiro válido, as operações seguintes deixam de fazer sentido.

Em seguida, Em seguida, chamamos a função que distribui os gradientes por meio de uma multiplicação matricial, calculando-os com base nos resultados da propagação para frente anterior..

   if(!MatMulGrad(cStack.getOutput(), cStack.getGradient(),
                  NeuronOCL.getOutput(), cStack.getPrevOutput(),
                  Gradient, cStack.GetStackSize(),
                  cStack.GetDimension(), 1, cStack.GetVariables(), true))
      return false;

Nesse ponto, recalculamos a sensibilidade de cada estado da pilha ao erro acumulado na saída. Na prática, essa etapa nos permite determinar quais estados anteriores exerceram maior influência sobre o resultado atual.

Se houver uma função de ativação definida no objeto de origem dos dados, ajustamos a distribuição dos gradientes levando em conta as transformações não lineares.

   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), cStack.getPrevOutput(),
                       cStack.getPrevOutput(), NeuronOCL.Activation()))
         return false;

Depois, propagamos os gradientes do erro referentes ao estado atual pelo caminho principal da pilha.

   if(!NeuronOCL.CalcHiddenGradients(cStack.AsObject()))
      return false;

Por fim, acumulamos, no objeto que fornece os dados de origem, os gradientes recebidos pelos dois fluxos de informação.

   if(!SumAndNormilize(NeuronOCL.getGradient(), cStack.getPrevOutput(),
                       NeuronOCL.getGradient(), cStack.GetDimension(), false, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

Dessa forma, o método oferece suporte completo à propagação reversa, transformando o módulo de correlação de uma simples ferramenta analítica em um elemento ativo da arquitetura treinável. Ele reúne e redistribui cuidadosamente as informações de erro, criando a base para o aprendizado adaptativo eficiente de todo o sistema.


Codificador

Chegamos a uma das etapas principais. Todos os blocos fundamentais do codificador STE-FlowNet já estão prontos, e agora é hora de reuni-los em uma arquitetura coerente. Nesta etapa, criamos a classe de nível superior CNeuronSTEFlowNetEncoder, que assume o papel de coordenadora e conecta a linha recorrente, o módulo de dinâmica do fluxo e o mecanismo de correlação.

class CNeuronSTEFlowNetEncoder   :  public CNeuronSpikeConvGRU
  {
protected:
   CLayer            cRecurrentLine;
   CLayer            cFlow;
   CLayer            cCorrelation;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronSTEFlowNetEncoder(void);
                    ~CNeuronSTEFlowNetEncoder(void);
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint &chanels[], uint &units[], uint group_size,
                          uint groups, uint heads, uint dimension_k, uint stack_size,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual CNeuronBaseOCL* GetLayer(uint layer);
   virtual uint      Layers(void)   const { return (cRecurrentLine.Total() + 1) / 3; }
   //--
   virtual int       Type(void)   override const   {  return defNeuronSTEFlowNetEncoder;   }
   //--- 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;
  };

Dentro da classe, são declarados três componentes internos: cRecurrentLin, cFlow e cCorrelation. Trata-se de arrays dinâmicos de ponteiros para objetos, responsáveis, respectivamente, pelo processamento da sequência temporal, pela extração das informações de fluxo e pela avaliação das dependências locais entre os estados.

A inicialização de um novo objeto é realizada no método Init, que desempenha um papel central na construção de toda a cadeia computacional do codificador. Sua tarefa é montar uma cascata de blocos recorrentes, de correlação e residuais que, em conjunto, realizam o processamento dos dados em fluxo contínuo.

bool CNeuronSTEFlowNetEncoder::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                    uint &chanels[], uint &units[], uint group_size,
                                    uint groups, uint heads, uint dimension_k, uint stack_size,
                                    ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(chanels.Size() != units.Size())
      return false;

Tudo começa pela verificação da consistência dos parâmetros de entrada. A dimensão do array que contém o número de canais deve coincidir com a do array de elementos analisados na sequência e, ao mesmo tempo, indicar a quantidade de blocos internos do codificador. Caso contrário, não é possível prosseguir com a montagem.

Depois disso, é chamado o método homônimo da classe pai, que define os parâmetros fundamentais da última camada e serve como ponto de referência para toda a arquitetura.

   const uint layers = chanels.Size() - 1;
   if(!CNeuronSpikeConvGRU::Init(numOutputs, myIndex, open_cl, units[layers], chanels[layers] + stack_size,
                                 chanels[layers], optimization_type, batch))
      return false;

Em seguida, os três contêineres são limpos e preparados: um para a linha recorrente, outro para a parte de fluxo e outro para os módulos de correlação. São eles que armazenarão os blocos criados.

   CNeuronSSAMResNeXtBlock*   conv_res = NULL;
   CNeuronStackCorrelation*   correl = NULL;
   CNeuronSpikeConvGRU*       gru = NULL;
   CNeuronBaseOCL*            neuron = NULL;
//---
   cRecurrentLine.Clear();
   cFlow.Clear();
   cCorrelation.Clear();
   cRecurrentLine.SetOpenCL(OpenCL);
   cFlow.SetOpenCL(OpenCL);
   cCorrelation.SetOpenCL(OpenCL);

Depois, iniciamos o laço principal, no qual a estrutura do codificador é montada gradualmente. Em cada iteração, o primeiro elemento adicionado é um bloco residual SSAM-ResNeXt, que reforça as dependências locais e permite que o mecanismo de atenção destaque os padrões mais relevantes.

   uint index = 0;
   for(uint i = 0; i <= layers; i++)
     {
      conv_res = new CNeuronSSAMResNeXtBlock();
      if(!conv_res ||
         !conv_res.Init(0, index, OpenCL, chanels[i], chanels[i + 1],
                        units[i], units[i + 1], group_size, groups, heads,
                        dimension_k, optimization, iBatch) ||
         !cRecurrentLine.Add(conv_res))
        {
         DeleteObj(conv_res)
         return false;
        }

Na sequência, é adicionado um neurônio básico, utilizado para concatenar os valores do fluxo principal com os coeficientes de correlação entre os estados analisados.

      index++;
      neuron = new CNeuronBaseOCL();
      if(!neuron ||
         !neuron.Init(0, index, OpenCL, units[i + 1] * (chanels[i + 1]*stack_size), optimization, iBatch) ||
         !cRecurrentLine.Add(neuron))
        {
         DeleteObj(neuron)
         return false;
        }

Se a camada atual não for a última, inserimos na cadeia um módulo GRU recorrente, responsável por integrar as dependências temporais e levar em conta a dinâmica dos sinais.

      if(i == layers)
         break;
      //---
      if(i < layers - 1)
        {
         index++;
         gru = new CNeuronSpikeConvGRU();
         if(!gru ||
            !gru.Init(0, index, OpenCL, units[i + 1], chanels[i + 1] + stack_size, chanels[i + 1],
                                                                          optimization, iBatch) ||
            !cRecurrentLine.Add(gru))
           {
            DeleteObj(gru)
            return false;
           }
        }

Depois disso, adicionamos o bloco de correlação, responsável por analisar as relações entre os canais e as representações armazenadas na pilha.

      index++;
      correl = new CNeuronStackCorrelation();
      if(!correl ||
         !correl.Init(0, index, OpenCL, stack_size, chanels[i + 1], units[i + 1], optimization, iBatch) ||
         !cCorrelation.Add(correl))
        {
         DeleteObj(correl)
         return false;
        }

Cada iteração é concluída com outro bloco SSAM-ResNeXt, agora inserido no caminho principal de fluxo. Isso cria um caminho paralelo de processamento e reforça as relações multicanais.

      index++;
      conv_res = new CNeuronSSAMResNeXtBlock();
      if(!conv_res ||
         !conv_res.Init(0, index, OpenCL, chanels[i + 1], chanels[i + 2],
                        units[i + 1], units[i + 2], group_size, groups, heads,
                        dimension_k, optimization, iBatch) ||
         !cFlow.Add(conv_res))
        {
         DeleteObj(conv_res)
         return false;
        }
     }
//---
   return true;
  }

Dessa forma, o método constrói gradualmente uma arquitetura ramificada:

  • uma sequência recorrente formada por blocos ResNeXt, neurônios básicos e GRU;
  • uma linha de correlação para analisar as relações entre os canais;
  • uma ramificação de processamento do fluxo que reforça as dependências espaciais.

Como resultado, o codificador passa a considerar simultaneamente a dinâmica temporal, destacar padrões locais e analisar relações multidimensionais, tornando-se uma ferramenta robusta e flexível para o processamento de dados complexos.

O método feedForward implementa a propagação para frente e, na prática, coloca em funcionamento toda a arquitetura montada durante a inicialização. Os dados de origem são utilizados como estado inicial e, a partir daí, começam a percorrer a linha recorrente.

bool CNeuronSTEFlowNetEncoder::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   CNeuronBaseOCL *recur = NeuronOCL;
   CNeuronBaseOCL *flow = cRecurrentLine[0];
   CNeuronStackCorrelation *correl = NULL;
   CNeuronSSAMResNeXtBlock* resnext = NULL;
   int layers = (cRecurrentLine.Total() + 1) / 3;
//---
   for(int i = 0; i < layers; i++)
     {
      //--- ResNeXt
      if(!cRecurrentLine[i * 3] || !cRecurrentLine[i * 3].FeedForward(recur))
         return false;
      recur = cRecurrentLine[i * 3];

Na primeira etapa de cada iteração do laço, é ativado o bloco ResNeXt, que processa o estado atual e atribui seu ponteiro à variável local recur. Em paralelo, é executado o módulo de correlação, que trabalha com o fluxo e extrai dependências intertemporais entre os estados.

      //--- Correlation
      correl = cCorrelation[i];
      if(!correl ||
         !correl.FeedForward(flow))
         return false;

Em seguida, os resultados dessas duas ramificações são combinados. A saída do ResNeXt e a saída do bloco de correlação são concatenadas e encaminhadas para o neurônio seguinte. Isso permite combinar os atributos espaciais locais com as informações sobre as relações entre os estados.

      //--- Concatenate
      resnext = recur;
      recur = cRecurrentLine[i * 3 + 1];
      if(!recur ||
         !Concat(resnext.getOutput(), correl.getOutput(), recur.getOutput(), resnext.GetChannelsOut(),
                                                        correl.GetStackSize(), resnext.GetUnitsOut()))
         return false;

Se a camada atual não for a última, entra em ação a célula GRU, que incorpora a dinâmica temporal global e realiza o processamento recorrente.

      if((i * 3 + 2) == cRecurrentLine.Total())
         break;
      //--- GRU
      if(!cRecurrentLine[i * 3 + 1] ||
         !cRecurrentLine[i * 3 + 2].FeedForward(recur))
         return false;
      recur = cRecurrentLine[i * 3 + 1];

Depois disso, a ramificação cFlow é atualizada, reforçando as dependências espaciais e criando uma via paralela para o fluxo de informações.

      if(!cFlow[i] ||
         !cFlow[i].FeedForward(flow))
         return false;
      flow = cFlow[i];
     }
//---
   return CNeuronSpikeConvGRU::feedforward(recur);
  }

Assim, em cada iteração do laço, os dados percorrem a sequência ResNeXt -> Correlation -> Concat -> GRU -> Flow, na qual cada elemento desempenha sua própria função.

Depois que todas as camadas são processadas, o controle é transferido para o método homônimo da classe pai, que conclui a propagação para frente. Como resultado, o método garante a passagem coordenada dos dados pelas linhas paralelas da arquitetura, mantendo o equilíbrio entre a dinâmica temporal, os padrões espaciais e as correlações entre os canais.

Hoje avançamos bastante e merecemos uma breve pausa. No próximo artigo, retomaremos o tema com novas energias e levaremos o trabalho iniciado até o fim.


Conclusão

Neste artigo, analisamos passo a passo como o codificador STE-FlowNet é construído e como funciona. Vimos como módulos independentes se combinam em uma arquitetura integrada. E, ao analisar o método de propagação para frente, ficou claro de que forma esses elementos interagem. Como resultado, obtivemos uma arquitetura capaz de extrair padrões espaço-temporais levando em conta a dinâmica oculta de causa e efeito, preservando a memória temporal por meio da pilha e avaliando imediatamente as correlações entre os estados.

Na próxima parte, avançaremos para a validação prática: testaremos o modelo com dados históricos reais, avaliaremos a velocidade de convergência, a robustez ao ruído do mercado e a viabilidade prática da abordagem. É justamente nessa etapa que poderemos responder à principal questão: a arquitetura proposta se mostra eficaz nas condições reais de mercado?

Referências


Programas utilizados no artigo

# Nome Tipo Descrição
1 Study.mq5 Expert Advisor EA para treinamento offline de modelos
2 StudyOnline.mq5 Expert Advisor EA para treinamento online de modelos
3 Test.mq5 Expert Advisor EA para teste do modelo
4 Trajectory.mqh Biblioteca de classes Estrutura de descrição do estado do sistema e da arquitetura dos modelos
5 NeuroNet.mqh Biblioteca de classes Biblioteca de classes para criação de redes neurais
6 NeuroNet.cl Biblioteca Biblioteca de código do programa OpenCL

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/20039

Arquivos anexados |
MQL5.zip (3222.94 KB)
Tabelas no paradigma MVC em MQL5: colunas configuráveis e ordenáveis Tabelas no paradigma MVC em MQL5: colunas configuráveis e ordenáveis
Neste artigo, implementaremos o ajuste da largura das colunas da tabela com o cursor do mouse, a ordenação da tabela pelos dados das colunas e adicionaremos uma nova classe para simplificar a criação de tabelas com base em quaisquer conjuntos de dados.
Redes neurais no trading: Percepção adaptativa da dinâmica de mercado (STE-FlowNet) Redes neurais no trading: Percepção adaptativa da dinâmica de mercado (STE-FlowNet)
O framework STE-FlowNet oferece uma nova perspectiva para a análise de dados financeiros, reagindo a eventos reais do mercado, e não a timeframes fixos. Sua arquitetura preserva as dependências locais e temporais, permitindo rastrear até mesmo pequenos impulsos na dinâmica dos preços.
Modelos matemáticos em estratégias de grade Modelos matemáticos em estratégias de grade
Neste artigo, veremos como aplicar a matemática às estratégias de grade. Analisaremos os princípios básicos de funcionamento da estratégia, suas vantagens e desvantagens. Você aprenderá a construir uma grade de negociação, definir parâmetros ideais e gerenciar os riscos de forma eficiente.
Métodos de amostragem MCMC - Algoritmo de Metropolis-Hastings Métodos de amostragem MCMC - Algoritmo de Metropolis-Hastings
O algoritmo de Metropolis-Hastings é um método fundamental de Monte Carlo via cadeias de Markov (MCMC), amplamente utilizado para aproximar distribuições a posteriori na inferência bayesiana. O artigo apresenta os fundamentos teóricos do algoritmo, a implementação da classe MHSampler em MQL5 e exemplos de aplicação com análise das amostras obtidas.