Redes neuronales en el trading: Modelos híbridos de secuencias de grafos (Final)
Introducción
En el artículo anterior, revisamos los aspectos teóricos del framework unificado de secuencias de grafos GSM++, un método innovador para el procesamiento de datos que combina las ventajas de diferentes arquitecturas. El GSM++ incluye tres pasos clave: la tokenización del grafo, la codificación local de nodos y la codificación global de dependencias. Esta estructura permite trabajar de forma más eficiente con datos representados como grafos, mejorando la capacidad analítica para resolver problemas complejos en finanzas y otras áreas relacionadas con el análisis de series temporales y datos estructurados.
Uno de los elementos más importantes del sistema es la tokenización jerárquica de grafos, que permite transformar datos complejos en una representación secuencial compacta. La metodología de tokenización utilizada en GSM++ mantiene las características topológicas y temporales de los datos de origen, lo que aumenta significativamente la precisión de la extracción de características. Además, este enfoque ayuda a reducir el coste computacional del análisis de grandes cantidades de datos equilibrando la velocidad de procesamiento de la información con la profundidad del análisis. Según la tarea que se vaya a realizar, se puede adaptar el nivel de detalle del análisis, lo que hace que la metodología sea versátil y flexible.
Un papel importante lo desempeña la codificación local de nodos, responsable del procesamiento de la información a nivel de los elementos individuales del grafo. Los métodos de análisis tradicionales suelen tener el problema de la redundancia de información, lo cual aumenta la carga computacional y complica el proceso de identificación de patrones. Sin embargo, el uso de mecanismos de codificación adaptativos permite destacar las características más relevantes de los nodos y transmitirlas de forma eficaz a los niveles de análisis posteriores. Esto ofrece la oportunidad de reducir la cantidad de información irrelevante y también mejora la capacidad del modelo para identificar relaciones locales entre nodos. Otra ventaja de la codificación local es su capacidad para adaptarse dinámicamente a los cambios en los datos de origen, lo cual resulta especialmente importante en los mercados financieros volátiles, donde los cambios repentinos pueden afectar significativamente a la precisión de las previsiones.
Otra mejora de la capacidad analítica se logra mediante el uso de un codificador híbrido que combina las capacidades de los modelos de recurrencia y los transformadores. Este enfoque combina las ventajas de ambos métodos: los mecanismos basados en la recurrencia ofrecen un procesamiento eficaz de las series temporales, pues permiten considerar la secuencialidad de los acontecimientos, mientras que los transformadores que utilizan el mecanismo de Self-Attention permiten detectar eficazmente relaciones complejas en los datos independientemente de su orden. Esta combinación hace que el modelo no solo sea más preciso, sino también más resistente a los cambios en la dinámica del mercado. Además, el codificador híbrido puede adaptarse a diferentes escenarios, lo cual permite personalizar el equilibrio entre precisión de predicción y eficiencia computacional según las exigencias de la tarea.

En la parte práctica del artículo anterior comenzamos a implementar nuestra propia visión de los planteamientos propuestos por los autores del framework GSM++ mediante MQL5. Dada la gran variabilidad de los datos financieros, abandonamos la clusterización jerárquica basada en la similitud (HAC) propuesta por los autores de framework. En su lugar, se usa un módulo de tokenización mixto entrenado que aumenta enormemente la flexibilidad y adaptabilidad del modelo al trabajar con datos reales del mercado.
El algoritmo de tokenización mixta (mixture of tokenization — MoT) implementado implica el uso de cuatro tipos diferentes de tokens para cada barra, lo que permite un análisis más detallado de la información del mercado. En este experimento, usaremos los siguientes enfoques para codificar los datos de origen:
- Tokenización de nodos: cada barra se trata como un elemento independiente del análisis, lo cual nos permite evaluar sus características individuales e identificar los parámetros clave que afectan a la evolución posterior.
- Tokenización de aristas: consiste en analizar las interdependencias entre barras adyacentes, identificando correlaciones y tendencias a corto plazo que pueden ser útiles para predecir cambios a corto plazo.
- Tokenización de subgrafos: analiza grupos de barras para revelar estructuras más complejas y patrones estables importantes para la previsión estratégica.
- Tokenización de subgrafos de secuencias unitarias individuales: posibilita un análisis profundo de las secuencias unitarias y las interdependencias en ellas, lo cual resulta especialmente importante a la hora de revelar patrones ocultos en los datos.
Estos tokens se combinan mediante el mecanismo de Attention Pooling, que se toma prestado del framework R-MAT. Este método permite al modelo centrarse en las características más relevantes, descartando los datos menos importantes, lo cual mejora notablemente el proceso de toma de decisiones. La principal ventaja del Attention Pooling es su capacidad para procesar eficazmente estructuras de datos complejas, destacando las características más relevantes y minimizando el impacto del ruido.
Para aplicar el enfoque propuesto, creamos el objeto CNeuronMoT, que hereda la funcionalidad básica del objeto CNeuronMHAttentionPoolingposibilitando una aplicación efectiva del algoritmo Attention Pooling. Este enfoque modular aumenta la adaptabilidad del modelo, permite procesar mejor los datos del mercado y mejora la calidad del análisis y la predicción de los movimientos de los precios, lo que hace que esta herramienta sea útil para el trading algorítmico.
La siguiente etapa del procesamiento de datos es la codificación local de los nodos. En nuestra aplicación, decidimos utilizar el módulo de suavizado adaptativo de características creado anteriormente. El método de suavizado adaptativo de características de nodos (Node-Adaptive Feature Smoothing — NAFS) permite formar incorporaciones de nodos más informativas que consideren tanto las características estructurales del grafo como las características de los nodos individuales. Este método se basa en el supuesto de que los distintos nodos pueden tener distintos grados de suavizado. Esto permite procesar de forma adaptativa cada nodo según su entorno. El NAFS adopta un enfoque combinado que utiliza el suavizado de bajo y alto orden, que considera de forma eficaz las interdependencias locales y globales en el gráfico.
El NAFS usa un método de conjunto para combinar características. Este planteamiento aumenta la solidez del modelo frente al ruido y hace más robusto el proceso de codificación. Ventajas de uso del módulo NAFS:
- El filtrado de datos flexible para destacar las características más relevantes y eliminar los componentes de ruido.
- La optimización del coste computacional, especialmente importante al analizar gráficos de gran tamaño y los datos de mercado de alta frecuencia.
- La adaptabilidad a condiciones cambiantes, gracias a la capacidad de ajuste dinámico de los parámetros de suavizado.
- La mejora de la precisión del modelo está garantizada por una combinación equilibrada de análisis detallado y alta generalizabilidad de los algoritmos.
El último elemento clave de GSM++ es el codificador híbrido. Los autores del framework proponen combinar en él el módulo Mamba y el Transformer. En nuestra aplicación, seguimos el concepto propuesto. Sin embargo, vamos a ir más allá y sustituir Mamba por Quimera y el Transformer por el Hidformer.
Chimera usa modelos de espacio de estados bidimensionales (2D-SSM), lo que le permite modelar eficazmente las dependencias a lo largo del eje temporal y una dimensión adicional relacionada con la topología del grafo. Este enfoque mejora considerablemente la capacidad de analizar las complejas interrelaciones de los mercados. Las ventajas de Quimera incluyen:
- La codificación bivariante de las dependencias contribuye a mejorar la detección de patrones de mercado ocultos y la precisión de las previsiones;
- Una mayor expresividad del modelo, que permite un análisis más profundo de las complejas relaciones no lineales entre activos;
- La adaptabilidad a los cambios dinámicos del mercado permite al modelo ajustarse rápidamente a las condiciones cambiantes del mercado.
El Hidformer tiene una arquitectura de doble torre y, a diferencia del Transformer clásico, divide el procesamiento de los datos de origen en dos flujos: un codificador analiza las dependencias temporales y el otro los componentes de frecuencia de los datos del mercado. Esta solución permite modelizar con mayor precisión la dinámica de los procesos de mercado. Las principales ventajas del Hidformer son:
- La separación del análisis en componentes de tiempo y frecuencia, ofreciendo una predicción más precisa de la dinámica del mercado;
- El uso de la atención recursiva en el codificador temporal y de la atención lineal en el codificador frecuencial, que reduce la complejidad computacional y mejora el rendimiento.
Así, el uso de Chimera y el Hidformer en GSM++ permitirá lograr una gran precisión en la codificación de las dependencias, minimizar el impacto del ruido del mercado y mejorar la fiabilidad de las previsiones analíticas.
Ajustamos el módulo SSM
Le recordamos que durante la comprobación del modelo construido con el framework Quimera, hemos prestado atención a la duración del mantenimiento de la posición. Entonces partimos del supuesto de que el modelo solo capta las tendencias a largo plazo, ignorando las fluctuaciones a corto plazo. En un intento de eliminar este inconveniente, hemos decidido modernizar ligeramente el objeto implementado anteriormente y añadir un objeto interno más del modelo bidimensional de espacio de estados. Los algoritmos actualizados se implementan en el objeto CNeuronChimeraPlus, cuya estructura le mostramos a continuación.
class CNeuronChimeraPlus : public CNeuronChimera { protected: CNeuron2DSSMOCL cSSMPlus; CLayer cDiscretizationPlus; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; //--- virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronChimeraPlus(void) {}; ~CNeuronChimeraPlus(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window_in, uint window_out, uint units_in, uint units_out, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronChimeraPlus; } //--- virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau); virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool Clear(void) override; };
Como podemos ver en la estructura presentada del nuevo objeto, no hemos reescrito completamente el objeto CNeuronChimera creado anteriormente. Por el contrario, lo hemos usado como clase padre, lo que nos ha permitido heredar toda la funcionalidad creada anteriormente. Sin embargo, la incorporación del nuevo tercer módulo 2D-SSM y del correspondiente bloque de proyección de datos requiere una redefinición del conjunto de métodos virtuales que ya conocemos. La inicialización de los objetos recién declarados y heredados se realiza en el método Init, cuya estructura de parámetros se hereda completamente del método similar de la clase padre.
bool CNeuronChimeraPlus::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window_in, uint window_out, uint units_in, uint units_out, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronChimera::Init(numOutputs, myIndex, open_cl, window_in, window_out, units_in, units_out, optimization_type, batch)) return false;
En el cuerpo del método, lo primero que hacemos es llamar al método homónimo de la clase padre, que ya se encarga de implementar los puntos de control de los valores recibidos y los algoritmos de inicialización de los objetos heredados.
Una vez se hayan ejecutado con éxito las operaciones de los métodos de la clase padre, pasaremos a la fase de inicialización de los objetos recién declarados que proporcionan la funcionalidad ampliada del modelo. Uno de los componentes clave añadidos en esta fase es el módulo opcional de modelo bidimensional de espacio de estados (2D-SSM).
Cabe recordar que en el método de la clase padre ya hemos inicializado dos módulos 2D-SSM, cada uno de los cuales realiza una tarea específica. Uno opera en las dimensionalidades de resultados especificadas, proporcionando una codificación estándar de las dependencias espaciales, mientras que el otro utiliza un espacio de características ampliado, que permite abarcar relaciones más complejas y multinivel entre los elementos del análisis.
Para aumentar la generalizabilidad del modelo y mejorar la precisión del procesamiento de los datos de mercado, el módulo 2D-SSM adicional, a diferencia de los existentes, trabajará en el espacio de características especificado, pero con una proyección ampliada a lo largo de la dimensión temporal. Así, esta arquitectura crea las condiciones para realizar análisis más precisos de series temporales y datos de mercado distribuidos espacialmente.
int index = 0; if(!cSSMPlus.Init(0, index, OpenCL, window_in, window_out, units_in, 2 * units_out, optimization, iBatch)) return false;
A continuación, tenemos que realizar una proyección de los resultados en un subespacio determinado. Y aquí debemos señalar que no podemos realizar inmediatamente proyecciones a lo largo de la dimensión temporal. Como consecuencia, necesitaremos montar una pequeña secuencia interna de objetos.
En primer lugar, preparemos un array dinámico las y variables locales para registrar los punteros a los objetos que se van a crear.
CNeuronTransposeOCL *transp = NULL; CNeuronConvOCL *conv = NULL; cDiscretizationPlus.Clear(); cDiscretizationPlus.SetOpenCL(OpenCL);
Lo primero que debemos hacer es crear un objeto de transposición de datos que nos permitirá llevar los datos al aspecto deseado.
index++; transp = new CNeuronTransposeOCL(); if(!transp || !transp.Init(0, index, OpenCL, 2 * units_out, window_out, optimization, iBatch) || !cDiscretizationPlus.Add(transp)) { delete transp; return false; }
A continuación, añadimos una capa convolucional de proyección de datos en la dimensionalidad establecida de la dimensión temporal.
index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, index, OpenCL, 2 * units_out, 2 * units_out, units_out, window_out, 1, optimization, iBatch) || !cDiscretizationPlus.Add(conv)) { delete conv; return false; } conv.SetActivationFunction(None);
Después retornamos los datos a la representación original mediante otro objeto de transposición.
index++; transp = new CNeuronTransposeOCL(); if(!transp || !transp.Init(0, index, OpenCL, window_out, units_out, optimization, iBatch) || !cDiscretizationPlus.Add(transp)) { delete transp; return false; } transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation()); //--- return true; }
Con esto daremos por completo el proceso de inicialización de los objetos internos y finalizaremos el método, retornando previamente el resultado lógico de las operaciones al programa que realiza la llamada.
El siguiente paso en nuestro trabajo consiste en construir el método feedForward. Nótese aquí que en la salida del método similar de la clase padre se dan datos normalizados. Como ya sabe, esta operación modifica la distribución de los datos. Y sumar esos datos con los resultados no normalizados del flujo de información añadido puede dar lugar a un desplazamiento impredecible hacia una de las líneas troncales. Para evitarlo, reescribiremos por completo el método de pasada directa.
bool CNeuronChimeraPlus::feedForward(CNeuronBaseOCL *NeuronOCL) { for(uint i = 0; i < caSSM.Size(); i++) { if(!caSSM[i].FeedForward(NeuronOCL)) return false; } if(!cSSMPlus.FeedForward(NeuronOCL)) return false;
En los parámetros del método obtenemos el puntero al objeto de datos de origen, que pasamos inmediatamente a los métodos homónimos de los modelos internos de espacio de estados. Los modelos internos de espacio de estados generan resultados en tres proyecciones distintas.
A continuación, usamos los objetos de discretización interna para llevar los resultados de los modelos del espacio de estados a una forma comparable.
if(!cDiscretization.FeedForward(caSSM[1].AsObject())) return false; CNeuronBaseOCL *inp = NeuronOCL; CNeuronBaseOCL *current = NULL; for(int i = 0; i < cDiscretizationPlus.Total(); i++) { current = cDiscretizationPlus[i]; if(!current || !current.FeedForward(inp)) return false; inp = current; }
Además, obtenemos la proyección de los datos de origen sobre la línea troncal de enlaces residuales.
inp = NeuronOCL; for(int i = 0; i < cResidual.Total(); i++) { current = cResidual[i]; if(!current || !current.FeedForward(inp)) return false; inp = current; }
Por último, sumamos los datos de los cuatro flujos de información. En este caso, la normalización de los valores solo se efectúa en la fase final.
inp = cDiscretizationPlus[-1]; if(!SumAndNormilize(caSSM[0].getOutput(), cDiscretization.getOutput(), Output, 1, false, 0, 0, 0, 1) || !SumAndNormilize(Output, inp.getOutput(), Output, 1, false, 0, 0, 0, 1) || !SumAndNormilize(Output, current.getOutput(), Output, cDiscretization.GetFilters(), true, 0, 0, 0, 1)) return false; //--- return true; }
Después retornamos el resultado lógico de las operaciones al programa que realiza la llamada y finalizamos el método.
La siguiente etapa de nuestro trabajo consistirá en construir los algoritmos de pasada inversa. Como viene siendo habitual, los implementaremos en dos métodos: calcInputGradients y updateInputWeights. En el framework del primero, se realizan las operaciones de distribución del gradiente de error entre los objetos; los participantes del proceso. El segundo sirve para ajustar los parámetros entrenados de los modelos. Y en esta etapa, a diferencia del método de pasada directa, podemos utilizar la funcionalidad de la clase padre.
En los parámetros del método de distribución del gradiente de error obtenemos el puntero al mismo objeto de datos de origen, solo que esta vez tenemos que pasarle los valores del gradiente de error como la medida de la influencia de los datos de origen en el resultado del modelo.
bool CNeuronChimeraPlus::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!CNeuronChimera::calcInputGradients(NeuronOCL)) return false;
Sin embargo, a diferencia del enfoque habitual, no comprobaremos la relevancia del puntero recibido, sino que lo transmitiremos inmediatamente al método homónimo de la clase padre. Allí ya se han implementado los puntos de control necesarios y un algoritmo para distribuir el gradiente de error sobre los tres flujos de información heredados (dos 2D-SSM y una línea troncal de enlaces residuales).
Ahora solo debemos pasar el gradiente de error a lo largo de la línea troncal añadida. Aquí, primero corregimos el gradiente de error obtenido de los objetos posteriores mediante la derivada de la función de activación de la última capa del modelo de discretización añadido.
CNeuronBaseOCL *current = cDiscretizationPlus[-1]; if(!current || !DeActivation(current.getOutput(), current.getGradient(), Gradient, current.Activation())) return false;
Y, a continuación, pasamos los valores obtenidos por el bloque de discretización en sentido inverso. Para ello organizamos un ciclo de enumeración inversa de elementos con llamadas consecutivas de los métodos homónimos de los objetos correspondientes.
for(int i = cDiscretizationPlus.Total() - 2; i >= 0; i--) { current = cDiscretizationPlus[i]; if(!current || !current.calcHiddenGradients(cDiscretizationPlus[i + 1])) return false; }
A continuación, pasamos el gradiente de error por un modelo bidimensional del espacio de estados.
if(!cSSMPlus.calcHiddenGradients(current.AsObject())) return false;
Y solo nos quedará pasar el gradiente de error a la capa de datos de origen. Pero aquí deberemos considerar que el gradiente de error transmitido a través de los tres flujos de información heredados ya está almacenado en el búfer del objeto de datos de origen. Para conservar los datos obtenidos, sustituimos los punteros al búfer de gradiente de error del objeto de datos de origen.
current = cResidual[0]; CBufferFloat *temp = NeuronOCL.getGradient(); if(!NeuronOCL.SetGradient(current.getGradient(), false) || !NeuronOCL.calcHiddenGradients(cSSMPlus.AsObject()) || !SumAndNormilize(temp, NeuronOCL.getGradient(), temp, 1, false, 0, 0, 0, 1) || !NeuronOCL.SetGradient(temp, false)) return false; //--- return true; }
A continuación, transferimos el gradiente de error del modelo bidimensional del espacio de estados a la capa de datos de entrada. Luego sumamos los valores obtenidos con los acumulados anteriormente y retornamos al estado inicial los punteros a los búferes de datos.
Solo nos quedará retornar el resultado lógico de las operaciones al programa que realiza la llamada y finalizar el método.
Con esto concluirá nuestro análisis de los algoritmos del módulo Quimera complementado. Podrá leer por sí mismo el código completo de la clase CNeuronChimeraPlus y todos sus métodos en el archivo adjunto.
Construimos un descodificador híbrido
Tras construir el módulo Chimera modernizado, vamos a trabajar en el codificador híbrido. Como ya hemos mencionado, en nuestra implementación este contendrá un módulo Chimera y un bloque Hidformer. Y aquí debemos recordar que el objeto Hidformer recibe como entrada los datos del estado del sistema analizado, y genera como salida el tensor de acciones del agente. Y en tal contexto, lo más correcto sería llamar a nuestro nuevo objeto decodificador híbrido. La estructura del mecanismo se resume a continuación.
class CNeuronHypridDecoder : public CNeuronHidformer { protected: CNeuronChimeraPlus cChimera; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronHypridDecoder(void){}; ~CNeuronHypridDecoder(void){}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint layers, uint stack_size, uint nactions, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronHypridDecoder; } //--- virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool Clear(void) override; };
Como podemos ver, la estructura presentada declara un único objeto interno: el módulo Chimera modificado, cuyos algoritmos se han descrito anteriormente. El objeto CNeuronHidformer se utiliza como clase padre, lo que permite evitar la duplicación redundante de funcionalidad y aplicar eficientemente métodos y estructuras ya implementados sin necesidad de crear explícitamente una instancia adicional dentro del objeto. No obstante, deberemos redefinir el conjunto habitual de métodos virtuales.
El objeto interno se declara estáticamente, lo cual significa que el constructor y el destructor de la nueva clase permanecerán vacíos. La inicialización de los objetos declarados y heredados se realizará en el método Init.
bool CNeuronHypridDecoder::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint layers, uint stack_size, uint nactions, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronHidformer::Init(numOutputs, myIndex, open_cl, window_key, window_key, nactions, heads, layers, stack_size, nactions, optimization_type, batch)) return false;
En los parámetros del método de inicialización obtenemos una serie de constantes que nos permitirán definir de forma inequívoca la arquitectura del objeto a crear. Aquí cabe señalar que la estructura de parámetros se hereda completamente del método homónimo de la clase padre. Sin embargo, cuando llamamos habitualmente a un método de una clase padre, no le transmitimos los valores recibidos de la misma forma. Y esto se debe al hecho de que estamos planeando pasar los resultados del funcionamiento del objeto interno Quimera a la entrada del método de pasada directa de la clase padre en lugar de los datos de origen recibidos del programa externo. En consecuencia, también durante la inicialización de los objetos heredados de la clase padre, deberemos especificar las dimensionalidades de los resultados del módulo interno como datos de entrada resultantes. Y aquí especificaremos la dimensionalidad de las características al nivel del valor dado del vector de estado interno. En este caso, la longitud de la secuencia debe ser igual al espacio de acciones del agente. En otras palabras, a la salida del módulo Quimera, esperamos obtener un tensor de estado latente, cada fila del cual representará algún token de un elemento individual de acciones del agente.
Una vez ejecutadas con éxito las operaciones del método de la clase padre, llamaremos al método homónimo del módulo Quimera, especificando las dimensiones de los datos de origen y el tensor de resultados deseado.
if(!cChimera.Init(0, 0, OpenCL, window, window_key, units_count, nactions, optimization, iBatch)) return false; //--- return true; }
Luego retornaremos el resultado lógico de las operaciones y finalizaremos el método.
Creo que se habrá dado cuenta de que el algoritmo del método de inicialización de objetos es bastante sencillo. El resto de los métodos tienen el mismo algoritmo sencillo. Por ejemplo, el método feedForward, como es habitual, recibe en sus parámetros un puntero al objeto de datos de origen, que se transmite inmediatamente al método homónimo del módulo Chimera.
bool CNeuronHypridDecoder::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cChimera.FeedForward(NeuronOCL)) return false; return CNeuronHidformer::feedForward(cChimera.AsObject()); }
Los valores obtenidos se transmiten al método homónimo de la clase padre. Y el resultado lógico de las operaciones se devuelve al programa que realiza la llamada. A continuación, finalizaremos el método.
Le sugiero que se familiarice con el resto de métodos de esta clase. Su código completo se puede encontrar fácilmente en el archivo adjunto.
Arquitectura del modelo
Una vez finalizado el trabajo de construcción de ladrillos separados del framework GSM++ considerado, crearemos la arquitectura integral del modelo. En este caso, entrenaremos un solo modelo, el del Actor. La descripción de la arquitectura del modelo se ofrece en el método CreateDescriptions.
bool CreateDescriptions(CArrayObj *&actor) { //--- CLayerDescription *descr; //--- if(!actor) { actor = new CArrayObj(); if(!actor) return false; }
En los parámetros del método obtendremos el puntero a un array dinámico para registrar la secuencia de objetos de la descripción de la arquitectura del modelo. Y comprobamos directamente la relevancia del puntero obtenido. Si es necesario, crearemos una nueva instancia del objeto.
A continuación, crearemos las descripciones de la capa de datos de origen. Aquí, como de costumbre, utilizaremos una capa totalmente conectada de tamaño suficiente.
//--- Actor actor.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
Tenemos previsto introducir los datos "brutos" recibidos del terminal en la entrada del modelo. Y su procesamiento primario se realizará ya mediante el modelo. A efectos de procesamiento primario, se usará una capa de normalización por lotes para convertir los valores dispares de los datos de origen en una forma comparable.
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
A continuación, vendrá un módulo mixto de tokenización.
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMoT; descr.window = BarDescr; descr.count = HistoryBars; descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
Luego utilizaremos un módulo S3 que aprenderá el método óptimo para la permutación de tokens. Este permitirá encontrar el mejor orden de los elementos dadas sus interdependencias y la importancia en la estructura general de datos.
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronS3; descr.count = HistoryBars; descr.window = BarDescr; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
Los resultados del procesamiento de los datos se enviarán al codificador del nodo local, cuya función desempeñará el módulo NAFS.
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronNAFS; descr.count = HistoryBars; descr.window = BarDescr; descr.window_out = BarDescr; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
El tensor de acciones del Agente se generará en el módulo decodificador híbrido, cuyos algoritmos hemos descrito anteriormente.
//--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronHypridDecoder; //--- Windows { int temp[] = {BarDescr, 120, NActions}; //Window, Stack Size, N Actions if(ArrayCopy(descr.windows, temp) < int(temp.Size())) return false; } descr.count = HistoryBars; descr.window_out = 32; descr.step = 4; // Heads descr.layers = 3; descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
Resulta importante señalar que la arquitectura del Agente que hemos desarrollado se centra únicamente en el análisis de los estados del entorno. Sin embargo, esto no es suficiente para una evaluación completa y precisa del riesgo, ya que el modelo no considera los activos disponibles y su impacto en las decisiones.
Para hacer frente a este problema, en el siguiente paso ampliaremos la arquitectura con un bloque de gestión de riesgos que hemos tomado prestado de los modelos discutidos anteriormente.
//--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMacroHFTvsRiskManager; //--- Windows { int temp[] = {3, 15, NActions, AccountDescr}; //Window, Stack Size, N Actions, Account Description if(ArrayCopy(descr.windows, temp) < int(temp.Size())) return false; } descr.count = 10; descr.window_out = 16; descr.step = 4; // Heads descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- layer 7 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = NActions / 3; descr.window = 3; descr.step = 3; descr.window_out = 3; descr.activation = SIGMOID; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; } //--- return true; }
Después de crear la descripción de la arquitectura del modelo, devolveremos el resultado lógico de las operaciones al programa que realiza la llamada y finalizaremos el método.
El código completo del método de descripción de la arquitectura del modelo figura en el anexo. También encontrará el código de los programas de entrenamiento y prueba del modelo, que se han mantenido de trabajos anteriores. Le sugiero que se familiarice con ellos.
Simulación
Hemos trabajado mucho para hacer realidad nuestra propia visión de los planteamientos propuestos por los autores del framework GSM++. Y hemos llegado a una de las etapas esenciales: probar la eficacia de las soluciones aplicadas con datos históricos reales.
Cabe señalar que las capas neuronales finales del Agente replican en gran medida la arquitectura usada en nuestra implementación del framework del Hidformer. Se aplica la misma estructura del módulo de gestión de riesgos, y la salida del descodificador híbrido usa un objeto CNeuronHidformer. Esta similitud arquitectónica hace que merezca la pena comparar el rendimiento del nuevo modelo con el framework del Hidformer.
Para que la comparación sea correcta, ambos modelos se entrenarán con la misma muestra generada anteriormente para el entrenamiento del Hidformer. Recuerde:
- La muestra de entrenamiento incluye datos históricos del par de divisas EURUSD en el marco temporal M1 para todo el año 2024.
- Los parámetros de todos los indicadores analizados se mantendrán por defecto, sin optimización adicional, lo cual excluirá la influencia de factores de terceros.
- Las pruebas del modelo entrenado se realizarán con los datos históricos de enero de 2025, manteniendo constantes todos los demás parámetros para garantizar la objetividad de la comparación.
Ahora le presentamos los resultados de las pruebas.

Durante el periodo de prueba, el modelo ha realizado 15 transacciones, lo que resulta bastante poco para una negociación de alta frecuencia en el marco temporal M1. Esta cifra es incluso inferior a la mostrada por el modelo básico (Hidformer). Solo 7 de ellas han cerrado con beneficios, lo que supone un 46,67%. Este porcentaje también es inferior al de referencia del 62,07%. Aquí vemos una disminución de la precisión de las posiciones cortas. Sin embargo, hemos observado una ligera disminución del tamaño de las posiciones no rentables con un crecimiento relativo del indicador similar de las transacciones rentables.
Mientras que en el modelo básico la relación entre la media de posiciones rentables y perdedoras era de 1,6, en el nuevo modelo este indicador supera la marca del 4. Esto casi ha duplicado el beneficio total del periodo de prueba, con un aumento similar del factor de beneficio. Este hecho puede indicar que la estrategia aplicada en la nueva arquitectura hace hincapié en la minimización de las pérdidas y el aumento de los beneficios de las posiciones exitosas. A largo plazo, esto puede dar lugar a resultados financieros más sostenibles. Sin embargo, la brevedad del periodo de prueba y el número reducido de transacciones comerciales concluidas no nos permiten juzgar la eficacia del modelo a lo largo de un intervalo de tiempo prolongado.
Conclusión
Hoy hemos presentado un método unificado de procesamiento de secuencias de grafos, el GSM++, que combina enfoques avanzados en el análisis de los datos de mercado. La principal ventaja de este framework es su representación híbrida de datos, que incluye tokenización jerárquica, codificación local de nodos y codificación global de dependencias. Este enfoque multinivel nos permite extraer con eficacia patrones significativos y generar incorporaciones altamente informativas, lo cual resulta fundamental para las tareas de previsión de series temporales financieras.
En la parte práctica de nuestro trabajo hemos presentado la implementación de nuestra propia visión de los enfoques propuestos usando MQL5. Aquí merece la pena prestar atención a la presencia de diferencias significativas entre las soluciones aplicadas y los planteamientos propuestos por los autores del framework. Por consiguiente, todos los resultados obtenidos durante las pruebas se refieren exclusivamente a la solución implantada.
El modelo que hemos entrenado ha demostrado su capacidad para sacar provecho de datos ajenos a la muestra de entrenamiento, aunque no en los volúmenes que querríamos ver. Esto sugiere un posible potencial de los enfoques aplicados, pero se requiere más trabajo para entrenar el modelo en una muestra más representativa y probarlo de forma exhaustiva. Aquí también podemos añadir el trabajo de búsqueda de un conjunto más óptimo de indicadores analizados y sus parámetros. Al fin y al cabo, el modelo busca patrones en los datos de entrenamiento, no los crea.
Enlaces
Programas usados en el artículo
| # | Nombre | Tipo | Descripción |
|---|---|---|---|
| 1 | Research.mq5 | Asesor | Asesor de recopilación de datos |
| 2 | ResearchRealORL.mq5 | Asesor | Asesor experto para recopilar ejemplos con el método Real-ORL |
| 3 | Study.mq5 | Asesor | Asesor de entrenamiento de modelos |
| 4 | Test.mq5 | Asesor | Asesor para la prueba de modelos |
| 5 | Trajectory.mqh | Biblioteca de clases | Estructura de descripción del estado del sistema y la arquitectura del modelo |
| 6 | NeuroNet.mqh | Biblioteca de clases | Biblioteca de clases para crear una red neuronal |
| 7 | NeuroNet.cl | Biblioteca | Biblioteca de código del programa OpenCL |
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/17310
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 10): Flujo externo (II) VWAP
Redes generativas antagónicas (GAN) para datos sintéticos en modelos financieros (Parte 2): Creación de símbolos sintéticos para pruebas
Optimización con el juego del caos — Game Optimization (CGO)
Indicador de estimación de fuerza y debilidad de pares de divisas en MQL5 puro
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso