Русский Português
preview
Redes neuronales en el trading: Previsión probabilística de series temporales (Final)

Redes neuronales en el trading: Previsión probabilística de series temporales (Final)

MetaTrader 5Sistemas comerciales |
194 2
Dmitriy Gizlyk
Dmitriy Gizlyk

Introducción

Las series temporales financieras siguen siendo uno de los tipos de datos más difíciles de analizar. Su comportamiento difícilmente puede calificarse de estable: no existen patrones claros, sino más bien una combinación de tendencias, ciclos, fluctuaciones y cambios estructurales inesperados. Por ello, los modelos tradicionales de regresión lineal o de media móvil, a pesar de su simplicidad y rapidez, resultan insuficientes en la mayoría de los casos. Por otro lado, las soluciones modernas de redes neuronales, si bien poseen un enorme poder expresivo, con frecuencia sufren de sobreajuste, inestabilidad y falta de interpretabilidad.

En estas condiciones, los enfoques híbridos que combinan construcciones matemáticas de eficacia probada con la flexibilidad de las redes neuronales resultan particularmente interesantes. Precisamente sobre este equilibrio se construye el framework K²VAE, un sistema diseñado para analizar y pronosticar series temporales, teniendo en cuenta sus particularidades. La principal fortaleza de este framework reside en su capacidad para captar la dinámica oculta de un sistema, aprendiendo de las secuencias de estados del mercado y ajustando sus conclusiones a medida que se dispone de nuevos datos.

El framework se basa en tres ideas fundamentales, cada una de las cuales complementa a las demás y compensa sus debilidades. La primera es la representación de Koopman, cuya esencia consiste en traducir el comportamiento de un sistema no lineal a un subespacio lineal. Esta transformación, aunque aproximada, ofrece una herramienta poderosa para la descripción y la predicción, que nos permite trabajar con series temporales no a nivel de estadísticas superficiales, sino a través de la reconstrucción de dinámicas profundas.

La segunda idea consiste en usar el filtro de Kalman, pero no en su forma canónica, sino en una forma estabilizada adecuada para su uso en arquitecturas de redes neuronales estocásticas. En este caso, el filtro de Kalman funciona como un corrector adaptativo: mejora las estimaciones obtenidas del módulo KoopmanNet basándose en nuevas observaciones y, simultáneamente, proporciona información sobre el grado de confianza en cada previsión. Esto resulta especialmente valioso no solo al predecir precios, sino también para evaluar la probabilidad de ciertos escenarios.

La tercera idea es la integración de un autoencoder variacional. Este nos permite construir previsiones probabilísticas, en lugar de puntuales: en lugar de un único escenario posible, obtenemos una distribución completa que refleja tanto la tendencia principal como las posibles desviaciones.

De este modo, el K²VAE se convierte no solo en otro modelo del arsenal de análisis, sino en un sistema capaz de describir cuantitativamente la incertidumbre, considerando escenarios alternativos y adaptándose a las nuevas condiciones del mercado.

En artículos anteriores, ya examinamos con detalle los componentes clave de este framework. Así, describimos cómo funciona KoopmanNet, cómo se forma la representación latente, cómo se integra el filtro de Kalman en la arquitectura y cómo se produce el entrenamiento conjunto de todos los elementos. Además, prestamos especial atención a la estabilidad de los cálculos y a la mecánica de difusión de la información dentro del modelo.



Arquitectura del modelo

Tras la construcción paso a paso de los componentes individuales que conforman la base arquitectónica del framework K²VAE, vamos a pasar a la siguiente etapa lógica: la integración de todos los elementos en un único sistema funcional.

Cabe destacar que, al igual que en artículos anteriores, nuestro objetivo no era construir un modelo predictivo en su forma clásica y aislada. En este caso, la previsión es solo un subproducto, secundario a la tarea más fundamental: crear una representación latente, expresiva y estable del estado actual del entorno. Dicha representación no solo resume la información entrante, sino que también sirve como entrada clave para que el Agente de aprendizaje tome una decisión comercial.

Por consiguiente, consideraremos el framework K²VAE no como un modelo de previsión independiente, sino como un Codificador de estado del entorno avanzado integrado en una arquitectura previamente implementada basada en el enfoque Actor–Director–Critic. En este caso, el componente VAE se convierte en el mecanismo para generar una descripción probabilística de escenarios futuros, KoopmanNet es responsable de la dinámica lineal en el espacio latente y el filtro de Kalman realiza la corrección y el refinamiento teniendo en cuenta las observaciones entrantes. Todo esto contribuye a un único objetivo: proporcionar al Agente una representación del entorno externo que contenga la máxima cantidad de información útil para tomar decisiones comerciales informadas y estadísticamente sólidas.

La integración de los componentes K²VAE en la arquitectura Actor–Director–Critic proporciona la profundidad y la estabilidad necesarias al sistema. Gracias a esto, el estado latente generado por el Codificador contiene información no solo sobre el estado actual de las cosas, sino también sobre las trayectorias probables de su cambio. Esto mejora cualitativamente el comportamiento del Agente: no se limita a reaccionar a las señales actuales, sino que actúa teniendo en cuenta los posibles acontecimientos, lo cual es especialmente importante en condiciones de alta volatilidad e imprevisibilidad del mercado.

Aquí es donde entra en juego el método CreateDescriptions. Precisamente él se encarga de recopilar todas las descripciones arquitectónicas, desde el Codificador y los módulos de predicción hasta los componentes de toma de decisiones. Este método sienta las bases para el aprendizaje futuro al definir qué capas se usarán dentro de cada componente de nuestro sistema y en qué orden.

bool CreateDescriptions(CArrayObj *&encoder,
                        CArrayObj *&forecast1,
                        CArrayObj *&forecast2,
                        CArrayObj *&forecast3,
                        CArrayObj *&actor,
                        CArrayObj *&director,
                        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(!director)
     {
      director = new CArrayObj();
      if(!director)
         return false;
     }
   if(!critic)
     {
      critic = new CArrayObj();
      if(!critic)
         return false;
     }

En los parámetros del método obtenemos los punteros a una serie de arrays dinámicos, una para cada uno de los modelos. Son contenedores en los que se escribirá la descripción arquitectónica de los bloques de la red neuronal. Antes de comenzar a rellenarlos, debemos comprobar obligatoriamente la exactitud de los punteros recibidos. Si falta el objeto requerido, no intentaremos trabajar con la memoria inválida, sino que crearemos cuidadosamente una nueva instancia del array. Este paso puede parecer una rutina técnica, pero en la práctica es lo que sienta las bases para la estabilidad de todo el trabajo futuro.

Después de esto, comienza la parte principal, es decir, rellenaremos cada array con las capas y conexiones correspondientes que forman la configuración completa de la red neuronal. Comenzaremos con el elemento central del sistema: el Codificador de estado del entorno. Precisamente en este componente integramos las ideas principales establecidas en el framework K²VAE. Recordemos que la tarea del Codificador no consiste simplemente en recopilar información sobre la situación del mercado, sino en crear una representación latente expresiva e informativa que pueda servir como base fiable para que el Agente tome decisiones comerciales.

Los datos de entrada sin procesar procedentes directamente del terminal los transmitimos a la capa totalmente conectada. Esta actúa aquí como una interfaz entre el mundo exterior y la lógica interna del modelo.

//--- 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, los datos se transfieren a la etapa de procesamiento principal: la capa de normalización por lotes. Es aquí donde las distintas características de entrada, que difieren en escala, alcance o significado físico, se unifican en una forma comparable. Este paso resulta importante no solo desde el punto de vista de la estabilidad numérica, sino también desde el punto de vista del entrenamiento del modelo completo: los datos normalizados permiten que la red neuronal converja más rápido y capture con mayor precisión las relaciones entre las características.

En este contexto, la normalización por lotes actúa como un filtro que suaviza el ruido estadístico e iguala las distribuciones de los componentes individuales de la señal de entrada. Esto crea un entorno estable y predecible para transformaciones posteriores, minimizando las distorsiones en la etapa inicial del procesamiento.

Una vez normalizados, los datos estarán listos para el siguiente paso importante: la partición espacial o el parcheo. Los autores del framework K²VAE original proponen dividir los datos de origen preparados en parches que no se superpongan, cada uno de los cuales representa un fragmento del flujo de información general. En este caso, los valores de diferentes canales se mezclan dentro de un mismo parche, lo cual permite que el modelo aprenda de forma natural a identificar las interdependencias entre las características.

Sin embargo, en nuestra implementación, hemos ido más allá y decidido no limitarnos al esquema de parcheo estándar, sino que implementamos una estructura más compleja inspirada en varias arquitecturas modernas que ya hemos comentado en nuestros artículos. En primer lugar, enriquecemos aún más el array de características de entrada añadiendo derivadas que reflejan las desviaciones paso a paso de los valores de cada canal. Esto nos permite considerar la dinámica de las señales a corto plazo y aumentar la sensibilidad del modelo a los cambios en la estructura del mercado, manteniendo al mismo tiempo su robustez frente al ruido.

//--- 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;
     }

A continuación, añadimos etiquetas temporales a los datos, un elemento importante que permite al modelo tener en cuenta no solo los valores de las características, sino también su posición en el tiempo. Estas etiquetas ofrecen a la red neuronal contexto sobre el momento real del evento, ya sea un minuto, una hora, un día de la semana u otra escala. Esto permite que el modelo detecte patrones recurrentes y fluctuaciones estacionales características de las series temporales financieras. Introducir este tipo de información temporal resulta especialmente importante cuando se trabaja con cotizaciones históricas, donde la ciclicidad juega un papel clave en la formulación de estrategias de negociación.

//--- 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;
     }
//--- 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;

Conviene destacar un punto importante: los diferentes canales de información de entrada pueden tener ritmos internos diferentes, es decir, presentar ciclos con periodos distintos. En estas condiciones, la simple combinación mecánica de valores de todos los canales en una sola secuencia puede neutralizar dependencias estacionales ocultas y distorsionar la dinámica de las señales.

Para evitar que estas características se pierdan, implementamos un mecanismo de convolución adaptativa. Su función consiste en analizar la naturaleza de cada secuencia temporal y generar parches considerando las características individuales del canal. De esta forma, cada canal dispone del mismo número de representaciones en el espacio latente, manteniendo al mismo tiempo la sensibilidad a su ciclicidad única.

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronAdaptConv;
   descr.count = Segments;
   descr.window = 2 * prev_out / Segments;
   descr.variables = prev_count;
   prev_out = descr.window_out = EmbeddingSize;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_count = descr.count;
   uint prev_var = descr.variables;

A los parches generados les añadimos la codificación RoPE (Rotary Positional Encoding), que proporciona al modelo el conocimiento de la posición relativa de cada elemento en la secuencia temporal. A diferencia de las codificaciones posicionales clásicas, RoPE incorpora información de la posición usando la rotación de un vector de características en el espacio latente. Esto permite preservar la estructura temporal de los datos durante el procesamiento posterior por parte de las capas transformadoras, así como transmitir de forma eficiente información sobre las distancias entre eventos. Este enfoque resulta especialmente importante en las tareas de análisis de series temporales, donde el orden y el intervalo entre los eventos desempeñan un papel fundamental.

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

A continuación, transponemos el tensor tridimensional resultante, que representa segmentos de canales independientes, al formato de una secuencia temporal de segmentos mixtos. Esta transformación nos permite pasar de una estructura de la forma [Canales × Parches × Características] a una forma más adecuada para procesar el modelo secuencial [Parches × Canales × Características], donde cada parche ahora representa una porción de tiempo que contiene datos de todos los canales simultáneamente. De esta forma, preparamos los datos para su posterior procesamiento en el Codificador K²VAE, centrando la atención del modelo en las relaciones entre las características dentro del flujo temporal general.

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

Esta presentación de datos es totalmente coherente con las ideas clave expuestas por los autores del framework K²VAE, en particular el deseo de integrar información heterogénea en un único espacio latente. Sin embargo, a diferencia de la implementación básica, conservamos propiedades importantes de la estructura de la serie temporal, en particular la ciclicidad de los canales individuales, lo que resulta fundamental para los datos financieros. Además, usando parches avanzados, desviaciones graduales y etiquetas temporales, enriquecimos la representación de entrada, mejorando la capacidad del modelo para reconocer patrones consistentes y dependencias estacionales sin perder los matices de las fuentes de datos individuales.

Los parches generados, que contienen información enriquecida y estructurada sobre el estado actual del entorno, se transmiten al Codificador K²VAE. En esta etapa, se produce la codificación probabilística: cada secuencia de entrada se transforma en una distribución de incorporaciones en la salida del bloque. Cada elemento individual de esta distribución representa una descripción condensada, compacta pero expresiva, de uno de los posibles escenarios para el desarrollo de la serie temporal analizada. De este modo, el modelo genera no solo una estimación puntual del estado futuro, sino también un conjunto de hipótesis probabilísticas que reflejan la diversidad de trayectorias potenciales. Esto resulta especialmente importante en el contexto de la elevada incertidumbre inherente a los mercados financieros.

//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronK2VAEEncoder;
     {
      uint temp[] = {prev_count,                        // units in
                     NScenarios,                        // Scenarios
                     NExperts,                          // MoE
                     TopK                               // Top K
                    };
      if(ArrayCopy(descr.units, temp) < int(temp.Size()))
         return false;
     }
   descr.step = NHeads;
     {
      uint temp[] = {prev_out * prev_var,               // window
                     prev_out * prev_var / descr.step,  // dimension Key
                     2 * prev_out*prev_var / NExperts   // dimension MoE
                    };
      if(ArrayCopy(descr.windows, temp) < int(temp.Size()))
         return false;
     }
   descr.layers = 3;
   descr.variables = 1;
   descr.batch = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Cabe señalar que la distribución en el espacio latente obtenida a la salida del Codificador K²VAE será la que se transmita al Agente para la posterior toma de decisiones comerciales. Sin embargo, para garantizar la expresividad y el valor práctico de este estado latente, es decir, su capacidad para contener información sobre los escenarios más probables para el comportamiento futuro de la serie temporal, necesitamos construir dentro del modelo un flujo de previsión completo.

Como hemos mencionado anteriormente, el objetivo de nuestro framework no es crear un previsión precisa en el sentido clásico. Sin embargo, necesitamos organizar el flujo de aprendizaje de manera que el gradiente de error pueda propagarse desde la salida final hasta cada elemento de la distribución de probabilidad de incorporación. Para ello, añadimos una capa especializada de TimeMoEAttention.

Esta capa desempeña una función importante: agrega incorporaciones probabilísticas, formando una representación unificada basada en ellas, y de este modo crea la visibilidad necesaria de un modelo de previsión tradicional. Esta solución nos permite combinar la naturaleza probabilística del espacio latente con los requisitos de aprendizaje y optimización, garantizando un aprendizaje de distribución completo basado en el resultado final.

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTimeMoEAttention;
   descr.window_out = EmbeddingSize;
     {
      uint temp[] = {prev_out, prev_out, 8, TopK};               //Window Main, Window Cross, Experts dimension, TopK
      if(ArrayCopy(descr.windows, temp) < ArraySize(temp))
         return false;
     }
     {
      uint temp[] = {prev_var, prev_var * NScenarios, NExperts}; //Units Main, Units Cross, Experts
      if(ArrayCopy(descr.units, temp) < ArraySize(temp))
         return false;
     }
   descr.layers = 3;
   descr.step = NHeads;                                          // Attention heads
   descr.batch = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }//---
   CLayerDescription *latent = descr; 

A continuación, pasamos a la formación de la arquitectura de tres modelos de previsión clásicos, cada uno de los cuales se encarga de realizar previsiones en su propio horizonte de planificación: a corto plazo, a medio plazo y a largo plazo. Estos modelos desempeñan un papel importante: despliegan la representación latente resultante, formada en el Codificador, de vuelta al espacio de datos observado. De este modo, cada estado se interpreta en términos de valores reales que reflejan posibles escenarios de comportamiento futuro de la serie temporal.

Cabe señalar que las arquitecturas de estos modelos de previsión se han analizado en detalle en nuestros trabajos anteriores. Estos han demostrado su eficacia en la práctica, y por este motivo fueron adoptados directamente sin modificaciones. En este artículo no entraremos en los detalles técnicos de su construcción.

El siguiente paso lógico es construir la arquitectura del Actor de negociación, el elemento central del sistema de toma de decisiones. Su tarea consiste en elegir la mejor estrategia de negociación según la información disponible. A diferencia de los modelos predictivos clásicos, el Actor no intenta predecir directamente el comportamiento futuro del mercado, sino que utiliza una descripción probabilística de los posibles escenarios proporcionada por el Codificador para evaluar la eficacia potencial de cada acción posible.

El modelo de Actor recibe dos fuentes de información como entrada. El primero es el estado actual del balance, que incluye posiciones activas, capital y otra información operativa. La segunda es la distribución de representaciones latentes obtenidas del Codificador K²VAE, donde cada representación describe una de las posibles opciones para el desarrollo de la situación del mercado en el futuro. Esta distribución no solo contiene información sobre las trayectorias más probables, sino que también transmite el nivel de incertidumbre del modelo con respecto a estos escenarios.

Como antes, usamos una capa totalmente conectada como primera capa, que actúa como interfaz externa del modelo. Precisamente a través de ella el Actor recibe información sobre el estado del saldo como entrada.

//--- Actor
   latent = encoder.At(LatentLayer);
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = AccountDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = AccountDescr;
   descr.batch = BatchSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Los datos de entrada se hacen comparables mediante una capa de normalización por lotes. Esto nos permite eliminar las grandes diferencias entre las características y garantizar el funcionamiento estable de las capas subsiguientes del modelo, independientemente de cuáles sean los valores iniciales y la naturaleza de los datos.

Una vez que los datos del estado de la cuenta se han convertido a un formato comparable, se transmiten al módulo de atención cruzada, que considera esta información en el contexto de la distribución de probabilidad de los estados subsiguientes del entorno.

Debemos destacar una diferencia clave con respecto a los modelos clásicos: E Actor no se limita a reaccionar ante la situación actual, sino que evalúa los riesgos de la toma de decisiones considerando la amplitud y la forma de la distribución de los escenarios futuros. Cuanto mayor sea la confianza del Codificador en la previsión (y esto se expresa en una distribución más estrecha y concentrada), menor será la variedad de escenarios que reciba el Actor. En este caso, podemos actuar con mayor decisión, por ejemplo, aumentando el tamaño de la posición o utilizando parámetros de transacción más agresivos. De este modo, la estrategia se adapta de forma dinámica al grado cambiante de incertidumbre del mercado.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronCrossDMHAttention;
     {
      uint temp[] = {AccountDescr,           // Inputs window
                     latent.windows[0]       // Cross window
                    };
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
     {
      uint temp[] = {1,                      // Inputs units
                     latent.units[1]         // Cross units
                    };
      if(ArrayCopy(descr.units, temp) < (int)temp.Size())
         return false;
     }
   descr.step = NHeads;                      // Heads
   descr.window_out = 8;
   descr.batch = 1e4;
   descr.layers = 2;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

La información procesada se introduce a continuación en una red neuronal totalmente conectada de tres capas (MLP), que completa la cadena de procesamiento y da forma a la decisión de trading final. Precisamente en esta etapa se produce la integración de todas las características: el estado actual de la cuenta, la representación probabilística del futuro y el contexto identificado mediante el mecanismo de atención. El modelo analiza la totalidad de estos factores y determina la dirección de la transacción y sus parámetros. Esta estructura, a pesar de su aparente simplicidad, desempeña un papel crucial para garantizar la flexibilidad y adaptabilidad de todo el sistema comercial.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.batch = BatchSize;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = TANH;
   descr.batch = BatchSize;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 2 * NActions;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

A la salida del Actor, se usa una cabeza estocástica para generar una decisión comercial, implementada a través de la capa CNeuronVAEOCL.

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronVAEOCL;
   descr.count = NActions;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = NActions / 3;
   descr.window = 3;
   descr.step = 3;
   descr.window_out = 3;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

A primera vista, dicha solución puede parecer excesivamente arriesgada: combinar la distribución probabilística de escenarios futuros con la naturaleza probabilística de la cabeza del Actor aumenta, en efecto, el grado de aleatoriedad en el proceso de toma de decisiones. Y en el contexto de las condiciones reales del mercado, esto suscita preocupaciones legítimas.

Sin embargo, la esencia de este enfoque radica en que la naturaleza estocástica del Actor se revela por completo solo en las primeras etapas del entrenamiento del modelo. Durante este periodo, el Actor explora activamente posibles opciones de comportamiento, aprendiendo de una amplia gama de reacciones del entorno. A medida que se acumula la experiencia y se reevalúan los resultados obtenidos, la distribución de los resultados se concentra cada vez más en torno a las mejores estrategias, y la dispersión disminuye. Como resultado, el comportamiento del Actor se estabiliza, adquiriendo la dirección y el enfoque necesarios para tomar decisiones con confianza en condiciones reales de mercado.

Los modelos de evaluación del Director y del Crítico replican en gran medida la arquitectura del Actor, manteniendo el marco general y los principios del procesamiento de datos. La principal diferencia radica en la fuente del flujo de datos de entrada principal: en lugar del estado de equilibrio, toman como entrada el tensor de acciones generadas por el Actor. Esto les permite evaluar cada acción de forma más específica y con mayor detalle.

Sin embargo, la característica de toma de decisiones estocástica propia del Actor está ausente en este caso. En lugar de una distribución probabilística de acciones, el Director y el Crítico generan calificaciones específicas e indicadores de calidad para cada acción. Esta retroalimentación detallada ayuda al Actor a perfeccionar sus estrategias y mejorar la eficacia de sus decisiones comerciales.

El código fuente completo que describe el diseño arquitectónico de todos los modelos se ofrece en el archivo adjunto.



Entrenamiento

Una vez finalizada la fase de construcción arquitectónica de todos los componentes del modelo, vamos a pasar al siguiente paso: el proceso de entrenamiento. Al igual que en trabajos anteriores, el proceso se organiza en dos etapas. En la primera fase, realizamos un entrenamiento offline utilizando los datos históricos disponibles directamente desde el terminal. La particularidad de este método reside en que la formación se realiza sin necesidad de una muestra de entrenamiento previamente preparada. En lugar de ello, integramos un mecanismo de evaluación de acciones dentro del propio procedimiento de entrenamiento. Esto amplía significativamente la cobertura de datos históricos disponibles para la etapa inicial de entrenamiento y elimina la restricción del etiquetado manual o la creación de conjuntos de datos especializados.

En este punto, conviene prestar especial atención a un aspecto importante que se reveló durante los experimentos anteriores. Al usar el modelo para evaluar las acciones del Actor en un horizonte limitado de estados previstos, se observa un comportamiento negativo persistente. El modelo tiende a generar niveles de stop-loss y take-profit demasiado ambiciosos que no se alcanzan dentro del periodo de evaluación establecido. Y esto conlleva aferrarse a posiciones perdedoras durante demasiado tiempo sin una justificación suficiente para revertirlas o salir del mercado. Este comportamiento se vuelve dominante y el modelo pierde efectivamente la capacidad de responder a los cambios del mercado a corto plazo, reduciendo la estrategia a seguir pasivamente la tendencia global.

Para contrarrestar este efecto, hemos introducido dos cambios clave en el proceso de entrenamiento. En primer lugar, hemos reducido significativamente el factor de descuento (Discount Factor), centrando nuestros esfuerzos en generar beneficios rápidos. Esta decisión permite que el modelo se centre en los resultados inmediatos de sus acciones, dando mayor importancia a las consecuencias a corto plazo. Al fin y al cabo, las pérdidas actuales dentro de este enfoque tienen un impacto más significativo en la recompensa final que las ganancias potenciales en un futuro lejano, incluso si estas últimas parecen más impresionantes. De este modo, el modelo comienza a evitar estrategias basadas en la espera pasiva de un cambio de tendencia del mercado y reduce la tendencia a permanecer inactivo ante las pérdidas, lo cual aumenta su adaptabilidad a la dinámica real del mercado.

La segunda solución, que ya resultaba constructiva, se ha implementado en el método CheckAction. En este caso, hemos abandonado la evaluación de las acciones dentro de un horizonte de previsión limitado y hemos elegido un enfoque en el que la evaluación se lleva a cabo sobre todo el periodo histórico disponible. Este paso nos permite mejorar significativamente la precisión de la retroalimentación, ya que en la gran mayoría de los casos sabemos exactamente qué nivel de negociación (stop loss o take profit) se alcanzará dentro de esta historia extendida. Esto, a su vez, ofrece una evaluación más objetiva de cada acción y permite que el sistema de aprendizaje distinga con mayor claridad entre decisiones eficaces e ineficaces.

double CheckAction(CBufferFloat *action, double equity, uint start_position)
  {
   if(!action || start_position >= Rates.Size())
      return 0;

En los parámetros del método obtenemos:

  • action — tensor de acción de trading: volúmenes, niveles SL/TP;
  • equity — estado actual de la equidad utilizada para estimar la reducción máxima;
  • start_position — índice del estado de apertura de la posición en el array de datos del mercado Rates.

El método comienza comprobando la relevancia del puntero al objeto action y el índice de posición inicial, que no debe ir más allá del array de cotizaciones.

A continuación, se extraen los parámetros de la transacción.

double buy_lot = MathMax(double(action[0] - action[3]), 0);
double sell_lot = MathMax(double(action[3] - action[0]), 0);

De este modo, el sistema admite un suministro de volumen independiente según la dirección. Por ejemplo, si action [0] > action [3], se asume que se abre una posición de compra; de lo contrario, se asume una posición de venta.

Luego se calcula el margen al precio actual, se calcula el costo de un punto de movimiento (point_cost) y, en caso de volumen insuficiente (menor que el mínimo permitido), el método devuelve la pérdida esperada (lost profit) según la amplitud de la vela.

double marg = 0;
if(!OrderCalcMargin(ORDER_TYPE_BUY, Symb.Name(), 1, Symb.Ask(), marg))
   return 0;
double point_cost = Symb.TickValue() / Symb.TickSize();
if(MathMax(buy_lot, sell_lot) < Symb.LotsMin())
  {
   double loss = -MathMax(Rates[start_position].high - Rates[start_position].open,
                          Rates[start_position].open - Rates[start_position].low) *
                 point_cost * equity / (2 * marg);
   return loss;
  }
if((marg * MathMax(buy_lot, sell_lot)) >= equity)
  {
   double loss = -MathMax(Rates[start_position].high - Rates[start_position].open,
                          Rates[start_position].open - Rates[start_position].low) *
                 point_cost * MathMax(buy_lot, sell_lot);
   return loss;
  }
point_cost *= MathAbs(buy_lot - sell_lot);

Una situación similar ocurre en el caso de un volumen inflado que no está cubierto por los fondos disponibles (margen * lote > capital). Esto incentiva al modelo a realizar transacciones dentro de los límites permitidos, eliminando la inacción.

A continuación, comprobaremos la eficacia de las acciones propuestas. Primero, vamos a revisar las posiciones largas.

//---
  double tp = 0, sl = 0, profit = 0, reward = 0;
  int stops = MathMax(Symb.StopsLevel(), 10);
  int spread = Symb.Spread();
  if(buy_lot > 0)
    {
     tp = action[1] * MaxTP;
     sl = action[2] * MaxSL;
     if(int(tp) < stops || int(sl) < (stops + spread))
       {
        double loss = -MathMax(Rates[start_position].high - Rates[start_position].open,
                               Rates[start_position].open - Rates[start_position].low) *
                      point_cost * buy_lot;
        return loss;
       }
     tp = (tp + spread) * Symb.Point() + Rates[start_position].open;
     sl = Rates[start_position].open - (sl + spread) * Symb.Point();
     reward = profit = -spread * Symb.Point() * point_cost;

Los niveles TakeProfit y StopLoss se extraen de los elementos del tensor action. Posteriormente, se ajustan y se comprueba que cumplan con los niveles de stop del bróker.

Si se cumplen las condiciones, el TP y el SL se convierten a valores de precio absoluto. De lo contrario, calculamos el monto de las ganancias perdidas.

A continuación, se realiza una simulación del movimiento del precio sobre un fragmento de datos históricos. Para ello, organizamos un ciclo de iteración a través de datos históricos en orden cronológico.

Cabe señalar que el array Rates es una serie temporal. Por lo tanto, para mantener la secuencia histórica, los datos se clasifican en orden inverso.

for(uint i = start_position; i >=0; i--)
  {
   if(sl >= Rates[i].low)
     {
      double p = (Rates[i].open - sl) * point_cost;
      profit -= p;
      reward -= p * MathPow(DiscFactor, float(i - start_position));
      break;
     }

El cuerpo del ciclo implementa un enfoque estrictamente orientado al riesgo: se da prioridad en la verificación al nivel de stop loss. Esta decisión se basa en el sentido común: las pérdidas en el mercado, por regla general, se producen de forma repentina y mucho más rápido de lo que se acumulan las ganancias. Si se alcanza el nivel de stop, la cantidad de pérdidas se determina en términos monetarios y teniendo en cuenta el factor de descuento.

El uso de un factor de descuento permite un equilibrio flexible entre los resultados inmediatos y los diferidos, entrenando al modelo para elegir acciones que conduzcan a rendimientos estables. Cuanto mayor sea el factor, más orientado estará el modelo hacia los beneficios a largo plazo, y viceversa. Sin embargo, esta flexibilidad tiene una desventaja: el uso de descuentos dificulta la estimación precisa de la magnitud de la reducción. Las pérdidas que se producen después de un número significativo de pasos están sujetas a un descuento significativo y pueden ser percibidas por el modelo como insignificantes. Como resultado, el modelo tiende a aferrarse a posiciones perdedoras con la esperanza de una reversión, lo que en condiciones reales de mercado puede provocar una caída fatal hasta el nivel de StopOut. Para evitar este tipo de comportamiento, además de la estimación con descuento, se usa un control absoluto de las pérdidas.

El logro del nivel de take profit se verifica de manera similar.

if(tp <= Rates[i].high)
  {
   double p = (tp - Rates[i].open) * point_cost;
   profit += p;
   reward += p * MathPow(DiscFactor, float(i - start_position));
   break;
  }

Si no se alcanza ninguno de los niveles de negociación, la ganancia/pérdida actual se fija en el precio de apertura de la siguiente vela.

double p = (Rates[i - 1].open - Rates[i].open) * point_cost;
profit += p;
reward += p * MathPow(DiscFactor, float(i - start_position));

Aquí es especialmente importante enfatizar el uso del precio de apertura de la siguiente barra, y no el precio de cierre de la actual. Si bien en la mayoría de los casos estos valores coinciden o difieren solo ligeramente, debemos considerar las peculiaridades de la dinámica del mercado. Nuestro modelo toma una decisión comercial al inicio de una nueva barra, por lo que es correcto tener en cuenta este precio al fijar los resultados de las posiciones abiertas.

Este enfoque nos permite mantener el realismo de la simulación y no ignorar la posibilidad de que se produzcan brechas de precios, algo típico en segmentos de mercado muy volátiles o en momentos en que se publican noticias importantes. El uso del precio de apertura de la siguiente barra también enfatiza la secuencia de eventos de negociación, destacando la lógica de causa y efecto entre una decisión y su implementación en un entorno de mercado real.

A continuación, comparamos las pérdidas acumuladas sin considerar el factor de descuento con el nivel de equidad en el momento de la apertura de la posición. Este enfoque le permite controlar que los fondos sean suficientes para llevar a cabo una operación comercial. Si las pérdidas superan el capital disponible, es decir, si se agota el depósito, aumentaremos la penalización en la función de recompensa y finalizaremos la simulación de inmediato. Esto simula una situación real del mercado en la que la falta de fondos provoca la interrupción de la actividad comercial, y garantiza una evaluación más precisa y segura de la estrategia de negociación durante el proceso de entrenamiento del modelo.

    if(-profit >= equity)
      {
       reward-=1000;
       break;
      }
   }
}

Evaluaremos la posición corta de manera similar.

   if(sell_lot > 0)
     {
      tp = action[4] * MaxTP;
      sl = action[5] * MaxSL;
      if(int(tp) < stops || int(sl) < (stops + spread))
        {
         double loss = -MathMax(Rates[start_position].high - Rates[start_position].open,
                                Rates[start_position].open - Rates[start_position].low) *
                       point_cost * sell_lot;
         return loss;
        }
      tp = Rates[start_position].open - (tp + spread) * Symb.Point();
      sl = Rates[start_position].open + (sl - spread) * Symb.Point();
      for(uint i = start_position; i >=0; i--)
        {
         if(sl <= Rates[i].high)
           {
            double p = (sl - Rates[i].open) * point_cost;
            profit -= p;
            reward -= p * MathPow(DiscFactor, float(i - start_position));
            break;
           }
         if(tp >= Rates[i].low)
           {
            double p = (Rates[i].open - tp) * point_cost;
            profit += p;
            reward += p * MathPow(DiscFactor, float(i - start_position));
            break;
           }
         double p = (Rates[i - 1].open - Rates[i].open) * point_cost;
         profit -= p;
         reward -= p  * MathPow(DiscFactor, float(i - start_position));
         if(-profit >= equity)
           {
            reward-=1000;
            break;
           }
        }
     }
//---
   return reward;
  }

Posteriormente, se finaliza el funcionamiento del método, devolviendo la recompensa acumulada (considerando el factor de descuento) al programa que realiza la llamada.

El código completo del asesor de entrenamiento offline para el modelo "…\Experts\K2VAE\Study.mq5" se proporciona en el archivo adjunto. Todos los programas utilizados para la elaboración del artículo también se presentan allí.



Simulación

Como ya hemos mencionado, el modelo se entrena en dos etapas sucesivas. En primer lugar, realizamos un entrenamiento offline con una historia de 15 años del par EURUSD con el marco temporal H1. Este conjunto de datos abarca todo tipo de situaciones de mercado: desde movimientos laterales prolongados hasta tendencias pronunciadas, desde periodos de calma hasta picos de volatilidad. Esto permite que el modelo estudie una variedad de comportamientos del mercado. El Codificador ha aprendido así a identificar patrones clave y a transformar el estado del mercado en una representación compacta pero informativa, que se convierte en la base para la toma de decisiones del Agente. El Actor, basándose en los comentarios del Crítico y del Director, elabora una estrategia sostenible que puede funcionar eficazmente en diferentes condiciones.

A continuación, se ejecuta la segunda fase: el entrenamiento online utilizando datos de 2024, organizado en el simulador de estrategias de MetaTrader 5. En este caso, el modelo funciona prácticamente en tiempo real, analizando el mercado vela por vela. Nos hemos encontrado con ruido, fluctuaciones aleatorias y distorsiones típicas de un mercado en vivo. Este enfoque nos ha permitido no solo seguir entrenando el modelo, sino también adaptar su comportamiento a la dinámica del mundo real, mejorar la estrategia y aumentar la resiliencia ante la incertidumbre.

Tras completar el entrenamiento, hemos probado el modelo utilizando datos nuevos (cotizaciones de enero a marzo de 2025), manteniendo todos los parámetros utilizados durante el entrenamiento. A continuación le resumimos los resultados de las pruebas.

Los resultados de las pruebas demuestran que el modelo ha generado beneficios positivos durante el periodo histórico seleccionado. El ingreso neto total ha sido de 821.90$ con un depósito inicial de 100.0$, lo que indica crecimiento del capital. Cabe señalar que el coeficiente de rentabilidad (Profit Factor) se ha situado en 1,06, lo que indica que los beneficios superan ligeramente las pérdidas.

Los indicadores de negociación muestran que el número de operaciones rentables ha sido aproximadamente igual al número de operaciones perdedoras, alrededor del 49 % y el 51 %, respectivamente, lo que indica un equilibrio entre las posiciones ganadoras y perdedoras.

El gráfico muestra que la curva de saldo es generalmente ascendente, a pesar de las caídas notables y los periodos de descenso. Resulta especialmente destacable la clara tendencia al alza del saldo en enero y la primera quincena de febrero. Al mismo tiempo, el mes de marzo se presenta claramente poco rentable. Esto podría indicar la necesidad de seguir entrenando el modelo a lo largo de un periodo histórico más extenso.


Conclusión

En conclusión, observamos que el framework K²VAE propuesto como parte de nuestro Agente comercial, ha demostrado su eficacia y ha sido confirmado con datos históricos reales. El modelo combina un profundo conocimiento de la dinámica oculta del mercado, un ajuste de riesgo adaptativo y la generación de escenarios probabilísticos, lo cual da como resultado un crecimiento del capital. Al mismo tiempo, la disminución de la eficiencia durante un largo periodo de pruebas indica la necesidad de encontrar formas de mejorar la capacidad de generalización del modelo.


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/18807

Archivos adjuntos |
MQL5.zip (2916.14 KB)
Adam John Bradley
Adam John Bradley | 15 jul 2025 en 06:37

He estado teniendo algunos problemas VAE.mqh y han encontrado los siguientes trabajos en torno a la cuestión bastante bien.

bool CVAE::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!OpenCL || !NeuronOCL)
      return false;
//---
   if(!OpenCL.SetArgumentBuffer(def_k_VAECalcHiddenGradient, def_k_vaehg_input, NeuronOCL.getOutput().GetIndex()))
      return false;
   if(!OpenCL.SetArgumentBuffer(def_k_VAECalcHiddenGradient, def_k_vaehg_inp_grad, NeuronOCL.getGradient().GetIndex()))
      return false;
   if(!OpenCL.SetArgumentBuffer(def_k_VAECalcHiddenGradient, def_k_vaehg_random, m_cRandom.GetIndex()))
      return false;
   if(!OpenCL.SetArgumentBuffer(def_k_VAECalcHiddenGradient, def_k_vaehg_gradient, Gradient.GetIndex()))
      return false;
   if(!OpenCL.SetArgument(def_k_VAECalcHiddenGradient, def_k_vaehg_kld_mult, m_fKLD_Mult))
      return false;
      
   // Calcular los tamaños de trabajo adecuados
   uint neurons_count = (uint)Neurons();
   uint local_size = 64;  // o 32, 128, 256 dependiendo de tu GPU
   
   // Redondea el tamaño global para que sea divisible por el tamaño local
   uint global_size = ((neurons_count + local_size - 1) / local_size) * local_size;
   
   uint work_offset[] = {0};
   uint work_size[] = {global_size};
   uint local_work_size[] = {local_size};
   
   if(!OpenCL.Execute(def_k_VAECalcHiddenGradient, 1, work_offset, work_size, local_work_size))
      return false;
  //--- 
   return true;
  }
Chao Tang
Chao Tang | 21 jul 2025 en 12:15
我也尝试了相同的测试路径,先是使用Study通过历史数据学习了1000000次,生成了相关的nnw文件之后。采用StudyOnline学习一年的数据(这个时候是没有任何收入的,10000美金的账户很快就没钱了,运行结束之后更新了部分的nnw文件)。最后使用Test测试和StudyOnline的结果基本一致(没有任何盈利)
Simulación de mercado: Position View (VI) Simulación de mercado: Position View (VI)
En este artículo, haremos diversas mejoras para que el indicador de posición refleje lo que realmente ocurre en el servidor de trading, en términos de posiciones y del estado actual de estas. Debo recordar que estas aplicaciones, que se mostrarán aquí, no pretenden en ningún caso sustituir ningún elemento disponible en MetaTrader 5. Tampoco deben usarse sin los debidos cuidados y criterios, ya que su objetivo es presentar un código didáctico, es decir, con fines de aprendizaje sobre cómo funciona el sistema. El motivo por el que digo que el código es didáctico es que el uso de mensajes, en algunos casos, no es la mejor forma de implementar ciertas funcionalidades.
De novato a experto: Noticias animadas utilizando MQL5 (V) Sistema de recordatorio de eventos De novato a experto: Noticias animadas utilizando MQL5 (V) Sistema de recordatorio de eventos
En esta discusión, exploraremos nuevas mejoras a medida que integramos una lógica mejorada de alertas de eventos para los acontecimientos del calendario económico que muestra el EA «News Headline». Esta mejora es fundamental, ya que garantiza que los usuarios reciban notificaciones oportunas poco antes de que tengan lugar eventos importantes. Acompáñanos en este análisis para descubrir más.
Del básico al intermedio: Navegando por la SandBox Del básico al intermedio: Navegando por la SandBox
En este artículo veremos dos formas de observar e incluso tener cierta interacción con el contenido de una SandBox, tomando MetaTrader 5 como base. Entender el contenido que se muestra en este artículo será fundamental para entender lo que se verá en los próximos artículos.
Red neuronal cuántica en MQL5 (Parte I): Creamos un archivo de inclusión Red neuronal cuántica en MQL5 (Parte I): Creamos un archivo de inclusión
El artículo presenta un nuevo enfoque para la creación de sistemas de negociación basados en principios cuánticos e inteligencia artificial. El autor describe el desarrollo de una red neuronal única que va más allá del aprendizaje automático clásico al integrar la mecánica cuántica con las arquitecturas de inteligencia artificial modernas.