Русский Português
preview
Redes neuronales en el trading: Actor—Director—Crítico (Final)

Redes neuronales en el trading: Actor—Director—Crítico (Final)

MetaTrader 5Sistemas comerciales |
78 2
Dmitriy Gizlyk
Dmitriy Gizlyk

Introducción

En el artículo anterior, aprendimos los aspectos teóricos del framework Actor—Director—Crítico (Actor—Director—Critic), que supone una versión ampliada de la arquitectura Actor—Critic. La arquitectura clásica Actor—Critic ha sido la piedra angular de muchos algoritmos RL exitosos. En él, el agente se divide en dos partes: El Actor propone acciones y el Crítico las evalúa según la recompensa recibida del entorno. Este vinculación permite lograr gradualmente una estrategia en la que las acciones tienen cada vez más sentido y las evaluaciones son cada vez más precisas. Sin embargo, a pesar de su elegancia y eficacia, en las tareas del mundo real, especialmente en el trading, esta arquitectura se enfrenta a algunas limitaciones importantes.

El principal problema se manifiesta en las primeras fases del aprendizaje. Durante este periodo, el Actor aún no sabe qué acciones son útiles, mientras que el Crítico no puede evaluarlas adecuadamente, ya que él mismo está empezando a aprender. Esto crea un efecto de vagabundeo a ciegas: el agente realiza muchas acciones aleatorias, ineficaces y, a veces, incluso perjudiciales, recibiendo una retroalimentación poco informativa o tardía. En un entorno de mercado en el que los errores pueden salir caros, este planteamiento resulta insostenible.

Para resolver este problema fundamental, los autores del framework Actor—Director—Critic propusieron añadir un nuevo componente, el Director, que aporta al sistema otro canal de evaluación de las acciones. A diferencia del Crítico, que emite un juicio continuo de una acción basándose en las recompensas del entorno, el Director categoriza las acciones de forma binaria: "conviene" o "no conviene" a una estrategia determinada. Esto permite al agente navegar por el espacio de soluciones posibles con mayor rapidez y claridad.

Es importante subrayar que el Director no es un filtro y no restringe la libertad de acción del agente. Más bien, complementa la evaluación del Crítico ofreciendo una opinión más categórica, mientras que el Crítico puede mostrarse inseguro a la hora de evaluar una acción (sobre todo en las primeras fases), el Director informa inmediatamente de si la acción se ajusta al patrón de comportamiento que ha aprendido. Esto permite al Actor evitar repetir acciones erróneas de antemano, ahorrando recursos de aprendizaje y acelerando el proceso de desarrollo de una estrategia sostenible.

Como resultado se obtiene una interacción sinérgica de los tres componentes: El Actor aprende a elegir acciones, el Crítico aprende a evaluarlas en cuanto a las recompensas esperadas, y el Director aprende a indicar claramente qué acciones deben evitarse por completo. Esto crea un doble sistema de retroalimentación: continuo y binario, que permite descartar más rápidamente las direcciones poco prometedoras y centrarse en las estrategias productivas.

A continuación le mostramos la visualización del framework Actor—Director—Critic realizada por el autor.

Visualización del autor del framework Actor—Director—Critic

En la parte práctica del artículo anterior, presentamos una descripción detallada de la arquitectura de los modelos entrenados. Y aquí debemos decir que hemos hecho cambios bastante serios en comparación con la implementación del framework descrito por parte del autor.

En primer lugar, hemos superpuesto nuestra visión de los enfoques propuestos a un framework multiagente HiSSD, del que ya hemos hablado. Además, en nuestra implementación, el Director y el Crítico se entrenan con características latentes de las habilidades genéricas del Agente: las representaciones comprimidas derivadas de las capas internas del Codificador del estado del entorno, en lugar de con sus características básicas. Gracias a ello, se forma una visión generalizada de las pautas de actuación, lo que permite evaluarlas en el contexto de una lógica de comportamiento global y no de una situación de mercado específica. Este enfoque posibilita una retroalimentación más estable y estratégicamente significativa, fundamental en un entorno de información limitada y gran incertidumbre, como suele ocurrir en los mercados financieros.

En este artículo, la atención se centra en el entrenamiento de los modelos. También hemos introducido algunas modificaciones en este proceso en comparación con el planteamiento original.

Una de las principales diferencias ha sido la división del entrenamiento en dos fases. La primera consiste en el entrenamiento offline de todos los componentes del sistema a partir de una muestra de entrenamiento previamente seleccionada. Esta etapa permite al agente acumular experiencia inicial y formar una estrategia básica de comportamiento sin el riesgo de influir en las operaciones reales. En este caso, cada modelo se entrena usando su propia función objetivo adaptada a su papel en la arquitectura.

El segundo paso consiste en afinar los modelos online. Aquí interviene la interacción directa del agente con el entorno. El Actor perfecciona su política de comportamiento en tiempo real, mientras que el Crítico y el Director siguen aprendiendo, mejorando la calidad de las estimaciones y clasificaciones según los datos actualizados. Esta etapa permite al agente adaptarse a la situación actual del mercado, manteniendo al mismo tiempo el enfoque estratégico incorporado en el entrenamiento offline.

Este planteamiento en dos fases logra un equilibrio entre estabilidad y adaptabilidad. El agente elabora una política de partida fiable y luego la ajusta para adaptarla a las condiciones reales. Como resultado, esperamos contar con un sistema sostenible y de aprendizaje efectivo que pueda adaptarse a los cambios del mercado sin perder integridad estratégica.


Aprendizaje offline

El algoritmo de la primera etapa de entrenamiento (offline) se implementa como el asesor experto "...\Experts\ADC\Study.mq5". La mayor parte de su código lo hemos tomado prestado de un proyecto anterior, y no es de extrañar. La solución arquitectónica de los modelos entrenados se basa en el framework HiSSD, con el que ya nos hemos familiarizado.

Esta continuidad nos ha permitido utilizar soluciones ya probadas sin tener que reinventar la rueda. Sin embargo, no ha sido posible prescindir por completo de los cambios.

La incorporación de dos componentes adicionales (Crítico y Director) ha ampliado considerablemente la funcionalidad del sistema. Su integración ha requerido la modificación de la lógica del programa modelo de entrenamiento. En el marco de este trabajo, analizaremos con detalle solo el algoritmo del método Train, en el que se organiza casi todo el proceso de entrenamiento directo del modelo.

Al igual que antes, este método no contiene parámetros y obtiene todos los datos necesarios de las variables globales inicializadas anteriormente. En el cuerpo del método, primero creamos e inicializamos una serie de variables locales que utilizaremos para almacenar temporalmente información intermedia durante el proceso de aprendizaje.

void Train(void)
  {
//---
   vector<float> probability = vector<float>::Full(Buffer.Size(), 1.0f / Buffer.Size());
//---
   vector<float> result, target, state;
   matrix<float> fstate = matrix<float>::Zeros(1, NForecast * BarDescr);
   bool Stop = false;
//---
   uint ticks = GetTickCount();

Una vez completado el trabajo preparatorio, podemos proceder a la construcción del entrenamiento directo del modelo, que organizamos dentro de un sistema de ciclo anidados. El ciclo exterior recorrerá los minipaquetes de datos dentro de un número determinado de iteraciones de entrenamiento.

for(int iter = 0; (iter < Iterations && !IsStopped() && !Stop); iter += Batch)
  {
   int tr = SampleTrajectory(probability);
   int start = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (Buffer[tr].Total - 2 - NForecast - Batch));
   if(start <= 0)
     {
      iter -= Batch;
      continue;
     }

La fase inicial del aprendizaje offline consiste en muestrear una única trayectoria a partir de la experiencia almacenada en el búfer de reproducción de experiencias. A continuación, se selecciona aleatoriamente una condición del entorno específica en esta trayectoria, que se convierte en el punto de partida para generar un nuevo minipaquete de datos de entrenamiento del modelo. De este modo, se garantiza una variedad de ejemplos de entrenamiento.

Antes de empezar a trabajar con este nuevo minipaquete, debemos realizar un procedimiento obligatorio de borrado de los búferes internos de almacenamiento temporal de datos de todos los modelos entrenados. Esto resulta especialmente importante para los bloques recurrentes, que, como sabemos, tienen "memoria" y son capaces de preservar el contexto de estados anteriores acumulando información de pasos temporales previos. Sin embargo, al cambiar entre segmentos de trayectorias no relacionadas, esta memoria puede jugarnos una mala pasada.

El contexto almacenado en los estados ocultos del minipaquete anterior ya solo resulta relevante para la nueva muestra y puede provocar distorsiones en las señales generadas. Por consiguiente, restablecer los estados temporales antes de cada nuevo minipaquete no es solo una medida de precaución, sino un requisito previo para el análisis correcto e independiente del fragmento de datos históricos actual.

if(
   !cEncoder.Clear()
   || !cTask.Clear()
   || !cActor.Clear()
   || !cProbability.Clear()
   || !cDirector.Clear()
   || !cCritic.Clear()
)
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }
result = vector<float>::Zeros(NActions);

Tras inicializar el minipaquete, podemos pasar a la siguiente etapa: la organización de un ciclo anidado de iteración secuencial de los estados del entorno. Estos estados se procesan estrictamente en su orden cronológico, exactamente como fueron observados originalmente por el agente mientras interactuaba con el entorno comercial.

Preservar la coherencia histórica no es una mera formalidad, sino un requisito vital para el entrenamiento eficaz de los modelos recurrentes de bloques. A diferencia de las capas clásicas de completamente conectadas, que trabajan con datos de origen aislados, las redes neuronales recurrentes generan estados ocultos basándose en la información acumulada de pasos anteriores. Su fuerza reside en su capacidad para identificar dependencias temporales, patrones de comportamiento y señales recurrentes en las series temporales.

Si la estructura temporal de los datos cambia, aunque sea parcialmente, el modelo puede perder la capacidad de reconocer las relaciones causales que se manifiestan precisamente en la dinámica. Y esto significa que el valor de tales ejemplos de entrenamiento se reduce drásticamente.

Por ello, cada paso temporal del minipaquete se procesa estrictamente en orden: sin mezclas, sin saltos, sin retrocesos. Este enfoque permite a los componentes recurrentes construir una visión interna del contexto de forma constante, acumulando conocimientos sobre la evolución del entorno del mercado.

for(int i = start; i < MathMin(Buffer[tr].Total, start + Batch); i++)
  {
   if(!state.Assign(Buffer[tr].States[i].state) ||
      MathAbs(state).Sum() == 0 ||
      !bState.AssignArray(state))
     {
      iter -= Batch + start - i;
      break;
     }
   //---

En el cuerpo del ciclo anidado, cargamos los datos de descripción del estado del entorno analizado desde el búfer de reproducción de experiencias y formamos los armónicos de la marca temporal.

bTime.Clear();
double time = (double)Buffer[tr].States[i].account[7];
double x = time / (double)(D'2024.01.01' - D'2023.01.01');
bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
x = time / (double)PeriodSeconds(PERIOD_MN1);
bTime.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
x = time / (double)PeriodSeconds(PERIOD_W1);
bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
x = time / (double)PeriodSeconds(PERIOD_D1);
bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
if(bTime.GetIndex() >= 0)
   bTime.BufferWrite();

Después, formamos un vector de descripción del estado de la cuenta.

//--- Account
float PrevBalance = Buffer[tr].States[MathMax(i - 1, 0)].account[0];
float PrevEquity = Buffer[tr].States[MathMax(i - 1, 0)].account[1];
float profit = float(bState[0] / _Point * (result[0] - result[3]));
bAccount.Clear();
bAccount.Add(1);
bAccount.Add((PrevEquity + profit) / PrevEquity);
bAccount.Add(profit / PrevEquity);
bAccount.Add(MathMax(result[0] - result[3], 0));
bAccount.Add(MathMax(result[3] - result[0], 0));
bAccount.Add((bAccount[3] > 0 ? profit / PrevEquity : 0));
bAccount.Add((bAccount[4] > 0 ? profit / PrevEquity : 0));
bAccount.Add(0);
bAccount.AddArray(GetPointer(bTime));
if(bAccount.GetIndex() >= 0)
   bAccount.BufferWrite();

Llegados a este punto, debemos destacar una importante técnica metodológica utilizada en el proceso de aprendizaje offline. Hablamos del llamado enfoque de "trayectoria casi perfecta". Con este enfoque, rompemos deliberadamente el aislamiento cronológico al permitir que el algoritmo se adelante a los futuros estados del entorno que ya figuran en la muestra de entrenamiento.

A partir de esta información, generamos un tensor de acción mejorado que refleja las decisiones estratégicamente mejor informadas. Obviamente, es muy probable que sean distintas de las aceptadas por el agente en el momento de la interacción inicial con el mercado. Dicho tensor no copia el comportamiento del agente, sino que representa un punto de referencia aproximado hacia el que el agente debe guiarse en el proceso de entrenamiento. Por eso lo llamamos "casi perfecto".

No obstante, esta estrategia provoca discrepancias naturales entre las acciones reales realizadas por el agente y las acciones ideales generadas post facto. Esto, a su vez, obliga a ajustar también otros aspectos del modelo.

En concreto, nos vemos obligados a recalcular el vector de descripción del estado de la cuenta para que coincida con las acciones de la "trayectoria casi perfecta". Sin esto, el Agente se entrenará con datos inconsistentes, donde las acciones y sus consecuencias son se corresponden. Y esto provocará inevitablemente la distorsión de la señal.

Una vez preparado el conjunto de datos de entrada, llamamos secuencialmente los métodos de paso directa de los modelos entrenados, durante los cuales se generan algunos valores predichos. La tarea de entrenar los modelos consiste en minimizar la desviación de los valores predichos obtenidos con respecto a los resultados deseados.

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

Aquí cabe señalar que en esta fase no pasamos directamente por los modelos de evaluación de la acción Agente - Crítico y Director. Esto se debe al uso de enfoques de "trayectoria casi perfecta". Pero volveremos a esta cuestión un poco más tarde.

Una vez realizadas con éxito las operaciones de pasada directa de los modelos entrenados, podemos proceder a generar los valores objetivo. Primero cargamos una serie de estados del entorno posteriores para un horizonte de planificación determinado desde el búfer de reproducción de experiencias.

//--- Look for target
target = vector<float>::Zeros(NActions);
bActions.AssignArray(target);
if(!state.Assign(Buffer[tr].States[i + NForecast].state) ||
   !state.Resize(NForecast * BarDescr) ||
   MathAbs(state).Sum() == 0)
  {
   iter -= Batch + start - i;
   break;
  }
if(!fstate.Resize(1, NForecast * BarDescr) ||
   !fstate.Row(state, 0) ||
   !fstate.Reshape(NForecast, BarDescr))
  {
   iter -= Batch + start - i;
   break;
  }
for(int j = 0; j < NForecast / 2; j++)
  {
   if(!fstate.SwapRows(j, NForecast - j - 1))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      Stop = true;
      break;
     }
  }

Estos datos son los datos de destino de nuestro Codificador de estados del entorno. Por consiguiente, podemos realizar una pasada inversa del modelo anterior para optimizar sus parámetros con el fin de minimizar la desviación de los valores previstos con respecto a los valores objetivo.

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

Esta misma información sobre los estados del entorno posteriores es la base para formar el tensor de la operación comercial "casi perfecta". Al mismo tiempo, construimos una nueva operación considerando la anterior, lo que permite modelar toda una estrategia comercial en lugar de acciones impulsivas separadas. El resultado es una cadena comercial conectada contextualmente en la que cada acción posterior se guía por la lógica de la anterior. Esto es especialmente importante en entornos de aprendizaje offline en los que el agente no recibe información del entorno en tiempo real y debe extraer patrones de datos ya registrados.

Este método permite nivelar la influencia del ruido del mercado, que en los datos reales se manifiesta a través de fluctuaciones aleatorias. Así, en la trayectoria "casi perfecta", el comportamiento del agente parece más suave, racional y estratégicamente alineado. Potencialmente, esto acelerará el aprendizaje del modelo y formará una lógica comercial estable que posteriormente se adaptará también online.

El proceso de entrenamiento de una transacción comercial "casi perfecta" ya se describió detalladamente en el artículo anterior, así que no lo repetiremos ahora.

El tensor generado de la operación "casi perfecta" sirve como valor objetivo para entrenar a nuestro gestor de Actor de alto nivel.

//--- Actor Policy
if(!cActor.backProp(GetPointer(bActions), (CNet*)GetPointer(cTask), -1))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

Llegados a este punto, pasamos al componente más importante de la arquitectura de aprendizaje: el entrenamiento de modelos para evaluar las acciones del Actor, es decir, el Crítico y el Director. Son estos elementos los responsables de generar la retroalimentación que conduce la estrategia del agente, ayudándole a distinguir las acciones productivas de las ineficaces.

No obstante, aquí nos enfrentamos a un problema fundamental: el espacio de acciones posibles en cada estado del entorno es extremadamente grande. Evaluar la totalidad de las decisiones que puede tomar un Actor es una tarea prácticamente imposible en el marco de unos recursos computacionales razonables. Además, muchas de estas acciones nunca serán realizadas por el agente, lo que implica que no aportan ningún valor de aprendizaje.

Por lo tanto, al entrenar al Crítico, solemos elegir una estrategia de evaluación local. En lugar de intentar abarcar todo el espacio de acción, nos centramos en su vecindad, cerca de las decisiones reales tomadas por el agente en el paso temporal actual. Es en este subespacio local donde tiene lugar la búsqueda de las direcciones de optimización (desplazamientos de vectores que pueden provocar potencialmente el crecimiento de la función objetivo, es decir, un aumento de la rentabilidad).

Y aquí es donde el enfoque de la "trayectoria casi perfecta" nos ofrece una poderosa ventaja. Al disponer de un comportamiento de referencia obtenido mirando hacia el futuro, podemos desplazar el foco de la evaluación de lo que el agente ha hecho realmente a lo que debería haber hecho. En otras palabras, entrenamos al Crítico no solo para que distinga lo bueno de lo malo, sino para que se oriente hacia acciones cercanas al ideal estratégico ofrecido como ruta "casi perfecta".

En este paradigma, realizamos la pasada directa del Crítico para evaluar una operación comercial "casi perfecta".

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

E inmediatamente realizamos operaciones de pasada inversa para minimizar la desviación de la estimación obtenida respecto a la calculada a partir de datos históricos reales.

float reward = float((result[0] - result[3]) * fstate[0, 0] / Point());
Result.Clear();
if(!Result.Add(reward)
   || !cCritic.backProp(Result, (CNet*)GetPointer(cEncoder), LatentLayer)
   || !cEncoder.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer, true)
  )
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

La situación del entrenamiento del Director es muy similar a la del Crítico, pero presenta algunos matices importantes. Como cualquier clasificador, el Director necesita un conjunto equilibrado de ejemplos positivos y negativos a partir de los cuales aprender los límites entre las acciones "buenas" y "malas" de los agentes.

Con los ejemplos positivos, todo resulta más o menos obvio. En su papel intervienen las operaciones formadas dentro de la trayectoria "casi perfecta". Estas acciones representan decisiones estratégicamente informadas, generadas sobre la base del análisis de los futuros estados del entorno y, por lo tanto, merecen una valoración elevada.

Sin embargo, resulta imposible entrenar un clasificador robusto solo con ejemplos positivos. También debemos contar con un grupo representativo de decisiones "negativas", que demuestren comportamientos no acordes con nuestros objetivos. Y aquí surge una dificultad metodológica: la muestra de entrenamiento, por su naturaleza, no cubre todo el espacio de acciones potenciales, solo representa las decisiones que el agente ha tomado realmente en el pasado.

Para resolver este problema, hemos recurrido a una heurística sencilla pero eficaz: como ejemplos negativos, usamos conjuntos de valores aleatorios generados dentro del rango aceptable de acciones del agente. Resulta muy probable que estas acciones aleatorias sean incoherentes con los objetivos estratégicos del modelo y pueden considerarse ruido o errores de comportamiento.

Debemos señalar que la alternancia de ejemplos positivos y negativos en el proceso de aprendizaje también se organiza aleatoriamente. Esto evita que el modelo se sobreentrene en una de las categorías y ofrece un límite de clasificación más estable. Este enfoque hace que el entrenamiento del Director sea más flexible y generalizable, y sus señales más nítidas y seguras, lo que resulta especialmente importante en el entorno de gran incertidumbre típico de los mercados financieros.

Como resultado, el Director se convierte en un poderoso punto de referencia binario capaz de rechazar tajantemente las acciones poco constructivas y guiar al Agente hacia zonas más prometedoras del espacio estratégico.

//--- Director
Result.Clear();
if((MathRand() / 32767.0) > 0.5)
   Result.Add(1);
else
  {
   target = vector<float>::Zeros(NActions);
   for(int i = 0; i < NActions; i++)
      target[i] = float(MathRand() / 32767.0);
   bActions.AssignArray(target);
   Result.Add(0);
  }
if(!cDirector.feedForward(GetPointer(bActions), 1, false, (CNet*)GetPointer(cEncoder), LatentLayer)
   || !cDirector.backProp(Result, (CNet*)GetPointer(cEncoder), LatentLayer)
   || !cEncoder.backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer, true)
  )
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   Stop = true;
   break;
  }

A continuación, se entrenan un Controlador de bajo nivel y un modelo predictivo para determinar las probabilidades de la dirección del próximo movimiento de precios. Estos bloques de código se han trasladado del artículo anterior sin modificaciones; en este se ofrecía una descripción detallada de los mismos. Le sugiero que los deje para el estudio individual. En el archivo adjunto se incluye el código completo del asesor de entrenamiento de modelos offline.



Entrenamiento online

La siguiente fase de nuestro trabajo consiste en afinar los modelos entrenados online, donde nos enfrentamos a una clase completamente distinta de retos y limitaciones. Mientras que la fase offline se centraba en un aprendizaje profundo y generalizado basado en datos retrospectivos y una "trayectoria casi perfecta", la fase online consiste en adaptarse a las condiciones en tiempo real.

Una de las principales ventajas del aprendizaje online es obtener información directa del entorno. El Actor toma una decisión, la acción se lleva a cabo y, casi inmediatamente, se conoce su efecto. Esto permite ajustar rápidamente el comportamiento del agente, reforzando las estrategias acertadas y descartando las ineficaces.

No obstante, este enfoque no está exento de serias limitaciones. La principal de ellas es la incapacidad de mirar hacia el futuro, como se hizo al construir la "trayectoria casi perfecta" offline. En el aprendizaje online, el agente toma decisiones basadas en el estado actual y su estrategia, sin acceso al "conocimiento del futuro". Esto cambia radicalmente el entorno de aprendizaje y requiere una reorientación hacia un paradigma más clásico de aprendizaje por refuerzo.

Aquí, el aprendizaje se basa en el ensayo y el error, y la calidad de las decisiones tomadas se evalúa post facto. Así, aumenta la importancia de los modelos evaluativos, el Crítico y elDirector, que actúan como consejos internos para el Actor, guiando su comportamiento. Sin embargo, a diferencia del modo offline, ahora se afinan durante la propia actividad comercial, adaptándose continuamente al cambiante entorno del mercado.

Pero lo primero es lo primero. Vamos a construir el algoritmo de aprendizaje online dentro del EA "…\Experts\ADC\StudyOnline.mq5". El alcance del artículo es limitado, por lo tanto, nos centraremos en una revisión detallada del método OnTick solamente. Aquí es donde se procesa el evento de llegada de un nuevo tick. Y en él también implementamos el algoritmo principal de aprendizaje.

void OnTick()
  {
//---
   if(!IsNewBar())
      return;

En primer lugar, debemos señalar que nuestros modelos solo analizan datos históricos de velas cerradas y no están diseñados para responder rápidamente a cada tick. Por consiguiente, hasta que no se cierre la siguiente vela, no deberemos analizar el entorno en detalle. Al fin y al cabo, el resultado será el mismo. Por consiguiente, para minimizar las operaciones innecesarias, comprobamos el nuevo evento de cierre de barra al principio del método. De lo contrario, esperamos el siguiente tick.

Tras el cierre de la siguiente barra, solicitamos datos históricos en el terminal para una profundidad especificada y formamos los búferes de datos iniciales.

   int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, 1), HistoryBars, Rates);
   if(!ArraySetAsSeries(Rates, true))
      return;
//---
   RSI.Refresh();
   CCI.Refresh();
   ATR.Refresh();
   MACD.Refresh();
   Symb.Refresh();
   Symb.RefreshRates();
//---
   float atr = 0;
   for(int b = 0; b < (int)HistoryBars; b++)
     {
      float open = (float)Rates[b].open;
      float rsi = (float)RSI.Main(b);
      float cci = (float)CCI.Main(b);
      atr = (float)ATR.Main(b);
      float macd = (float)MACD.Main(b);
      float sign = (float)MACD.Signal(b);
      if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE ||
         macd == EMPTY_VALUE || sign == EMPTY_VALUE)
         continue;
      //---
      int shift = b * BarDescr;
      sState.state[shift] = (float)(Rates[b].close - open);
      sState.state[shift + 1] = (float)(Rates[b].high - open);
      sState.state[shift + 2] = (float)(Rates[b].low - open);
      sState.state[shift + 3] = (float)(Rates[b].tick_volume / 1000.0f);
      sState.state[shift + 4] = rsi;
      sState.state[shift + 5] = cci;
      sState.state[shift + 6] = atr;
      sState.state[shift + 7] = macd;
      sState.state[shift + 8] = sign;
     }
//---

Aquí cargamos datos sobre el estado de la cuenta y las posiciones abiertas.

   sState.account[0] = (float)AccountInfoDouble(ACCOUNT_BALANCE);
   sState.account[1] = (float)AccountInfoDouble(ACCOUNT_EQUITY);
//---
   double buy_value = 0, sell_value = 0, buy_profit = 0, sell_profit = 0;
   double position_discount = 0;
   double multiplyer = 1.0 / (60.0 * 60.0 * 10.0);
   int total = PositionsTotal();
   datetime current = TimeCurrent();
   for(int i = 0; i < total; i++)
     {
      if(PositionGetSymbol(i) != Symb.Name())
         continue;
      double profit = PositionGetDouble(POSITION_PROFIT);
      switch((int)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
            buy_value += PositionGetDouble(POSITION_VOLUME);
            buy_profit += profit;
            break;
         case POSITION_TYPE_SELL:
            sell_value += PositionGetDouble(POSITION_VOLUME);
            sell_profit += profit;
            break;
        }
      position_discount += profit - (current - PositionGetInteger(POSITION_TIME)) * multiplyer * MathAbs(profit);
     }
   sState.account[2] = (float)buy_value;
   sState.account[3] = (float)sell_value;
   sState.account[4] = (float)buy_profit;
   sState.account[5] = (float)sell_profit;
   sState.account[6] = (float)position_discount;
   sState.account[7] = (float)Rates[0].time;

Después, formamos los armónicos de la marca temporal.

   bTime.Clear();
   double time = (double)Rates[0].time;
   double x = time / (double)(D'2024.01.01' - D'2023.01.01');
   bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
   x = time / (double)PeriodSeconds(PERIOD_MN1);
   bTime.Add((float)MathCos(x != 0 ? 2.0 * M_PI * x : 0));
   x = time / (double)PeriodSeconds(PERIOD_W1);
   bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
   x = time / (double)PeriodSeconds(PERIOD_D1);
   bTime.Add((float)MathSin(x != 0 ? 2.0 * M_PI * x : 0));
   if(bTime.GetIndex() >= 0)
      bTime.BufferWrite();
//---
   bAccount.Clear();
   bAccount.Add((float)((sState.account[0] - PrevBalance) / PrevBalance));
   bAccount.Add((float)(sState.account[1] / PrevBalance));
   bAccount.Add((float)((sState.account[1] - PrevEquity) / PrevEquity));
   bAccount.Add(sState.account[2]);
   bAccount.Add(sState.account[3]);
   bAccount.Add((float)(sState.account[4] / PrevBalance));
   bAccount.Add((float)(sState.account[5] / PrevBalance));
   bAccount.Add((float)(sState.account[6] / PrevBalance));
   bAccount.AddArray(GetPointer(bTime));
//---
   if(bAccount.GetIndex() >= 0)
      if(!bAccount.BufferWrite())
         return;
//---
   bState.AssignArray(sState.state);

Aquí cabe señalar que tenemos previsto utilizar los datos generados de la descripción del estado actual del entorno en dos direcciones. Obviamente, los usaremos como parte de la pasada directa de nuestro Agente con la generación de nuevos oficios. Sin embargo, solo podremos recibir recompensas del entorno por nuestras acciones cuando se forme la siguiente barra. Existe una brecha temporal.

Por otro lado, en este punto podemos evaluar la efectividad de las acciones realizadas por el Agente en la anterior marca temporal. Y nos interesa hacerlo antes de actualizar el estado de los modelos que aún conservan los resultados de los análisis del entorno anteriores.

Por consiguiente, primero introducimos los datos de descripción del estado del entorno actual generados anteriormente en los modelos objetivo y generamos una estimación predictiva del estado bajo las condiciones de uso por parte del Agente de la política de comportamiento actual.

if(!bFirstRun)
  {
   //--- Target Nets
   if(!cEncoder[1].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL)
      || !cTask[1].feedForward((CBufferFloat*)GetPointer(bState), 1, false,
                                                    (CNet*)GetPointer(cEncoder[1]), LatentLayer)
      || !cActor[1].feedForward((CBufferFloat*)GetPointer(bAccount), 1, false,
                                                                        GetPointer(cTask[1]), -1)
      || !cCritic[2].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer)
      || !cCritic[3].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer)
      || !cCritic[4].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer)
      || !cCritic[5].feedForward(GetPointer(cActor[1]), -1, GetPointer(cEncoder[1]), LatentLayer)
     )
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return;
     }

El uso de modelos objetivo desempeña un papel importante en la construcción de una estrategia de comportamiento armoniosa e integral, minimizando el impacto del ruido del mercado, ya que permite al Agente centrarse no solo en la recompensa actual, sino también en la recompensa futura prevista.

A continuación, pasamos a el entrenamiento del Crítico. Aquí cabe recordar que los autores del framework ActorDirectorCritic propusieron utilizar 2 Críticos en paralelo con dos modelos objetivo para cada uno. En primer lugar, generamos los valores objetivo de evaluación de las acciones recientes del Agente, teniendo en cuenta la recompensa recibida en esta etapa por el primer Crítico, y realizamos una pasada directa e inversa del modelo.

//--- Critic 1
cCritic[2].getResults(Result);
float reward = Result[0];
cCritic[4].getResults(Result);
reward = (reward + Result[0]) / 2 * DiscFactor + float(sState.account[1] - PrevEquity);
Result.Clear();
if(!Result.Add(reward)
   || !cCritic[0].backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   return;
  }

Aquí debemos señalar que los autores del framework prevén la transmisión de un gradiente de error sobre la evaluación mínima de las acciones realizadas. Nosotros, sin embargo, tras darnos cuenta, haremos las cosas de forma un poco diferente. Así, transferimos el gradiente de error del modelo con el mínimo error medio de estimación de la acción. De esta forma, pasamos de una estimación mínima a una más precisa.

El segundo aspecto importante del aprendizaje online es la oportunidad de actualizar las políticas del Actor. Una solución obvia y técnicamente sencilla sería utilizar un contador de iteraciones fijo. Tras un número determinado de pasos, la estrategia del agente se actualiza. Y debemos admitir que este enfoque está bastante justificado si consideramos las condiciones del aprendizaje online real, donde cada estado del entorno es único y es imposible volver a él.

Sin embargo, tenemos previsto usar una potente herramienta de modelización: el simulador de estrategias de MetaTrader 5. Esto nos permite reproducir repetidamente las mismas secuencias de eventos, simulando de hecho un proceso online con posibilidad de pasadas repetidas.

Y ahí radica un problema potencial. Si usamos un enfoque ingenuo con iteraciones de conteo duro, entonces en cada repetición del aprendizaje, las actualizaciones de la estrategia del Actor recaerán sobre los mismos estados del entorno. Y esto reduce drásticamente la variabilidad de la muestra de entrenamiento, creando sesgos artificiales e impidiendo que el agente aprenda patrones estables.

Para evitar esto, vamos a adoptar un enfoque estocástico para activar las actualizaciones de las políticas. En lugar de un contador determinista, podemos generar un valor entero aleatorio y solo realizar una actualización de la estrategia si es múltiplo de un número dado. Este mecanismo conserva la regularidad necesaria de las optimizaciones, pero las hace poco predecibles en un contexto temporal. Así evitamos el sobreentrenamiento con datos concretos.

if(cCritic[0].getRecentAverageError() <= cCritic[1].getRecentAverageError() &&
   (MathRand() % ActorUpdate) == 0)
   if(!cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
      || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
     )
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return;
     }

Después repetimos las mismas operaciones para el segundo Crítico.

//--- Critic 2
cCritic[3].getResults(Result);
reward = Result[0];
cCritic[5].getResults(Result);
reward = (reward + Result[0]) / 2 * DiscFactor + float(sState.account[1] - PrevEquity);
Result.Clear();
if(!Result.Add(reward)
   || !cCritic[1].backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer))
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   return;
  }
if(cCritic[0].getRecentAverageError() > cCritic[1].getRecentAverageError() &&
   (MathRand() % ActorUpdate) == 0)
   if(!cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
      || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
     )
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return;
     }

Esta vez es mucho más sencillo con el Director. Las operaciones rentables se clasifican como positivas, mientras que todas las demás lo hacen como negativas.

//--- Director
Result.Clear();
if((sState.account[1] - PrevEquity) > 0)
   Result.Add(1);
else
   Result.Add(0);
if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
   || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
   || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
  )
  {
   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
   return;
  }

Aquí ajustamos los parámetros del modelo predictivo según la dirección de la vela cerrada.

 //--- Probability
 vector<float> target = vector<float>::Zeros(NActions / 3);
 if(sState.state[0] > 0)
    target[0] = 1;
 else
    if(sState.state[0] < 0)
       target[1] = 1;
 if(!Result.AssignArray(target)
    || !cProbability.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
    || !cEncoder[0].backPropGradient((CBufferFloat*)NULL, (CBufferFloat*)NULL, LatentLayer)
   )
   {
    PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
    return;
   }
}

Una vez finalizadas las iteraciones de optimización del modelo, podemos proceder a generar una nueva operación comercial. Ahora debemos introducir la descripción generada previamente del estado actual del entorno en los modelos entrenados, incluidos los modelos de evaluación de la acción del Actor.

//--- New state
   if(!cEncoder[0].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CBufferFloat*)NULL)
      || !cTask[0].feedForward((CBufferFloat*)GetPointer(bState), 1, false, (CNet*)GetPointer(cEncoder[0]),
                                                                                                   LatentLayer)
      || !cActor[0].feedForward((CBufferFloat*)GetPointer(bAccount), 1, false, (CNet*)GetPointer(cTask[0]), -1)
      || !cProbability.feedForward((CNet*)GetPointer(cEncoder[0]), LatentLayer, (CBufferFloat*)NULL)
      || !cDirector.feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
      || !cCritic[0].feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
      || !cCritic[1].feedForward((CNet*)GetPointer(cActor[0]), -1, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
     )
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return;
     }

Y luego guardamos en variables globales los datos que necesitaremos al procesar los datos de una nueva vela cerrada.

PrevBalance = sState.account[0];
PrevEquity = sState.account[1];

Después, pasaremos al trabajo que se requiere para ejecutar transacciones comerciales. En primer lugar, obtenemos el vector de resultados de la actuación de nuestro Actor.

   vector<float> temp;
   cActor[0].getResults(temp);
//---
   if(temp.Size() < NActions)
      temp = vector<float>::Zeros(NActions);

Luego eliminamos del tensor los volúmenes que se absorben mutuamente.

double min_lot = Symb.LotsMin();
double step_lot = Symb.LotsStep();
double stops = (MathMax(Symb.StopsLevel(), 1) + Symb.Spread()) * Symb.Point();
if(temp[0] >= temp[3])
  {
   temp[0] -= temp[3];
   temp[3] = 0;
  }
else
  {
   temp[3] -= temp[0];
   temp[0] = 0;
  }

Y pasamos a descifrar los resultados del Actor. Si no hay volumen de posiciones largas en ellos, cerramos las posiciones existentes que se hayan podido abrir antes.

//--- buy control
   if(temp[0] < min_lot || (temp[1] * MaxTP * Symb.Point()) <= 2 * stops ||
                                 (temp[2] * MaxSL * Symb.Point()) <= stops)
     {
      if(buy_value > 0)
         CloseByDirection(POSITION_TYPE_BUY);
     }

Si es necesario abrir o mantener posiciones largas, pasaremos los valores obtenidos a volúmenes comerciales y niveles de precios reales.

else
  {
   double buy_lot = min_lot + MathRound((double)(temp[0] - min_lot) / step_lot) * step_lot;
   double buy_tp = NormalizeDouble(Symb.Ask() + temp[1] * MaxTP * Symb.Point(), Symb.Digits());
   double buy_sl = NormalizeDouble(Symb.Ask() - temp[2] * MaxSL * Symb.Point(), Symb.Digits());

Si hay posiciones abiertas previamente, podemos realizar el trailing de los niveles comerciales.

if(buy_value > 0)
   TrailPosition(POSITION_TYPE_BUY, buy_sl, buy_tp);

Luego ajustamos el volumen de la posición actual, cerrando parcialmente o añadiendo la que falta. Esta última situación también puede incluir la apertura de una nueva posición.

 if(buy_value != buy_lot)
   {
    if((buy_value - buy_lot) >= min_lot)
       ClosePartial(POSITION_TYPE_BUY, buy_value - buy_lot);
    else
       if((buy_lot - buy_value) >= min_lot)
          if(!Trade.Buy(buy_lot - buy_value, Symb.Name(), Symb.Ask(), buy_sl, buy_tp))
             if(Trade.CheckResultRetcode() == 10019)
               {
                Result.Clear();
                Result.Add(0);
                if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
                   || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
                   || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
                  )
                  {
                   PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
                   return;
                  }
               }
   }
}

Tenga en cuenta que si se produce un error de insuficiencia de fondos para abrir una nueva posición o añadir el volumen faltante, inmediatamente informaremos de ello a través del Director, señalando la decisión negativa.

El ajuste de una posición corta se realiza del mismo modo.

//--- sell control
   if(temp[3] < min_lot || (temp[4] * MaxTP * Symb.Point()) <= 2 * stops ||
                                  (temp[5] * MaxSL * Symb.Point()) <= stops)
     {
      if(sell_value > 0)
         CloseByDirection(POSITION_TYPE_SELL);
     }
   else
     {
      double sell_lot = min_lot + MathRound((double)(temp[3] - min_lot) / step_lot) * step_lot;;
      double sell_tp = NormalizeDouble(Symb.Bid() - temp[4] * MaxTP * Symb.Point(), Symb.Digits());
      double sell_sl = NormalizeDouble(Symb.Bid() + temp[5] * MaxSL * Symb.Point(), Symb.Digits());
      if(sell_value > 0)
         TrailPosition(POSITION_TYPE_SELL, sell_sl, sell_tp);
      if(sell_value != sell_lot)
        {
         if((sell_value - sell_lot) >= min_lot)
            ClosePartial(POSITION_TYPE_SELL, sell_value - sell_lot);
         else
            if((sell_lot - sell_value) >= min_lot)
               if(!Trade.Sell(sell_lot - sell_value, Symb.Name(), Symb.Bid(), sell_sl, sell_tp))
                  if(Trade.CheckResultRetcode() == 10019)
                    {
                     Result.Clear();
                     Result.Add(0);
                     if(!cDirector.backProp(Result, (CNet*)GetPointer(cEncoder[0]), LatentLayer)
                        || !cActor[0].backPropGradient((CNet*)GetPointer(cTask[0]), -1, -1, false)
                        || !cTask[0].backPropGradient((CNet*)GetPointer(cEncoder[0]), LatentLayer, -1, true)
                       )
                       {
                        PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
                        return;
                       }
                    }
        }
     }

Al final del método, comprobamos la llegada del momento de actualización de los modelos objetivo y, si es necesario, llamamos los métodos para copiar suavemente los parámetros de los modelos entrenados a los modelos objetivo.

   bFirstRun = false;
//---
   if((int(Rates[0].time / PeriodSeconds(TimeFrame)) % TragetUpdate) == 0)
     {
      if(MathRand() / 32767.0 > 0.5)
         cCritic[2].WeightsUpdate(GetPointer(cCritic[0]), tau);
      else
         cCritic[4].WeightsUpdate(GetPointer(cCritic[0]), tau);
      if(MathRand() / 32767.0 > 0.5)
         cCritic[3].WeightsUpdate(GetPointer(cCritic[1]), tau);
      else
         cCritic[5].WeightsUpdate(GetPointer(cCritic[1]), tau);
      cEncoder[1].WeightsUpdate(GetPointer(cEncoder[0]), tau);
      cTask[1].WeightsUpdate(GetPointer(cTask[0]), tau);
      cActor[1].WeightsUpdate(GetPointer(cActor[0]), tau);
     }
   if(PrevBalance < 50)
      ExpertRemove();
  }

Después de eso, finalizamos el método y esperamos el cierre de la siguiente vela.

En el archivo adjunto encontrará el código completo del asesor de entrenamiento de modelos online.



Simulación

Hemos realizado un importante trabajo para adaptar e implementar las ideas clave del framework Actor—Director—Critic usando MQL5, integrando además sus componentes en la arquitectura de los modelos entrenados. Asimismo, hemos trabajado minuciosamente la lógica de interacción entre el Actor, el Director y el Crítico, y hemos aplicado planteamientos originales para el entrenamiento de los agentes. Ahora ha llegado la etapa final y, quizás, la más excitante: probar la eficacia de las soluciones aplicadas con datos históricos reales.

Las pruebas de rendimiento del framework se realizan con datos históricos en condiciones casi reales. Esto permite una evaluación objetiva de la capacidad de las soluciones arquitectónicas y algorítmicas seleccionadas para hacer frente a la dinámica y la incertidumbre de los mercados financieros. Además, este enfoque podrá identificar los puntos fuertes y débiles de la aplicación actual y esbozar las áreas de mejora y optimización.

Para formar la muestra de entrenamiento utilizan pasadas aleatorios del agente en el simulador de estrategias de MetaTrader 5, lo cual nos permite recoger una amplia gama de escenarios de comportamiento. Hemos elegido como base las cotizaciones históricas del par de divisas EURUSD en el marco temporal M1 para todo el año 2024.

El entrenamiento inicial del modelo se realiza offline sin actualizar la muestra de entrenamiento hasta que se estabilicen los errores de predicción de los modelos. Luego pasaremos al simulador de estrategias de MetaTrader 5 y continuaremos ajustando los parámetros de los modelos hasta obtener resultados estables.

La evaluación objetiva de la calidad de la política comercial formada en «condiciones de combate» puede llevarse a cabo con los resultados de las pruebas de los modelos entrenados fuera de la muestra de entrenamiento. Como periodo de prueba hemos seleccionado los datos históricos de EneroMarzo de 2025. Este periodo temporal no se utiliza en el entrenamiento, lo que evita el sobreentrenamiento y confiere a los resultados un valor práctico real.

Todos los demás parámetros, incluidos el entorno de mercado, el marco temporal, el modelo de simulación de ejecución y la configuración del terminal permanecen sin modificaciones. De este modo se obtiene una evaluación clara de la calidad de la estrategia aprendida, sin la influencia de factores externos.

A continuación le presentamos los resultados de las pruebas, ofreciendo además una evaluación visual del modelo de comportamiento del agente.

Durante el periodo de prueba, el modelo ha realizado 684 transacciones. De ellas, 268 se han cerrado con beneficios, lo que supone algo más del 39%. Sin embargo, en general, durante el periodo de prueba, el modelo ha sido capaz de generar beneficios debido a que la transacción media rentable es casi 2 veces superior al mismo indicador de las transacciones deficitarias.



Conclusión

En el marco de este artículo, hemos aprendido los aspectos teóricos del framework Actor—Director—Critic e implementado nuestra visión de los enfoques propuestos mediante MQL5. Al mismo tiempo, lo hemos integrado plenamente en la arquitectura multiagente existente. El resultado es un agente modular, flexible y que aprende de forma eficiente, capaz de considerar no solo las evaluaciones locales de las acciones (a través del Crítico), sino también el contexto estratégico de la lógica del comportamiento (a través del Director). Este enfoque ofrece una retroalimentación más precisa y sostenida al Actor, lo que permite al agente descartar más rápidamente las acciones ineficaces y explorar direcciones productivas en el espacio político.

Las pruebas han confirmado el rendimiento del enfoque propuesto y demostrado que Actor—Director—Critic es capaz de tomar mejores decisiones, demostrando un comportamiento seguro incluso en condiciones de incertidumbre en el mercado.

Sin embargo, querríamos recordarle una vez más que los programas presentados en este artículo son solo de carácter demostrativo y muestran las capacidades del framework. Antes de aplicar las soluciones propuestas en condiciones comerciales reales, debemos entrenar los modelos con una muestra de entrenamiento más representativa, con las consiguientes pruebas exhaustivas.


Enlaces


Programas usados en el artículo

#NombreTipoDescripción
1Research.mq5AsesorAsesor de recopilación de datos
2ResearchRealORL.mq5
Asesor
Asesor experto para recopilar ejemplos con el método Real-ORL
3Study.mq5AsesorAsesor de entrenamiento de modelos offline
4StudyOnline.mq5
Asesor
Asesor de entrenamiento de modelos online
4Test.mq5AsesorAsesor para la prueba de modelos
5Trajectory.mqhBiblioteca de clasesEstructura de descripción del estado del sistema y la arquitectura del modelo
6NeuroNet.mqhBiblioteca de clasesBiblioteca de clases para crear una red neuronal
7NeuroNet.clBibliotecaBiblioteca de código del programa OpenCL

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

Sergey Chalyshev
Sergey Chalyshev | 21 abr 2025 en 17:58
El analfabetismo genera analfabetismo

https://nukadeti.ru/basni/krylov-kvartet

Dmitriy Gizlyk
Dmitriy Gizlyk | 24 abr 2025 en 13:52
Sergey Chalyshev #:
El analfabetismo genera analfabetismo

https://nukadeti.ru/basni/krylov-kvartet

¿Puede decirme cómo hacerlo?

Trading por pares: Trading algorítmico con optimización automática en la diferencia de puntuación Z Trading por pares: Trading algorítmico con optimización automática en la diferencia de puntuación Z
En este artículo, veremos qué es el trading por pares y cómo se realiza el comercio de correlaciones. También crearemos un asesor experto para automatizar el trading por pares y añadiremos la capacidad de optimizar automáticamente dicho algoritmo comercial a partir de los datos históricos. Además, como parte del proyecto, aprenderemos a calcular la divergencia de dos pares utilizando la puntuación z.
De principiante a experto: programando velas japonesas De principiante a experto: programando velas japonesas
En este artículo damos el primer paso en la programación MQL5, incluso para principiantes. Le mostraremos cómo transformar patrones de velas familiares en un indicador personalizado completamente funcional. Los patrones de velas son valiosos porque reflejan la acción real del precio y señalan cambios en el mercado. En lugar de escanear gráficos manualmente (un enfoque propenso a errores e ineficiencias), analizaremos cómo automatizar el proceso con un indicador que identifica y etiqueta patrones para usted. A lo largo del camino, exploraremos conceptos clave como indexación, series de tiempo, rango verdadero promedio (para mayor precisión en la volatilidad variable del mercado) y el desarrollo de una biblioteca de patrones de velas reutilizables personalizada para usar en proyectos futuros.
Creación de un sistema personalizado de detección de regímenes de mercado en MQL5 (Parte 1): Indicador Creación de un sistema personalizado de detección de regímenes de mercado en MQL5 (Parte 1): Indicador
Este artículo detalla la creación de un sistema de detección de regímenes de mercado MQL5 utilizando métodos estadísticos como la autocorrelación y la volatilidad. Se proporciona el código para que las clases clasifiquen las condiciones de tendencia, rango y volatilidad y un indicador personalizado.
Criterios de tendencia. Final Criterios de tendencia. Final
En este artículo veremos cómo aplicar en la práctica algunos criterios de tendencia, y también intentaremos desarrollar algunos criterios nuevos. La atención se centrará en la eficacia de la aplicación de estos criterios al análisis de datos de mercado y al trading.