Русский Português
preview
Redes neuronales en el trading: Modelo de consultas temporales (Final)

Redes neuronales en el trading: Modelo de consultas temporales (Final)

MetaTrader 5Sistemas comerciales |
26 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Introducción

El framework TQNet constituye uno de los enfoques más elegantes y flexibles para la construcción de modelos de redes neuronales capaces de trabajar eficazmente con series temporales y datos estructurados. Su principal ventaja radica en su capacidad para combinar las dependencias locales y globales, creando un equilibrio singular entre la profundidad del análisis y la eficiencia computacional. A diferencia de las arquitecturas tradicionales, TQNet combina de forma orgánica los mecanismos de procesamiento secuencial de datos y los métodos de captura de dependencias a largo plazo. Esto resulta especialmente valioso cuando se trabaja con series temporales financieras, en las que son importantes tanto las fluctuaciones instantáneas como las tendencias extendidas en el tiempo.

El algoritmo se basa en una organización original de los cálculos. En estos cálculos, el tensor de correlaciones globales desempeña un papel fundamental. Crea una especie de mapa de relaciones entre los elementos de la secuencia, lo que permite al modelo identificar patrones locales y construir una representación integral de la estructura de los datos.

El proceso de cálculo dentro de TQNet puede describirse como una secuencia de pasos coordinados. En la primera etapa, los datos de partida pasan por una capa de formación de características locales, en la que se extraen las características clave del estado actual. A continuación, se activa el mecanismo de correlación global, que aplica a estas características las dependencias estructurales acumuladas a lo largo de todos los pasos anteriores. Este esquema iterativo permite que el modelo se adapte con flexibilidad a las condiciones cambiantes y tenga en cuenta los patrones ocultos. La particularidad de este enfoque radica en que el modelo no se limita a un orden rígido de procesamiento de los datos: las relaciones pueden establecerse en cualquier dirección dentro de la secuencia, lo que amplía sus posibilidades en tareas de predicción y análisis.

El valor práctico de TQNet se pone de manifiesto en situaciones en las que los datos están sujetos a ruido o presentan una estructura interna compleja. Los mercados financieros, las previsiones meteorológicas, el análisis de los procesos industriales… En todos estos casos, no solo es importante el nivel de precisión de la previsión, sino también la robustez del modelo ante valores atípicos y cambios en la dinámica. Gracias a un algoritmo bien diseñado para la actualización de los parámetros y al uso eficaz de los recursos computacionales, TQNet ofrece un funcionamiento estable incluso con conjuntos de datos amplios, sin requerir costes excesivos de entrenamiento.

A continuación se muestra una visualización del framework realizada por el autor:

En el artículo anterior conocimos los aspectos teóricos del framework TQNet: su arquitectura modular, su capacidad para adaptarse con flexibilidad a las realidades del mercado y su gestión eficiente de los recursos. Sin embargo, la teoría no supone más que un armazón. En aquel artículo también analizamos el objeto CCircleParams, que permite almacenar correlaciones globales en forma de objetos conmutables dinámicamente en cada paso temporal. Hoy seguimos trabajando en la construcción, mediante MQL5, de los enfoques propuestos por los autores del framework TQNet.


Módulo TQ-MHA

El siguiente paso lógico, al que llegamos tras la implementación satisfactoria del algoritmo de organización del carrusel de tensores de parámetros de correlación, es la creación del módulo TQ-MHA. En la concepción de los autores, este módulo desempeña un papel especial: actúa como un filtro inteligente capaz de analizar los parámetros de correlación acumulados no de forma aislada, sino en estrecha relación con los datos originales que se analizan. En otras palabras, analiza las estadísticas e intenta captar la red de interrelaciones, complementándola con el contexto actual del mercado. Este enfoque puede compararse con el trabajo de un analista experimentado, que no se limita a tablas y gráficos fríos, sino que siempre comprueba la solidez de las cifras, contrastándolas con los acontecimientos reales y la dinámica del mercado.

Si establecemos un paralelismo con soluciones arquitectónicas que ya conocemos, el TQ-MHA se asemeja en muchos aspectos al módulo de atención cruzada. Sin embargo, hay una particularidad: en lugar de una comparación directa entre dos flujos de información —las claves y las consultas—, nos encontramos ante un proceso más sutil. El módulo TQ-MHA superpone los datos originales sobre matrices de relaciones de correlación, destacando las intersecciones más significativas. No se trata simplemente de una optimización técnica, sino de un nivel de análisis cualitativamente nuevo que permite al framework interpretar las señales del mercado en un contexto más amplio.

En este sentido, cabe destacar otro aspecto interesante. En los módulos de atención cruzada que implementamos anteriormente, según los cánones de las arquitecturas Transformer, siempre estaba presente el bloque FeedForward: un elemento compacto, pero importante, que desempeña la función de transformador no lineal de datos entre capas de atención. En cambio, los autores del framework TQNet omiten formalmente este bloque, como si dejaran fuera de escena todo un paso de la cadena habitual. A primera vista, esto puede parecer una simplificación, pero al analizar con más detenimiento la lógica interna del framework, descubrimos que tras el TQ-MHA aparece el módulo MLP, que, en esencia, reproduce toda la funcionalidad del bloque FeedForward clásico.

La diferencia radica únicamente en un matiz: las funciones de activación. Sin embargo, para nosotros esto no supone un obstáculo; al contrario, abre un espacio para la experimentación. Podemos sustituir sin mayor dificultad la función de activación por otra que se adapte mejor a nuestros objetivos y a las características específicas de las series temporales financieras.

De este modo, hemos llegado al diseño de un objeto especializado, el CNeuronTQMHA, que integra la lógica de la atención cruzada teniendo en cuenta los parámetros de correlación acumulados y garantiza una interacción fluida con el resto de módulos del framework. A continuación se muestra la estructura de la nueva clase.

class CNeuronTQMHA   :  public CNeuronCrossAttention
  {
protected:
   uint              iTimeframe;
   CCircleParams     cParams;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput);
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer, CBufferFloat *SecondInput,
                       CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput);

public:
                     CNeuronTQMHA(void) {};
                    ~CNeuronTQMHA(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint window, uint window_key, uint heads,
                          uint units_in, uint period, uint timeframe,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   const   {  return defNeuronTQMHA;   }
   //--- métodos para trabajar con archivos
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void      SetOpenCL(COpenCLMy *obj);
  }; 

La clase CNeuronTQMHA hereda la funcionalidad básica y la mayor parte de los objetos internos del objeto de atención cruzada CNeuronCrossAttention. Sin embargo, a diferencia de la variante básica de atención cruzada, está orientada a trabajar con parámetros de correlación obtenidos de un carrusel de tensores.

En el cuerpo de la clase solo declaramos dos elementos clave. El primero es iTimeframe, que fija el tamaño del paso temporal para cambiar la matriz de correlaciones en nuestro carrusel. El segundo es cParams, una instancia de la clase CCircleParams, que se encarga de los parámetros de correlación acumulados. Son precisamente estos parámetros los que constituyen la materia prima que la atención multicabeza analizará en el contexto de los datos originales.

Los objetos internos de nuestra clase se declaran de forma estática, por lo que el constructor y el destructor pueden dejarse vacíos: no ocurre nada superfluo durante la creación o eliminación. Todas las operaciones pesadas se han trasladado al método de inicialización. Esto proporciona una semántica clara y predecible del ciclo de vida del objeto: la creación es barata, y la preparación para el funcionamiento, controlada y explícita.

El método Init lleva a cabo varios pasos importantes y comprueba minuciosamente el resultado de cada uno de ellos.

bool CNeuronTQMHA::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                        uint window, uint window_key, uint heads,
                        uint units_count, uint period, uint timeframe,
                        ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronCrossAttention::Init(numOutputs, myIndex, open_cl, window,
                                   window_key, heads, units_count, window,
                                   units_count, optimization_type, batch))
      return false;

En primer lugar, el control se transfiere al método homónimo de la clase base, heredando y configurando así la lógica básica de la atención cruzada. Si, por cualquier motivo, la inicialización básica falla, el método retorna inmediatamente false; esta salida temprana evita que se propague un estado incorrecto más adelante en la pila y facilita la depuración.

A continuación, configuramos la función de activación entre las capas del bloque FeedForward.

FF[0].SetActivationFunction(GELU);

Como ya se ha comentado anteriormente, el MLP que utilizan los autores del framework tras el TQ-MHA desempeña, en esencia, la función de un bloque FeedForward clásico. Además, cambiar la función de activación permite ajustar de forma flexible la no linealidad a las características específicas de las series financieras. GeLU ofrece una aproximación más suave y, a menudo, se comporta de forma más estable con datos ruidosos en comparación con los tramos rígidos de ReLU. Los impulsos instantáneos del mercado no alteran esta función de forma tan brusca, por lo que el entrenamiento resulta más uniforme.

El parámetro iTimeframe está protegido por un límite inferior de valores; se trata de una protección sencilla, pero importante: el valor del parámetro no puede descender hasta cero. En la práctica, esto significa que, incluso si la configuración es errónea, el módulo funcionará en el modo mínimamente razonable.

   iTimeframe = MathMax(1, timeframe);
   if(!cParams.Init(0, 0, OpenCL, window * units_count, period, optimization, iBatch))
      return false;
   if(!cParams.Zeros())
      return false;
//---
   return true;
  }

El fragmento clave de la inicialización es la preparación del objeto cParams. Aquí creamos explícitamente un marco interno de correlaciones globales. Para cada ventana temporal (window) y para cada secuencia unitaria analizada (units_count) se crea el elemento correspondiente en el tensor de correlaciones. La inicialización vincula estos búferes al contexto de OpenCL, lo que garantiza una mayor eficiencia con grandes volúmenes de datos.

Por último, la llamada a cParams.Zeros establece la estrategia de inicialización de partida recomendada por los autores: todos los parámetros de la memoria global se ponen a cero. Se trata de una decisión de diseño importante: la inicialización a cero elimina los sesgos iniciales en las correlaciones y proporciona al modelo una pizarra en blanco, en la que este establece por sí mismo las relaciones basándose únicamente en los datos de la muestra de entrenamiento. Este tipo de inicio suele ser preferible a la inicialización aleatoria en tareas en las que no queremos introducir ruido externo a nivel de los parámetros de memoria.

Si todos los pasos se han completado con éxito, el método devuelve true: el objeto está totalmente listo para funcionar; se han creado los búferes, se ha configurado la función de activación y la memoria periódica está lista y limpia. En definitiva, este método establece una plataforma estable, predecible y eficaz para las operaciones posteriores, en las que CNeuronTQMHA comenzará a relacionar realmente los vectores θ acumulados con los datos analizados y a aprender a partir de las cotizaciones históricas.

Tras describir la estructura de la clase, lo lógico es pasar a analizar su procedimiento de trabajo clave: el algoritmo de pasada directa. El método comienza con un paso muy sencillo, pero fundamental: comprobar si el puntero obtenido al búfer SecondInput sigue siendo válido.

bool CNeuronTQMHA::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput)
  {
   if(!SecondInput)
      return false;

En este búfer esperamos obtener una referencia para posicionar el carrusel de parámetros de correlación. Si no existe, la ejecución se interrumpe inmediatamente y se devuelve false. Esta comprobación protege el módulo frente a llamadas incorrectas y evita que se realicen cálculos innecesarios.

A continuación, determinamos la posición actual en el carrusel. Para ello, leemos el elemento cero del búfer SecondInput. En la práctica, esperamos obtener aquí la hora de apertura de la última barra.

   int pos = int(SecondInput[0]);
   pos = (pos / int(iTimeframe)) % cParams.GetPeriod();
   if(!cParams.SetPosition(pos) ||
      !cParams.FeedForward())
      return false;

El valor entero obtenido se convierte a continuación en el índice de posición del carrusel de parámetros. En primer lugar, al dividir por iTimeframe, agrupamos los pasos consecutivos en bloques según el paso especificado; esto nos da el número de bloque. A continuación, el resto de la división por el tamaño del periodo de datos da el índice de un elemento concreto en el carrusel de parámetros de correlación. En conjunto, estas dos operaciones ponen en práctica el concepto de memoria periódica: para los instantes de tiempo que se encuentran en un mismo bloque y están separados por la duración de un periodo, se utilizará el mismo conjunto de vectores TQ.

El siguiente bloque cambia la copia activa de los parámetros y ejecuta a través de ella una pasada directa, durante la cual se preparan para su uso los parámetros de correlación. Si se produce un error en cualquiera de estos pasos, el método finaliza con el resultado false. Este comportamiento garantiza que, a partir de ahí, siempre trabajemos con un conjunto de parámetros globales correctamente preparado.

Los pasos siguientes repiten el funcionamiento del módulo de atención cruzada. Podríamos delegar la ejecución en el método homónimo de la clase padre, pero, desde el punto de vista semántico, esto plantea un problema. En la implementación básica, las conexiones residuales discurren por la línea troncal de consultas (Query). En nuestro caso, Query no es una representación local de la ventana actual, sino vectores TQ acumulados, es decir, la memoria global. Si hacemos pasar esa misma memoria por la ruta de las conexiones residuales, corremos el riesgo de sobrescribir la señal local actual Key/Value y de suprimir características importantes que provienen de los datos originales. Esto se notará rápidamente en los mercados financieros: el modelo empezará a basarse más en el marco histórico y a reaccionar peor ante los nuevos impulsos, por lo que la precisión disminuirá.

Por eso reutilizamos deliberadamente la lógica de bajo nivel para el cálculo de la atención, pero implementamos nuestra propia lógica para las conexiones residuales y la normalización de los resultados. Esto mantiene la influencia de los datos actuales y, al mismo tiempo, utiliza los conocimientos globales TQ al calcular los pesos de atención.

Una vez activados y preparados los vectores de correlación globales, los proyectamos en el espacio de consultas.

   if(!Q_Embedding.FeedForward(cParams.AsObject()))
      return false;
//---
   if(!KV_Embedding.FeedForward(NeuronOCL))
      return false;

Aquí, Q_Embedding realiza una proyección lineal de las correlaciones globales y las despliega por cabezas de atención.

Al mismo tiempo, se generan claves y valores a partir de los datos de origen recibidos de un programa externo. Como resultado, aparecen dos flujos: las consultas globales, que contienen la «memoria» de las dependencias a largo plazo, y los Key/Value locales, que reflejan el comportamiento actual del mercado.

La siguiente llamada al método heredado attentionOut se encarga de calcular la atención multicabeza. En esta fase se lleva a cabo una operación MHA estándar. El resultado es una matriz de atención por cabezas, agregada en el búfer común MHAttentionOut.

Es importante que aquí se implemente precisamente la lógica de combinación de lo global (Query de θTQ) y lo local (Key/Value de los datos originales): este es el núcleo del enfoque TQ.

   if(!attentionOut())
      return false;
//---
   if(!W0.FeedForward(GetPointer(MHAttentionOut)))
      return false;
//---
   if(!SumAndNormilize(W0.getOutput(), NeuronOCL.getOutput(), AttentionOut.getOutput(), iWindow))
      return false;

Una vez obtenidos los resultados de la atención, se someten a una proyección de salida. Se trata precisamente de la matriz WO, que vuelve a combinar las salidas de todas las cabezas de atención en la dimensión del espacio de los datos originales.

A continuación, se suman los datos de los dos flujos de información y se normalizan los resultados obtenidos. Este es el paso estándar de las conexiones residuales. A la proyección de la atención se le suma el tensor de datos de entrada, tras lo cual el resultado se normaliza según la ventana iWindow. Este diseño estabiliza el entrenamiento, conserva la información de la entrada y evita la atenuación de las señales. La normalización también desempeña aquí una función de protección frente a la deriva de la distribución, lo cual es fundamental para las series financieras con volatilidad variable.

Luego se ejecuta la secuencia del bloque MLP.

   if(!FF[0].FeedForward(GetPointer(AttentionOut)))
      return false;
   if(!FF[1].FeedForward(GetPointer(FF[0])))
      return false;
//---
   if(!SumAndNormilize(FF[1].getOutput(), AttentionOut.getOutput(), Output, iWindow))
      return false;
//---
   return true;
  }

Se trata de dos etapas de un perceptrón multicapa con una no linealidad intermedia (en nuestra implementación utilizamos GeLU). En esencia, estas capas desempeñan la misma función que el FeedForward tradicional en los transformadores: amplían la representación, introducen no linealidad, refuerzan las características útiles y ajustan el espectro de frecuencias en el que el modelo «escucha» la señal.

Por último, la segunda llamada a SumAndNormilize combina la salida del MLP con la representación anterior, la normaliza y genera el resultado final. Este paso completa todo el ciclo de procesamiento: desde la extracción de vectores TQ globales hasta la obtención de una representación adaptada y normalizada para el siguiente nivel del modelo.

Si todos los pasos se han completado correctamente, el método devuelve true, lo que indica que la pasada directa se ha realizado correctamente y que los resultados están listos para su uso.

Un detalle arquitectónico importante es la secuencia de comprobaciones tempranas y retornos de emergencia: ahorran recursos y evitan un funcionamiento inconsistente con los búferes OpenCL, lo cual es fundamental en entornos de producción.

Pasando de forma natural desde el análisis de la pasada directa, abordemos la etapa espejo: la distribución de los gradientes del error dentro de CNeuronTQMHA. Aquí seguimos el orden inverso de los cálculos: desde las salidas del bloque hacia atrás, hasta los datos de entrada, acumulando cuidadosamente las señales de error y distribuyéndolas entre todos los componentes: MLP, Attention, proyecciones y la memoria global cParams.

bool CNeuronTQMHA::calcInputGradients(CNeuronBaseOCL *prevLayer,
                                      CBufferFloat *SecondInput,
                                      CBufferFloat *SecondGradient,
                                      ENUM_ACTIVATION SecondActivation = None)
  {
   if(!prevLayer)
     return false;

En el cuerpo del método, comprobamos inmediatamente que el puntero obtenido al objeto de datos de origen sea correcto. Este paso constituye una medida de protección sencilla, pero de vital importancia. Si no hay una capa anterior, no tiene sentido continuar con la cadena de cálculos.

A continuación, empezamos a extraer los gradientes desde el final de la cadena MLP. Los parámetros internos de la MLP reciben la contribución de error correcta, y los gradientes se preparan cuidadosamente para su transmisión a lo largo de la cadena.

   if(!FF[0].CalcHiddenGradients(FF[1].AsObject()))
      return false;
   if(!AttentionOut.CalcHiddenGradients(FF[0].AsObject()))
      return false;

La siguiente línea de código devuelve la señal desde la MLP al punto donde la MLP recibía los datos: a AttentionOut. En otras palabras, le indicamos al bloque de atención: esta es la parte del error que se explica por el trabajo de la MLP; distribúyela entre sus entradas. Esto garantiza la coherencia entre el refinamiento no lineal de la representación y la propia representación formada por Attention.

Pero antes de continuar, debemos calcular los gradientes de error del segundo flujo de información de las conexiones residuales, generando el gradiente para la proyección W0 y su posterior transmisión hacia abajo.

   if(!SumAndNormilize(FF[1].getGradient(), AttentionOut.getGradient(), W0.getGradient(),
                                                                         iWindow, false))
      return false;
   if(!MHAttentionOut.CalcHiddenGradients(W0.AsObject()))
      return false;
   if(!AttentionInsideGradients())
      return false;

El siguiente paso traslada el gradiente al interior de la salida de la atención multicabeza que se había formado antes de la proyección W0. En otras palabras, «desenrollamos» la proyección W0: su gradiente ya se ha calculado, ahora hay que saber qué errores llegan a las propias cabezas de atención.

La llamada a AttentionInsideGradients es una operación clave para analizar las particularidades internas de Attention. Aquí se calculan los gradientes con respecto a todos los componentes internos del mecanismo de atención y, en última instancia, a los tensores Query, Key y Value. Esta etapa se encarga de transformar la señal de error agregada desde el espacio de las cabezas de atención agregadas en contribuciones individuales para las consultas globales y los Key/Values locales.

Luego transmitimos los gradientes acumulados hacia los datos de entrada (prevLayer: entrada local). Llamamos al método correspondiente para distribuir correctamente el error entre los pesos y preparar los gradientes para el propio prevLayer.

   if(!prevLayer.CalcHiddenGradients(KV_Embedding.AsObject()))
      return false;
   if(!cParams.CalcHiddenGradients(Q_Embedding.AsObject()))
      return false;

Al mismo tiempo, realizamos el tratamiento simétrico de la memoria global: los gradientes obtenidos para las consultas (Query) se transfieren a cParams. De este modo, el entrenamiento no solo afecta a las proyecciones locales, sino también a los propios parámetros de la memoria correlacional global: θTQ recibe correctamente su contribución de error, que posteriormente se utilizará para actualizar los pesos.

Tras separar las contribuciones, debemos completar el gradiente de error de los datos originales con los valores de la línea troncal de los datos originales. Pero antes los ajustaremos en función de la derivada de la función de activación de la capa anterior. Y solo entonces se pueden sumar los resultados de los dos flujos de información.

   if(!DeActivation(prevLayer.getOutput(), W0.getPrevOutput(), W0.getGradient(),
                                                                    prevLayer.Activation()))
      return false;
   if(!SumAndNormilize(prevLayer.getGradient(), W0.getPrevOutput(), prevLayer.getGradient(),
                                                                          iWindow_K, false))
      return false;
//---
   return true;
  }

Este es un paso importante: garantiza que el gradiente final que se transmite a prevLayer tenga en cuenta tanto el efecto de la entrada local como la contribución de la proyección W0, distribuida correctamente teniendo en cuenta la normalización aplicada en la pasada directa.

Una vez completadas con éxito todas estas etapas, el método indica que los gradientes se han distribuido correctamente entre todos los componentes internos del módulo y que todo está listo para el paso de actualización de los pesos.

Tras la cuidadosa distribución de los gradientes de error entre todas las estructuras internas de la neurona, sigue un paso natural y lógico de su aplicación práctica: la actualización de los parámetros. El método updateInputWeights cierra el ciclo de entrenamiento. Toma los gradientes calculados y los aplica sucesivamente a las proyecciones, a la MLP y, lo que es especialmente importante, a la propia memoria global de correlaciones.

El enfoque modular que utilizamos permite crear un método bastante conciso para actualizar los parámetros. Nos limitamos a transferir el control de forma secuencial a los métodos homónimos de los objetos internos, pasando los punteros correctos a los búferes correspondientes de los datos de origen.

bool CNeuronTQMHA::updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput)
  {
   if(!cParams.UpdateInputWeights())
      return false;
   if(!Q_Embedding.UpdateInputWeights(cParams.AsObject()))
      return false;
   if(!KV_Embedding.UpdateInputWeights(NeuronOCL))
      return false;
   if(!W0.UpdateInputWeights(GetPointer(MHAttentionOut)))
      return false;
   if(!FF[0].UpdateInputWeights(GetPointer(AttentionOut)))
      return false;
   if(!FF[1].UpdateInputWeights(GetPointer(FF[0])))
      return false;
//---
   return true;
  }

El método updateInputWeights es el cierre preciso y deliberado del ciclo de entrenamiento. Garantiza que la memoria global de correlaciones y todas sus representaciones, junto con las transformaciones posteriores, reciban actualizaciones correctas y coherentes, lo cual es especialmente importante para una aplicación estable y fiable de TQNet en tareas de predicción de series temporales financieras.

El código completo de la clase CNeuronTQMHA y de todos sus métodos se incluye en el archivo adjunto.


Arquitectura del modelo

Tras ensamblar todos los componentes clave del framework TQNet, pasamos a la siguiente etapa: la descripción de la arquitectura de los modelos entrenables. Al igual que antes, nuestro objetivo es entrenar un sistema de trading capaz de analizar por sí mismo la situación del mercado y tomar decisiones de trading. El entrenamiento se realiza siguiendo el paradigma «Actor-Critic», en el que TQNet actúa como codificador del estado del entorno. Forma una representación compacta e informativa de la situación actual del mercado, en la que se basa el Actor al elegir acciones, y el Critic al evaluar su calidad.

A diferencia de la versión inicial de TQNet, hemos ampliado el modelo en lo que respecta a la preparación de datos y la generación de características: se han añadido bloques de preprocesamiento y agregación de información contextual, lo que mejora la robustez y la capacidad de generalización del sistema. La creación de la descripción de la arquitectura de los modelos se implementa en el método CreateDescriptions. En él se genera esa estructura básica de capas en la que se basa toda la lógica posterior de entrenamiento e inferencia.

bool CreateDescriptions(CArrayObj *&encoder,
                        CArrayObj *&actor,
                        CArrayObj *&critic
                       )
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         return false;
     }
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
         return false;
     }
   if(!critic)
     {
      critic = new CArrayObj();
      if(!critic)
         return false;
     }

El método comienza con una tarea sencilla, pero importante: si los punteros a los contenedores de capas están vacíos, crea nuevos objetos. No se trata de una mera formalidad: garantizamos que cada uno de los tres bloques de la arquitectura contará con su propia colección independiente de descripciones de capas, lista para ser completada de forma secuencial. Esta asignación explícita aporta transparencia. El código posterior puede dar por hecha la existencia del contenedor y no comprobar su validez en varios puntos.

A continuación, pasamos a la construcción del Codificador, el elemento central de nuestro modelo. Limpiamos el contenedor y añadimos sucesivamente las descripciones de las capas; además, cada etapa va acompañada de una estricta comprobación de éxito: si, por cualquier motivo, no se consigue crear el objeto o añadirlo al array, el método libera cuidadosamente los recursos y devuelve un error. Esta disciplina en la gestión de la memoria y los estados resulta útil tanto en la depuración como en producción: evita que los problemas se propaguen por la pila.

//--- Codificador
   encoder.Clear();
//--- Capa de entrada
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   uint prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- capa 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormWithNoise;
   descr.count = prev_count;
   descr.batch = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

La primera capa del codificador es la capa de neurona base CNeuronBaseOCL; el número de neuronas se define como el producto HistoryBars * BarDescr. La idea es clara: alimentamos la red con una ventana histórica dividida por características descriptivas. Esta capa no contiene activación: actúa como una puerta de entrada, es decir, como un búfer al que simplemente pasamos los datos originales.

El siguiente bloque — CNeuronBatchNormWithNoise — conserva la dimensionalidad de la capa anterior, pero añade normalización y regularización por ruido. No se trata de un detalle decorativo: las series financieras están sujetas a la deriva de la distribución y a valores atípicos. BatchNorm con ruido aumenta la robustez frente a estas anomalías, normaliza las entradas para las capas posteriores y, al mismo tiempo, estimula al modelo para que no se sobreajuste a pequeñas variaciones aleatorias.

Tras la normalización, pasamos a la transformación de la estructura temporal mediante CNeuronConcatDiff. Esta capa recopila el historial por barras, pero lo hace centrándose en las diferencias con un paso (step = 1), lo que proporciona una representación explícita de la dinámica: no solo de los niveles, sino también de sus cambios. Esta técnica suele dar al modelo una mejor representación de las señales de tendencia, la inercia y los giros locales.

//--- capa 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatDiff;
   prev_count = descr.count = HistoryBars;
   descr.layers = BarDescr;
   descr.step = 1;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

A continuación viene la capa CMamba4CastEmbeding, que implementa la lógica multiventana de embedding de marcas temporales sinusoidales. Esto da al modelo la capacidad de observar simultáneamente patrones a corto y largo plazo, formando una representación rica del estado del mercado.

//--- capa 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defMamba4CastEmbeding;
   prev_count = descr.count = HistoryBars;
   descr.window = 2 * BarDescr;
   uint prev_out = descr.window_out = NSkills;
     {
      uint temp[] = {PeriodSeconds(PERIOD_D1), PeriodSeconds(PERIOD_MN1)};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Aquí conviene señalar que las operaciones anteriores se realizaron en la representación de una serie temporal multivariante. Para que el framework TQNet funcione correctamente, necesitamos una secuencia de series temporales unitarias. Por lo tanto, el siguiente paso consiste en transponer el tensor, reestructurando las dimensiones para la posterior aplicación de la atención.

//--- capa 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   prev_count = descr.window = prev_out;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_out = descr.count;

Y ahora nos acercamos al núcleo: la capa CNeuronTQMHA. Aquí especificamos el tamaño del periodo como el producto 24*7, que corresponde a una semana calendario en el marco temporal H1. El tamaño del paso corresponde al número de segundos que hay en una barra del marco temporal indicado. Queremos que el carrusel TQ almacene patrones semanales y los reutilice con un paso del marco temporal horario.

//--- capa 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTQMHA;
     {
      uint temp[] = {prev_out, 64, 24*7, PeriodSeconds(PERIOD_H1)};
                   // window, window_key, period, timeframe
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.step=NHeads;
   descr.count=prev_count;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   uint count = prev_count;
   uint window = prev_out;

Tenga en cuenta que utilizamos una semana calendario (7 días), aunque el instrumento analizado no cotice durante los fines de semana. Se trata de una medida obligada, ya que calculamos el elemento del carrusel a partir de la hora de apertura de la barra.

Después viene una capa convolucional con activación TANH, que sirve para comprimir y proyectar las representaciones obtenidas a un horizonte de planificación dado. Esta es la etapa en la que una representación rica se traduce en señales de pronóstico concretas.

//--- capa 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out=descr.window_out = NForecast;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- capa 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   prev_count = descr.window = prev_out;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_out=descr.count;

A continuación, se vuelve a realizar una transposición para devolver los datos a una representación de secuencia multivariante similar a la de los datos originales.

Otra capa convolucional tiene como objetivo reducir la dimensionalidad de las características al nivel de los datos originales.

//--- capa 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out=descr.window_out = BarDescr;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Estas pasadas convolucionales actúan como agregadores locales: acumulan información sobre los componentes de la predicción y generan una representación lista para la desnormalización.

El codificador culmina con la capa CNeuronRevInDenormOCL. Este módulo devuelve las predicciones a la escala original, restaurando la media y la varianza de los datos analizados, que se habían eliminado en la etapa de preprocesamiento.

//--- capa 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronRevInDenormOCL;
   descr.count = prev_count*prev_out;
   descr.layers = 1;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Para las aplicaciones financieras, esto es fundamental: los pronósticos deben tener una escala interpretable, apta para el trading y el cálculo de riesgos.

En todo el diseño se aprecia una idea unificadora: primero, preparar y normalizar cuidadosamente la entrada; luego, extraer características dinámicas y embeddings a diferentes escalas; después, aplicar la atención potenciada por TQ; y, por último, agregar y proyectar los valores de pronóstico en el espacio de los datos originales, restaurando la escala.

Las arquitecturas de los modelos del Actor y del Crítico se han tomado de trabajos anteriores sin cambios, por lo que no nos detendremos ahora en examinarlas en detalle. La solución arquitectónica completa de todos los modelos entrenables se presenta en el archivo adjunto.


Pruebas

El proceso de entrenamiento se estructura en dos etapas sucesivas y complementarias, lo que proporciona tanto un fundamento sólido como flexibilidad para trabajar en condiciones reales de mercado.

En la primera etapa offline, llevamos a cabo un entrenamiento exhaustivo con datos históricos del par EURUSD en el marco temporal H1 correspondiente a todo el año 2024. Ese año incluyó un conjunto completo de regímenes de mercado: fases laterales relativamente tranquilas, tendencias sostenidas, picos repentinos de volatilidad y periodos de mayor ruido, por lo que resulta ideal como escuela para el modelo.

La segunda etapa consiste en un ajuste fino online. El entrenamiento se ha llevado a cabo en un entorno lo más parecido posible al trading real: en el probador de estrategias MetaTrader 5, el modelo procesaba el flujo de velas de forma secuencial, tal y como ocurriría en tiempo real. El modo online revela propiedades totalmente distintas de las del entrenamiento por lotes: la capacidad de soportar el ruido, reaccionar ante los cambios de liquidez, tener en cuenta correctamente los retrasos y el efecto acumulado del deslizamiento. Durante el ajuste, simulamos condiciones reales de ejecución para que el comportamiento del modelo fuera predecible al trasladarlo a condiciones reales de trading.

La etapa final y, quizá, la más rigurosa fue la verificación con una muestra totalmente externa: las cotizaciones desde enero hasta marzo de 2025. Todos los parámetros del modelo y los hiperparámetros se mantuvieron fijos; no se realizó ningún ajuste adicional en función de estos datos. Esta comprobación ofrece una visión objetiva de la eficacia práctica: refleja la capacidad del algoritmo para mantener la previsibilidad y la estabilidad en nuevas condiciones.

A continuación se muestran los resultados de las pruebas:

Los resultados de la prueba mostraron una ligera ventaja positiva. Con un depósito inicial de 100 dólares, el beneficio neto ascendió a 21,07. El porcentaje de operaciones rentables se situó en torno al 49 %. Además, las posiciones cortas se cerraban con ganancias un poco más a menudo que las largas. Cada operación rentable generaba de media 1,13 dólares, mientras que la pérdida media ascendía a 0,92. Un factor de beneficio de 1,18, con un beneficio esperado de 0,09 dólares por operación, indica que el margen de seguridad es mínimo.

La regresión lineal de la curva de rentabilidad muestra una correlación de 0,86, pero el error sigue siendo notable. Esto confirma el carácter ruidoso de los resultados.

El perfil de riesgo aún dista mucho de ser óptimo. Las reducciones profundas, con un beneficio esperado reducido por operación, no permiten aumentar el capital de forma segura. La carga sobre el margen es, en este caso, baja, lo que significa que el problema no radica en el tamaño de las posiciones, sino en la calidad de los puntos de entrada y en la disciplina de cierre.


Conclusión

En este trabajo hemos analizado en detalle los fundamentos teóricos del framework TQNet, así como las particularidades de su integración en la arquitectura de modelos de trading entrenables. Se analizaron los principios clave de construcción de los módulos, incluidos los mecanismos modificados de atención cruzada, lo que permitió lograr un funcionamiento más flexible y estable del algoritmo en las condiciones de los mercados financieros.

En la parte práctica del proyecto, implementamos los enfoques propuestos en TQNet mediante MQL5, complementándolos con mejoras propias en la generación de características y el preprocesamiento de datos. El modelo se entrenó en dos etapas: con datos históricos (entrenamiento offline) y en condiciones similares a las del mercado real (ajuste online en el probador de estrategias de MetaTrader 5).

Las pruebas finales con cotizaciones nuevas, no utilizadas previamente, mostraron una evolución positiva. El balance del modelo mostró un crecimiento sostenido tras un periodo de reducción inicial, lo que indica la capacidad del algoritmo para adaptarse a las condiciones cambiantes del mercado.

Los resultados obtenidos confirman que TQNet, en combinación con soluciones arquitectónicas mejoradas y un proceso de entrenamiento en dos etapas, es capaz de proporcionar una esperanza matemática positiva.


Enlaces


Programas utilizados en el artículo

# Nombre Tipo Descripción
1 Study.mq5 Asesor experto Asesor experto para el entrenamiento offline de modelos
2 StudyOnline.mq5 Asesor experto Asesor experto para el entrenamiento online de modelos
3 Test.mq5 Asesor experto Asesor experto para probar el modelo
4 Trajectory.mqh Biblioteca de clase Estructura de la descripción del estado del sistema y de la arquitectura de los modelos
5 NeuroNet.mqh Biblioteca de clase Biblioteca de clases para crear una red neuronal
6 NeuroNet.cl Biblioteca Biblioteca de código del programa OpenCL

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/19181

Archivos adjuntos |
MQL5.zip (3010.22 KB)
Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Redes neuronales en el trading: Framework de predicción cruzada de dominios de series temporales (Final) Redes neuronales en el trading: Framework de predicción cruzada de dominios de series temporales (Final)
El artículo trata sobre la construcción práctica del modelo TimeFound para la predicción de series temporales. Asimismo, se analizan las etapas clave de la implementación de los principales enfoques del framework con MQL5.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Superando las limitaciones del aprendizaje automático (Parte 2): Falta de reproducibilidad Superando las limitaciones del aprendizaje automático (Parte 2): Falta de reproducibilidad
El artículo analiza por qué los resultados de las operaciones pueden diferir significativamente entre distintos brókeres, incluso utilizando la misma estrategia y el mismo símbolo financiero, debido a la descentralización de los precios y a las discrepancias en los datos. Este artículo ayuda a los desarrolladores de MQL5 a comprender por qué sus productos pueden recibir opiniones mixtas en el Market de MQL5, e insta a los desarrolladores a adaptar sus estrategias a los brókers específicos para garantizar resultados transparentes y reproducibles. Esto podría convertirse en una práctica recomendada importante y específica del sector, que beneficiaría a nuestra comunidad si se adoptara de forma generalizada.