English Русский 中文 Deutsch 日本語 Português
preview
Redes neuronales: así de sencillo (Parte 47): Espacio continuo de acciones

Redes neuronales: así de sencillo (Parte 47): Espacio continuo de acciones

MetaTrader 5Sistemas comerciales | 25 octubre 2023, 12:24
388 0
Dmitriy Gizlyk
Dmitriy Gizlyk

Introducción

En nuestros trabajos anteriores, entrenamos al agente solo para determinar la dirección del comercio. El rango de acciones del Agente se limitaba a solo 4 opciones:

  • comprar, 
  • vender, 
  • mantener/esperar
  • cerrar todas las posiciones.

Aquí no vemos funciones de gestión de capital ni riesgos. En todas las operaciones comerciales usamos el lote mínimo, lo cual resulta suficiente para evaluar los enfoques de entrenamiento, pero no para construir una estrategia comercial. Una estrategia comercial rentable debe tener obligatoriamente un algoritmo de gestión del capital.

Asimismo, para crear una estrategia comercial estable, debemos gestionar los riesgos, y este bloque también falta en nuestros diseños. Sí, el asesor evalúa la situación del mercado en cada nueva vela comercial y toma una decisión sobre la operación comercial, pero cada próxima barra conlleva riesgos para nuestra cuenta. El movimiento de precios dentro de una barra puede resultar perjudicial para nuestro balance, por eso siempre se recomienda utilizar stop loss. Este sencillo enfoque nos permite limitar los riesgos por operación.


1. Características del aprendizaje del espacio continuo de acciones.

Resulta lógico que a la hora de entrenar al Agente y elaborar su política comercial, debamos considerar estos puntos, pero aquí surge la pregunta: ¿cómo entrenar el modelo para predecir el volumen de la transacción y los niveles de cierre de la posición? Esto se puede lograr fácilmente usando algoritmos de aprendizaje supervisado donde podamos especificar los valores objetivo requeridos ofrecidos por el instructor, pero existen algunas complicaciones al utilizar algoritmos de aprendizaje por refuerzo.

Permítanme recordarles que antes hemos usado dos enfoques para entrenar modelos de refuerzo: la predicción de la recompensa y la probabilidad de obtener la recompensa máxima.

Una posible forma de resolver este problema es definir valores discretos para todos los parámetros de una transacción comercial y crear una acción aparte para cada una de las opciones posibles. Esto nos permitirá tener en cuenta algunos aspectos de la gestión del capital y del riesgo.

No obstante, este enfoque no está exento de inconvenientes. La selección de parámetros discretos de la transacción requiere algo de trabajo por nuestra parte en la etapa de preparación de datos. Su selección supondrá siempre un compromiso entre el número de opciones y una flexibilidad suficiente en la toma de decisiones del Agente. En este caso, el número de combinaciones de acciones posibles podría aumentar significativamente, lo cual redundará en un modelo más complejo, y al mismo tiempo, conllevará un aumento de su tiempo de entrenamiento. Al fin y al cabo, durante el proceso de aprendizaje deberemos analizar la recompensa por cada una de las posibles acciones.

Por ejemplo, si tomamos solo 3 valores discretos para el volumen comercial, 3 niveles de stop loss y 5 niveles de take profit, entonces necesitaremos 90 elementos solo para definir el espacio de acciones en 2 direcciones comerciales (3 * 3 * 5 * 2 = 90). Añada además las acciones para mantener y cerrar una posición, y ya tendremos 92 opciones en el abanico de posibles acciones del agente.

Estará de acuerdo con que esta escasa libertad de acción del Agente provoca un aumento significativo en el número de neuronas en la salida del modelo, y con la suma de cada valor discreto de cualquiera de los parámetros de la transacción, el número de neuronas aumentará progresivamente.

Además, entrenar un modelo más complejo puede requerir ejemplos adicionales del conjunto de entrenamiento con todas las consecuencias que de ello se derivan.

No obstante, existen otros enfoques: los llamados algoritmos de entrenamiento de un agente en un espacio de acciones continuo. Un agente entrenado con tales algoritmos puede seleccionar acciones de un rango continuo de valores, lo cual le permite gestionar de forma más flexible y precisa los parámetros de las transacciones, incluido el volumen de operaciones y los niveles de stop loss y take profit.

Uno de los algoritmos más populares para entrenar a un agente en un espacio continuo de acciones es el Deep Deterministic Policy Gradient (DDPG). En el DDPG, el modelo consta de dos redes neuronales: el Actor y el Crítico. El Actor predice la acción óptima según el estado actual, mientras que el Crítico evalúa dicha acción. Ya hemos visto una solución similar en el artículo “Algoritmo actor-crítico con ventaja (Advantage actor-critic)”. Existen similitudes entre los enfoques de los algoritmos indicados, si bien se diferencian en el algoritmo de entrenamiento del Actor.

En el DDPG, se entrena a un Actor utilizando el ascenso de gradiente para optimizar una política determinista. El actor predice directamente la acción óptima según el estado actual, en lugar de modelar la distribución probabilística de las acciones como en el algoritmo actor-crítico con ventaja.

El entrenamiento del actor en el DDPG se produce calculando el gradiente de la función de valor del Crítico respecto a las acciones del Actor y utilizando este gradiente para actualizar los parámetros del Actor. Suena un poco complicado, pero esto permite al Actor encontrar la acción óptima que maximiza la puntuación del Crítico.

Es importante señalar que el DDPG se refiere a algoritmos off-policy. El modelo se entrena con los datos obtenidos de las interacciones previas con el entorno, independientemente de la actual estrategia de toma de decisiones. Esta importante propiedad del algoritmo permite su uso en entornos complejos y estocásticos, donde la predicción de la dinámica del entorno puede resultar difícil o inexacta. Nos encontramos con una mala calidad de las previsiones del mercado financiero al probar el algoritmo EDL.

El algoritmo Deep Deterministic Policy Gradient se basa en los principios básicos de Deep Q-Network (DQN) e incorpora muchos de sus enfoques, incluyendo el búfer de reproducción de experiencia y el modelo objetivo. Echemos un vistazo más de cerca al algoritmo.

Como ya hemos mencionado, el modelo consta de 2 redes neuronales: el Actor y el Crítico. El Actor recibe como entrada el estado del entorno. A la salida del Actor, obtenemos la acción a partir de una distribución continua de valores. En nuestro caso, formaremos el volumen de las transacciones, así como los niveles de stop loss y take profit. Dependiendo de la arquitectura del modelo y el planteamiento del problema, podemos usar valores absolutos o relativos. Para aumentar el nivel de exploración del entorno, podemos añadir algo de ruido a la acción generada.

Así, realizaremos la acción elegida por el Actor y nos trasladamos a un nuevo estado del entorno. Como respuesta a la acción que realizaremos, obtendremos una recompensa del entorno.

Luego recopilamos los conjuntos de datos "Estado - Acción - Nuevo estado - Recompensa" en el búfer de reproducción de experiencia. Todo según los clásicos de los algoritmos de aprendizaje por refuerzo.

Al igual que en el DQN, seleccionamos un paquete para entrenar el modelo desde el búfer de reproducción de experiencia. Los estados de este paquete de datos de entrenamiento se envían a la entrada del Actor. Antes de cambiar los parámetros, lo más probable es que obtengamos una acción similar a la almacenada en el búfer de reproducción de experiencia. Pero a diferencia de Actor-Crítico con ventaja, el Actor no retorna una distribución de probabilidad, sino una acción de la distribución continua.

Para evaluar el valor de una acción determinada, transmitiremos el estado actual y la acción generada al Crítico. Partiendo los datos recibidos, el crítico pronosticará la recompensa, como en el DQN clásico.

Al igual que el DQN, el crítico se entrena para minimizar la desviación estándar entre la recompensa prevista y la real del búfer de repetición de experiencia. Para construir una política holística, se usará el modelo Target Net, pero como para evaluar el estado posterior el Crítico necesita un conjunto de datos del estado y la acción, para formar una acción a partir del estado posterior también usaremos el modelo objetivo del Actor.

Lo más destacado del DDPG es que no utilizaremos valores objetivo de salida para entrenar al Actor. En su lugar, simplemente tomaremos el valor del gradiente de error del modelo del Crítico sobre nuestra acción y lo pasaremos a través del modelo del Actor.

Por lo tanto, durante el entrenamiento de la función Q del Crítico, utilizamos el gradiente de error sobre la acción para optimizar las acciones del Agente. Podemos decir que el Actor es parte integral de la función Q, y que aprender la función Q lleva a la optimización de la función del Actor.

Pero aquí debemos prestar atención a que en el proceso de entrenamiento del Crítico optimizamos sus parámetros para la evaluación más correcta de la pareja estado-acción, mientras que en el proceso de entrenamiento del Actor, optimizamos sus parámetros para aumentar la recompensa prevista, en igualdad de condiciones.

Los autores del método recomiendan usar una actualización suave de los modelos objetivo. El reemplazo simple del modelo objetivo por uno entrenado con una cierta periodicidad se sustituirá por el recálculo de los parámetros del modelo objetivo, considerando la tasa de actualización de los parámetros del modelo entrenado. Según los autores, este enfoque ralentiza la actualización de los modelos objetivo, pero aumenta la estabilidad del proceso de aprendizaje.


2. Implementación usando MQL5

Después de la introducción teórica al método Deep Deterministic Policy Gradient (DDPG), procederemos a su implementación práctica utilizando MQL5. Comenzaremos organizando el proceso de actualización suave de los modelos de destino. La función de suma ponderada de 2 parámetros en sí no es complicada, pero tiene 2 momentos a considerar.

Primero, la operación deberá realizarse con todos los parámetros del modelo, y dado que la operación de cada parámetro individual es completamente independiente de otros parámetros del mismo modelo, resulta sencillo ejecutarlas en paralelo.

En segundo lugar, todas las operaciones para entrenar y trabajar con modelos se realizan en el contexto de OpenCL, mientras que las operaciones de copiado de datos entre la memoria de contexto y la memoria principal son bastante caras, por lo que siempre nos hemos esforzado en minimizarlas. Resulta lógico que los parámetros también deban recalcularse en el contexto de OpenCL.

2.1. Actualización suave de los modelos objetivo

Primero crearemos un kernel SoftUpdate para realizar las operaciones. El algoritmo del kernel es bastante simple. En los parámetros del kernel, transmitiremos los punteros a dos búferes de datos (los parámetros del modelo objetivo y el entrenado) y el factor de actualización como una constante.

__kernel void SoftUpdate(__global float *target, 
                         __global const float *source, 
                         const float tau
                        )
  {
   const int i = get_global_id(0);
   target[i] = target[i] * tau + (1.0f - tau) * source[i];
  }

En cada flujo por separado actualizaremos solo un parámetro. Por lo tanto, la cantidad de flujos será igual al número de parámetros actualizados.

A continuación, tendremos que organizar el proceso en el lado del programa principal.

Permítanme recordarle que los parámetros de nuestro modelo se distribuyen entre diferentes objetos según el tipo de capa neuronal. Esto significa que deberemos añadir un método para actualizar los parámetros de cada clase para organizar el trabajo de la capa neuronal. Veamos el ejemplo de la clase básica de la capa neuronal CNeuronBaseOCL.

Como actualizaremos los parámetros de la capa neuronal actual, en los parámetros del método solo necesitaremos transmitir el puntero a la capa neuronal del modelo entrenado y el coeficiente de actualización.

bool CNeuronBaseOCL::WeightsUpdate(CNeuronBaseOCL *source, float tau)
  {
   if(!OpenCL || !Weights || !source || !source.Weights)
      return false;

En el cuerpo del método, verificaremos la validez del puntero recibido al objeto de la capa neuronal, y junto con él comprobaremos los punteros a los objetos internos necesarios.

Aquí comprobamos la correspondencia entre los tipos de las dos capas neuronales y las dimensiones de las matrices de parámetros.

   if(Type() != source.Type())
      return false;
   if(Weights.Total() != source.Weights.Total())
      return false;

Después de transmitir con éxito el bloque de control, organizaremos la transferencia de los parámetros al kernel,

   uint global_work_offset[1] = {0};
   uint global_work_size[1] = {Weights.Total()};
   ResetLastError();
   if(!OpenCL.SetArgumentBuffer(def_k_SoftUpdate, def_k_su_target, Weights.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_SoftUpdate, def_k_su_source, source.getWeightsIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(def_k_SoftUpdate, def_k_su_tau, (float)tau))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }

y pondremos el kernel en la cola de ejecución. No olvide controlar el proceso de ejecución de las operaciones en cada paso.

   if(!OpenCL.Execute(def_k_SoftUpdate, 1, global_work_offset, global_work_size))
     {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      return false;
     }
//---
   return true;
  }

Luego finalizaremos el método.

Como todos los objetos que organizan el trabajo de las capas neuronales de varias arquitecturas en nuestra clase se heredan de la clase básica CNeuronBaseOCL, todas las clases heredarán el método creado. Pero solo permite actualizar la matriz de pesos de la clase básica. En todas las clases que añaden objetos internos optimizables adicionales se debe anular el método. Por ejemplo, en la capa convolucional CNeuronConvOCL añadiremos una matriz de parámetros de convolución. Para actualizarla, redefiniremos el método WeightsUpdate. Para ofrecer soporte a la redefinición de los métodos heredados, mantendremos todos los parámetros del método sin cambios.

bool CNeuronConvOCL::WeightsUpdate(CNeuronBaseOCL *source, float tau)
  {
   if(!CNeuronBaseOCL::WeightsUpdate(source, tau))
      return false;

Pero en el cuerpo del método no repetiremos todo el bloque de controles. En cambio, llamaremos al método de la clase principal y verificaremos el resultado de las operaciones.

A continuación, cabe señalar que en los parámetros obtendremos el puntero al objeto de la clase básica de la red neuronal. Hemos hecho esto así a propósito. Especificar el tipo de clase principal nos permitirá transmitir el puntero a cualquiera de sus descendientes, y esto es lo que necesitamos para organizar un método virtual en todas las clases heredadas.

No obstante, la cuestión es que en este estado no podremos acceder a la matriz de pesos de convolución de la capa obtenida en los parámetros. En la clase principal, simplemente no existirá tal objeto, solo aparecerá en la clase de capa convolucional, y no tenemos ninguna duda de que el puntero a la capa convolucional se transmitirá en los parámetros. De hecho, en el método de la clase padre, verificaremos la correspondencia de los tipos de la capa neuronal actual y el obtenido en los parámetros. Para trabajar con este objeto de capa convolucional, solo necesitaremos asignar el puntero resultante al objeto dinámico de capa convolucional, y luego verificar el cumplimiento de los tamaños de la matriz.

   CNeuronConvOCL *temp = source;
   if(WeightsConv.Total() != temp.WeightsConv.Total())
      return false;

A continuación, repetiremos el procedimiento de transferencia de datos y colocaremos el kernel en la cola de ejecución. Tenga en cuenta que solo se cambiarán los objetos del búfer de datos utilizados.

   uint global_work_offset[1] = {0};
   uint global_work_size[1] = {WeightsConv.Total()};
   ResetLastError();
   if(!OpenCL.SetArgumentBuffer(def_k_SoftUpdate, def_k_su_target, WeightsConv.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(def_k_SoftUpdate, def_k_su_source, temp.WeightsConv.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(def_k_SoftUpdate, def_k_su_tau, (float)tau))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.Execute(def_k_SoftUpdate, 1, global_work_offset, global_work_size))
     {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      return false;
     }
//---
   return true;
  }

De forma similar, crearemos los métodos en todas las demás clases de capas neuronales en las que añadiremos objetos con parámetros optimizados. No ofreceremos el código completo de los métodos de la clase, podrá leerlos en el archivo adjunto.

Debemos decir que el algoritmo operativo de nuestra biblioteca no ofrece al usuario acceso directo a las capas neuronales del modelo. El usuario siempre trabajará con la clase de nivel superior del modelo de red neuronal. Por lo tanto, después de añadir los métodos a las clases de la capa neuronal, crearemos el método homónimo en nuestra clase de modelo CNet::WeightsUpdate. En los parámetros, el método obtendrá el puntero a la red neuronal entrenada y el coeficiente de actualización, mientras que en el cuerpo del método, organizaremos un ciclo de búsqueda en todas las redes neuronales del modelo y llamaremos a los métodos para actualizar la capa neuronal. El algoritmo es bastante simple y no vamos a presentar su código en este artículo. Podrá familiarizarse por sí mismo con él en el archivo adjunto.

2.2. Comunicación entre Actor y Crítico

Tras organizar el proceso de actualización suave de modelos, procederemos directamente a organizar el proceso de entrenamiento del modelo. Diremos de inmediato que nuestro modelo es una especie de simbiosis del algoritmo de DDPG con los enfoques previamente estudiados. En particular, hemos decidido que ambas redes neuronales (Actor y Crítico) utilicen un único bloque de procesamiento preliminar de los datos originales.

Permítanme recordarle que el Actor toma una decisión sobre la acción óptima según el estado obtenido del entorno. El Crítico recibirá como entrada una descripción del estado del entorno y de la acción del Actor. A partir de los datos recibidos, realizará un pronóstico de la recompensa esperada (evaluará la acción del Actor). Como podemos ver, el Actor y el Crítico recibirán una descripción del entorno. Para minimizar las operaciones repetidas, hemos decidido organizar un bloque de procesamiento preliminar de los datos originales en el cuerpo del Actor. El crítico deberá transmitir una representación comprimida del estado del entorno a partir del estado latente del Actor. Y para minimizar el volumen de datos transmitidos entre el Actor y el Crítico en el lado del programa principal, hemos decidido crear métodos adicionales de pasada directa e inversa con transferencia en los punteros, no de búferes de datos individuales, sino directamente de punteros al modelo de datos de origen y al identificador de la capa con los datos de origen.

Primero veremos la organización del método de pasada directa CNet::feedForward. Los parámetros del método prevén la transmisión de los dos punteros a las redes neuronales (datos fuente originales y adicionales) y los dos identificadores de capas neuronales en estas redes.

bool CNet::feedForward(CNet *inputNet, int inputLayer=-1, CNet *secondNet = NULL, int secondLayer = -1)
  {
   if(!inputNet || !opencl)
      return false;

Hemos añadido valores predeterminados a los parámetros, lo cual permite utilizar el método transmitiendo solo un puntero al modelo de datos de origen principales.

En el cuerpo del método, verificaremos el puntero recibido al modelo de datos de origen principales. Y si no hay datos, saldremos del método con un resultado negativo.

A continuación, verificaremos el identificador de la capa neuronal en el modelo de datos de origen principales. Si por alguna razón no se ha especificado, usaremos la última capa neuronal del modelo.

   if(inputLayer<0)
      inputLayer=inputNet.layers.Total()-1;

En la siguiente etapa, organizaremos el trabajo para acceder a los datos adicionales. Así, crearemos un puntero nulo a un objeto de búfer de datos, y comprobaremos la relevancia del puntero al modelo de datos de origen adicionales.

   CBufferFloat *second = NULL;
   bool del_second = false;
   if(!!secondNet)
     {
      if(secondLayer < 0)
         secondLayer = secondNet.layers.Total() - 1;
      if(secondNet.GetOpenCL() != opencl)
        {
         secondNet.GetLayerOutput(secondLayer, second);
         if(!!second)
           {
            if(!second.BufferCreate(opencl))
              {
               delete second;
               return false;
              }
            del_second = true;
           }
        }
      else
        {
         if(secondNet.layers.Total() <= secondLayer)
            return false;
         CLayer *layer = secondNet.layers.At(secondLayer);
         CNeuronBaseOCL *neuron = layer.At(0);
         second = neuron.getOutput();
        }
     }

Si tenemos un puntero válido al modelo de datos fuente adicionales, habrá 2 opciones para el desarrollo de los eventos:

  1. Si el modelo de datos de origen adicionales y el modelo actual se cargan en diferentes contextos de OpenCL. Luego tendremos que recargar los datos en cualquier caso. Después copiaremos los datos de la capa correspondiente del modelo de datos al nuevo búfer y crearemos un búfer en el contexto requerido.
  2. Ambos modelos estarán en el mismo contexto de OpenCL. Los datos ya existen en la memoria contextual. Solo necesitaremos copiar el puntero al búfer de resultados de la capa neuronal deseada.

Tras obtener el búfer con los datos de origen adicionales, pasaremos al modelo de los datos de origen principales. Al igual que arriba, verificaremos si los modelos están cargados en la memoria del mismo contexto de OpenCL. De lo contrario, simplemente copiaremos los datos de origen al búfer y llamaremos al método de pasada directa desarrollado previamente.

   if(inputNet.opencl != opencl)
     {
      CBufferFloat *inputs;
      if(!inputNet.GetLayerOutput(inputLayer, inputs))
        {
         if(del_second)
            delete second;
         return false;
        }
      bool result = feedForward(inputs, 1, false, second);
      if(del_second)
         delete second;
      return result;
     }

Si ambos modelos están en el mismo contexto de OpenCL, reemplazaremos la capa de datos de origen con la capa neuronal especificada del modelo de datos de origen.

   CLayer *layer = inputNet.layers.At(inputLayer);
   if(!layer)
     {
      if(del_second)
         delete second;
      return false;
     }
   CNeuronBaseOCL *neuron = layer.At(0);
   layer = layers.At(0);
   if(!layer)
     {
      if(del_second)
         delete second;
      return false;
     }
   if(layer.At(0) != neuron)
      if(!layer.Update(0, neuron))
        {
         if(del_second)
            delete second;
         return false;
        }

Después de eso, organizaremos un ciclo de iteración de todas las capas neuronales, seguido de una llamada a los métodos de pasada directa.

   for(int l = 1; l < layers.Total(); l++)
     {
      layer = layers.At(l);
      neuron = layer.At(0);
      layer = layers.At(l - 1);
      if(!neuron.FeedForward(layer.At(0), second))
        {
         if(del_second)
            delete second;
         return false;
        }
     }
//---
   if(del_second)
      delete second;
   return true;
  }

Al finalizar las iteraciones del ciclo, saldremos del método con un resultado positivo.

Luego crearemos el método CNet::backProp de manera similar. Encontrará su código en el archivo adjunto.

Usaremos ambos métodos cuando entrenemos al Crítico, pero para entrenar al Actor, necesitaremos otro método de pasada inversa. El hecho es que en el método de pasada inversa, antes de transmitir el gradiente de error a través de las capas neuronales, primero determinaremos la desviación de los resultados de la pasada directa de los valores objetivo. El método de DDPG eliminará este proceso para el Actor. Y para la implementación práctica de este algoritmo se creará el método CNet::backPropGradient.

En los parámetros del método, transmitiremos los punteros a los dos búferes de datos: los datos de origen adicionales y el gradiente de error para ellos. Ambos búferes tienen valores predeterminados, lo cual permite ejecutar el método sin especificar los parámetros.

bool CNet::backPropGradient(CBufferFloat *SecondInput = NULL, CBufferFloat *SecondGradient = NULL)
  {
   if(
! layers || 
! opencl)
      return false;
   CLayer *currentLayer = layers.At(layers.Total() - 1);
   CNeuronBaseOCL *neuron = NULL;
   if(CheckPointer(currentLayer) == POINTER_INVALID)
      return false;

En el cuerpo del método, primero verificaremos la relevancia de los punteros a los objetos del array dinámico de capas neuronales y el contexto de OpenCL, y declararemos las variables locales necesarias.

Luego organizaremos un ciclo para distribuir el gradiente de error en todas las capas neuronales del modelo.

//--- Calc Hidden Gradients
   int total = layers.Total();
   for(int layerNum = total - 2; layerNum >= 0; layerNum--)
     {
      CLayer *nextLayer = currentLayer;
      currentLayer = layers.At(layerNum);
      if(CheckPointer(currentLayer) == POINTER_INVALID)
         return false;
      neuron = currentLayer.At(0);
      if(!neuron || !neuron.calcHiddenGradients(nextLayer.At(0), SecondInput, SecondGradient))
         return false;
     }

Tenga en cuenta que al organizar el proceso, asumiremos que el gradiente de error ya está en el búfer de la última capa neuronal. Esto está contemplado en el algoritmo de DDPG (gradiente de error del Crítico según las acciones del Agente). No hay control sobre la presencia del gradiente de error. Y la aplicación del método será responsabilidad del usuario.

Tras distribuir el gradiente de error, actualizaremos las matrices de coeficientes de peso.

   CLayer *prevLayer = layers.At(total - 1);
   for(int layerNum = total - 1; layerNum > 0; layerNum--)
     {
      currentLayer = prevLayer;
      prevLayer = layers.At(layerNum - 1);
      neuron = currentLayer.At(0);
      if(!neuron.UpdateInputWeights(prevLayer.At(0), SecondInput))
         return false;
     }

Aquí deberemos recordar que en los métodos de la capa neuronal solo colocaremos kernels en la cola de ejecución. Pero antes de realizar la pasada directa posterior, deberemos asegurarnos de que la operación de pasada inversa esté completa. Para asegurarnos de ello, cargaremos los resultados de la última actualización del kernel de la matriz de pesos.

   bool result=false;
   for(int layerNum = 0; layerNum < total; layerNum++)
     {
      currentLayer = layers.At(layerNum);
      CNeuronBaseOCL *temp = currentLayer.At(0);
      if(!temp)
        continue; 
      if(!temp.TrainMode() || !temp.getWeights())
         continue;
      if(!temp.getWeights().BufferRead())
         continue;
      result=true;
      break;
     }
//---
   return result;
  }

Con esto concluiremos nuestro trabajo de actualización de los métodos y las clases de nuestra biblioteca. Podrá ver su código entero en el archivo adjunto.

2.3. Creando el asesor de entrenamiento del modelo

A continuación, crearemos y entrenaremos el modelo utilizando el algoritmo de DDPG. La organización del proceso de entrenamiento se implementará en el asesor "DDPG\Study.mq5".

Como ya hemos mencionado, el modelo creado combinará elementos del DDPG y los enfoques discutidos anteriormente. Esto se reflejará en la arquitectura de nuestro modelo. Para describir la arquitectura, crearemos la función CreateDescriptions.

En los parámetros, la función recibirá los punteros a dos arrays dinámicos para registrar los objetos que describen la arquitectura de las capas neuronales del Actor y el Crítico. En el cuerpo de la función, verificaremos la relevancia de los punteros recibidos y, si es necesario, crearemos nuevos objetos de matriz.

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

Comenzaremos con una descripción de la arquitectura del Actor. Aquí utilizaremos los desarrollos de GCRL y construiremos un modelo con dos flujos de datos de origen. La toma de decisiones del Actor se basará en el estado actual del entorno (datos históricos). Para ellos crearemos una capa de datos de origen del tamaño correspondiente.

//--- Actor
   actor.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.window = 0;
   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 = prev_count;
   descr.batch = 1000;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Los datos en bruto se procesarán usando una capa de normalización por lotes y se transmitirán a través de un bloque de capas convolucionales.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count - 1;
   descr.window = 2;
   descr.step = 1;
   descr.window_out = 8;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = prev_count;
   descr.window = 8;
   descr.step = 8;
   descr.window_out = 8;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

A continuación, comprimiremos los datos en dos capas completamente conectadas. Todo esto puede recordarle el codificador anteriormente usado.

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 256;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 128;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Una evaluación de la situación del mercado puede resultar suficiente para determinar la dirección de las operaciones y los niveles de stop loss y take profit, pero insuficiente para las funciones de gestión del capital. Y en esta etapa, por analogía con el planteamiento de la tarea del modelo, añadiremos información sobre el estado de la cuenta.

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = 256;
   descr.window = prev_count;
   descr.step = AccountDescr;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

Recuerde el identificador de esta capa y el tamaño del vector con sus resultados. Precisamente de esta capa es de donde tomaremos la representación latente del estado del entorno como datos originales del Crítico.

Luego vendrá el bloque de toma de decisiones de las capas totalmente conectadas.

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 256;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 256;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

En la salida del actor, tendremos una capa completamente conectada de 6 elementos que representarán el volumen de la transacción, su stop loss y su take profit (3 elementos para comprar y 3 para vender).

//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 6;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!actor.Add(descr))
     {
      delete descr;
      return false;
     }

De forma simplificada, no añadiremos elementos para las acciones de cierre de posiciones y espera del punto de entrada/salida adecuado. Supondremos que las posiciones se cerrarán mediante stop loss o take profit, mientras que la generación de valores incorrectos para uno de los indicadores de transacción se corresponderá con la ausencia de una operación comercial.

El modelo del Crítico utiliza el estado actual del entorno y la acción del Actor para predecir las recompensas. En nuestro caso, ambos flujos de información provienen del modelo del Actor, pero de diferentes capas neuronales, y, en consecuencia, de diferentes búferes de datos. Para combinar los dos flujos de datos, utilizaremos una capa de concatenación de datos. Esto se reflejará en la arquitectura del modelo del Crítico de la siguiente manera. El primer flujo de información (representación latente del estado actual) lo transmitiremos a la capa de datos de origen. El tamaño de esta capa deberá corresponderse con el tamaño de la capa neuronal del Actor de la que planeamos tomar los datos.

//--- Critic
   critic.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = 256;
   descr.window = 0;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }

Los datos provienen del estado interno del otro modelo, así que podemos saltarnos la capa de normalización de datos.

A continuación, utilizaremos una capa de concatenación para combinar los dos flujos de información. El tamaño de los datos adicionales será igual al tamaño de la capa de resultados del Actor.

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConcatenate;
   descr.count = 128;
   descr.window = prev_count;
   descr.step = 6;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }

Luego vendrá un bloque de decisión que constará de dos capas completamente conectadas.

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 128;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 128;
   descr.activation = LReLU;
   descr.optimization = ADAM;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }

En la salida del Crítico se utilizará una capa completamente conectada con un elemento sin función de activación. Aquí esperaremos obtener el valor previsto de la recompensa,

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   descr.count = 1;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!critic.Add(descr))
     {
      delete descr;
      return false;
     }
//---
   return true;
  }

y para no confundirnos en el futuro con el identificador de la capa de representación latente del estado del entorno, definiremos una constante en forma de macrosustitución.

#define                    LatentLayer  6

Ahora que hemos decidido la arquitectura de los modelos, comenzaremos a trabajar en el algoritmo del asesor. Primero crearemos el método OnInit para inicializar el asesor. Al comienzo del método, como antes, inicializaremos los objetos de indicador y operaciones comerciales.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   if(!Symb.Name(_Symbol))
      return INIT_FAILED;
   Symb.Refresh();
//---
   if(!RSI.Create(Symb.Name(), TimeFrame, RSIPeriod, RSIPrice))
      return INIT_FAILED;
//---
   if(!CCI.Create(Symb.Name(), TimeFrame, CCIPeriod, CCIPrice))
      return INIT_FAILED;
//---
   if(!ATR.Create(Symb.Name(), TimeFrame, ATRPeriod))
      return INIT_FAILED;
//---
   if(!MACD.Create(Symb.Name(), TimeFrame, FastPeriod, SlowPeriod, SignalPeriod, MACDPrice))
      return INIT_FAILED;
   if(!RSI.BufferResize(HistoryBars) || !CCI.BufferResize(HistoryBars) ||
      !ATR.BufferResize(HistoryBars) || !MACD.BufferResize(HistoryBars))
     {
      PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
      return INIT_FAILED;
     }
//---
   if(!Trade.SetTypeFillingBySymbol(Symb.Name()))
      return INIT_FAILED;

Luego intentaremos cargar los modelos previamente entrenados. Si no existen, iniciaremos el proceso de creación de los modelos.

Aquí merece la pena prestar atención a un matiz. Si antes creábamos un modelo de entrenamiento y lo copiábamos completamente en el modelo objetivo, ahora inicializaremos el modelo de entrenamiento y el modelo objetivo con parámetros aleatorios. Además, ambos modelos utilizarán la misma arquitectura.

//--- load models
   float temp;
   if(!Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !Critic.Load(FileName + "Crt.nnw", temp, temp, temp, dtStudied, true) ||
      !TargetActor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) ||
      !TargetCritic.Load(FileName + "Crt.nnw", temp, temp, temp, dtStudied, true))
     {
      CArrayObj *actor = new CArrayObj();
      CArrayObj *critic = new CArrayObj();
      if(!CreateDescriptions(actor, critic))
        {
         delete actor;
         delete critic;
         return INIT_FAILED;
        }
      if(!Actor.Create(actor) || !Critic.Create(critic) ||
         !TargetActor.Create(actor) || !TargetCritic.Create(critic))
        {
         delete actor;
         delete critic;
         return INIT_FAILED;
        }
      delete actor;
      delete critic;
      //---
     }

A continuación, transferiremos todos los modelos a un único contexto de OpenCL, lo cual nos permitirá operar con los punteros a los búferes de datos sin realizar el copiado físico al transferir información entre modelos.

   COpenCLMy *opencl = Actor.GetOpenCL();
   Critic.SetOpenCL(opencl);
   TargetActor.SetOpenCL(opencl);
   TargetCritic.SetOpenCL(opencl);

A esto le seguirá un bloque para monitorear la correspondencia de las arquitecturas modelo.

   Actor.getResults(Result);
   if(Result.Total() != 6)
     {
      PrintFormat("The scope of the actor does not match the actions count (%d <> %d)", 6, Result.Total());
      return INIT_FAILED;
     }
   ActorResult = vector<float>::Zeros(6);
//---
   Actor.GetLayerOutput(0, Result);
   if(Result.Total() != (HistoryBars * BarDescr))
     {
      PrintFormat("Input size of Actor doesn't match state description (%d <> %d)", Result.Total(), (HistoryBars * BarDescr));
      return INIT_FAILED;
     }
//---
   Actor.GetLayerOutput(LatentLayer, Result);
   int latent_state = Result.Total();
   Critic.GetLayerOutput(0, Result);
   if(Result.Total() != latent_state)
     {
      PrintFormat("Input size of Critic doesn't match latent state Actor (%d <> %d)", Result.Total(), latent_state);
      return INIT_FAILED;
     }

Luego inicializaremos las variables globales y finalizaremos el método.

   PrevBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   PrevEquity = AccountInfoDouble(ACCOUNT_EQUITY);
   FirstBar = true;
   Gradient.BufferInit(AccountDescr, 0);
   Gradient.BufferCreate(opencl);
//---
   return(INIT_SUCCEEDED);
  }

Hemos determinado que los modelos objetivo se actualizarán después de cada episodio, por lo tanto, esta funcionalidad se ha incluido en el método de desinicialización del asesor. Primero actualizaremos los modelos objetivo, y luego los guardaremos. Tenga en cuenta que guardaremos los modelos objetivo, no los entrenados. Por lo tanto, queremos minimizar el reentrenamiento del modelo para un solo episodio.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   TargetActor.WeightsUpdate(GetPointer(Actor), Tau);
   TargetCritic.WeightsUpdate(GetPointer(Critic), Tau);
   TargetActor.Save(FileName + "Act.nnw", Actor.getRecentAverageError(), 0, 0, TimeCurrent(), true);
   TargetCritic.Save(FileName + "Crt.nnw", Critic.getRecentAverageError(), 0, 0, TimeCurrent(), true);
   delete Result;
  }

El proceso real de entrenamiento del modelo se realizará en el flujo de acciones. En nuestro caso, entrenaremos el modelo en el simulador de estrategias en el modo de pasada por la historia. No crearemos un búfer de reproducción de experiencia. Su función será desempeñada por el propio simulador de estrategias. Así, todo el proceso de aprendizaje se organizará en la función OnTick.

Al comienzo de la función, verificaremos la aparición del evento de apertura de una nueva vela. Después de eso, actualizaremos los datos de los indicadores y los datos históricos sobre el movimiento del precio del instrumento en los búferes.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(!IsNewBar())
      return;
//---
   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();

El proceso de preparación de datos ha sido transferido al completo desde los asesores anteriormente analizados, por lo que me saltaré su descripción. Encontrará el código completo del asesor y todas sus funciones en el archivo adjunto.

Tras preparar los datos iniciales, comprobaremos si previamente se ha realizado un pasada directa del modelo entrenado, y de ser así, realizaremos una pasada inversa. Para evaluar el estado actual, realizaremos un pasada directa del modelo objetivo. Tenga en cuenta que primero realizaremos un pasada directa del modelo objetivo del Actor, y ya teniendo en cuenta la acción formada, realizaremos un pasada directa del modelo objetivo del Crítico. Al valor resultante le sumaremos la recompensa real del sistema en forma de cambio en el balance de la cuenta. Además, si no hay posiciones abiertas, añadiremos una penalización para alentar al Actor a operar de forma activa, y realizaremos la pasada inversa primero del Crítico y luego del Actor.

   if(!FirstBar)
     {
      if(!TargetActor.feedForward(GetPointer(State), 1, false, GetPointer(Account)))
         return;
      if(!TargetCritic.feedForward(GetPointer(TargetActor), LatentLayer, GetPointer(TargetActor)))
         return;
      TargetCritic.getResults(Result);
      float reward = (float)(account[0] - PrevBalance + Result[0]);
      if(account[0] == PrevBalance)
         if((buy_value + sell_value) == 0)
            reward -= 1;
      Result.Update(0, reward);
      if(!Critic.backProp(Result, GetPointer(Actor)) || !Actor.backPropGradient(GetPointer(PrevAccount), GetPointer(Gradient)))
         return;
     }

Tenga en cuenta que para la pasada inversa del Crítico, utilizaremos el método backProp actualizado con transmisión del búfer de valores objetivo y el puntero al modelo del Actor. Al mismo tiempo, no indicaremos el identificador de la capa latente, ya que previamente (durante la pasada directa) hemos reemplazado los objetos,

pero para la pasada inversa del Actor, utilizaremos el método backPropGradient, en el que el gradiente de pasada inversa del Crítico se propaga a través del modelo.

La ejecución de la pasada inversa del Crítico y el Actor nos permitirá optimizar la función Q de nuestro modelo.

A continuación, realizaremos una pasada directa a través del modelo entrenado.

   if(!Actor.feedForward(GetPointer(State), 1, false, GetPointer(Account)))
      return;
   if(!Critic.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor)))
      return;

Aquí merece la pena prestar atención al siguiente aspecto: en el proceso de entrenamiento de la función Q, solo mejoraremos la calidad de la predicción de la recompensa esperada, pero no entrenaremos al Actor para incrementar la rentabilidad de sus acciones. Para ello, el algoritmo de DDPG prevé la actualización de los parámetros del Actor para aumentar la recompensa prevista. Vale la pena señalar que en este punto pasaremos el gradiente de error a través del Crítico, pero no actualizaremos sus parámetros. Por lo tanto, deshabilitaremos la actualización de las matrices de peso del Crítico estableciendo el indicador TrainMode en false, y después de la pasada inversa del Actor, retornaremos la posición de la bandera a true.

   if(!FirstBar)
     {
      Critic.getResults(Result);
      Result.Update(0, Result.At(0) + MathAbs(Result.At(0) * 0.0001f));
      Critic.TrainMode(false);
      if(!Critic.backProp(Result, GetPointer(Actor)) || !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient)))
         return;
      Critic.TrainMode(true);
     }

Luego guardaremos en variables globales el valor para las operaciones en la siguiente barra,

   FirstBar = false;
   PrevAccount.AssignArray(GetPointer(Account));
   PrevAccount.BufferCreate(Actor.GetOpenCL());
   PrevBalance = account[0];
   PrevEquity = account[1];

y luego solo nos quedará descifrar los resultados del trabajo del Actor y realizar operaciones comerciales. En este ejemplo, entrenaremos a un Actor para que proporcione valores absolutos del volumen de transacciones y los niveles comerciales. Lo único que haremos es normalizar los datos y convertir los niveles en valores de precios específicos.

   vector<float> temp;
   Actor.getResults(temp);
   float delta = MathAbs(ActorResult - temp).Sum();
   ActorResult = temp;
//---
   double min_lot = Symb.LotsMin();
   double stops = MathMax(Symb.StopsLevel(), 1) * Symb.Point();
   double buy_lot = MathRound((double)ActorResult[0] / min_lot) * min_lot;
   double sell_lot = MathRound((double)ActorResult[3] / min_lot) * min_lot;
   double buy_tp = NormalizeDouble(Symb.Ask() + ActorResult[1], Symb.Digits());
   double buy_sl = NormalizeDouble(Symb.Ask() - ActorResult[2], Symb.Digits());
   double sell_tp = NormalizeDouble(Symb.Bid() - ActorResult[4], Symb.Digits());
   double sell_sl = NormalizeDouble(Symb.Bid() + ActorResult[5], Symb.Digits());
//---
   if(ActorResult[0] > min_lot && ActorResult[1] > stops && ActorResult[2] > stops && buy_sl > 0)
      Trade.Buy(buy_lot, Symb.Name(), Symb.Ask(), buy_sl, buy_tp);
   if(ActorResult[3] > min_lot && ActorResult[4] > stops && ActorResult[5] > stops && sell_tp > 0)
      Trade.Sell(sell_lot, Symb.Name(), Symb.Bid(), sell_sl, sell_tp);

Permítanme recordarles que no hemos previsto una acción aparte del Actor para esperar una situación adecuada. En su lugar, utilizaremos valores incorrectos de los indicadores de transacción. Por lo tanto, antes de enviar una solicitud comercial, verificaremos la exactitud de los parámetros obtenidos.

Vale la pena señalar un punto más que no está previsto en el algoritmo analizado, pero que he añadido yo. No contradice el método considerado, sino que solo introduce algunas restricciones en la política de entrenamiento del Actor. Por lo tanto, querría introducir algún marco en el volumen de la posición abierta y el tamaño de los niveles comerciales.

Al recibir parámetros de transacción incorrectos o inflados, hemos formado un vector de valores objetivo aleatorios dentro de los límites especificados y realizado una pasada inversa del Actor, similar a los métodos de aprendizaje supervisado, lo cual, en mi opinión, debería retornar los resultados del funcionamiento del Actor a los límites establecidos.

   if(temp.Min() < 0 || MathMax(temp[0], temp[3]) > 1.0f || MathMax(temp[1], temp[4]) > (Symb.Point() * 5000) ||
      MathMax(temp[2], temp[5]) > (Symb.Point() * 2000))
     {
      temp[0] = (float)(Symb.LotsMin() * (1 + MathRand() / 32767.0 * 5));
      temp[3] = (float)(Symb.LotsMin() * (1 + MathRand() / 32767.0 * 5));
      temp[1] = (float)(Symb.Point() * (MathRand() / 32767.0 * 500.0 + Symb.StopsLevel()));
      temp[4] = (float)(Symb.Point() * (MathRand() / 32767.0 * 500.0 + Symb.StopsLevel()));
      temp[2] = (float)(Symb.Point() * (MathRand() / 32767.0 * 200.0 + Symb.StopsLevel()));
      temp[5] = (float)(Symb.Point() * (MathRand() / 32767.0 * 200.0 + Symb.StopsLevel()));
      Result.AssignArray(temp);
      Actor.backProp(Result, GetPointer(PrevAccount), GetPointer(Gradient));
     }
  }

Obviamente, una alternativa podría ser utilizar una función de activación restringida (por ejemplo, una sigmoide), pero entonces limitaríamos estrictamente el rango de valores posibles, y durante el entrenamiento, se podrían alcanzar rápidamente los valores límite, lo cual ralentizaría el posterior entrenamiento del modelo.

Tras completar todas las operaciones, entraremos en modo de espera para el siguiente tick.

En el archivo adjunto encontrará el código completo del asesor y todos los programas utilizados en el artículo.


3. Simulación

Una vez finalizado el trabajo sobre el modelo del asesor de entrenamiento, pasaremos a comprobar los resultados del trabajo realizado. Al igual que antes, el modelo se entrenará con los datos históricos del marco temporal H1 del instrumento EURUSD en un periodo que partirá desde principios de 2023. Todos los parámetros de los indicadores y del entrenamiento del modelo utilizarán valores predeterminados.

Entrenamiento de modelos

El entrenamiento del modelo en tiempo real introducirá sus propios ajustes y no permitirá el uso de varios agentes paralelos. Por tanto, las primeras comprobaciones sobre el funcionamiento correcto del algoritmo del asesor se realizarán en el modo de ejecución única, y luego se seleccionará el modo de optimización lenta, activando solo un agente de optimización local.

Para regular el número de iteraciones de entrenamiento, se añadirá el parámetro externo Agent, que no se utilizará en el algoritmo del asesor.

Gestionando el número de pasadas de optimización

Después de aproximadamente 3000 pasadas, hemos podido obtener un modelo capaz de generar ganancias en el conjunto de entrenamiento. Durante un periodo de entrenamiento de 5 meses, el modelo ha realizado 334 transacciones. Más del 84% de ellas han sido rentables. Como resultado se ha obtenido un beneficio del 33% del capital inicial. Al mismo tiempo, la reducción de los balances ha sido inferior al 1%, mientras que la equidad se ha reducido un 7,6%. El factor de beneficio ha superado el valor 26, y el factor de recuperación ha ascendido a 3,16. El siguiente gráfico muestra una tendencia alcista en el balance. Y la línea de saldo casi siempre está por debajo de la línea de Equidad, lo cual indica que se están abriendo posiciones en la dirección correcta. Al mismo tiempo, la carga sobre el depósito es de aproximadamente el 20%. Es una cifra bastante elevada, pero que no supera el beneficio acumulado.

Resultados del entrenamiento del modelo

Resultados del entrenamiento del modelo

Desafortunadamente, fuera del conjunto de entrenamiento, los resultados del funcionamiento del asesor han sido más modestos.


Conclusión

En este artículo, hemos analizado la aplicación del aprendizaje por refuerzo en el contexto de un espacio continuo de acciones y nos hemos familiarizado con el método Deep Deterministic Policy Gradient (DDPG). Este enfoque abre nuevas oportunidades para entrenar agentes en la gestión del capital y el riesgo, un aspecto esencial para el comercio exitoso.

Asimismo, hemos desarrollado y probado un asesor para entrenar el modelo. Dicho asesor no solo predice la dirección del comercio, sino que también determina el volumen de las transacciones y los niveles de stop loss y take profit. Esto permite al Agente gestionar las inversiones de forma más eficaz.

Durante las pruebas, hemos entrenado el modelo para generar ganancias en el conjunto de entrenamiento, pero, lamentablemente, el entrenamiento realizado no ha resultado suficiente para obtener resultados similares fuera del conjunto de entrenamiento. El punto débil de nuestra implementación es el entrenamiento online del modelo, que no permite el uso paralelo de varios agentes para aumentar el nivel de exploración del entorno y reducir el tiempo de entrenamiento del modelo.

Los resultados obtenidos nos permiten tener la esperanza de entrenar el modelo para que funcione de forma estable fuera del conjunto de entrenamiento.


Enlaces

  • Continuous Control with Deep Reinforcement Learning
  • Redes neuronales: así de sencillo (Parte 27): Aprendizaje Q profundo (DQN)
  • Redes neuronales: así de sencillo (Parte 29): Algoritmo actor-crítico con ventaja (Advantage actor-critic)
  • Redes neuronales: así de sencillo (Parte 46): Aprendizaje por refuerzo dirigido a objetivos (GCRL)

  • Programas usados en el artículo

    # Nombre Tipo Descripción
    1 Study.mq5  Asesor Asesor de entrenamiento del agente
    2 Test.mq5 Asesor Asesor para la prueba de modelos
    3 NeuroNet.mqh Biblioteca de clases Biblioteca de clases para crear una red neuronal
    4 NeuroNet.cl Biblioteca Biblioteca de código de programa OpenCL


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

    Archivos adjuntos |
    MQL5.zip (305.57 KB)
    DoEasy. Elementos de control (Parte 32): "ScrollBar" horizontal, desplazamiento con la rueda del ratón DoEasy. Elementos de control (Parte 32): "ScrollBar" horizontal, desplazamiento con la rueda del ratón
    En este artículo completaremos el desarrollo de la funcionalidad del objeto de barra de desplazamiento horizontal. Asimismo, haremos posible el desplazamiento del contenido del contenedor moviendo el control deslizante de la barra de desplazamiento y girando la rueda del ratón. También introduciremos ciertas adiciones a la biblioteca considerando la nueva política de ejecución de órdenes aparecida en el terminal y los nuevos códigos de error de ejecución en MQL5.
    Posibilidades de ChatGPT de OpenAI en el marco de desarrollo de MQL4 y MQL5 Posibilidades de ChatGPT de OpenAI en el marco de desarrollo de MQL4 y MQL5
    En este artículo, experimentaremos y analizaremos la inteligencia artificial ChatGPT de OpenAI para comprender sus capacidades y reducir el tiempo y la intensidad del trabajo en el desarrollo de nuestros asesores, indicadores y scripts. Asimismo, repasaremos rápidamente esta tecnología e intentaremos ver cómo usarla correctamente para programar en MQL4 y MQL5.
    Desarrollo de un sistema de repetición — Simulación de mercado (Parte 17): Ticks y más ticks (I) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 17): Ticks y más ticks (I)
    Aquí vamos a empezar a ver cómo implementar algo realmente interesante y curioso. Pero al mismo tiempo, es extremadamente complicado debido a algunas cuestiones que muchos confunden. Y lo peor que puede pasar es que algunos operadores que se autodenominan profesionales no tienen idea de la importancia de estos conceptos en el mercado de capitales. Sí, a pesar de que el enfoque aquí es la programación, comprender algunas cuestiones relacionadas con las operaciones en los mercados es de suma importancia para lo que vamos a empezar a implementar aquí.
    Evaluación de modelos ONNX usando métricas de regresión Evaluación de modelos ONNX usando métricas de regresión
    La regresión es una tarea que consiste en predecir un valor real a partir de un ejemplo sin etiquetar. Para evaluar la precisión de las predicciones de los modelos de regresión, se usan las llamadas métricas de regresión.