Русский Português
preview
Redes neuronales en el trading: Modelo multidimensional de extremo a extremo para la previsión de series temporales (Final)

Redes neuronales en el trading: Modelo multidimensional de extremo a extremo para la previsión de series temporales (Final)

MetaTrader 5Sistemas comerciales |
53 8
Dmitriy Gizlyk
Dmitriy Gizlyk

Introducción

En artículos anteriores, pasamos de la introducción a los aspectos teóricos a la implementación práctica de los enfoques propuestos, revelando paso a paso la arquitectura y la lógica de GinAR, un framework de red neuronal original desarrollado específicamente para analizar y pronosticar series temporales en condiciones de incertidumbre estructural y altos niveles de ruido. Basado en una arquitectura multicapa y multicomponente, GinAR combina con seguridad los principios de atención, transformaciones de grafos y dinámica recurrente, logrando así la capacidad no solo de ver la estructura de los datos, sino también de adaptarse a ella en tiempo real.

Hoy vamos a abordar la parte más importante y, quizás, la más interesante: la evaluación del rendimiento del framework GinAR en condiciones reales. Ya hemos superado las etapas de diseño de la arquitectura, la implementación de los componentes clave y la construcción de los algoritmos de pasada directa e inversa. Todos los elementos del sistema han sido minuciosamente desarrollados, probados y adaptados a las particularidades de las series temporales financieras. Ahora es el momento de combinarlos y ponerlos a prueba, para comprobar así la eficacia con la que GinAR aborda las tareas prácticas de análisis y previsión.

En este artículo, no solo estamos finalizando el desarrollo; estamos dando por concluido un concepto que ha requerido mucho esfuerzo para llevarlo a buen término. El objetivo principal será entrenar y probar el modelo en condiciones similares a las reales.

Antes de pasar a las pruebas, recordemos brevemente cómo está estructurado el framework GinAR. Se basa en la celda GinARCell original, que combina varios componentes clave:

  • La atención por interpolación (IA) es un mecanismo de atención contextual que preprocesa el flujo de entrada y centra el modelo en los elementos relevantes. Se trata de una especie de filtro de percepción primaria que nos permite identificar patrones significativos y recuperar elementos faltantes antes de la transformación principal.
  • Los bloques AGCN (Adaptive Graph Convolution Network) son tres módulos, uno de los cuales es responsable del procesamiento principal (cX_AGNC), mientras que los otros dos son responsables de las señales de control (ForgetGate y ResetGate). Su tarea consiste en extraer información estructural y topológica, así como en modelar el flujo basándose en dependencias temporales y estructurales.
  • El bloque de contexto es un almacenamiento de estado temporal que proporciona memoria profunda y continuidad en el flujo de procesamiento. Su actualización se produce según un principio equilibrado basado en la interacción con las puertas de control.
  • El mecanismo de control dual: ForgetGate y ResetGate, que funcionan según el principio de filtrado y reescritura del contexto. Se implementa como celdas AGCN independientes, lo que permite una gestión flexible del estado interno del modelo.

La respuesta de la celda se forma en función de la activación de ELU y los multiplicadores de atención contextual, lo que garantiza una transición fluida pero sensible entre los intervalos temporales.

Esta combinación hace que GinAR sea verdaderamente único. A diferencia de las redes recurrentes clásicas, no solo recuerda el pasado, sino que también puede estructurarlo. A diferencia de los modelos GCN estándar, no opera sobre una topología fija, sino que la aprende sobre la marcha. Finalmente, a diferencia de los transformadores generalizados, GinAR conserva la localidad y la adaptación dinámica, dos características esenciales para el análisis de series temporales financieras.

A continuación le presentamos la visualización del framework GinAR realizada por el autor.

Visualización del framework GinAR realizada por el autor

Este artículo no solo sirve como conclusión de la parte técnica, sino también como una especie de demostración de la madurez del diseño al completo. Así, veremos cómo se comporta el framework en la práctica, qué cuellos de botella surgen y cuál es la precisión y la estabilidad del modelo con datos nuevos. Y, lo que es más importante, evaluaremos si realmente es capaz de brindar al tráder la ventaja que subyace a toda esta complejidad arquitectónica.



El bloque GinAR

La arquitectura de GinAR está organizada jerárquicamente. Consta de varias capas sucesivas de GinAR y finaliza con un Decodificador construido sobre la base de un perceptrón multicapa (MLP). Cada capa GinAR, a su vez, contiene un conjunto de celdas GinARCell que realizan un procesamiento paso a paso de la secuencia temporal, de forma similar a como los fotogramas de una película forman una escena continua.

En la salida de cada capa, solo se almacena el estado oculto final de la última celda. Estos estados se combinan en un tensor final hⁿₐₗₗ, que contiene una representación comprimida (y máximamente informativa) de toda la secuencia analizada. Precisamente este tensor se suministra a la entrada del decodificador MLP, que consta de dos capas totalmente conectadas con una función de activación ReLU entre ellas.

En nuestro caso, vale la pena prestar atención a un punto arquitectónico importante relacionado con la organización del flujo de datos en la estructura multicapa GinAR. La aplicación secuencial de varias capas, donde cada capa subsiguiente recibe la salida de la anterior como entrada, es en sí misma una práctica completamente estándar y encaja bien con la lógica del modelo lineal.

Sin embargo, el modelo GinAR original usa un esquema más complejo: después del procesamiento secuencial por varias capas, el resultado no supone solo la salida de la última capa, sino una representación combinada obtenida al concatenar las salidas de todas las capas. Este enfoque permite al Decodificador utilizar simultáneamente todo el espectro de características internas formadas en diferentes niveles de abstracción, lo que sin duda fortalece el modelo. Sin embargo, también rompe la estructura lineal de transferencia de datos que subyace a nuestras implementaciones y requiere lógica adicional para recopilar y combinar datos de diferentes capas.

El objeto de construcción de modelos de nivel superior que usamos no proporcionaba originalmente operaciones para fusionar las salidas de múltiples capas en un único tensor, lo cual hacía imposible implementar directamente esta parte de la arquitectura original de GinAR. Para superar esta limitación, hemos desarrollado un bloque GinAR especial que gestiona el procesamiento secuencial de los datos de origen utilizando un conjunto de celdas CNeuronGinARCell, y luego acumula los resultados de todas las celdas en un único tensor. El tensor resultante se pasa como entrada al Decodificador, reproduciendo la peculiaridad clave del modelo original: la integración de características obtenidas en diferentes niveles de abstracción.

Pasando a la consideración de la arquitectura del bloque GinAR, vale la pena destacar una diferencia importante con respecto al modelo original. Además de la estructura básica, añadimos una matriz entrenable de dependencias predefinidas entre los componentes de los datos analizados. Esta se basa en un mecanismo de aprendizaje adaptativo similar al utilizado en las matrices de dependencia estructural dentro de cada celda GinARCell. Sin embargo, la diferencia clave radica en el área de aplicación. Si en cada celda se forman matrices adaptativas por separado, reflejando patrones locales, entonces la matriz que hemos introducido tiene un carácter global y es común a todas las celdas dentro de un bloque GinAR. Esto nos permite capturar correlaciones sólidas y repetibles entre las variables a lo largo del análisis, lo cual refuerza la consistencia estructural del modelo.

Para implementar el bloque GinAR dentro de nuestra arquitectura, se ha creado una clase especial CNeuronGinAR, que hereda de CNeuronSwiGLUOCL. Esta jerarquía permite mantener la compatibilidad con el resto del modelo y aprovechar la activación convolucional con la no linealidad flexible SwiGLU. Sin embargo, los elementos funcionales clave y la lógica de funcionamiento se han ampliado y redefinido sustancialmente. Más abajo resumimos la estructura del nuevo objeto:

class CNeuronGinAR   :  public CNeuronSwiGLUOCL
  {
protected:
   CParams              cEa;
   CNeuronSwiGLUOCL     cWx;
   CNeuronSwiGLUOCL     cWe;
   CNeuronBaseOCL       cWconcat_ex;
   CNeuronConvOCL       cEn;
   CNeuronTransposeOCL  cEnT;
   CNeuronBaseOCL       cEnEnT;
   CNeuronSoftMaxOCL    cApre;
   CNeuronGinARCell     caCells[4];
   CNeuronBaseOCL       cConcat;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronGinAR(void) {};
                    ~CNeuronGinAR(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint units_count, uint dimension,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(const int file_handle) override;
   virtual bool      Load(const int file_handle) override;
   //---
   virtual int       Type(void) override const  {  return defNeuronGinAR; }
   virtual void      TrainMode(bool flag) override;
   virtual void      SetOpenCL(COpenCLMy *obj);
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau);
   //---
   virtual bool      Clear(void) override;
  };

La clase CNeuronGinAR es un bloque completo del framework GinAR, implementado en el entorno MQL5 utilizando OpenCL. Está diseñada para procesar series temporales y extraer patrones complejos basados en una jerarquía de interrelaciones lineales. La clase contiene varios componentes clave responsables de generar e interpretar la matriz global de dependencias estructurales, así como una serie de celdas CNeuronGinARCell que implementan el procesamiento secuencial de los datos de origen. Posteriormente, sus resultados se concatenan para formar la representación final de la secuencia analizada.

La estructura de clases se basa en los principios de modularidad y reutilización. Todos los elementos internos se declaran de forma estática. Esto significa que la gestión de su ciclo de vida (creación, destrucción y liberación de memoria) se realiza de manera automática, sin necesidad de intervención explícita de un constructor o destructor.

El método Init se usa para configurar todos los componentes internos, actuando como un único punto de entrada al proceso de inicialización. Este requiere parámetros que especifican la arquitectura del objeto y el tipo de optimización de parámetros.

bool CNeuronGinAR::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                        uint units_count, uint dimension,
                        ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronSwiGLUOCL::Init(numOutputs, myIndex, open_cl, caCells.Size()*dimension,
                              caCells.Size()*dimension, dimension, units_count, 1,
                              optimization_type, batch))
      return false;

En primer lugar, se llama al método homónimo de la clase padre CNeuronSwiGLUOCL, que en nuestra implementación genera representaciones agregadas de los resultados de las celdas GinARCell. Posteriormente, los componentes recién declarados e incluidos en la arquitectura de bloques se inicializan de forma secuencial.

En primer lugar va la matriz entrenable de dependencias estructurales globales entre las variables de la serie temporal.

int index = 0;
if(!cEa.Init(0, index, OpenCL, Neurons(), optimization, iBatch))
   return false;
cEa.SetActivationFunction(None);

A diferencia del enfoque original, donde se forma una matriz de covarianza predefinida basada en el conocimiento previo o el análisis preliminar de la estructura de la secuencia predicha, nuestra implementación usa una matriz entrenable de dependencias estructurales que es común a todas las celdas GinARCell dentro de un solo bloque. Este enfoque elimina la necesidad de ajustes manuales o análisis de datos externos y ofrece un ajuste más flexible y adaptativo de las relaciones entre variables durante la fase de entrenamiento.

Luego inicializamos las transformaciones lineales cWx y cWe. Su tarea consiste en llevar los datos de origen y la matriz de covarianza a una dimensionalidad común antes de combinarlos en un único tensor cWconcat_ex.

   index++;
   if(!cWx.Init(0, index, OpenCL, dimension, dimension, dimension, units_count, 1,
                                                            optimization, iBatch))
      return false;
   index++;
   if(!cWe.Init(0, index, OpenCL, dimension, dimension, dimension, units_count, 1,
                                                            optimization, iBatch))
      return false;
   if(!cWconcat_ex.Init(0, index, OpenCL, 2 * Neurons(), optimization, iBatch))
      return false;
   cWconcat_ex.SetActivationFunction(None);

La capa convolucional cEn está diseñada para realizar una mezcla profunda de características y sus dependencias.

   index++;
   if(!cEn.Init(0, index, OpenCL, 2 * dimension, 2 * dimension, dimension, units_count,
                                                              1, optimization, iBatch))
      return false;
   cEn.SetActivationFunction(None);
   index++;
   if(!cEnT.Init(0, index, OpenCL, units_count, dimension, optimization, iBatch))
      return false;
   index++;
   if(!cEnEnT.Init(0, index, OpenCL, units_count * units_count, optimization, iBatch))
      return false;
   cEnEnT.SetActivationFunction(GELU);

Primero transponemos los datos obtenidos y luego multiplicamos las representaciones originales y transpuestas para obtener una matriz simétrica de interdependencias entre las variables cEnEnT. La función de activación GELU se usa para reforzar las relaciones significativas.

A continuación, normalizamos la matriz de dependencias usando la función SoftMax, convirtiendo los datos en una distribución de probabilidad.

   index++;
   if(!cApre.Init(0, index, OpenCL, units_count * units_count, optimization, iBatch))
      return false;
   cApre.SetHeads(units_count);

El siguiente paso consiste en inicializar la matriz de celdas CNeuronGinARCell, cada una de las cuales es responsable de procesar la secuencia analizada en su propio nivel de jerarquía. A pesar de su independencia lógica, todas las celdas tienen una arquitectura idéntica, lo cual simplifica su procedimiento de inicialización: un solo ciclo es suficiente.

   for(uint i = 0; i < caCells.Size(); i++)
     {
      index++;
      if(!caCells[i].Init(0, index, OpenCL, units_count, dimension, optimization, iBatch))
         return false;
     }
   index++;
   if(!cConcat.Init(0, index, OpenCL, GetWindow()*units_count, optimization, iBatch))
      return false;
//---
   return true;
  }

Los resultados de las operaciones de celda se agregan en el objeto cConcat. Y tras inicializar con éxito todos los componentes internos, devolvemos el resultado lógico de las operaciones al programa que realiza la llamada.

Debemos destacar que el método Init no gestiona el ciclo de vida de los objetos listados. Su función consiste en configurar rigurosamente los parámetros y construir la arquitectura de capas, transfiriendo el control al ciclo de trabajo del modelo. Este enfoque garantiza una alta estabilidad, previsibilidad del comportamiento y un uso eficiente de la memoria.

Una vez completada la fase de inicialización, todos los componentes del bloque CNeuronGinAR están listos para ejecutarse en la pasada directa. Aquí es donde se manifiesta la interacción entre los módulos estructurales, los estados ocultos de las celdas y la matriz de dependencias entrenables. El método feedForward se encarga de realizar el ciclo completo de procesamiento de la secuencia de origen, incluyendo la construcción de una matriz de dependencias estructurales y la generación del tensor de salida final, que se pasará al siguiente nivel de la arquitectura neuronal.

bool CNeuronGinAR::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
//--- Calculate Apre
   if(bTrain)
     {
      if(!cEa.FeedForward())
         return false;
      if(!cWe.FeedForward(cEa.AsObject()))
         return false;
     }

En la primera etapa, se construye una matriz de dependencias entrenable. Si el modelo está en modo de entrenamiento (bTrain == true), primero se activa el tensor de dependencia estructural global, seguido del procesamiento de su salida mediante una capa de proyección lineal.

En paralelo, se procesa el flujo de información principal de los datos de origen, pasando a través de la capa de proyección lineal cWx.

//---
   if(!cWx.FeedForward(NeuronOCL))
      return false;
   if(!Concat(cWx.getOutput(), cWe.getOutput(), cWconcat_ex.getOutput(), 
              cWx.GetWindowOut(), cWe.GetWindowOut(), cWx.GetUnits()))
      return false;

A continuación, los resultados de ambas direcciones se combinan en un tensor usando la operación Concat, cuyo resultado se introduce en la capa convolucional cEn.

   if(!cEn.FeedForward(cWconcat_ex.AsObject()))
      return false;
   if(!cEnT.FeedForward(cEn.AsObject()))
      return false;
   if(!MatMul(cEn.getOutput(), cEnT.getOutput(), cEnEnT.getOutput(), cEnT.GetCount(),
                                                  cEnT.GetWindow(), cEnT.GetCount()))
      return false;
   if(cEnEnT.Activation() != None)
      if(!Activation(cEnEnT.getOutput(), cEnEnT.getOutput(), cEnEnT.Activation()))
         return false;

Luego el tensor resultante se transpone (cEnT) y luego el tensor de origen se multiplica matricialmente con su versión transpuesta. En esta etapa, se forma la matriz de covarianza, a la que, si es necesario, se le añade no linealidad usando una función de activación determinada.

La matriz de covarianza final se construye en la capa cApre, donde los datos se transforman en una representación probabilística usando la función SoftMax.

if(!cApre.FeedForward(cEnEnT.AsObject()))
   return false;
if(!IdentSum(cApre.getOutput(), cApre.getOutput(), cApre.Heads()))
   return false;

Para ofrecer estabilidad y un nivel básico de autoconexión, esta matriz se complementa con una matriz diagonal unitaria. Es decir, cada variable no solo recibe información sobre las relaciones con las demás, sino que también mantiene una orientación básica hacia sí misma. El resultado es una estructura equilibrada de pesos de atención que puede tener en cuenta tanto las dependencias globales como la importancia local de cada elemento. Es esta matriz final la que luego se pasa a la entrada de cada GinARCell, actuando como un mecanismo universal para la amplificación selectiva de la señal.

A continuación, comienza el procesamiento principal de la secuencia original a través de la matriz de celdas. A la entrada de la primera celda se suministran los datos analizados recibidos de un programa externo. Y cada celda subsiguiente analiza los resultados de la anterior, aumentando el nivel de detalle de la representación latente. De esta manera, se forma una secuencia de representaciones latentes que reflejan la naturaleza jerárquica del análisis de series temporales.

//--- GimAR Cells
   CNeuronBaseOCL *temp = NeuronOCL;
   for(uint i = 0; i < caCells.Size(); i++)
     {
      if(!caCells[i].FeedForward(temp, cApre.getOutput()))
         return false;
      temp = caCells[i].AsObject();
     }
//---
   if(!Concat(caCells[0].getOutput(), caCells[1].getOutput(), caCells[2].getOutput(),
              caCells[3].getOutput(), cConcat.getOutput(), GetWindow() / 4,
              GetWindow() / 4, GetWindow() / 4, GetWindow() / 4, GetUnits()))
      return false;
//---
   return CNeuronSwiGLUOCL::feedForward(cConcat.AsObject());
  }

La etapa final del método consiste en combinar las salidas de todas las celdas en un tensor común (cConcat), que luego se introduce en la entrada del método homónimo en la clase padre CNeuronSwiGLUOCL para su posterior procesamiento. Esta estructura garantiza un flujo de información coherente y eficiente a través del bloque GinAR, lo cual permite que el comportamiento del sistema se aproxime lo máximo posible a la arquitectura original, a pesar de las limitaciones arquitectónicas del framework utilizado.

Después de completar la pasada directa y calcular los resultados del trabajo del bloque, pasamos a la segunda etapa clave: la propagación del gradiente de error (propagación inversa). Aquí es donde comienza el proceso inverso de ajuste del modelo: el error del Decodificador se retroalimenta a cada elemento de la arquitectura para ajustar los pesos y optimizar el modelo.

El método calcInputGradients se encarga de implementar un recorrido completo inverso a través de toda la arquitectura del bloque, comenzando desde la salida y terminando con los datos de origen. Como la estructura de bloques es compleja y consta tanto de operaciones secuenciales como de operaciones de ramificación y fusión sobre tensores, la pasada inversa requiere una ejecución por etapas y estrictamente coordinada.

bool CNeuronGinAR::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;
//---
   if(!CNeuronSwiGLUOCL::calcInputGradients(cConcat.AsObject()))
      return false;
   if(!DeConcat(caCells[0].getPrevOutput(), caCells[1].getPrevOutput(),
                caCells[2].getPrevOutput(), caCells[3].getGradient(),
                cConcat.getGradient(), GetWindow() / 4,
                GetWindow() / 4, GetWindow() / 4,
                GetWindow() / 4, GetUnits()))
      return false;

El método comienza con una llamada al método homónimo en la clase padre CNeuronSwiGLUOCL, que pasa el gradiente al tensor concatenado de los resultados de las celdas internas, cConcat. Como la pasada directa ha formado este tensor concatenando las salidas de cuatro celdas GinAR, ahora es necesario distribuir correctamente el gradiente de vuelta a través de los cuatro flujos de datos. Utilizando el método DeConcat, pasamos el gradiente de error a cada celda según su impacto en el resultado final.

Un punto importante que conviene destacar en especial se refiere a la lógica de propagación del gradiente de error dentro del bloque GinAR. A pesar de la simetría externa de las celdas, su carga funcional durante el proceso de aprendizaje no es la misma.

En primer lugar, solo se usa la última celda para formar el tensor de resultado del bloque final mediante concatenación. Todas las celdas anteriores tienen una doble función: sus resultados no solo participan en la concatenación, sino que también sirven como datos de entrada para la siguiente celda GinARCell en la jerarquía. Esto significa que el gradiente de error debe entrar en cada una de esas celdas desde dos fuentes. Por consiguiente, al calcular el gradiente para las tres primeras celdas, recopilamos el gradiente en dos direcciones de uso de datos y los sumamos correctamente. Esto garantiza un flujo continuo de errores y una retroalimentación precisa en cada nivel de la jerarquía.

//--- GimAR Cells
   cApre.getGradient().Fill(0);
   for(uint i = caCells.Size() - 1; i > 0; i--)
      if(!caCells[i - 1].CalcHiddenGradients(caCells[i].AsObject(), cApre.getOutput(),
                                             cApre.getPrevOutput(),
                                             (ENUM_ACTIVATION)cApre.Activation()) ||
         !SumAndNormilize(caCells[i - 1].getGradient(), caCells[i - 1].getPrevOutput(),
                          caCells[i - 1].getGradient(), cWx.GetWindow(), false, 0, 0, 0, 1) ||
         !SumAndNormilize(cApre.getGradient(), cApre.getPrevOutput(),
                          cApre.getGradient(), GetUnits(), false, 0, 0, 0, 1))
         return false;
   if(!NeuronOCL.CalcHiddenGradients(caCells[0].AsObject(), cApre.getOutput(),
                                     cApre.getPrevOutput(), (ENUM_ACTIVATION)cApre.Activation()) ||
      !SumAndNormilize(cApre.getGradient(), cApre.getPrevOutput(),
                       cApre.getGradient(), GetUnits(), false, 0, 0, 0, 1))
      return false;

En segundo lugar, surge una situación aún más sutil al calcular el gradiente de la matriz de covarianza global representada en el objeto cApre. Esta matriz se ha transmitido simultáneamente a las cuatro GinARCells. Por consiguiente, durante la pasada inversa, su gradiente debe acumular información de cuatro direcciones independientes, de cada celda. Esto requiere agregar todos los gradientes entrantes uno por uno, manteniendo una forma y escala consistentes. Los errores en esta parte pueden provocar cambios bruscos en los pesos y, como consecuencia, inestabilidad en el entrenamiento.

A continuación, comienza la propagación inversa a través del bloque de generación de la matriz de dependencias. Primero, se calcula el gradiente para cEnEnT, después de lo cual los valores se corrigen mediante la derivada de la función de activación correspondiente.

//---
   if(!cEnEnT.CalcHiddenGradients(cApre.AsObject()))
      return false;
   if(cEnEnT.Activation() != None)
      if(!DeActivation(cEnEnT.getOutput(), cEnEnT.getGradient(),
                       cEnEnT.getGradient(), cEnEnT.Activation()))
         return false;
   if(!MatMulGrad(cEn.getOutput(), cEn.getPrevOutput(),
                  cEnT.getOutput(), cEnT.getGradient(),
                  cEnEnT.getGradient(), cEnT.GetCount(),
                  cEnT.GetWindow(), cEnT.GetCount()))
      return false;
   if(!cEn.CalcHiddenGradients(cEnT.AsObject()))
      return false;
   if(!SumAndNormilize(cEn.getGradient(), cEn.getPrevOutput(),
                       cEn.getGradient(), cEnT.GetWindow(), false, 0, 0, 0, 1))
      return false;
   if(cEn.Activation() != None)
      if(!DeActivation(cEn.getOutput(), cEn.getGradient(),
                       cEn.getGradient(), cEn.Activation()))
         return false;

Luego se calcula el gradiente para la operación de multiplicación de matrices MatMulGrad, distribuyendo el error entre la capa convolucional cEn y su copia transpuesta cEnT. En este caso, también reducimos el gradiente del error de la representación transpuesta al nivel de la capa convolucional y sumamos los valores obtenidos de los dos flujos de información. Este esquema nos permite construir una representación diferenciable completa de la matriz de covarianza simétrica, conservando así la precisión y la integridad de la transmisión de errores incluso dentro de una topología compleja.

Los valores obtenidos los propagamos al nivel del tensor cWconcat_ex.

if(!cWconcat_ex.CalcHiddenGradients(cEn.AsObject()))
   return false;
if(!DeConcat(cWx.getGradient(), cWe.getGradient(), cWconcat_ex.getGradient(),
             cWx.GetWindowOut(), cWe.GetWindowOut(), cWx.GetUnits()))
   return false;

La salida de cWconcat_ex se ha formado a partir de los resultados de cWx y cWe. Ahora sus gradientes están dispuestos en orden inverso, pasando por una posible desactivación. Después de esto, se inicia un pasada inversa desde cWx al objeto de datos de origen NeuronOCL.

No obstante, conviene recordar que el gradiente de error de la primera celda ya se había transferido al nivel de los datos de origen. Por lo tanto, utilizaremos un truco que consiste en reemplazar punteros a búferes de datos y luego sumar los valores obtenidos de dos flujos de información.

   if(cWx.Activation() != None)
      if(!DeActivation(cWx.getOutput(), cWx.getGradient(),
                       cWx.getGradient(), cWx.Activation()))
         return false;
   if(cWe.Activation() != None)
      if(!DeActivation(cWe.getOutput(), cWe.getGradient(),
                       cWe.getGradient(), cWe.Activation()))
         return false;
   CBufferFloat* temp = NeuronOCL.getGradient();
   if(!NeuronOCL.SetGradient(NeuronOCL.getPrevOutput(), false))
      return false;
   if(!NeuronOCL.CalcHiddenGradients(cWx.AsObject()))
      return false;
   if(!SumAndNormilize(NeuronOCL.getGradient(), temp,
                       NeuronOCL.getGradient(), cWx.GetWindow(), false, 0, 0, 0, 1))
      return false;
   if(!NeuronOCL.SetGradient(temp, false))
      return false;

El método finaliza transfiriendo el gradiente de error al nivel de la matriz de dependencia global cEa.

   if(!cEa.CalcHiddenGradients(cWe.AsObject()))
      return false;
//---
   return true;
  }

Y una vez que todas las iteraciones se hayan completado con éxito, retornamos el resultado lógico del método al programa que realiza la llamada.

De este modo, el método calcInputGradients garantiza una distribución correcta del gradiente en toda la estructura del bloque GinAR. La arquitectura de la pasada inversa refleja con exactitud la estructura de la pasada directa, lo cual resulta especialmente importante al utilizar mecanismos de atención complejos y componentes anidados.

El código completo de esta clase y todos sus métodos se ofrece en el archivo adjunto y está disponible para su estudio independiente.



Arquitectura del modelo

Tras crear todos los componentes necesarios para construir el framework GinAR, pasamos al siguiente paso: describir la arquitectura de los modelos entrenados. Aquí, la lógica de construcción va mucho más allá de la simple previsión de series temporales: nuestra tarea es construir un agente comercial completo en el que el framework GinAR desempeñe el papel del Codificador de estado del entorno.

Cada componente del modelo es responsable de una función estrictamente definida. El Codificador es responsable de extraer y resumir la información de los datos históricos. Los módulos de previsión, divididos en tres modelos paralelos, realizan pronósticos en distintos horizontes temporales. A continuación, encontramos los bloques del Actor y el Crítico, que se corresponden con la arquitectura del aprendizaje por refuerzo (Reinforcement Learning): el primero genera acciones de negociación, mientras que el segundo evalúa el rendimiento esperado del comportamiento del modelo en el estado actual del mercado.

Para describir la arquitectura, se utiliza el método CreateDescriptions, que genera configuraciones para todos los módulos. En el primer paso, el método inicializa los contenedores para cada modelo: El Codificador, los módulos predictivos, el Actor y el Crítico. Estos contenedores se implementan como matrices de objetos: las descripciones de las capas neuronales.

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

Tras la inicialización, comienza la formación de la estructura del modelo. El Codificador comienza con una capa básica totalmente conectada que simplemente alimenta el modelo con datos históricos.

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

A continuación, se aplica una capa de normalización por lotes con ruido añadido, cuyo objetivo es estabilizar la distribución de los datos de origen e introducir una ampliación de representaciones en el sistema para aumentar la resistencia del modelo al sobreajuste.

Posteriormente, se añaden canales de características diferenciales entre barras adyacentes. Esto permite que el sistema registre no solo los valores absolutos, sino también la dirección y la intensidad de los cambios. Esto resulta especialmente importante en condiciones de mercado irregulares y de alta frecuencia.

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

La siguiente capa se encarga de enriquecer las representaciones con armónicos de marcas temporales.

//--- layer 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_H1), PeriodSeconds(PERIOD_D1)};
      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;
     }

Los datos preparados pasan luego por una capa de transposición, que cambia los ejes de representación: las secuencias temporales se convierten en características espaciales y se introducen en la entrada de un filtro convolucional. Una capa convolucional con función de activación tanh extrae patrones clave y dependencias estructurales en las incorporaciones, obteniendo así una representación comprimida y concentrada de las regularidades ocultas del mercado.

//--- layer 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;
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.variables = 1;
   prev_out = descr.window_out = EmbeddingSize;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

El codificador culmina en una capa que implementa el framework de trabajo GinAR. Aquí es donde se combinan e interpretan todas las características obtenidas previamente utilizando la jerarquía de las celdas GinAR. Estas operan de forma síncrona, utilizando una matriz de covarianza común y adaptable para modelar las relaciones estructurales ocultas en los datos. Esta capa no solo transmite información, sino que crea una representación activa del estado del mercado que puede considerarse lista para ser usada en la toma de decisiones comerciales.

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

De este modo, los enfoques del framework GinAR se integran en la arquitectura del modelo de forma coherente, lógica y sin excesos. El estado oculto resultante generado por el Codificador es una descripción de alto nivel de la situación del mercado, adecuada tanto para la previsión como para la generación de decisiones comerciales.

Para entrenar el Codificador de estado del entorno, se utiliza una estrategia de propagación de extremo a extremo del gradiente de error desde varios subsistemas independientes. En primer lugar, se trata de tres modelos de pronóstico, cada uno de los cuales se especializa en continuar la serie temporal dentro de su propio horizonte de planificación. Estos módulos analizan el mismo estado oculto generado por el Codificador, pero lo interpretan de manera diferente según el horizonte de planificación establecido. Esto hace posible una comprensión multidimensional de la situación actual del mercado.

Además, a ellos se conecta un Actor, un componente responsable de generar una decisión comercial. Se basa en la misma representación comprimida del estado del entorno y la interpreta como una guía para la acción. Por consiguiente, el Codificador está sometido a presión desde cuatro direcciones a la vez: su salida debe ser informativa para la previsión y, al mismo tiempo, adecuada para su aplicación práctica en condiciones de incertidumbre del mercado.

Debemos destacar que la arquitectura de todos estos modelos se ha tomado al completo del trabajo anterior. Por ello, en el marco del presente estudio, no nos detendremos en su descripción detallada. Toda la atención se centra en la nueva parte: la integración del framework GinAR en el sistema de aprendizaje.

El código completo de todos los componentes, incluidas las configuraciones de las capas, se ofrece en el archivo adjunto.



Entrenamiento

Una vez completada la construcción de todos los componentes y la configuración de la arquitectura de los modelos entrenados, pasamos a la etapa final y, quizás, la más crucial: el entrenamiento de los modelos. Nuestro sistema usa un enfoque combinado que aúna las ventajas de los modos de aprendizaje offline y online. Esto permite no solo garantizar un entrenamiento inicial estable del modelo, sino también adaptar su comportamiento a condiciones de negociación específicas, incluidos los cambios en la fase del mercado, el nivel de volatilidad y la frecuencia de las señales de negociación.

El proceso de entrenamiento, al igual que en trabajos anteriores, se implementa en dos etapas. La primera etapa abarca el entrenamiento offline. Aquí, los modelos se entrenan con datos históricos. Además, usamos un enfoque que no requiere la recopilación previa de una muestra de entrenamiento. En cambio, generamos el estado del entorno directamente a partir de los datos históricos del terminal y, en paralelo, simulamos el estado de la cuenta. Este enfoque ofrece una gran flexibilidad y permite el uso de periodos históricos arbitrarios sin necesidad de preparar manualmente el conjunto de datos.

La segunda etapa es la configuración online. Se realizan en condiciones lo más parecidas posible a las de una operación real en el simulador de estrategias de MetaTrader 5. El modelo continúa aprendiendo a medida que se enfrenta a nuevos escenarios de mercado nunca antes vistos y se adapta a ellos en tiempo real. Esta fase nos permite eliminar las inconsistencias residuales acumuladas durante el entrenamiento offline y preparar el sistema para su uso totalmente autónomo en una cuenta real.

Debemos señalar que el aprendizaje en dos etapas no es un concepto nuevo dentro de nuestra investigación. Hemos aplicado con éxito esta estrategia en trabajos anteriores. Sin embargo, en el proyecto actual, durante la fase de entrenamiento offline, se han introducido cambios fundamentalmente importantes, debido tanto a consideraciones teóricas como a la experiencia práctica en la implementación de los modelos anteriores.

Posiblemente haya notado que a la arquitectura actual le falta un componente llamado Director. Recordemos que en varios artículos recientes el Director se ha utilizado como un modelo evaluativo adicional, trabajando en paralelo con el Crítico. Su función principal consiste en clasificar de forma binaria las acciones del Agente en rentables y no rentables. Este tipo de retroalimentación rigurosa ha demostrado ser eficaz en las primeras etapas del entrenamiento, ayudando a acelerar la adopción inicial de las políticas.

Sin embargo, en la implementación de hoy tomamos un camino diferente. Y hemos decidido combinar dos enfoques de entrenamiento (aprendizaje por refuerzo y aprendizaje supervisado) directamente en la fase de entrenamiento offline. Esta solución ha resultado más elegante y, al mismo tiempo, más eficaz en cuanto a la calidad y la orientación de la retroalimentación.

La esencia del cambio es la siguiente: paralelamente a la evaluación de las acciones actuales del Agente, que lleva a cabo el Crítico, proporcionamos al Actor acciones de referencia formadas sobre la base de un análisis del movimiento futuro de los precios. Como en la fase de aprendizaje offline tenemos acceso a la serie temporal completa, incluidos los movimientos de precios posteriores, podemos calcular la llamada trayectoria de acción casi perfecta. Estas acciones, formuladas a posteriori, no se limitan a indicar una dirección; se convierten en una especie de estándar hacia el cual se orienta la política del Actor.

De esta manera, implementamos un mecanismo en el que el Agente aprende tanto de sus propios errores como del ejemplo de estrategias rentables precalculadas. Esto nos permite evitar la retroalimentación binaria dura del Director sin perder su efecto formativo. Por el contrario, la calidad de la señal orientadora aumenta, mientras que el proceso de desarrollo de políticas rentables se vuelve más manejable y sostenible.

El enfoque propuesto se implementa en el método Train del asesor "… \MQL5\Experts\GinAR\Study.mq5". El algoritmo del método es la etapa central del aprendizaje offline, que combina métodos de aprendizaje por refuerzo y aprendizaje supervisado. Este se basa en la simulación consistente de situaciones de mercado a partir de datos históricos y en el entrenamiento de una estrategia de negociación basada en el análisis tanto de los estados actuales como de las trayectorias idealizadas de las decisiones de negociación.

Todo comienza con la preparación de la muestra de entrenamiento: primero se selecciona un rango especificado por el usuario de la historia de precios y luego se calculan los indicadores técnicos que se analizan para cada periodo de tiempo. Al mismo tiempo, nos aseguramos de que todos los indicadores estén calculados y listos para su uso; de lo contrario, el proceso se interrumpirá.

void Train(void)
  {
   int start = iBarShift(Symb.Name(), TimeFrame, Start);
   int end = iBarShift(Symb.Name(), TimeFrame, End);
   int bars = CopyRates(Symb.Name(), TimeFrame, 0, start, Rates);
//---
   if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) ||
      !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
//---
   int count = -1;
   bool calculated = false;
   do
     {
      count++;
      calculated = (RSI.BarsCalculated() >= bars &&
                    CCI.BarsCalculated() >= bars &&
                    ATR.BarsCalculated() >= bars &&
                    MACD.BarsCalculated() >= bars
                   );
      Sleep(100);
      count++;
     }
   while(!calculated && count < 100);
   if(!calculated)
     {
      PrintFormat("%s -> %d The training data has not been loaded", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
//---
   if(!ArraySetAsSeries(Rates, true))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
   bars -= end + HistoryBars + NForecast;
   if(bars < 0)
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }

Luego se crean los búferes que se utilizarán para almacenar los estados iniciales, las marcas temporales y los valores objetivo.

Entonces comienza el ciclo principal de entrenamiento. El entrenamiento está organizado como una serie de épocas, es decir, de recorridos completos a través de la historia elegida. En cada época, se lleva a cabo una simulación coherente de las situaciones del mercado.

//---
   vector<float> result, target, neg_target;
   bool Stop = false;
//---
   uint ticks = GetTickCount();
//---
   for(int epoch = 0; (epoch < Epochs && !IsStopped() && !Stop); epoch ++)
     {
      if(!cEncoder.Clear())
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         ExpertRemove();
         return;
        }
      for(int posit = start - HistoryBars - NForecast - 1; posit >= end; posit--)
        {
         if(!CreateBuffers(posit, GetPointer(bState), GetPointer(bTime), Result))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            ExpertRemove();
            return;
           }
         const vector<float> account = SampleAccount(GetPointer(bState), datetime(bTime[0]));
         const vector<float> target_action = OraculAction(account, Result);
         if(!bAccount.AssignArray(account))
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            ExpertRemove();
            return;
           }

En cada paso, se forma el estado actual del mercado, representado como un vector numérico, y luego se determina la escala temporal correspondiente. Estos datos se introducen en el Codificador, que transforma el estado en una representación oculta compacta: una representación condensada del estado del mercado.

//--- Feed Forward
if(!cEncoder.feedForward((CBufferFloat*)GetPointer(bState), 1, false,
                                   (CBufferFloat*)GetPointer(bTime)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
 }

El vector latente resultante se usa en tres direcciones a la vez. Por un lado, se transmite a tres modelos de previsión, cada uno de los cuales es responsable de su propio horizonte de planificación: a corto plazo, a medio plazo y a largo plazo. Estos modelos ofrecen sus pronósticos para los próximos periodos.

for(uint f = 0; f < caForecast.Size(); f++)
   if(!caForecast[f].feedForward(GetPointer(cEncoder), -1, (CBufferFloat*)NULL))
     {
      PrintFormat("%s -> %d - Forecast %d", __FUNCTION__, __LINE__, f);
      Stop = true;
      break;
     }

Por otro lado, la misma representación oculta se envía al Actor responsable de tomar la decisión de negociación. Según el estado del mercado y de la situación de la cuenta, se forma un tensor de acción.

if(!cActor.feedForward(GetPointer(bAccount), 1, false, GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

La decisión adoptada es evaluada por el Crítico, quien determina cuán razonable es dadas las circunstancias.

if(!cCritic.feedForward(GetPointer(cActor), -1, GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Tras realizar la pasada directa, necesitamos proporcionar retroalimentación a los modelos. En primer lugar, se ajustan los parámetros de los modelos de pronóstico.

//--- Study
for(uint f = 0; f < caForecast.Size(); f++)
   if(!caForecast[f].backProp(Result, (CBufferFloat*)NULL) ||
     !cEncoder.backPropGradient((CBufferFloat*)NULL))
     {
     PrintFormat("%s -> %d - Forecast %d", __FUNCTION__, __LINE__, f);
      Stop = true;
      break;
     }

La evaluación de las acciones del Actor se basa en el cálculo de la recompensa: la variación del capital normalizada respecto al saldo de referencia. En caso de pérdida, la penalización se duplica para que la retroalimentación sea más severa y convincente. La recompensa recibida se envía al crítico. Y a partir de ahí reducimos el gradiente de error al Actor y al Codificador.

//---
cActor.getResults(Action);
double equity = bAccount[2] * bAccount[0] * EtalonBalance / (1 + bAccount[1]);
double reward = CheckAction(Action, equity, posit - NForecast + 1) / EtalonBalance;
if(reward < 0)
   reward *= 2;
Result.Clear();
if(!Result.Add(float(reward)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cCritic.backProp(Result, GetPointer(cEncoder), LatentLayer) ||
  !cActor.backPropGradient(GetPointer(cEncoder), LatentLayer, -1, false) ||
  !cEncoder.backPropGradient((CBufferFloat*)NULL, NULL, LatentLayer, true))
 {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
  break;
  }

Así es como se produce el primer aprendizaje: basándose en la reacción real del mercado ante la acción elegida.

Pero el proceso no termina ahí. Entonces entra en juego la segunda parte del enfoque: el aprendizaje supervisado. En lugar de basarse únicamente en la respuesta real del mercado, el sistema usa información que no está disponible en las operaciones reales: el conocimiento del futuro. Para cada estado, se calcula una trayectoria de referencia casi perfecta: la acción que habría producido el mejor resultado, a juzgar por la evolución posterior de los precios. Esta acción se transfiere al Actor, y él la compara con su propia política. De esta forma, enseñamos la estrategia no solo mediante ensayo y error, sino que también ofrecemos un punto de referencia claro a seguir. Este esquema de dos capas (control real e idealizado) acelera el aprendizaje y hace que el comportamiento del modelo resulte más robusto y racional.

//--- Oracul
if(!Action.AssignArray(target_action))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
reward = CheckAction(Action, equity, posit - NForecast + 1) / EtalonBalance;
if(!cActor.backProp(Action, GetPointer(cEncoder), LatentLayer) ||
   !cEncoder.backPropGradient((CBufferFloat*)NULL, NULL, LatentLayer, true))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

El uso de la acción de referencia no se limita a la formación del Actor. También pasamos esta acción a la entrada del Crítico, que, a diferencia de la primera pasada, evalúa un comportamiento que es obviamente correcto. Después de la pasada directa, con esta acción idealizada, se realiza la propagación inversa del error al Codificador. Esto permite al Crítico comprender con mayor precisión la estructura de la función de recompensa, obtener información adicional sobre aquellas áreas del espacio de estados donde el comportamiento del Agente debería ser especialmente claro y, como resultado, aumentar su eficacia en el entrenamiento posterior del Actor.

if(!cCritic.feedForward(Action, 1, false, GetPointer(cEncoder), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!Result.Update(0, float(reward)))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
if(!cCritic.backProp(Result, GetPointer(cEncoder), LatentLayer) ||
   !cEncoder.backPropGradient((CBufferFloat*)NULL, NULL, LatentLayer, true))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
//---

De este modo, se completa un ciclo de aprendizaje completo para un estado. Además, el aprendizaje se produce simultáneamente a través de dos vías: mediante el comportamiento real y mediante el comportamiento de referencia. Este enfoque de doble contorno no solo permite una reducción de errores más rápida, sino que también posibilita la formación de representaciones internas resistentes al ruido del mercado y a las fluctuaciones locales.

Durante todo el procedimiento, se actualiza un informe visual en la terminal con una frecuencia determinada. La pantalla muestra los errores promedio de todos los modelos. Dichos datos permiten al desarrollador supervisar el progreso del entrenamiento en tiempo real, evaluar la estabilidad del proceso e identificar posibles fallos o anomalías.

    if(GetTickCount() - ticks > 500)
      {
       double percent = (epoch + 1.0 - double(posit - end) / 
                        (start - end - HistoryBars - NForecast))
                        / Epochs * 100.0;
       string str = "";
       for(uint f = 0; f < caForecast.Size(); f++)
          str += StringFormat("%-12s%d %6.2f%% -> Error %15.8f\n", "Forecast", f,
                                 percent, caForecast[f].getRecentAverageError());
       str += StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Actor", percent, 
                                                 cActor.getRecentAverageError());
       str += StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Critic", percent, 
                                                cCritic.getRecentAverageError());
       Comment(str);
       ticks = GetTickCount();
      }
   }
}

Una vez finalizado el entrenamiento en todas las épocas, el algoritmo suma los resultados, mostrando el error promedio de cada modelo. Después de ello, el asesor se desconecta.

   Comment("");
//---
   for(uint f = 0; f < caForecast.Size(); f++)
      PrintFormat("%s -> %d -> %-15s%d %10.7f", __FUNCTION__, __LINE__, "Forecast", f, 
                                               caForecast[f].getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Actor", 
                                                      cActor.getRecentAverageError());
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, 
                                            "Critic",cCritic.getRecentAverageError());
   ExpertRemove();
//---
  }

En definitiva, el método Train demuestra cómo construir un marco de entrenamiento para estrategias de trading basado tanto en el rendimiento en el mundo real como en parámetros de referencia ideales derivados del futuro. Esta arquitectura garantiza no solo la adaptación a los datos históricos, sino también un movimiento dirigido hacia el beneficio potencial.

El código completo de este asesor, así como de todos los programas usados en la elaboración de este artículo, se ofrece en el archivo adjunto.



Simulación

Como ya hemos mencionado, el proceso completo de entrenamiento del modelo se ha estructurado en dos etapas consecutivas. En la primera de ellas, utilizamos un entrenamiento offline realizada con datos históricos del par de divisas EURUSD con un marco temporal H1 para todo el año 2024. Este periodo abarca una amplia gama de escenarios de mercado, desde movimientos laterales prolongados hasta rápidos movimientos de tendencia, desde periodos lentos y tranquilos hasta explosiones de volatilidad. Esta diversidad ha permitido que el modelo pudiera hacer frente tanto a situaciones típicas como atípicas, lo cual resulta fundamental para su fiabilidad y versatilidad.

Durante el proceso de entrenamiento, el Codificador ha aprendido a identificar patrones repetitivos en los datos de mercado y a comprimirlos en una representación compacta, pero rica en detalles. Este estado interno del mercado se ha convertido en la base sobre la cual el Actor, utilizando la retroalimentación del Crítico, ha formulado una estrategia sostenible capaz de operar eficazmente en diferentes condiciones. Además, durante la fase de entrenamiento offline, se le han proporcionado las llamadas trayectorias casi perfectas: acciones de referencia elaboradas a partir del conocimiento de los movimientos futuros de los precios. Estas señales no solo han orientado las políticas hacia el aumento de las ganancias, sino que también han proporcionado una guía fiable para la formulación de estrategias, especialmente en las etapas iniciales del aprendizaje, cuando la experiencia del modelo aún es limitada.

Tras completar la formación offline, pasamos a la segunda fase: el perfeccionamiento online, realizado en condiciones muy similares a las del mercado real. El entrenamiento se ha realizado en el simulador de estrategias de MetaTrader 5, donde el modelo ha analizado el mercado paso a paso, vela por vela, en modo de transmisión en tiempo real. Esto no solo nos ha permitido probar la resistencia del modelo al ruido, las distorsiones del mercado y las fluctuaciones aleatorias, sino que también se ha convertido en una importante herramienta de adaptación: el modelo no solo ha memorizado, sino que ha aprendido a funcionar en condiciones reales e impredecibles. Este enfoque ha aumentado significativamente su capacidad de supervivencia, reducido el sobreajuste y mejorado su capacidad de generalización.

El último paso ha sido probar el modelo con datos completamente nuevos: las cotizaciones correspondientes al periodo comprendido entre enero y marzo de 2025. Todos los parámetros y ajustes internos utilizados durante el entrenamiento se han mantenido sin cambios. De este modo, los resultados obtenidos nos permiten evaluar objetivamente no solo la exactitud, sino también la fiabilidad práctica del enfoque propuesto.

Los resultados de las pruebas del modelo ofrecen una comprensión objetiva de su eficacia y estabilidad reales fuera de la muestra de entrenamiento. El depósito inicial de 100 dólares se ha incrementado a 1087,74 dólares, lo que equivale a un aumento de capital de más de 10 veces, un resultado impresionante a primera vista. Sin embargo, si profundizamos, se hace evidente una característica distintiva: el modelo demuestra una alta eficiencia al comienzo del periodo de prueba, pero luego, a medida que se aleja de la muestra de entrenamiento, su rendimiento comienza a disminuir.

El gráfico de saldo refleja claramente este efecto. El primer tercio del periodo se caracteriza por un rápido crecimiento, una trayectoria casi ideal con mínimas reducciones y un crecimiento estable. Sin embargo, a partir de mediados de febrero, y especialmente en marzo, la curva alcanza una meseta e incluso muestra signos de deterioro: aumentan las fluctuaciones, se incrementan las rachas de pérdidas y la tasa de ganancias disminuye notablemente.

Este efecto también se confirma en las métricas numéricas. El factor de recuperación es inferior a 1 (0,96), lo cual indica una relación beneficio/pérdida inferior a la ideal. La caída relativa de equidad alcanza el 65,59%, lo que supone un nivel de riesgo bastante elevado, sobre todo teniendo en cuenta que la caída absoluta al comienzo de la prueba era cercana a cero. El factor de beneficio es de 1,12, el valor mínimo aceptable para una estrategia que se considera sostenible. El equilibrio entre operaciones rentables y perdedoras también está sesgado en contra del modelo: la proporción de operaciones rentables es inferior al 48%.

Este comportamiento se explica desde un punto de vista lógico por lo limitado de la muestra de entrenamiento. El modelo se ha entrenado con datos que se remontan únicamente a 2024, y si bien representaba diferentes fases del mercado, este sigue siendo un organismo vivo y en constante cambio. A medida que el modelo se aleja del periodo conocido, se encuentra cada vez más con situaciones que le resultan desconocidas. Sin episodios de entrenamiento adicionales, su comportamiento se vuelve cada vez menos relevante, sobre todo en condiciones nuevas e inestables.

Una forma obvia de resolver este problema es ampliar la muestra de entrenamiento. Incluir datos de años anteriores —por ejemplo, de 2020 o incluso de 2015— podría permitir que el modelo abarcase una gama mucho más amplia de escenarios de mercado. Esto proporcionará no solo una mayor capacidad de generalización, sino también una mejor comprensión de patrones de acción de precios poco frecuentes pero de vital importancia. Otra alternativa podría ser el entrenamiento online gradual, con actualizaciones periódicas de los pesos del modelo a medida que llegan nuevos datos, lo que mantendría el modelo actualizado sin necesidad de un reentrenamiento completo.



Conclusión

Durante nuestro trabajo, hemos demostrado la aplicabilidad práctica del framework GinAR en condiciones de mercado reales. Desde el desarrollo de la arquitectura y el entrenamiento paso a paso hasta las pruebas finales, todo el proceso ha confirmado la viabilidad del enfoque propuesto.

A pesar del alto rendimiento al comienzo del periodo de prueba, la dinámica posterior ha revelado las limitaciones naturales del modelo asociadas con la reducida muestra de entrenamiento. Esto pone de relieve un principio fundamental del trabajo con series temporales: la solidez de una estrategia depende directamente de su capacidad para generalizar el conocimiento más allá del contexto de entrenamiento.

Los resultados obtenidos ofrecen una base sólida para futuras investigaciones y mejoras. En particular, una dirección obvia para el desarrollo sería la ampliación del volumen de datos de entrenamiento y la introducción de un mecanismo de aprendizaje continuo. Todo esto abre la posibilidad de construir sistemas de negociación verdaderamente adaptables e inteligentes, capaces no solo de sobrevivir, sino también de generar ganancias de forma constante en condiciones de mercado difíciles.


Enlaces


Programas usados en el artículo

# Nombre Tipo Descripción
1 Study.mq5 Asesor Asesor de entrenamiento de modelos offline
2 StudyOnline.mq5 Asesor Asesor de entrenamiento de modelos online
3 Test.mq5 Asesor Asesor para la prueba de modelos
4 Trajectory.mqh Biblioteca de clases Estructura de descripción del estado del sistema y la arquitectura del modelo
5 NeuroNet.mqh Biblioteca de clases 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/18914

Archivos adjuntos |
MQL5.zip (2935.06 KB)
Andrew_7543
Andrew_7543 | 8 ago 2025 en 12:51
(gracias) Bueno, en realidad, ¿qué otra opción me quedaba? Por eso seguí ese camino...
Andrew_7543
Andrew_7543 | 8 ago 2025 en 16:05
Dmitriy Gizlyk MathPow por::MathPow; esto permitirá acceder a las funciones del compilador, en lugar de a las declaradas en la clase.
Además, hay problemas similares en el módulo NeuroNet.mqh:

 call resolves to 'bool CNeuronPSBlock::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,float,ENUM_OPTIMIZATION,uint)' instead of 'bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 3513 25
   see declaration of function 'CNeuronPSBlock::Init' NeuroNet.mqh 48451 22
   see declaration of function 'CNeuronConvSAMOCL::Init' NeuroNet.mqh 11308 22
no one of the overloads can be applied to the function call NeuroNet.mqh 21835 18
could be one of 2 function(s) NeuroNet.mqh 21835 18
   bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 11308 22
   bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,float,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 11309 22
call resolves to '<NA>' instead of 'bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 21835 18
   see declaration of function 'CNeuronConvOCL::Init' NeuroNet.mqh 10973 22
no one of the overloads can be applied to the function call NeuroNet.mqh 21840 18
could be one of 2 function(s) NeuroNet.mqh 21840 18
   bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 11308 22
   bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,float,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 11309 22
call resolves to '<NA>' instead of 'bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 21840 18
   see declaration of function 'CNeuronConvOCL::Init' NeuroNet.mqh 10973 22
no one of the overloads can be applied to the function call NeuroNet.mqh 21846 18
could be one of 2 function(s) NeuroNet.mqh 21846 18
   bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 11308 22
   bool CNeuronConvSAMOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,float,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 11309 22
call resolves to '<NA>' instead of 'bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 21846 18
   see declaration of function 'CNeuronConvOCL::Init' NeuroNet.mqh 10973 22
no one of the overloads can be applied to the function call NeuroNet.mqh 30722 16
could be one of 2 function(s) NeuroNet.mqh 30722 16
   bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 10973 22
   bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 10974 22
call resolves to '<NA>' instead of 'bool CNeuronProofOCL::Init(uint,uint,COpenCLMy*,int,int,int,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 30722 16
   see declaration of function 'CNeuronProofOCL::Init' NeuroNet.mqh 10856 22
no one of the overloads can be applied to the function call NeuroNet.mqh 30730 16
could be one of 2 function(s) NeuroNet.mqh 30730 16
   bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 10973 22
   bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 10974 22
call resolves to '<NA>' instead of 'bool CNeuronProofOCL::Init(uint,uint,COpenCLMy*,int,int,int,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 30730 16
   see declaration of function 'CNeuronProofOCL::Init' NeuroNet.mqh 10856 22
no one of the overloads can be applied to the function call NeuroNet.mqh 30755 16
could be one of 2 function(s) NeuroNet.mqh 30755 16
   bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 10973 22
   bool CNeuronConvOCL::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 10974 22
call resolves to '<NA>' instead of 'bool CNeuronProofOCL::Init(uint,uint,COpenCLMy*,int,int,int,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 30755 16
   see declaration of function 'CNeuronProofOCL::Init' NeuroNet.mqh 10856 22
wrong parameters count, 12 passed, but 15 requires NeuroNet.mqh 64222 22
   bool CNeuronTimeMoEAttention::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint) NeuroNet.mqh 63529 22
call resolves to 'bool CNeuronTimeMoEAttention::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint)' instead of 'bool CNeuronCrossDMHAttention::Init(uint,uint,COpenCLMy*,uint,uint,uint,uint,uint,uint,uint,ENUM_OPTIMIZATION,uint)' due to new rules of method hiding NeuroNet.mqh 64222 22
   see declaration of function 'CNeuronTimeMoEAttention::Init' NeuroNet.mqh 63529 22
   see declaration of function 'CNeuronCrossDMHAttention::Init' NeuroNet.mqh 50759 22
7 errors, 8 warnings 7 8
Dmitriy Gizlyk
Dmitriy Gizlyk | 9 ago 2025 en 07:57
Andrew_7543 #:
A continuación, se observan los mismos problemas en el módulo NeuroNet.mqh

Biblioteca corregida en el artículo «Redes neuronales en el trading: descomposición en lugar de escalado — Creación de módulos» - Artículos sobre MQL5

Andrew_7543
Andrew_7543 | 9 ago 2025 en 20:10
Gracias) ¡Todo funciona como antes de la actualización del terminal... y un gran respeto por el artículo!!!!
Andrew_7543
Andrew_7543 | 10 ago 2025 en 08:12
@Dmitriy Gizlyk, hola... No acabo de entender muy bien qué ocurre con la formación en línea: ¿qué periodo habéis elegido y qué ajustes habéis utilizado en el simulador de estrategias para acercaros a las condiciones reales?
De novato a experto: Revelando las sombras de las velas japonesas (mechas) De novato a experto: Revelando las sombras de las velas japonesas (mechas)
En este análisis, damos un paso adelante para descubrir la acción del precio subyacente que se esconde tras las mechas de las velas japonesas. Al integrar una función de visualización de mechas en el Market Periods Synchronizer, mejoramos la herramienta con mayor profundidad analítica e interactividad. Este sistema mejorado permite a los traders visualizar los rechazos de precios en marcos temporales superiores directamente en gráficos de marcos temporales inferiores, revelando estructuras detalladas que antes estaban ocultas.
Del básico al intermedio: Colas, listas y árboles (V) Del básico al intermedio: Colas, listas y árboles (V)
En este artículo, implementamos los primeros componentes de una estructura arbórea. Como sé que esta estructura puede resultar extremadamente compleja cuando se empieza a aprender, la implementaremos con calma y paso a paso. Así, todos podrán entender cómo funciona un árbol y cuál es el mejor momento para utilizarlo.
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.
Simulación de mercado: Position View (XII) Simulación de mercado: Position View (XII)
En este artículo, aprenderás a crear una señal visual en tu plataforma de trading para identificar directamente en el gráfico si la posición es larga o corta, sin necesidad de acceder al terminal. Además, el texto aborda la implementación de una funcionalidad que mejora la visualización al mover las líneas de take profit y stop loss, ocultando la línea horizontal que sigue al cursor del mouse mientras se reposicionan dichas líneas para evitar confusiones. La lectura ofrece conocimientos prácticos para personalizar sistemas de simulación de mercado.