Redes neuronales en el trading: Entrenamiento de metaparámetros basado en la heterogeneidad (Componentes principales)
Introducción
En el artículo anterior analizamos el framework HimNet, una herramienta práctica que no solo analiza los datos, sino que también tiene en cuenta su contexto. En el ámbito del trading, esto significa que el modelo distingue entre periodos de alta liquidez y de calma. HimNet se adapta automáticamente a estos cambios.
La arquitectura de HimNet está construida de forma sencilla y lógica: a la entrada se suministran segmentos de series temporales de longitud T para N localizaciones. En primer lugar, el modelo genera dos tipos de embeddings aprendibles: temporales y espaciales. Los embeddings temporales (hora del día, día de la semana) captan los ciclos y los regímenes: los picos matutinos en la apertura, las caídas diurnas y la calma nocturna. Los embeddings espaciales —un vector independiente para cada nodo o ticker— codifican el perfil de la serie temporal. Todos estos vectores se almacenan en diccionarios y se refinan durante el proceso de entrenamiento. Actúan como consultas a los pools de metaparámetros y permiten que el modelo seleccione pesos distintos para distintos contextos de mercado, definidos por el comportamiento de la convolución de grafos. Esta generación sobre la marcha ofrece la flexibilidad del metaaprendizaje sin un consumo desmesurado de memoria y recursos computacionales.
El núcleo de HimNet son los bloques recurrentes de grafos (GCRU), reforzados con la base de Chebyshev. Este recurso arquitectónico confiere al modelo un carácter a la vez potente y pragmático. GCRU interpreta la red de plataformas de negociación como un grafo: cada nodo es un ticker o una plataforma, y las aristas reflejan la correlación, el spread cruzado o una relación empírica. Los polinomios de Chebyshev ofrecen una implementación compacta de la agregación K-hop: el modelo tiene en cuenta la influencia de los vecinos a una distancia de hasta K aristas, pero lo hace sin necesidad de una diagonalización espectral costosa: de forma rápida, local y numéricamente estable. En pocas palabras, en lugar de calcular el espectro completo del grafo, los autores del framework construyen paso a paso un conjunto de matrices que capturan la influencia de los vecinos más cercanos de sus vecinos, y así sucesivamente, para luego combinar estos efectos teniendo en cuenta el contexto.
La codificación en HimNet se realiza a través de dos flujos de información paralelos. El primero (el codificador espacial) escanea el perfil de las plataformas: la profundidad de mercado, el volumen típico y la reacción histórica ante las noticias. El segundo (codificador temporal) captura los regímenes temporales: la apertura de la sesión de negociación, el flujo de noticias, la calma nocturna o el periodo de publicaciones macroeconómicas. Ambos codificadores generan representaciones ocultas que se combinan en un tensor latente unificado. No se trata de una suma mecánica, sino de una cuidadosa integración de significados: donde el tiempo dicta el grado de sensibilidad y el espacio, el método de agregación. Como resultado, obtenemos una visión global de la situación actual del mercado.
El decodificador toma esta representación unificada y la proyecta en un embedding espacio-temporal, que sirve como consulta al pool ST de metaparámetros. El pool devuelve un conjunto de parámetros específicos para esa combinación concreta de lugar y momento, y estos parámetros forman directamente los pesos del bloque GCRU en el decodificador. En otras palabras, los autores del framework recomponen sobre la marcha la propia estructura del procesamiento recurrente en función del régimen actual: en la apertura de la sesión se utilizan unos pesos y una sensibilidad determinados, y durante la noche, otros distintos. A continuación, el decodificador genera, de forma iterativa y paso a paso, previsiones para el horizonte de planificación establecido.
Desde el punto de vista del álgebra y los números, para cada estado modelamos K niveles de Chebyshev, concatenamos los resultados y los hacemos pasar por una matriz generada por el metapool a partir de la consulta del embedding. Los gradientes se propagan hacia atrás tanto hacia los parámetros de los pools como hacia los embeddings. Por eso, el modelo aprende no solo a predecir, sino también a identificar contextos espacio-temporales útiles. En la práctica, esto permite una adaptación que hace que el modelo elija el modo de funcionamiento más adecuado para ese momento concreto.
En el trading real, esto se traduce en ventajas concretas. Al abrirse la sesión principal, HimNet refuerza automáticamente los parámetros para responder rápidamente a los picos de volumen, lo que reduce el deslizamiento. En periodos de baja liquidez, el modelo realiza previsiones más conservadoras y reduce las entradas falsas, lo que protege frente a movimientos bruscos aislados provocados por operaciones aisladas. Esto implica una ampliación adecuada del spread cuando se retira la liquidez y un filtrado más estricto de los falsos gaps. Ante un shock repentino, el modelo ST pasa a un modo protegido, aumentando la robustez de las previsiones y reduciendo el riesgo de trampas del mercado.
Desde el punto de vista técnico, la arquitectura sigue siendo pragmática y apta para su uso industrial: los pools de metaparámetros son compactos y no provocan una avalancha de consumo de memoria ni de recursos computacionales, y los cálculos por potencias de Chebyshev se escalan fácilmente en GPU.
Los hiperparámetros reflejan, en esencia, un equilibrio razonable entre la flexibilidad del modelo y la velocidad de respuesta. Su selección se lleva a cabo mediante una validación basada en escenarios de mercado representativos. En nuestro caso, la interpretabilidad no es meramente decorativa: por la activación de candidatos concretos del pool y por los mapas de calor de los grafos adaptativos, se puede ver rápidamente qué regímenes distingue el modelo en ese momento, por qué se hicieron determinados pronósticos y qué reglas de ejecución conviene aplicar.
En la práctica, esto se convierte en una herramienta de control concreta: desde disparadores automáticos para pasar al modo conservador hasta informes transparentes para la gestión de riesgos, lo que hace que HimNet sea controlable y comprensible para el tráder y el ingeniero.
De este modo, HimNet combina una potente agregación local, metaaprendizaje adaptativo y una implementación práctica. No se trata simplemente de un modelo predictivo mejorado, sino de una herramienta que entiende los regímenes del mercado y sabe modificar su comportamiento a medida que estos cambian, sin dejar de ser predecible y controlable para el tráder y el gestor de riesgos.
A continuación se muestra una visualización del framework HimNet realizada por el autor.

La parte práctica del primer artículo confirmó que la idea funciona en un entorno real de ingeniería. Hemos trasladado a la GPU las operaciones pesadas de construcción de polinomios de Chebyshev y de retropropagación del error, implementando en OpenCL los kernels ChebStep y ChebStepGrad.
En el lado del programa principal se implementó el objeto CChebPolinom, que encapsula los kernels y proporciona una interfaz unificada para la inferencia y el cálculo de gradientes. El resultado ha sido un pipeline híbrido: un entrenamiento rápido y una inferencia fiable.
En este artículo continuamos el trabajo de implementar los enfoques del framework HimNet mediante MQL5.
Objeto de convolución de grafos
En la siguiente etapa de nuestro trabajo, pasamos a la creación de un objeto de convolución de grafos, que utilizará la base de Chebyshev como óptica computacional para la agregación K-hop. Este módulo es un ejecutor clave: recibe las ventanas de características preparadas, solicita a CChebPolinom las matrices 𝑇𝑘 ya calculadas y, apoyándose en los metaparámetros actuales, construye las secuencias de salida listas para pasar a la siguiente etapa del modelo. Es importante que sea a la vez flexible (que admita diferentes modos de metaparametrización), rápido y predecible (con una gestión rigurosa de la memoria y comprobaciones numéricas).
La arquitectura del nuevo objeto CNeuronHimNetGrapConv está concebida como un adaptador pragmático y modular entre las ventanas temporales de características y la convolución de grafos: combina la preparación de datos, la llamada a operaciones matriciales y la integración con kernels de OpenCL, sin sobrecargar la lógica de alto nivel del modelo con detalles de implementación.
class CNeuronHimNetGrapConv : public CNeuronTransposeRCDOCL { protected: CNeuronBaseOCL cX_G; public: CNeuronHimNetGrapConv(void) {}; ~CNeuronHimNetGrapConv(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint count, uint window, uint cheb_k, ENUM_OPTIMIZATION optimization_type, uint batch) override; //--- virtual bool FeedForward(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support); virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support); //--- virtual bool Load(const int file_handle) override; //--- virtual int Type(void) const { return defNeuronHimNetGrapConv; } virtual void SetOpenCL(COpenCLMy *obj); virtual uint GetCount(void) override const { return iWindow; } virtual uint GetWindow(void) override const { return GetDimension(); } virtual uint GetChebK(void) const { return iCount; } };
Dentro de la clase se encuentra un objeto auxiliar compacto, pero fundamental, llamado `cX_G`. Actúa como búfer de trabajo y garantiza un intercambio rápido de datos entre las distintas fases del cálculo. Dado que el objeto se declara de forma estática, el constructor y el destructor de la clase pueden dejarse vacíos: la inicialización y la liberación de memoria se producen una sola vez durante todo el ciclo de vida, sin costos indirectos innecesarios al crear y eliminar instancias.
La inicialización de una nueva instancia de la clase se lleva a cabo en el método Init. En él, primero llamamos al método homónimo de la clase base, que define las interfaces comunes de la neurona.
bool CNeuronHimNetGrapConv::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint count, uint window, uint cheb_k, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronTransposeRCDOCL::Init(numOutputs, myIndex, open_cl, cheb_k, count, window, optimization_type, batch)) return false; if(!cX_G.Init(0, 0, OpenCL, Neurons(), optimization, iBatch)) return false; //--- return true; }
A continuación, se inicializa el búfer auxiliar cX_G, que sirve como área de trabajo para los datos ya concatenados y almacena el resultado de la multiplicación de matrices. Si al menos una de estas operaciones falla, Init devuelve false y el objeto permanece en un estado seguro.
El método de pasada directa FeedForward actúa como un director de orquesta. En primer lugar, el método comprueba rigurosamente la compatibilidad de los objetos recibidos como parámetros: los objetos NeuronOCL y Support pasados deben existir. Además, en Support, los valores esperados de Dimension y Steps deben coincidir con la configuración del módulo.
bool CNeuronHimNetGrapConv::FeedForward(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support) { if(!NeuronOCL || !Support) return false; if(Support.GetDimension() != iWindow || Support.GetSteps() != iCount) return false; if(NeuronOCL.Neurons() != (Neurons() / iCount)) return false;
Además, se comprueba que el número de neuronas en el objeto de datos de entrada se ajuste a la forma esperada. Estas comprobaciones no son una mera formalidad, sino una garantía de que las operaciones matriciales que siguen no desemboquen en una catástrofe silenciosa en la GPU.
A continuación viene la operación clave de multiplicación de matrices, en la que los polinomios de Chebyshev obtenidos se multiplican por el tensor de la serie temporal analizada. El resultado de la operación se almacena en nuestro búfer cX_G.
if(!MatMul(Support.getOutput(), NeuronOCL.getOutput(), cX_G.getOutput(), iWindow, iWindow, GetDimension(), iCount, false)) return false;
En esencia, esta es la operación que construye X_G, la concatenación de las imágenes T0 X, T1 X, …, Tk X. En una analogía con el trading: el Support es un mapa de interrelaciones entre secuencias unitarias (polinomios de Chebyshev), NeuronOCL es una ventana de características de una secuencia temporal multimodal, y cX_G es la versión ya analizada de los datos originales, en la que se tiene en cuenta la influencia de los vecinos para cada secuencia unitaria. Es precisamente esta matriz ajustada la que debemos pasar más adelante.
Pero hay que tener en cuenta que, como resultado de la operación de multiplicación de matrices, obtenemos una secuencia de varios conjuntos de datos de entrada ajustados con distintos niveles de detalle {K-hop, N, C}. Y esta no es la representación de datos que se espera a la salida del objeto. Por eso, en el último paso del método de pasada directa, pasamos cX_G al método homónimo de la clase base, que transpone el tensor a la representación esperada. Esta secuencia hace que el código sea modular, lo que facilita las pruebas y la optimización.
Si en algún momento algo sale mal, el método devuelve cuidadosamente false, sin dejar efectos secundarios.
Una vez que la pasada directa ha calculado la respuesta de la capa, llega una etapa no menos importante: la propagación del gradiente del error. Es precisamente aquí donde se sientan las bases de la capacidad de toda la arquitectura para aprender a partir de datos históricos y adaptarse a escenarios reales del mercado.
El método calcInputGradients comienza con un bloque de comprobación de los parámetros recibidos, en el que verifica si son válidos los punteros pasados a la capa anterior y a la matriz de polinomios de Chebyshev. Esta comprobación es una especie de cuerda de seguridad para evitar cálculos erróneos en caso de que la estructura del modelo se desincronice.
bool CNeuronHimNetGrapConv::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support) { if(!NeuronOCL || !Support) return false; if(Support.GetDimension() != iWindow || Support.GetSteps() != iCount) return false; if(NeuronOCL.Neurons() != (Neurons() / iCount)) return false;
Luego el método comprueba las dimensiones: el número de pasos (K-hop) y el tamaño de la ventana deben ajustarse a lo esperado. Esta etapa es importante, ya que las convoluciones de grafos son sensibles a la falta de coincidencia de dimensiones: un error en este punto puede arrastrar consigo una cadena de gradientes erróneos y hacer que todo el proceso de entrenamiento se venga abajo.
Si se supera la comprobación, pasamos directamente al proceso de distribución del gradiente del error. En primer lugar, transponemos los gradientes obtenidos de la capa siguiente del modelo a la representación de los resultados de nuestra convolución.
if(!CNeuronTransposeRCDOCL::calcInputGradients(cX_G.AsObject())) return false; if(!MatMulGrad(Support.getOutput(), Support.getGradient(), NeuronOCL.getOutput(), NeuronOCL.getGradient(), cX_G.getGradient(), iWindow, iWindow, GetDimension(), iCount, false)) return false; //--- return true; }
La etapa final consiste en distribuir los gradientes del error entre los datos de entrada y los polinomios de Chebyshev. En este paso, el método hace como si descompusiera el efecto acumulado del error por los mismos canales por los que se propagó la información en la pasada directa, pero ahora en sentido contrario. Utilizaremos el método MatMulGrad, heredado del objeto base. Como resultado, obtenemos dos tipos de gradientes. El primero, con respecto a los propios datos de entrada de la capa, que luego se utilizarán para ajustar los pesos de las capas anteriores. El segundo, con respecto a los coeficientes de la descomposición de Chebyshev, lo que permite al framework reentrenar la estructura de grafos si las dependencias del mercado se desplazan en el tiempo.
De este modo, es precisamente en esta fase donde tiene lugar esa evaluación final: el sistema determina qué conexiones han resultado útiles y cuáles son superfluas, y prepara el terreno para el siguiente ciclo de optimización.
Desde el punto de vista de la aplicación práctica, CNeuronHimNetGrapConv es el momento en el que el modelo convierte los datos brutos en una señal interpretable para el tráder. Convierte ventanas de cotizaciones sin procesar en señales ajustadas localmente. La arquitectura de la clase está diseñada específicamente para que estas transformaciones sean rápidas, reproducibles y explicables. El código completo de esta clase y de todos sus métodos se incluye en el archivo adjunto.
Bloque recurrente
Una vez que hemos analizado la estructura interna del bloque básico de convolución de grafos basado en polinomios de Chebyshev y hemos visto cómo se distribuyen los flujos de datos de entrada y de gradientes del error dentro de este elemento relativamente compacto, ha llegado el momento de ascender a un nivel superior: al nodo que vincula el procesamiento local del grafo con la dinámica de los estados ocultos. Es precisamente aquí donde se forma la verdadera memoria del modelo, y es precisamente este componente el que se encarga de que la información no se pierda entre los pasos temporales, sino que se integre de forma orgánica en la estructura del grafo. Se trata de la clase CNeuronHimNetGCRU, un elemento central que actúa como una especie de gestor de señales entre las componentes temporal y espacial del modelo. A continuación se muestra la estructura del objeto.
class CNeuronHimNetGCRU : public CNeuronBaseOCL { protected: CNeuronBaseOCL cInpAndHidden; CNeuronHimNetGrapConv cZ_R; CNeuronConvOCL cZ_R_emb; CNeuronBaseOCL cZe_Re; CNeuronBaseOCL cZ; CNeuronBaseOCL cR; CNeuronBaseOCL cCandidate; CNeuronHimNetGrapConv cHC; CNeuronConvOCL cHC_emb; CNeuronBaseOCL cHCe; public: CNeuronHimNetGCRU(void) {}; ~CNeuronHimNetGCRU(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units, uint window, uint window_out, uint cheb_k, uint embed_dim, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support, CNeuronBaseOCL *Embedding); virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support, CNeuronBaseOCL *Embedding); virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support, CNeuronBaseOCL *Embedding); //--- virtual int Type(void) const { return defNeuronHimNetGCRU; } //--- métodos para trabajar con archivos virtual bool Save(int const file_handle); virtual bool Load(int const file_handle); //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetOpenCL(COpenCLMy *obj) override; virtual void SetActivationFunction(ENUM_ACTIVATION value) override { }; };
En este caso, ya no basta con aplicar una convolución sobre los vecinos del nodo o procesar los datos mediante una celda recurrente. Los mercados financieros, como sabemos, rara vez se comportan de forma lineal: los pares de divisas forman clústeres interrelacionados, las acciones se mueven por sectores y los activos de materias primas suelen resonar con la moneda de los países exportadores. Por eso, un modelo temporal por sí solo no basta: necesita un mapa de relaciones, y la convolución de grafos proporciona ese mapa. Pero, por sí sola, es estática. Para decidir en el momento si conviene reforzar la señal de EURUSD, cuando los pares vecinos GBPUSD y EURGBP muestran una dinámica opuesta, o si es mejor atenuarla. Se necesita una lógica de control. Eso es precisamente lo que implementa esta clase.
En el interior de este módulo no se esconde simplemente la mecánica de GRU habitual para los desarrolladores de modelos temporales, sino una versión de la misma adaptada a la estructura de grafos. Y si en la clase anterior de convolución de grafos nos ocupamos de un conjunto bastante compacto de elementos, aquí nos encontramos ante una configuración mucho más compleja. La lista de componentes internos es impresionante, pero cada uno de ellos ocupa un lugar estrictamente definido dentro de la lógica general de funcionamiento. Iremos desvelando su función poco a poco, a medida que construyamos los métodos de este objeto, para no sobrecargar la comprensión con detalles antes de tiempo. En este sentido, es importante destacar que todos estos objetos internos se declaran de forma estática, lo que facilita la gestión de la clase y evita tener que crear constructores y destructores engorrosos: permanecen vacíos, como unas puertas bien ajustadas que no chirrían ni requieren movimientos innecesarios cada vez que se ejecuta el algoritmo.
Al pasar de la estructura general de la clase a los detalles concretos de su funcionamiento, llegamos de forma natural al método de inicialización Init. Es precisamente aquí donde tiene lugar la disposición de las piezas de ajedrez: la creación y preparación de todos los componentes internos de los que dependerá posteriormente la dinámica de los cálculos. El método Init es una especie de batuta entre bastidores: mientras el código externo se limita a invocarlo, en su interior se despliega una secuencia compleja, pero armoniosa, de preparación de cada uno de los módulos.
bool CNeuronHimNetGCRU::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint units, uint window, uint window_out, uint cheb_k, uint embed_dim, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, units * window_out, optimization_type, batch)) return false; activation = None;
Todo empieza con la inicialización de la clase base. Aquí se crean todas las interfaces básicas heredadas de la capa neuronal. Tenga en cuenta que el número de neuronas se define como el producto del número de elementos de la secuencia por el tamaño de la ventana de características en la salida del objeto; de este modo, reservamos de antemano el espacio computacional para el contexto temporal completo.
Inmediatamente después, la función de activación se pone a cero. Esto se ha hecho deliberadamente: en esta etapa, es importante mantener la neutralidad para que las capas posteriores dispongan de un lienzo en blanco para su activación específica.
A continuación, se despliega la cadena de inicialización de los componentes internos. El primero, cInpAndHidden, actúa como una especie de pasarela que conecta los datos de entrada y el estado oculto. Su dimensión viene determinada por la suma de las ventanas de características de entrada y de salida, multiplicada por el número de elementos de la secuencia, lo que le permite acumular toda la información necesaria para su posterior procesamiento.
int index = 0; if(!cInpAndHidden.Init(0, index, OpenCL, (window + window_out)*units, optimization, iBatch)) return false; cInpAndHidden.SetActivationFunction(None);
Después entra en escena cZ_R, encargado de formar las matrices combinadas Z y R, que constituyen las puertas clave de GRU. Aquí ya se incorpora el mecanismo de trabajo con estructuras de grafos: además de las dimensiones del tensor de los datos analizados, el método recibe el número de términos del polinomio de Chebyshev, lo que influye directamente en la profundidad de la aproximación del grafo.
index++; if(!cZ_R.Init(0, index, OpenCL, units, window + window_out, cheb_k, optimization, iBatch)) return false; cZ_R.SetActivationFunction(None);
El siguiente en la cadena de inicialización es el objeto cZ_R_emb, una especie de puente entre el embedding y la dinámica convolucional. Su tarea es generar una matriz de parámetros de convolución, pero no de forma mecánica, sino a partir del embedding obtenido, para que posteriormente dichos parámetros puedan aplicarse en el pipeline computacional.
No se trata simplemente de una variable de servicio, sino de un intermediario clave: es precisamente aquí donde tiene lugar el reempaquetado inteligente de las conexiones entre las secuencias unitarias, transformándolas de un conjunto de observaciones dispersas en un mapa coherente de interrelaciones. Si establecemos un paralelismo con los mercados financieros, este paso se asemeja al trabajo de un analista experimentado que toma los datos brutos de la actividad de negociación y los transforma en un formato en el que las tendencias, las correlaciones y las dependencias con retardo empiezan a tomar forma, lo que permite predecir los movimientos futuros de los precios.
index++; if(!cZ_R_emb.Init(0, index, OpenCL, embed_dim, embed_dim, 2 * cheb_k * (window + window_out) * window_out, 1, units, optimization, iBatch)) return false; cZ_R_emb.SetActivationFunction(None); index++; if(!cZe_Re.Init(0, index, OpenCL, 2 * window_out * units, optimization, iBatch)) return false; cZe_Re.SetActivationFunction(None); index++; if(!cZ.Init(0, index, OpenCL, window_out * units, optimization, iBatch)) return false; cZ.SetActivationFunction(None); index++; if(!cR.Init(0, index, OpenCL, window_out * units, optimization, iBatch)) return false; cR.SetActivationFunction(None);
Una vez realizado todo esto, el método inicializa varios bloques más ligeros, pero no por ello menos significativos: cZe_Re, cZ y cR. Estos módulos forman los vectores finales Z y R, proporcionando un control flexible del olvido y la actualización de la información. Su dimensión depende directamente de la ventana de características y del número de elementos de la secuencia en el tensor de resultados; de hecho, esto refleja en qué medida el modelo es capaz de mantener en memoria el contexto actual del mercado.
A continuación, se crea cCandidate, un componente encargado de calcular el candidato al nuevo estado. Su dimensión se hereda del cInpAndHidden ya inicializado, lo que pone de relieve la estrecha relación entre la señal de entrada y el modelo propuesto del estado oculto.
index++; if(!cCandidate.Init(0, index, OpenCL, cInpAndHidden.Neurons(), optimization, iBatch)) return false; cCandidate.SetActivationFunction(None);
El segundo gran bloque —cHC— es otra convolución de grafos que ya funciona a nivel del estado actualizado. A este le corresponden sus propios parámetros de convolución cHC_emb, construidos a partir del embedding obtenido, pero con coeficientes diferentes. Y el elemento final es cHCe, que termina de formar la representación del estado oculto actualizado.
index++; if(!cHC.Init(0, index, OpenCL, units, window + window_out, cheb_k, optimization, iBatch)) return false; cHC.SetActivationFunction(None); index++; if(!cHC_emb.Init(0, index, OpenCL, embed_dim, embed_dim, (window + window_out) * window_out * cheb_k, 1, units, optimization, iBatch)) return false; cHC_emb.SetActivationFunction(None); index++; if(!cHCe.Init(0, index, OpenCL, window_out * units, optimization, iBatch)) return false; cHCe.SetActivationFunction(None); //--- if(!Output.Fill(0)) return false; //--- return true; }
Una vez inicializados todos los módulos, se lleva a cabo la preparación final: la puesta a cero del búfer de resultados. No olvidemos que estamos trabajando con un bloque recurrente. Por su parte, la puesta a cero del búfer de resultados garantiza que el modelo comience a funcionar en un estado limpio, sin distorsiones provocadas por el ruido del pasado.
Todo esto parece aparatoso solo a primera vista. En la práctica, se trata de una estructura bien definida en la que cada bloque desempeña su función: desde el empaquetado previo de los datos hasta el ajuste preciso del olvido y la actualización de la información en la estructura de grafosGRU. Para las tareas de los mercados financieros, esto es de vital importancia: el modelo debe ser capaz de tener en cuenta tanto las fluctuaciones locales de los precios (dentro de la ventana) como su contexto global (mediante una aproximación basada en grafos), reaccionando al mismo tiempo con flexibilidad ante nuevos acontecimientos y sin perder la inercia histórica.
Tras un análisis detallado del proceso de inicialización, es hora de profundizar en el funcionamiento del bloque recurrente de grafos y ver cómo el método de pasada directa feedForward da vida a la arquitectura. En primer lugar, comprobamos cuidadosamente que todos los componentes necesarios estén actualizados: el búfer principal de datos de origen, el tensor de polinomios de Chebyshev y los embeddings. Si falta al menos uno de ellos, el proceso no se inicia. Es como un tráder que, antes de abrir una posición, comprueba todos los indicadores y datos: sin tener una visión completa de la situación, no se puede hacer una previsión.
bool CNeuronHimNetGCRU::feedForward(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support, CNeuronBaseOCL *Embedding) { if(!NeuronOCL || !Support || !Embedding) return false; if(!SwapOutputs()) return false;
A continuación, se produce un intercambio de punteros a los búferes de datos de los resultados actuales y anteriores, lo que crea una memoria recurrente. Este paso es importante, ya que el pasado influye en el futuro, del mismo modo que las fluctuaciones del mercado de ayer determinan las expectativas de hoy.
Luego hay que tener en cuenta que, durante el proceso de inicialización, no hemos guardado los parámetros de configuración del bloque en variables independientes. Esto se ha hecho a propósito. Al fin y al cabo, todos ellos se duplican en los componentes internos. Y podemos extraerlos.
uint cheb_k = cZ_R.GetChebK(); uint units = cZ_R.GetCount(); uint window_out = Neurons() / units; uint window = cZ_R.GetWindow() - window_out;
Y solo tras finalizar el trabajo preparatorio pasamos a organizar el proceso. Los datos de entrada y el estado anterior se concatenan en el objeto cInpAndHidden, formando un flujo único de información. Este flujo —que reúne noticias, indicadores y señales— está listo para su posterior procesamiento.
if(!Concat(NeuronOCL.getOutput(), PrevOutput, cInpAndHidden.getOutput(), window, window_out, units)) return false;
A continuación, se activa cZ_R, donde los polinomios de Chebyshev realizan una convolución de grafos: los nodos adyacentes se influyen entre sí, pero dicha influencia está limitada a K-hop, lo que garantiza un uso eficiente de los recursos y la estabilidad de los cálculos.
if(!cZ_R.FeedForward(cInpAndHidden.AsObject(), Support)) return false; if(!cZ_R_emb.FeedForward(Embedding)) return false;
Al mismo tiempo, cZ_R_emb genera una matriz de parámetros de convolución a partir de los embeddings, del mismo modo que un cocinero añade especias a un plato para resaltar los matices del sabor: las características temporales y espaciales se empaquetan correctamente para su uso posterior.
Los resultados de la convolución se multiplican y se someten a una activación sigmoide, tras lo cual se dividen en las puertas Z y R. Estas controlan el flujo de información, determinando qué hay que conservar del pasado y qué hay que actualizar.
if(!MatMul(cZ_R.getOutput(), cZ_R_emb.getOutput(), cZe_Re.getOutput(), 1, (window + window_out)*cheb_k, 2 * window_out, units, true)) return false; if(!Activation(cZe_Re.getOutput(), cZe_Re.getOutput(), SIGMOID)) return false; if(!DeConcat(cZ.getOutput(), cR.getOutput(), cZe_Re.getOutput(), window_out, window_out, units)) return false;
Después se forma el estado candidato cCandidate, que combina la entrada actual y el estado anterior modificado. Este estado se envía al segundo bloque de convolución de grafos cHC, donde, de forma similar, se crean nuevos parámetros mediante cHC_emb y el resultado se procesa mediante la tangente hiperbólica, lo que da lugar a un estado oculto actualizado.
if(!ElementMult(cZ.getOutput(), PrevOutput, cZ.getPrevOutput())) return false; if(!Concat(NeuronOCL.getOutput(), cZ.getPrevOutput(), cCandidate.getOutput(), window, window_out, units)) return false; if(!cHC.FeedForward(cCandidate.AsObject(), Support)) return false; if(!cHC_emb.FeedForward(Embedding)) return false; if(!MatMul(cHC.getOutput(), cHC_emb.getOutput(), cHCe.getOutput(), 1, (window + window_out)*cheb_k, window_out, units, true)) return false; if(!Activation(cHCe.getOutput(), cHCe.getOutput(), TANH)) return false; if(!GateElementMult(PrevOutput, cHCe.getOutput(), cR.getOutput(), Output)) return false; //--- return true; }
En la fase final se lleva a cabo una combinación elemento por elemento del estado anterior con el candidato mediante la puerta R, y en la salida obtenemos una predicción lista para su análisis o para su posterior envío al decodificador. Si lo trasladamos al lenguaje de los mercados financieros, cada paso es un proceso de filtrado de señales en múltiples niveles: los movimientos pasados, los activos relacionados, las características temporales y los embeddings internos se combinan para generar una previsión precisa y estable. El método feedForward es aquí como un director que coordina una orquesta compuesta por multitud de instrumentos, transformando los caóticos datos del mercado en una armoniosa sinfonía de previsiones.
Ahora que hemos analizado en detalle el método de pasada directa, es hora de pasar a una etapa no menos importante y apasionante: la propagación del gradiente del error. Si la pasada directa se puede comparar con un tráder que elabora una previsión basándose en los datos actuales, la retropropagación del error es el momento en el que, al finalizar la jornada bursátil, se sienta, abre su diario de operaciones y analiza dónde han funcionado las señales y dónde ha fallado la previsión. Y, basándose en este análisis, se ajustan los parámetros internos del modelo para que al día siguiente funcione con mayor precisión.
El método calcInputGradients comienza con una comprobación minuciosa de todos los componentes: el tensor de datos de entrada, el objeto de polinomios de Chebyshev y los embeddings.
bool CNeuronHimNetGCRU::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support, CNeuronBaseOCL *Embedding) { if(!NeuronOCL || !Support || !Embedding) return false; //--- uint cheb_k = cZ_R.GetChebK(); uint units = cZ_R.GetCount(); uint window_out = Neurons() / units; uint window = cZ_R.GetWindow() - window_out;
Solo después de asegurarnos de que todo está en su sitio, pasamos a calcular los parámetros clave: el orden del polinomio de Chebyshev, el número de elementos de la secuencia y la dimensión de las características de entrada y salida. Estos valores establecen el marco para la propagación de los gradientes, determinando cómo se desplazará la señal de error a lo largo de la arquitectura.
En primer lugar, se procesa el gradiente de la operación de combinación elemento a elemento del estado oculto con los candidatos mediante la puerta R. La operación tiene en cuenta la influencia de todos los componentes utilizados. Imaginemos que evaluamos la contribución de cada indicador al resultado de la estrategia: algunas señales se ajustan más y otras menos, en función de su influencia en la previsión.
if(!GateElementMultGrad(PrevOutput, cR.getPrevOutput(), cHCe.getOutput(), cHCe.getGradient(), cR.getOutput(), cR.getGradient(), Gradient, None, TANH, cR.Activation())) return false;
A continuación, propagamos el gradiente del error a través de la operación de multiplicación matricial para actualizar los bloques cHC y cHC_emb. Es como filtrar las señales del mercado: el error recorre todas las capas, distribuyéndose de forma precisa para que cada componente de la red reciba su parte correspondiente de ajuste.
if(!MatMulGrad(cHC.getOutput(), cHC.getGradient(), cHC_emb.getOutput(), cHC_emb.getGradient(), cHCe.getGradient(), 1, (window + window_out)*cheb_k, window_out, units, true)) return false;
La llamada a calcInputGradients para cHC garantiza que los gradientes se propaguen correctamente al candidato del estado oculto, como si comprobáramos no solo los resultados de las operaciones individuales, sino también su interrelación dentro de la estrategia de trading global.
if(!cHC.calcInputGradients(cCandidate.AsObject(), Support)) return false; if(!DeConcat(NeuronOCL.getGradient(), cZ.getPrevOutput(), cCandidate.getGradient(), window, window_out, units)) return false;
Luego se lleva a cabo la desconcatenación: la distribución de los gradientes del error entre los datos de entrada y los candidatos del estado oculto.
La transmisión de los gradientes del error a través de la operación de multiplicación elemento a elemento y la posterior concatenación forman gradientes correctos para las puertas Z y R. La corrección de los valores obtenidos según la derivada de la función de activación garantiza un escalado correcto del error, evitando la sobrecarga del modelo.
if(!ElementMultGrad(cZ.getOutput(), cZ.getGradient(), PrevOutput, cCandidate.getPrevOutput(), cZ.getPrevOutput(), None, None)) return false; if(!Concat(cZ.getGradient(), cR.getGradient(), cZe_Re.getGradient(), window_out, window_out, units)) return false; if(!DeActivation(cZe_Re.getOutput(), cZe_Re.getGradient(), cZe_Re.getGradient(), SIGMOID)) return false;
Se presta especial atención a los polinomios de Chebyshev. En primer lugar, se guarda el gradiente anterior obtenido del objeto cHC. A continuación, se invoca calcInputGradients para cZ_R, tras lo cual los gradientes acumulados se suman cuidadosamente. Esto permite que el modelo tenga en cuenta la influencia directa e indirecta de los nodos vecinos del grafo, lo que garantiza una actualización estable y correcta de los pesos de la convolución de grafos.
if(!MatMulGrad(cZ_R.getOutput(), cZ_R.getGradient(), cZ_R_emb.getOutput(), cZ_R_emb.getGradient(), cZe_Re.getGradient(), 1, (window + window_out)*cheb_k, 2 * window_out, units, true)) return false; CBufferFloat* temp = Support.getGradient(); if(!Support.SetGradient(Support.getPrevOutput(), false) || !cZ_R.calcInputGradients(cInpAndHidden.AsObject(), Support) || !SumAndNormilize(temp, Support.getGradient(), temp, Support.GetDimension(), false, 0, 0, 0, 1) || !Support.SetGradient(temp, false)) return false;
El proceso del flujo de información principal concluye con la transmisión de los gradientes al nivel de los datos de entrada, combinando la información procedente de las dos vías principales. Con una corrección posterior basada en la derivada de la función de activación.
if(!DeConcat(cInpAndHidden.getPrevOutput(), cCandidate.getPrevOutput(), cInpAndHidden.getGradient(), window, window_out, units)) return false; if(!SumAndNormilize(NeuronOCL.getGradient(), cInpAndHidden.getPrevOutput(), NeuronOCL.getGradient(), window, false, 0, 0, 0, 1)) return false; if(NeuronOCL.Activation() != None) if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(), NeuronOCL.getGradient(), NeuronOCL.Activation())) return false;
A continuación, los embeddings se someten a un procedimiento similar: sus gradientes se corrigen mediante cHC_emb y cZ_R_emb. Luego se suman, lo que garantiza una actualización coherente de todos los parámetros.
if(!Embedding.CalcHiddenGradients(cHC_emb.AsObject())) return false; temp = Embedding.getGradient(); if(!Embedding.SetGradient(Embedding.getPrevOutput(), false) || !Embedding.CalcHiddenGradients(cZ_R_emb.AsObject()) || !SumAndNormilize(temp, Embedding.getGradient(), temp, cZ_R_emb.GetWindow(), false, 0, 0, 0, 1) || !Embedding.SetGradient(temp, false)) return false; //--- return true; }
Como resultado, calcInputGradients se convierte en un sofisticado sistema de redistribución de errores en toda la arquitectura GCRU. En la práctica, es como un tráder que no se limita a registrar los errores de la estrategia, sino que analiza en detalle cada uno de sus componentes: patrones temporales y espaciales, relaciones entre activos, reacción ante eventos del mercado, y utiliza esta información para mejorar la precisión de sus previsiones futuras. Este enfoque hace que el entrenamiento del modelo sea vivo, dinámico y lo más cercano posible a la lógica real del mercado.
Llegamos ahora a lo que, a primera vista, parece ser la parte más modesta del mecanismo: la actualización de los pesos. Sin embargo, como suele ocurrir en las arquitecturas con una lógica interna minuciosamente diseñada, tras esta concisión se esconde una jerarquía de gestión de parámetros bien pensada. Todo ese complejo sistema de puertas, polinomios de Chebyshev, multiplicaciones matriciales y activaciones se reduce, en definitiva, a configurar correctamente solo dos objetos clave: cZ_R_emb y cHC_emb. Es como si, en un complejo mecanismo de relojería con decenas de engranajes, en realidad solo dos ejes de regulación determinaran la precisión de todo el movimiento.
El método updateInputWeights comienza con una comprobación sencilla, pero fundamental: la existencia del objeto de embeddings. Sin él, la actualización no tiene sentido, ya que es precisamente a través de él como el modelo asimila las relaciones espaciales y semánticas del grafo. Si el objeto no existe, el método interrumpe inmediatamente su ejecución y devuelve false.
bool CNeuronHimNetGCRU::updateInputWeights(CNeuronBaseOCL *NeuronOCL, CChebPolinom *Support, CNeuronBaseOCL *Embedding) { if(!Embedding) return false; //--- if(!cZ_R_emb.UpdateInputWeights(Embedding)) return false; if(!cHC_emb.UpdateInputWeights(Embedding)) return false; //--- return true; }
A continuación, el control pasa a los dos almacenes principales de parámetros aprendibles: primero se actualizan los pesos responsables del bloque Z-R (cZ_R_emb) y, a continuación, los del bloque candidato H-C (cHC_emb). Ambas llamadas a UpdateInputWeights se ejecutan de forma secuencial, como dos tornillos de ajuste de precisión, cada uno de los cuales influye en su parte del nodo de cálculo.
Cabe destacar que esta concisión es el resultado de una organización arquitectónica bien concebida. El resto de capas, los búferes intermedios y las puertas ya han obtenido sus gradientes durante la retropropagación del error. Aquí, de hecho, damos la orden de incorporar estos cambios en los parámetros que realmente determinan el comportamiento del modelo a largo plazo. En otras palabras, si el método anterior (calcInputGradients) puede compararse con el análisis de las causas de los errores pasados, updateInputWeights es el acto de ajustar el sistema de trading: no nos limitamos a extraer conclusiones, sino que las incorporamos a los ajustes para que, mañana, la previsión sea más precisa y estable.
Este enfoque hace que la arquitectura no solo sea eficaz, sino también predecible: basta con modificar tan solo dos puntos para controlar una enorme cadena de cálculo, manteniendo su integridad y coherencia.
Hoy hemos trabajado mucho, así que es el momento de hacer una breve pausa. En el próximo artículo continuaremos con este recorrido. Completaremos lo iniciado, llevaremos la implementación hasta su conclusión lógica y la comprobaremos con datos históricos reales para ver cómo se manifiesta la teoría en la práctica.
Conclusión
En este artículo nos hemos centrado en uno de los eslabones clave de toda la arquitectura y hemos analizado paso a paso su funcionamiento, desde la formación de las conexiones internas hasta el mecanismo de propagación del gradiente del error y el procedimiento de actualización de los parámetros. Hemos visto cómo los distintos módulos, que a primera vista parecen autónomos, empiezan a interactuar al unísono, creando una estructura coordinada, preparada para el entrenamiento y el trabajo en condiciones reales de mercado. Resulta especialmente importante destacar que un sistema tan bien estructurado se basa únicamente en dos objetos con parámetros aprendibles, lo que lo hace no solo compacto, sino también predecible en su manejo.
En esta fase, hemos obtenido un elemento casi completamente desarrollado de la futura cadena de cálculo, que está listo para asumir la carga de datos de trading reales. En el próximo artículo daremos un paso decisivo: integraremos todos los componentes en una estructura única, realizaremos pruebas exhaustivas con series temporales históricas y comprobaremos en qué medida el algoritmo que hemos creado es capaz no solo de reproducir los patrones del pasado, sino también de orientarse con seguridad en el flujo cambiante del mercado.
Enlaces
- Heterogeneity-Informed Meta-Parameter Learning for Spatiotemporal Time Series Forecasting
- Otros artículos de la serie
Programas utilizados en el artículo
| # | Nombre | Tipo | Descripción |
|---|---|---|---|
| 1 | Study.mq5 | Asesor experto | Asesor experto en entrenamiento offline de modelos |
| 2 | StudyOnline.mq5 | Asesor experto | Asesor experto para el entrenamiento online de modelos |
| 3 | Test.mq5 | Asesor experto | Asesor experto para probar el modelo |
| 4 | Trajectory.mqh | Biblioteca de clases | Estructura de la descripción del estado del sistema y de la arquitectura de los modelos |
| 5 | NeuroNet.mqh | Biblioteca de clases | Biblioteca de clases para crear una red neuronal |
| 6 | NeuroNet.cl | Biblioteca | Biblioteca de código del programa OpenCL |
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/19250
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.
Redes neuronales en el trading: Entrenamiento de metaparámetros basado en la heterogeneidad (Final)
Desarrollo de asesores expertos autooptimizables en MQL5 (Parte 10): Factorización de matrices
Asesor experto de trading neuronal basado en PatchTST
De novato a experto: EA de informes — Configuración del flujo de trabajo
- 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