Русский Português
preview
Redes neuronales en el trading: Extracción eficiente de características para una clasificación precisa (Final)

Redes neuronales en el trading: Extracción eficiente de características para una clasificación precisa (Final)

MetaTrader 5Sistemas comerciales |
42 1
Dmitriy Gizlyk
Dmitriy Gizlyk

Introducción

Hemos llegado a la fase final de nuestra introducción al framework Mantis. En primer lugar, hemos analizado su base teórica con detalle: cómo el modelo percibe series temporales multicanal, por qué se necesitan señales diferenciales y cómo se forman los tokens locales. Luego hemos profundizado en la arquitectura y observado cómo se estructuran los parches y cómo el modelo aprende a extraer patrones estables del ruido del mercado. Ahora es el momento de unirlo todo.

El framework Mantis está construido como una secuencia de módulos independientes pero relacionados de manera lógica. Cada uno de ellos realiza una tarea especializada: desde el procesamiento inicial de los datos de origen hasta la formación de tokens y la agregación de información. La idea subyacente es: sesgo mínimo, máximos patrones extraídos de los propios datos.

Todo comienza con la llegada de los datos brutos del mercado. En lugar de procesarlo como un único array, el modelo separa cada tipo de datos en un canal aparte. Para cada uno de ellos se crea además un primer canal diferencial, lo cual permite registrar con claridad la dinámica a corto plazo. Estos canales diferenciales aumentan la sensibilidad del modelo antes de los cambios en la dirección del movimiento de precios. Por ejemplo, una aceleración repentina en el canal diferencial puede indicar el comienzo de un impulso mucho antes de que sea reconocido por los indicadores.

Cada uno de estos canales se alimenta a bloques convolucionales. Aquí es donde comienza el análisis inicial. Las convoluciones extraen características locales al aprender a distinguir micropatrones estables. En la salida de cada canal se obtienen 256 características que representan su comportamiento local.

Después de la convolución, comienza el siguiente paso: los datos se dividen en 32 parches disjuntos. Cada uno de ellos abarca una sección determinada de la serie temporal. Para cada canal se realiza un promedio (mean-pooling). De esta forma obtenemos 32 tokens, cada uno con una longitud de 256 características: este es un punto importante. Los parches son fragmentos localizados del comportamiento del mercado, como minigráficos comprimidos. Estos registran fases desde impulsos y consolidaciones hasta retrocesos y divergencias. Dicha partición hace que el comportamiento del modelo resulte más robusto y adaptativo, comenzando a ver el mercado no como un flujo de números, sino como una serie de patrones reconocibles.

En esta etapa, para cada canal de datos de origen, se toma un token del flujo original y diferencial. Estos tokens se escalan a un valor común, se normalizan y se combinan, después de lo cual, a través de la capa de proyección lineal, se forma un token total, que refleja el comportamiento de este canal en un área determinada. Este token contiene el estado, la velocidad de cambio y la fuerza, es decir, todo lo que permite a un tráder juzgar intuitivamente el estado de ánimo del mercado.

A continuación, la secuencia completa de tokens se introduce en los bloques de atención. Este es el nivel más importante donde los parches comienzan a comunicarse entre sí. El modelo estima qué partes del pasado son importantes para el estado actual: tal vez la señal que se produjo hace 20 barras ahora se esté volviendo crítica para evaluar una reversión. Esta capacidad de ver relaciones entre regiones distantes hace que el modelo resulte particularmente útil para analizar estructuras de mercado complejas.

La idea detrás de todo esto es simple pero poderosa: mantener la precisión local sin perder el contexto global. En el trading esto resulta fundamental. Por ejemplo, una secuencia de velas pequeñas durante una pausa podría no transmitir información, pero si aparecen después de un aumento brusco del volumen, e incluso a un nivel importante, el contexto cambia por completo. Mantis considera esto automáticamente.

El framework Mantis es más que una simple arquitectura, supone una herramienta flexible que puede adaptarse a diferentes tipos de estrategias comerciales.

A continuación le mostramos la visualización del autor del framework Mantis.


Arquitectura del modelo

En nuestro trabajo anterior, ya hemos construido los componentes centrales del framework Mantis y ahora hemos llegado a una etapa clave: la creación de la arquitectura de un modelo entrenable capaz de tomar decisiones comerciales en tiempo real. Al igual que un tráder experimentado que monitorea la acción del precio, el volumen, la estructura de las velas y el sentimiento general del mercado, y solo entonces, con el contexto en mente, toma una decisión de entrada, nuestro modelo debe aprender a percibir la dinámica del mercado no como un flujo de números, sino como un conjunto de procesos interconectados. Intentamos replicar un modelo de comportamiento que pueda reconocer patrones de mercado, detectar cambios de fase y adaptarse a nuevos escenarios. Es en este contexto que se construye la lógica del framework Mantis al completo.

Cabe destacar que el framework Mantis fue desarrollado originalmente como un clasificador de series temporales. Su arquitectura se centra en la segmentación y la revelación de patrones ocultos en secuencias complejas. Este enfoque resulta especialmente eficaz en el contexto financiero, donde los movimientos de precios a menudo quedan ocultos bajo capas de ruido del mercado. Mediante el uso de procesamiento de múltiples niveles, que incluye convoluciones, generación de parches locales y agregación de canales, el modelo aprende a ver la estructura en los datos del mercado en lugar de fluctuaciones aleatorias. Mantis permite no solo registrar señales, sino también determinar patrones de mercado.

Sin embargo, en el marco de nuestra tarea actual, Mantis ya no actúa como un clasificador independiente, sino como una especie de cimientos: la base de la capa sensorial del agente comercial. Nosotros usaremos la arquitectura Mantis para construir una incorporación (una representación condensada pero rica de la situación del mercado) que luego se integrará en la parte de control del modelo. De esta forma, el clasificador se convierte en los ojos del agente, dándole la capacidad de ver el tejido conductual del mercado.

Precisamente esta integración se transmite a la siguiente parte de la arquitectura, construida según el principio Actor-Director-Crítico. Este enfoque permite la división de responsabilidades entre los módulos y garantiza la estabilidad del comportamiento del agente. El Actor recibe la incorporación y forma una decisión comercial. El Director es un filtro estructural y corrector de comportamiento que clasifica las acciones propuestas por el Actor en aceptables y erróneas, proporcionando potentes señales de retroalimentación. Su propósito es eliminar decisiones obviamente imprudentes o inestables, sobre todo en áreas de turbulencia del mercado. El Crítico completa el ciclo evaluando la viabilidad estratégica de las acciones en función del estado actual y la historia de interacciones. Desempeña el papel de un asesor interno: incluso si una acción es técnicamente posible, ¿realmente vale la pena correr el riesgo en el contexto actual?

La arquitectura de todos los componentes del modelo se define a través del método CreateDescriptions, en el que se configuran secuencialmente las capas de cada módulo. El sistema de parámetros flexible permite escalar la arquitectura, adaptarla a varios instrumentos de mercado y agregar o deshabilitar elementos experimentales sin tener que reescribir la lógica central.

bool CreateDescriptions(CArrayObj *&encoder,
                        CArrayObj *&actor,
                        CArrayObj *&director,
                        CArrayObj *&critic
                       )
  {
//---
   CLayerDescription *descr;
//---
   if(!encoder)
     {
      encoder = new CArrayObj();
      if(!encoder)
         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 los cuatro arrays dinámicos, cada uno de los cuales está destinado a almacenar descripciones arquitectónicas de los componentes del modelo correspondientes: Codificador, Actor, Director y Crítico. Estos arrays actúan como contenedores en los que se agregan uno por uno los objetos que describen la estructura de los bloques de la red neuronal.

Dentro del cuerpo del método, primero se comprueba la validez de los punteros recibidos. Si alguno de ellos no está inicializado o indica una ubicación de memoria no válida, se crea automáticamente una nueva instancia del objeto correspondiente. Este enfoque elimina la necesidad de preparar manualmente arrays con antelación, lo cual aumenta la modularidad del código y su idoneidad para soluciones escalables.

Ahora pasaremos directamente a la descripción de la arquitectura del modelo. Y el primero de esta cadena será lógicamente el Codificador, el módulo clave a través del cual el modelo comienza a percibir el mercado. Aquí es donde se inicia el proceso de transformación de las series temporales sin procesar en una representación significativa de las condiciones del mercado.

En la primera etapa usamos una capa completamente conectada, que en este caso no realiza una función computacional, sino exclusivamente una función de interfaz. Su propósito es ofrecer una cómoda interfaz de búfer para cargar los datos de origen en el modelo.

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

Los datos de origen dispersos y de múltiples escalas se envían a la unidad de normalización por lotes. Aquí, todos los valores se llevan a una escala comparable, eliminando el sesgo en las amplitudes, lo cual resulta especialmente crítico cuando se trabaja con series temporales financieras multidimensionales. La normalización permite que el modelo perciba los datos no como un conjunto aleatorio de números, sino como un flujo de información lógicamente consistente.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormWithNoise;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

Para aumentar suavemente los datos durante el entrenamiento, utilizamos una capa de normalización por lotes con la adición de ruido controlado, lo que permite que el modelo resulte más resistente al ruido del mercado y evite el sobreajuste en fluctuaciones insignificantes. Este enfoque ayuda al sistema a percibir la información del mercado de forma más flexible, manteniendo un equilibrio entre adaptación y estabilidad.

El siguiente paso importante será la creación de canales diferenciales primarios. Esta capa genera características diferenciales (cambios en los valores entre puntos de tiempo adyacentes) que ayudan al modelo a captar la dinámica y la dirección del movimiento del mercado. En el trading, son estos cambios precisamente los que a menudo proporcionan la clave para entender las reversiones o continuaciones de tendencias, ya que la subida o bajada de un precio en un momento determinado puede ser a veces más importante que los valores absolutos. La creación de canales diferenciales nos permite ampliar significativamente el contenido de información de los datos de origen, aumentando la sensibilidad del modelo ante las tendencias locales y fluctuaciones rápidas.

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

Una vez formados los canales diferenciales primarios, se agregan armónicos de codificación temporal al conjunto de datos extendido. Este paso resulta crucial para que el modelo comprenda el contexto de las dependencias del tiempo y la naturaleza cíclica de los procesos del mercado. Los armónicos son una especie de balizas temporales que permiten al modelo reconocer no solo un momento en el tiempo, sino también patrones repetitivos, fluctuaciones estacionales y diferentes escalas temporales. La codificación temporal a través de armónicos proporciona al modelo una comprensión profunda de la estructura temporal.

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

Para considerar de manera efectiva las relaciones entre canales y las interdependencias en los datos, empleamos un bloque de convolución con diferentes ventanas. Este enfoque de múltiples ventanas permite que el modelo capte simultáneamente patrones locales de corto plazo y tendencias de largo plazo que se manifiestan en la dinámica de varios canales a la vez.

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

La etapa final de la preparación de datos es la capa de parcheo. Esta capa divide el tensor multicanal procesado en parches separados e inconexos: una especie de fragmentos de series temporales, cada uno de los cuales contiene información agrupada sobre las condiciones del mercado local.

Esta partición permite que el modelo se centre en segmentos individuales de los datos, lo cual reduce la complejidad computacional y facilita la identificación de patrones locales. Dentro de cada parche, se realiza una agregación adicional, ofreciendo una representación compacta pero informativa.

Como resultado, cada parche se convierte en un token aparte que contiene una descripción comprimida y estructurada de la situación del mercado durante un intervalo de tiempo limitado. Esto mejora sustancialmente la capacidad del modelo para captar patrones locales y forma una base sólida para las etapas posteriores de procesamiento y toma de decisiones.

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMantisPatching;
   descr.count = prev_count;
   descr.layers = prev_out;
   descr.window = EmbeddingSize;
   descr.window_out = NSkills;
   descr.step = Segments;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
   prev_count = descr.step;
   prev_out = descr.layers;

La unidad de atención en nuestro Codificador se implementa como un solo objeto: CNeuronMantisAttentionUnit. Este componente juega un papel clave a la hora de captar relaciones importantes entre parches y canales. En los parámetros del objeto, indicamos el número de módulos de atención cruzada internos, lo cual nos permite ajustar de forma flexible la profundidad y la amplitud del análisis de la información.

Cada módulo de atención cruzada se centra en identificar los elementos más significativos en el flujo de datos asociándolos con un token de clase entrenable. Esta arquitectura ofrece un filtrado de ruido eficaz y ayuda al modelo a centrarse en las señales que realmente influyen en las decisiones comerciales.

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMantisAttentionUnit;
   descr.step = 4;
   descr.count = prev_count;
     {
      int temp[] = {EmbeddingSize, EmbeddingSize};
      if(ArrayCopy(descr.windows, temp) < (int)temp.Size())
         return false;
     }
   descr.layers = 3;
   descr.window_out = NSkills;
   descr.window = prev_out;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

En la salida del módulo de atención cruzada se forma un token, una representación comprimida e informativa del estado analizado del entorno. En el contexto de nuestra tarea, la clasificación precisa de los estados no es una prioridad. Lo más importante para nosotros es obtener representaciones latentes suficientemente claras y distinguibles que permitan al Actor tomar decisiones comerciales informadas y justificadas.

Es por esto que evitamos deliberadamente complicar demasiado la arquitectura del Codificador. Este enfoque mantiene un equilibrio entre la profundidad de percepción del modelo y su eficiencia computacional, lo cual permite una adaptación flexible a las condiciones reales del mercado sin comprometer la calidad de las decisiones tomadas.

A continuación pasamos a la descripción de la arquitectura del Actor. Al igual que sucede con el Codificador, la entrada es una combinación de una capa completamente conectada y un bloque de normalización por lotes. Este tándem sirve como interfaz para recibir y estandarizar inicialmente los datos de origen. Sin embargo, en este caso, a lo largo de la línea troncal se suministra otra información, a saber, los datos sobre el estado actual de la cuenta comercial: el importe del saldo, las posiciones abiertas, el nivel de reducción y otros indicadores que reflejan el contexto interno del trabajo del Agente.

//---
   CLayerDescription *latent = encoder.At(7);
//--- Actor
   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 = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Paralelamente, la representación latente del entorno de mercado generada por el Codificador se introduce en el modelo a través de la línea auxiliar. La combinación de estos dos flujos de datos (el estado interno y el contexto externo) permite al Actor tomar decisiones que ya incorporan un elemento de pensamiento estratégico y gestión de riesgos. El modelo está entrenado para considerar no solo la situación actual del mercado, sino también sus propios recursos, el nivel de riesgo aceptable y la dinámica de la posición actual. Este enfoque hace que el comportamiento del Agente sea consciente y estable, incluso en condiciones de mayor turbulencia del mercado.

La consolidación de los datos de los dos flujos de información (contexto de mercado y estado de la cuenta comercial) se realiza en la capa de concatenación. Este objeto cumple una función simple pero importante: combina el vector de incorporación obtenido del Codificador con las características internas de la cuenta comercial en una única representación. De esta forma el modelo obtiene una visión completa de lo que está sucediendo.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = LatentCount;
   descr.window = AccountDescr;        // Inputs window
   descr.step = latent.windows[0];     // Cross window
   descr.batch = 1e4;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Tras combinar los datos, comienza el procesamiento profundo de características, implementado a través de una secuencia de tres capas completamente conectadas. La primera capa se encarga de extraer y sumar las características clave. La segunda refuerza dependencias significativas entre parámetros, revelando patrones potenciales. En tercer lugar, se forma una acción comercial. Esta transformación constante permite que el modelo responda no solo a las señales, sino que también tome decisiones informadas y estratégicamente acertadas.

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.batch = 1e4;
   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 = SoftPlus;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = NActions;
   descr.activation = SIGMOID;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

La arquitectura de los modelos del Director y el Crítico repite casi completamente la estructura del Actor. La principal diferencia radica en la dimensionalidad de los datos de origen. El tensor de acciones del Agente se introduce en la entrada de estos modelos a lo largo de la línea troncal, y su evaluación se forma en la salida: una evaluación de la clasificación para el Director y una evaluación estratégica para el Crítico. Como la lógica interna de estos modelos es similar, no sobrecargaremos el artículo repitiendo la descripción de estas. Para un estudio más detallado, el lector puede consultar el código adjunto a este artículo, que ofrece una descripción completa de la arquitectura de todos los componentes entrenables del sistema.


Aprendizaje por contraste

Tras construir la arquitectura del modelo, pasaremos a la siguiente etapa importante: el entrenamiento. Como hemos analizado con detalle en la sección teórica, una de las características clave del framework Mantis es el aprendizaje contrastivo autosupervisado. Este mecanismo permite al modelo generar tokens informativos y claramente distinguibles que describan el estado del entorno en una forma compacta pero expresiva. Así como un tráder experimentado puede distinguir a simple vista una fase de consolidación de un movimiento impulsivo, nuestro modelo debe aprender a identificar las condiciones características del mercado y presentarlas en forma condensada para su posterior uso por otros componentes del sistema.

En este caso, el entrenamiento contrastivo actúa como base para desarrollar la intuición de mercado del modelo. Sin un supervisor estricto, pero con una comprensión clara de las diferencias entre pares de estados, el Codificador aprende a construir representaciones que lo ayudarán a distinguir más eficazmente entre escenarios de mercado similares, pero esencialmente diferentes.

La implementación del algoritmo correspondiente se presenta en el asesor "…\MQL5\Experts\Mantis\StudyContrast.mq5". Este asesor gestiona el proceso de formación de pares de ejemplos positivos y negativos, realiza una iteración de entrenamiento, acumula estadísticas y guarda los parámetros entrenados.

Vale la pena señalar una diferencia importante y, sin exagerar, conceptual entre nuestra implementación del aprendizaje contrastivo y los enfoques clásicos. En esta etapa del entrenamiento vamos a abandonar deliberadamente la laboriosa etapa de formación de una muestra de entrenamiento. En lugar de ello, aprovecharemos todo el poder y la flexibilidad de la plataforma Metatráder 5 para implementar la generación dinámica de datos de entrenamiento durante el proceso de aprendizaje.

Lo único que debe hacer el usuario es especificar las fechas de inicio y finalización del periodo de entrenamiento en los parámetros del asesor. Todos los datos de mercado necesarios se solicitan y recuperan automáticamente del terminal en tiempo real. Este enfoque no solo simplifica el procedimiento, sino que también ofrece posibilidades mucho más amplias para entrenar el modelo.

//+------------------------------------------------------------------+
//| Input parameters                                                 |
//+------------------------------------------------------------------+
input datetime             Start          = D'2020.01.01';
input datetime             End            = D'2025.01.01';
input int                  Iterations     = 100000;
input int                  Batch          = 50;
input group                "---- Indicators ----"
input ENUM_TIMEFRAMES      TimeFrame   =  PERIOD_M1;
//---
input group                "---- RSI ----"
input int                  RSIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   RSIPrice    =  PRICE_CLOSE;   //Applied price
//---
input group                "---- CCI ----"
input int                  CCIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   CCIPrice    =  PRICE_TYPICAL; //Applied price
//---
input group                "---- ATR ----"
input int                  ATRPeriod   =  14;            //Period
//---
input group                "---- MACD ----"
input int                  FastPeriod  =  12;            //Fast
input int                  SlowPeriod  =  26;            //Slow
input int                  SignalPeriod =  9;            //Signal
input ENUM_APPLIED_PRICE   MACDPrice   =  PRICE_CLOSE;   //Applied price

A continuación, le sugiero analizar con más detalle cómo se implementa este proceso en el método Train. Este método ejecuta el ciclo completo de entrenamiento contrastivo y contiene la lógica para la preparación de datos, la gestión del búfer y las pasadas directa e inversa a través de la red.

El proceso comienza con la definición de los límites de los datos históricos. La función iBarShift se usa para determinar el desplazamiento desde la barra actual hasta el inicio y el final del entrenamiento.

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);

Luego, los búferes de todos los indicadores se reservan para la longitud de la historia cargada.

if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) ||
   !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }

El siguiente paso consiste en cargar los datos. Para ello se implementa un ciclo en el que se llaman sucesivamente los métodos Refresh de todos los indicadores y se comprueba el número de valores calculados. El ciclo finalizará después de que los datos se hayan cargado correctamente o después de que hayan transcurrido 100 intentos.

int count = -1;
bool load = false;
do
  {
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
   count++;
   load = (RSI.BarsCalculated() >= bars &&
           CCI.BarsCalculated() >= bars &&
           ATR.BarsCalculated() >= bars &&
           MACD.BarsCalculated() >= bars
          );
   Sleep(100);
   count++;
  }
while(!load && count < 100);
if(!load)
  {
   PrintFormat("%s -> %d The training data has not been loaded",
                                        __FUNCTION__, __LINE__);
   ExpertRemove();
   return;
  }

Al final del proceso de preparación de datos, establecemos la dirección de indexación deseada del array de cotizaciones (ArraySetAsSeries) y declaramos las variables locales necesarias.

   if(!ArraySetAsSeries(Rates, true))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
   bars -= end + HistoryBars;
   if(bars < 0)
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      ExpertRemove();
      return;
     }
//---
   vector<float> result, target, neg_target;
   bool Stop = false;

Ahora comienza el ciclo principal de aprendizaje. En cada iteración, se selecciona de forma aleatoria una posición del rango total: el índice de la barra a partir de la cual se formará el estado de referencia.

   uint ticks = GetTickCount();
//---
   for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch)
     {
      int posit = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
      if(!CreateBuffers(posit + end, GetPointer(bState), GetPointer(bTime)))
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         ExpertRemove();
         return;
        }

Para este estado, se crean búferes de datos de origen y se realiza una pasada directa del codificador.

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

Guardamos el resultado de la pasada directa como un estándar: una especie de muestra del estado del mercado al que el modelo intentará llegar. A continuación, y esto es importante, la pasada directa del Codificador se repite con los mismos datos de origen. Pero hay aquí una sutileza a considerar. La capa de normalización de lotes con adición de ruido incorporada en nuestra arquitectura actúa como un aumento de datos suave. Y esto significa que cuando se ejecuta nuevamente el modelo, ya no simplemente repite el resultado anterior, sino que recibe una salida ruidosa ligeramente modificada.

Precisamente esta propiedad es la que usamos para formar un par positivo. La pasada inversa se realiza con la referencia previamente guardada como objetivo. De esta manera, entrenamos el modelo para ignorar el ruido aleatorio y centrarnos en características significativas y estables del contexto. En pocas palabras, el modelo aprende a ver a través del ruido y a mantener una representación estable de las características clave del mercado.

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

Ahora viene la parte quizá más interesante: la formación de pares negativos. El número de pares negativos se indica en los parámetros del asesor experto mediante la variable Batch. Para generar la cantidad necesaria de pares negativos, creamos un ciclo anidado moviendo primero el token de referencia a un vector.

//--- Negotive
if(!Result.GetData(target))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
for(int b = 0; b < Batch; b++)
  {
   int negot = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
   int count = 0;
   while(negot == posit)
     {
      negot = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * bars);
      count++;
      if(count > 100)
        {
         PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
         Stop = true;
         break;
        }
     }
   if(Stop)
            break;

En el cuerpo del ciclo anidado, seleccionamos aleatoriamente otro estado del rango de entrenamiento diferente del estándar utilizado anteriormente. Aquí vale la pena destacar un punto importante: permitimos cualquier estado, siempre que no coincida con el estándar. En teoría, incluso son posibles las intersecciones de rangos.

Precisamente esto hace que el proceso de aprendizaje sea más vivo y realista. El modelo aprende a distinguir diferencias en estados muy similares. Si los rangos se superponen, la tarea del modelo consiste en resaltar precisamente aquellos matices que distinguen un estado de otro. En condiciones reales de mercado, son precisamente estas diferencias sutiles las que a menudo determinan el éxito de una decisión comercial.

De esta forma, nuestro aprendizaje contrastivo obliga al Codificador no solo a reconocer estados, sino a extraer características clave, creando tokens informativos y distinguibles necesarios para el trabajo posterior del Actor.

Para el estado seleccionado, generamos los búferes de datos de origen y realizamos una pasada directa del Codificador.

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

Luego viene el momento de formar los objetivos del ejemplo negativo de entrenamiento. Para ello, calculamos la diferencia entre el token obtenido en el paso actual y la representación de referencia que guardamos anteriormente. Este es el punto clave: al alejar el token actual de la referencia, creamos un valor objetivo para el ejemplo negativo.

Para potenciar este efecto, duplicamos la distancia entre la referencia y el estado actual, aumentando así la fuerza repulsiva. Como resultado, los estados ubicados cerca de la referencia se repelen solo ligeramente, mientras que aquellos que difieren sustancialmente se repelen con mucha mayor intensidad. Esto ayuda a expandir el espacio en blanco alrededor de la representación de referencia, lo cual hace que los tokens sean más distinguibles y resistentes al ruido.

Este mecanismo introduce una dinámica en el aprendizaje que garantiza una clara separación de los espacios latentes. El modelo no solo aprende a reconocer estados, sino que aprende a crear una zona de seguridad alrededor del estándar, de la cual otros estados diferentes se encuentran a una distancia notable. Esto mejora significativamente la calidad y la fiabilidad en la toma de decisiones posterior.

cEncoder.getResults(result);
neg_target = result * 2 - target;
neg_target = neg_target - neg_target.Max();
if(!neg_target.Activation(neg_target, AF_SOFTMAX) ||
   !Result.AssignArray(neg_target))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

El último paso, pero no menos importante, que debemos realizar es efectuar una pasada inversa del Codificador utilizando el objetivo negativo generado. Es esta etapa la que nos permite optimizar los parámetros del modelo, ajustándolo de manera que se alejen efectivamente los diferentes estados unos de otros en el espacio latente.

if(!cEncoder.backProp(Result, (CBufferFloat*)NULL))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Ahora que todas las operaciones de entrenamiento en este paso se han completado con éxito, el sistema no deja al usuario en la ignorancia: el estado del proceso se actualiza. Así ofrecemos información sobre el progreso actual mostrando el porcentaje de finalización y las tasas de error del modelo. Este enfoque transparente no solo aumenta la confianza en el rendimiento del algoritmo, sino que también ayuda a monitorear rápidamente la efectividad de su entrenamiento.

Después de esto, pasaremos a la siguiente iteración del ciclo de entrenamiento, donde se repite nuevamente todo el proceso descrito con nuevos datos. Este ciclo garantiza una mejora profunda y constante del modelo, lo cual permite generar gradualmente conocimientos más precisos e informativos, necesarios para tomar decisiones comerciales informadas.

    if(GetTickCount() - ticks > 500)
      {
       double percent = double(iter + b) * 100.0 / (Iterations);
       string str = StringFormat("%-12s %6.2f%% -> Error %15.8f\n", "Encoder",
                                   percent, cEncoder.getRecentAverageError());
       Comment(str);
       ticks = GetTickCount();
      }
   }
}

Una vez completadas con éxito todas las iteraciones de los ciclos de entrenamiento, enviamos los resultados del entrenamiento al registro. Luego inicializamos el proceso de finalización del programa.

   Comment("");
//---
   PrintFormat("%s -> %d -> %-15s %10.7f", __FUNCTION__, __LINE__, "Encoder",
                                           cEncoder.getRecentAverageError());
   ExpertRemove();
//---
  }

Se trata de una finalización de sesión elegante y controlada que garantiza que se guarden todos los datos importantes y se liberen los recursos correctamente.

El código completo para el programa de entrenamiento contrastivo del codificador se ofrece en el archivo adjunto. En este archivo también encontrarás implementaciones de programas de formación online y offline para los modelos del Actor, el Director y el Crítico. Estos programas han sido transferidos de desarrollos anteriores con modificaciones mínimas; en particular, hemos excluido de ellos el proceso de entrenamiento del Codificador, que ahora se implementa por separado. Este enfoque permite estructurar el entrenamiento, aumentando la flexibilidad y la capacidad de mantenimiento de todo el framework.  


Simulación

Organizaremos el proceso de entrenamiento en tres etapas consecutivas, lo cual garantiza la sistematicidad y fiabilidad de todo el modelo.

La primera etapa es el entrenamiento contrastivo del Codificador. Se lleva a cabo sobre datos históricos de los últimos cinco años para el par de divisas EURUSD en el marco temporal M1. Este volumen y nivel de detalle de los datos permite al Codificador generar representaciones latentes de las condiciones del mercado, informativas y de alta calidad, que formen la base para el funcionamiento futuro de todo el sistema.

A continuación viene la segunda etapa, a saber, el entrenamiento offline de los componentes clave del sistema: El Actor, el Director y el Crítico. Para este propósito, se usa una muestra de entrenamiento recopilada con datos del año 2024, conservando todos los parámetros especificados previamente. El proceso utiliza el concepto de trayectoria casi perfecta, que permite a los modelos aprender de los ejemplos más realistas de acciones y evaluaciones. Esta etapa resulta importante para consolidar estrategias básicas y criterios de toma de decisiones.

La tercera etapa es el ajuste fino en línea de los modelos, que se realiza directamente en el simulador de estrategias en el mismo intervalo histórico. Esto permite adaptar los modelos a las condiciones cambiantes del mercado y mejorar los parámetros con la máxima precisión.

Después de completar todas las etapas de entrenamiento, el modelo se prueba con datos de enero a marzo de 2025. En este caso, todos los parámetros usados durante las etapas de entrenamiento permanecen sin cambios. Este enfoque ofrece una evaluación justa y objetiva del rendimiento del modelo en un conjunto de datos nuevo y no utilizado previamente. Ahora le presentamos los resultados de las pruebas.

Durante el periodo de prueba, el modelo ha completado 881 transacciones, 447 de las cuales han sido rentables, lo que corresponde a una tasa de éxito del 50,74%. Esto indica un equilibrio neutral entre transacciones rentables y perdedoras. El factor de beneficio es 1,25.

En general, la estrategia muestra resultados positivos con un nivel moderado de riesgo y un crecimiento constante del capital en la primera mitad del período evaluado. No obstante, después de mediados de febrero, se ha producido un descenso de la rentabilidad y un claro movimiento lateral con disminución de la equidad.

Por tanto, la estrategia tiene razón de ser y muestra una dinámica positiva en el segmento de pruebas, sin embargo, necesita mejorar una serie de parámetros.


Conclusión

Como resultado de nuestro trabajo, hemos integrado completamente el framework Mantis en un modelo comercial algorítmico personalizado capaz de extraer características informativas de series temporales de alta frecuencia y transformarlas en decisiones significativas en tiempo real.

Hemos prestado especial atención a la etapa de entrenamiento contrastivo autosupervisado del Codificador, organizada en el asesor StudyContrast.mq5. Asimismo, hemos abandonado el muestreo estático, implementando la carga dinámica de datos desde el terminal y enriqueciéndolo con ejemplos vivos para el instrumento EURUSD M1 durante los últimos cinco años. Los pares positivos y negativos formados a través del aumento suave y el ruido controlado han permitido que el modelo practique la capacidad de ver a través del ruido del mercado.

Las pruebas finales del modelo para el periodo enero-marzo de 2025 han demostrado la rentabilidad de las soluciones implementadas. La estrategia tiene su razón de ser y muestra una dinámica positiva en el periodo de prueba, pero es necesario mejorar una serie de parámetros.


Enlaces


Programas usados en el artículo

# Nombre Tipo Descripción
1 Research.mq5 Asesor Asesor de recopilación de datos
2 ResearchRealORL.mq5
Asesor
Asesor experto para recopilar ejemplos con el método Real-ORL
3 StudyContrast.mq5 Asesor Asesor experto de aprendizaje contrastivo del Codificador
4 Study.mq5 Asesor Asesor de entrenamiento de modelos offline
5 StudyOnline.mq5
Asesor
Asesor de entrenamiento de modelos online
6 Test.mq5 Asesor Asesor para la prueba de modelos
7 Trajectory.mqh Biblioteca de clases Estructura de descripción del estado del sistema y la arquitectura del modelo
8 NeuroNet.mqh Biblioteca de clases Biblioteca de clases para crear una red neuronal
9 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/18329

Archivos adjuntos |
MQL5.zip (2794.71 KB)
[Eliminado] | 30 may 2025 en 17:49
Extraño, ¿por qué las operaciones sólo en una dirección. Y ¿cómo se forman las señales para el cierre de operaciones?
Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Integración de un modelo de IA en una estrategia de trading MQL5 ya existente Integración de un modelo de IA en una estrategia de trading MQL5 ya existente
Este tema se centra en la incorporación de un modelo de IA entrenado (como un modelo basado en redes LSTM o un modelo predictivo basado en aprendizaje automático) en una estrategia de trading MQL5 existente.
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.
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 20): Flujo externo (IV) — Correlation Pathfinder Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 20): Flujo externo (IV) — Correlation Pathfinder
Correlation Pathfinder ofrece un nuevo enfoque para comprender la dinámica de los pares de divisas, como parte de la serie de desarrollo de herramientas de análisis de la acción del precio. Esta herramienta automatiza la recopilación y el análisis de datos, lo que permite comprender cómo interactúan pares como el EUR/USD y el GBP/USD. Mejora tu estrategia de trading con información práctica y en tiempo real que te ayudará a gestionar el riesgo y a detectar oportunidades de forma más eficaz.