Redes neuronales en el trading: Pronóstico de series temporales con descomposición modal adaptativa (Final)
Introducción
En el artículo anterior, presentamos el framework ACEFormer, que es un modelo de pronóstico de series temporales específicamente adaptado a las particularidades de los mercados financieros. En él discutimos los principios básicos de la atención probabilística, los algoritmos para su implementación a nivel de programas OpenCL y los métodos para aumentar la eficiencia computacional manteniendo la precisión del pronóstico. Sin embargo, detrás de ese análisis quedó una pregunta igualmente importante: cómo se integran todos estos mecanismos en el programa principal y cómo garantizar una conexión fiable entre el módulo de cálculo y la lógica del algoritmo comercial.
En este artículo, continuaremos implementando los enfoques propuestos por los autores del framework ACEFormer. El enfoque principal residirá en la construcción de algoritmos en el lado del programa principal. Pero antes de pasar a la implementación técnica, recordemos la esencia y la fuerza del framework ACEFormer. Se basa en el algoritmo ACEEMD (Alias Complete Ensemble Empirical Mode Decomposition with Adaptive Noise), que tiene como objetivo eliminar el ruido en series temporales financieras. El ACEEMD resuelve el problema del efecto de borde y permite conservar puntos de inflexión clave en el gráfico sin perder información importante durante el suavizado. Se presta especial atención al primer modo (IMF), cuya eliminación nos permite deshacernos del ruido de alta frecuencia sin una supresión excesiva de la señal útil.
La salida es una representación modal adaptativa de la serie temporal, que se convierte en la entrada para el módulo de destilación construido sobre una arquitectura de transformador con atención probabilística. Este enfoque nos permite no solo filtrar el ruido del mercado sino también centrarnos en áreas realmente significativas de datos históricos, aumentando la precisión de los pronósticos en condiciones de alta volatilidad y estocasticidad.
Las representaciones procesadas se incorporan luego al bloque de Self-Attention clásico, que permite una consideración adicional del contexto global de la secuencia de origen y mejora la consistencia en diferentes intervalos temporales. Esta combinación de enfoque localizado y global permite que el modelo se centre simultáneamente en las reversiones pronunciadas y en las tendencias estables.
El elemento final de la arquitectura es una cabeza de predicción totalmente conectada que transforma una representación de alto nivel en un valor numérico concreto.
El ACEFormer combina las fortalezas de dos mundos: la robustez de la descomposición modal empírica y la flexibilidad de la atención profunda. Al mismo tiempo, el modelo sigue siendo lo suficientemente compacto para su uso en tiempo real en los terminales de usuario y no requiere recursos informáticos excesivos. Gracias a su arquitectura modular y las capacidades de procesamiento paralelo, el ACEFormer se adapta fácilmente a una amplia variedad de estrategias comerciales, desde modelos de impulso a corto plazo hasta sistemas de análisis posicional a medio plazo.
A continuación le presentamos la visualización del framework ACEFormer por parte del autor.

Construcción del objeto de atención probabilística
Después de revisar la implementación de los mecanismos de atención probabilística en el lado del programa OpenCL, pasamos a la siguiente etapa: la creación del módulo de alto nivel correspondiente en el lado del programa principal. Para ello, creamos un objeto especializado CNeuronMHProbAttention, en el que se organiza el algoritmo de atención probabilística completo.
Al crearlo, heredamos de la clase CResidualConv, cuya estructura ya implementa la arquitectura de dos capas convolucionales consecutivas con enlaces residuales. Esto nos permite centrarnos únicamente en la implementación de la lógica de atención probabilística sin tocar los mecanismos de propagación directa (Feed-Forward) implementados por la clase principal. Con esto logramos un alto grado de modularidad, así como facilidad de integración con otros componentes del modelo.
A continuación, le mostramos la estructura de la nueva clase.
class CNeuronMHProbAttention : public CResidualConv { protected: uint iWindow; uint iWindowKey; uint iHeads; uint iUnits; uint iTopKQuerys; uint iRandomKeys; int ibScore; //--- CNeuronConvOCL cQKV; CNeuronBaseOCL cQ; CNeuronBaseOCL cKV; CNeuronBaseOCL cRandomK; CNeuronBaseOCL cMHAttentionOut; CNeuronConvOCL cPooling; CNeuronTransposeOCL cTranspose[2]; CNeuronConvOCL cScaling; //--- virtual bool RandomKeys(CBufferFloat* indexes, int random, int units, int heads); virtual bool QueryImportance(void); virtual bool TopKIndexes(void); //--- virtual bool AttentionOut(void); virtual bool AttentionInsideGradients(void); //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *prevLayer) override; public: CNeuronMHProbAttention(void) {}; ~CNeuronMHProbAttention(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint units_count, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) const { return defNeuronMHProbAttention; } //--- 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; };
La estructura de la clase CNeuronMHProbAttention refleja la implementación paso a paso del mecanismo de atención probabilística, comenzando con la generación de clave aleatoria y el cálculo de la importancia de la consulta, y terminando con la propagación inversa del error. Cada componente se implementa como un nodo aparte, ofreciendo así una clara separación de responsabilidades y la capacidad de realizar ajustes.
Desde un punto de vista constructivo, la característica clave de la nueva clase CNeuronMHProbAttention es la declaración estática de todos los objetos internos. Esta solución no solo simplifica la gestión de la memoria, sino que además evita la asignación dinámica de recursos al crear una instancia de clase. Como resultado, el constructor y el destructor de la clase permanecen vacíos, lo cual aumenta la fiabilidad y previsibilidad del comportamiento del objeto cuando se crea y se destruye.
Toda la inicialización necesaria de objetos internos y variables se realiza de forma centralizada en el método Init, que actúa como una especie de constructor: es el responsable del correcto ensamblaje de todos los componentes, la configuración de parámetros y la asignación de los recursos necesarios.
bool CNeuronMHProbAttention::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint units_count, ENUM_OPTIMIZATION optimization_type, uint batch ) { if(!CResidualConv::Init(numOutputs, myIndex, open_cl, window, window, units_count, optimization_type, batch)) return false;
Como nuestro nuevo objeto se crea en base a CResidualConv, una clase que implementa una arquitectura convolucional con enlaces residuales, comenzamos el algoritmo del método de inicialización llamando al método homónimo de la clase padre. Esto le permite obtener directamente una infraestructura lista para usar, con capas convolucionales, enlaces residuales e inicialización de búferes. De esta forma, no desperdiciamos recursos en implementar lo obvio, sino que nos centramos en la lógica única de la atención.
A continuación, inicializamos los parámetros clave que determinan el comportamiento del mecanismo de atención.
iWindow = window; iWindowKey = MathMax(5, window_key); iHeads = MathMax(1, heads); iUnits = units_count; iTopKQuerys = int(MathMin(5 * MathMax(MathLog(iUnits),1), iUnits)); iRandomKeys = int(MathMin(5 * MathMax(MathLog(iUnits),1), iUnits));
Aquí, además de las ya conocidas, vemos 2 nuevas variables relacionadas con el mecanismo de atención probabilística:
- iTopKQuerys — número de Consultas más significativas seleccionadas para el análisis posterior;
- iRandomKeys — número de Claves aleatorias utilizadas en el proceso de selección de las Consultas más significativas.
Para mantener la escalabilidad, se usa la función logarítmica de la longitud total de la secuencia analizada al determinar los valores de las variables especificadas.
Tras guardar los parámetros del objeto, comienza el ensamblaje paso a paso de todos los elementos internos responsables de la implementación de la atención. Cada objeto se inicializa por separado en una secuencia estrictamente definida. Primero, vamos a inicializar cQKV, una capa convolucional que forma tres entidades a la vez: Q (Query), K (Key) y V (Value). Crea una representación densa de los datos analizados para todas las cabezas de atención codificándolas mediante la función de activación TANH.
int index = 0; if(!cQKV.Init(0, index, OpenCL, iWindow, iWindow, 3 * iWindowKey * iHeads, iUnits, optimization, iBatch)) return false; cQKV.SetActivationFunction(TANH);
Aquí debemos recordar que los kernels creados previamente para implementar el algoritmo de atención probabilística utilizan un búfer separado para las Consultas. Por ello, a continuación, creamos 2 objetos adicionales para separar las entidades.
index++; if(!cQ.Init(0, index, OpenCL, cQKV.Neurons() / 3, optimization, iBatch)) return false; index++; if(!cKV.Init(0, index, OpenCL, 2 * cQ.Neurons(), optimization, iBatch)) return false;
A continuación, creamos un objeto para almacenar los índices de Claves seleccionados de forma aleatoria.
index++; if(!cRandomK.Init(0, index, OpenCL, iHeads * MathMax(iRandomKeys, iTopKQuerys), optimization, iBatch)) return false;
Tenga en cuenta que el tamaño de la capa está determinado por el valor máximo de las Claves muestreadas y las Consultas más importantes. La idea es garantizar que exista suficiente memoria para todos los índices posibles que puedan necesitarse durante el cálculo. No sabemos de antemano cuál de los dos valores será mayor: el número de Claves aleatorias (iRandomKeys) o las Consultas más significativas (iTopKQuerys), por lo que vamos a utilizar el máximo de los dos valores. Y luego, escalamos el resultado por el número de cabezas de atención (iHeads), ya que cada cabeza trabaja independientemente y necesita su propio conjunto de índices.
De esta forma, utilizamos un objeto cRandomK para almacenar índices de las Claves seleccionadas aleatoriamente y las Consultas más importantes. Esto optimiza la estructura del módulo, reduce el número de objetos y simplifica la gestión de la memoria.
A continuación viene una capa para almacenar los resultados de la atención multidireccional de las Consultas más significativas.
index++; if(!cMHAttentionOut.Init(0, index, OpenCL, iTopKQuerys * iHeads * iWindowKey, optimization, iBatch)) return false;
Y detrás de ello, una capa de agregación adaptativa del trabajo de las cabezas de atención en una única representación.
index++; if(!cPooling.Init(0, index, OpenCL, iHeads * iWindowKey, iHeads * iWindowKey, iWindow, iTopKQuerys, optimization, iBatch)) return false; cPooling.SetActivationFunction(TANH);
En esta etapa, el algoritmo ya ha generado los resultados del mecanismo de atención probabilística. Sin embargo, debemos destacar una característica crítica: trabajamos con los resultados correspondientes únicamente a las Consultas más significativas (Top-K Queries). Esto significa que el tensor resultante tiene una dimensionalidad temporal significativamente menor que los datos de origen.
Como resultado, surge una inconsistencia constructiva: el bloque de atención proporciona una representación comprimida, mientras que la estructura clásica de Self-Attention implica la preservación de la dimensionalidad de los datos y resultados originales, lo cual es necesario, en particular, para la correcta adición de enlaces residuales que aseguren la estabilidad del entrenamiento y el soporte del flujo de gradiente.
Para resolver esta contradicción, vamos a aplicar la siguiente estrategia:
- Primero rotamos la matriz de resultados de atención de modo que la estructura de datos nos permita aplicar una transformación convolucional a lo largo del eje de características.
index++; if(!cTranspose[0].Init(0, index, OpenCL, iTopKQuerys, iWindow, optimization, iBatch)) return false;
- A continuación, se usa una capa convolucional cScaling, cuyo propósito consiste en restaurar la dimensionalidad de los resultados a la longitud de la secuencia original. De esta forma obtenemos un tensor comparable en dimensionalidad a la entrada original. Es de destacar que el escalamiento se realiza en el contexto de características individuales, es decir, cada punto temporal se reconstruye teniendo en cuenta la estructura global de la atención.
index++; if(!cScaling.Init(0, index, OpenCL, iTopKQuerys, iTopKQuerys, iUnits, iWindow, optimization, iBatch)) return false; cScaling.SetActivationFunction(None);
- Después de escalar, devolvemos los datos a su representación original aplicando de nuevo la operación de transposición.
if(!cTranspose[1].Init(0, index, OpenCL, iWindow, iUnits, optimization, iBatch)) return false;
Siguiendo esta secuencia de pasos, logramos la máxima compatibilidad entre el mecanismo de atención estocástica y las características arquitectónicas de la Self-Attention clásica. El modelo mantiene tanto la integridad estructural como una alta flexibilidad, al tiempo que garantiza la eficiencia computacional y la fiabilidad durante el entrenamiento.
Se presta especial atención a la creación del búfer ibScore, que almacena estimaciones de importancia intermedia (pesos de atención) para la propagación inversa del error. Se crea solo en el lado del contexto OpenCL.
ibScore = OpenCL.AddBuffer(sizeof(float) * iTopKQuerys * iUnits * iHeads, CL_MEM_READ_WRITE); if(ibScore < 0) return false; //--- return true; }
Tras inicializar todos los objetos internos, finalizamos el método, retornando previamente el resultado lógico de las operaciones realizadas.
A continuación, pasamos a la siguiente etapa de importancia: la organización del algoritmo de pasada directa. Sin embargo, antes de comenzar a trabajar con el flujo de datos principal, hay un pequeño trabajo preparatorio que debemos realizar.
Como ocurre con la mayoría de los módulos que implementamos, este usa métodos en los que se ponen en la cola de ejecución los kernels del programa OpenCL. Los algoritmos usados en ellos ya son familiares para el lector de artículos anteriores, y no tiene sentido duplicarlos. En su lugar, nos centraremos en el algoritmo de muestreo del índice de Claves, que se implementa en el método RandomKeys.
La esencia del mecanismo de atención probabilística consiste en reducir el número de Claves para buscar las Consultas más significativas. Esto reduce la carga computacional a la vez que agrega un componente estocástico que ayuda a evitar el sobreajuste y mejora la capacidad de generalización del modelo.
El método RandomKeys obtiene el puntero a un búfer de Índices que debe llenarse con los índices de Claves muestreados. Los parámetros random, units y heads indican la cantidad de valores aleatorios, la cantidad total de Claves disponibles y la cantidad de cabezas de atención, respectivamente.
bool CNeuronMHProbAttention::RandomKeys(CBufferFloat *indexes, int random, int units, int heads) { if(!indexes || random > units || indexes.Total() < (random * heads) ) return false;
Primero, comprobamos los valores obtenidos. Si no se indica el búfer, la cantidad de muestras aleatorias supera la cantidad de Claves disponibles o el búfer no es lo suficientemente grande, el método retornará inmediatamente false.
Después de pasar exitosamente el bloque de control, creamos una matriz random × heads, donde cada columna se corresponde con una cabeza de atención, y las filas se corresponden con las posiciones muestreadas de las Claves.
matrix<float> ind = matrix<float>::Zeros(random, heads);
Eisten otros dos escenarios posibles: Si el muestreo no es necesario (random == units), rellenamos la matriz con números consecutivos, es decir, usamos todas las Claves disponibles.
if(random == units) { for(int r = 0; r < random; r++) { for(int c = 0; c < heads; c++) ind[r, c] = (float)r; } }
Cuando el número de Claves a seleccionar (aleatorias) es menor que el número total disponible (units), existe el riesgo de obtener una muestra no representativa. El método más simple consiste simplemente en muestrear valores aleatorios de todo el rango, lo que puede dar lugar a repeticiones, acumulaciones locales o, por el contrario, caídas en algunas áreas del rango. Como resultado, el modelo puede perder partes importantes de los datos, lo que afectará negativamente su aprendizaje.
Para evitar este problema, nuestra implementación usa un muestreo estratificado uniformemente. Primero, dividimos todo el rango en segmentos iguales.
else { double step = double(units) / random;
Este paso puede ser fraccionario, lo cual resulta bastante aceptable, ya que el tamaño de la muestra rara vez es un múltiplo de la longitud de la secuencia completa.
Para cada estrato (segmento de rango), seleccionamos aleatoriamente un valor dentro de él.
for(int r = 0; r < random; r++) { for(int c = 0; c < heads; c++) ind[r, c] = float(int((r + MathRand() / 32767.0) * step)); } }
Así, para cada cabeza de atención creamos su propio conjunto de índices, pero todos ellos abarcan todo el rango, sin sesgos ni excesiva concentración en determinadas áreas.
Después de generar una muestra, esta se escribe en el búfer de Índices, desde donde luego se utiliza en un programa OpenCL.
if(!indexes.AssignArray(ind) || !indexes.BufferWrite()) return false; //--- return true; }
El enfoque aplicado posee una serie de ventajas obvias. En primer lugar, se garantiza una cobertura uniforme de todo el espacio de características. En segundo, se elimina la posibilidad de que los valores caigan fuera del rango aceptable y se minimiza la probabilidad de duplicados. Como resultado, la representatividad general de la muestra aumenta, lo cual significa que el modelo recibe una imagen más completa y equilibrada de los datos de origen. Esto resulta especialmente importante cuando se trabaja con series temporal financieras, donde tanto los periodos de calma como las áreas con cambios bruscos son igualmente significativos.
Tras completar el trabajo preparatorio, pasamos a la etapa principal: organizar el método feedForward, que implementa el algoritmo de atención probabilística completo.
bool CNeuronMHProbAttention::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cQKV.FeedForward(NeuronOCL)) return false;
El primer paso consiste en llamar al método FeedForward de la capa convolucional cQKV, que, como su nombre indica, es responsable de formar un tensor concatenado de Consultas, Claves y Valores. En la salida se forma un único tensor en el que se suceden tres entidades.
Sin embargo, para seguir trabajando necesitamos descomponer este tensor en dos: uno para las Consultas (Q), y otro para las Claves y Valores (K y V). Para este propósito, se utiliza el método DeConcat, que extrae las partes relevantes de la unión general QKV y las pasa a cQ y cKV.
if(!DeConcat(cQ.getOutput(), cKV.getOutput(), cQKV.getOutput(), iWindowKey * iHeads, 2 * iWindowKey * iHeads, iUnits)) return false;
Luego, se lanza el algoritmo anteriormente discutido para muestrear los índices de las Claves en el método RandomKeys.
if(!RandomKeys(cRandomK.getOutput(), iRandomKeys, iUnits, iHeads)) return false;
El siguiente paso consiste en llamar a dos métodos: QueryImportance y TopKIndexes. Estos realizan la colocación de los kernels en la cola de ejecución, ordenando la importancia de las Solicitudes y seleccionando las más informativas.
if(!QueryImportance() || !TopKIndexes()) return false;
Después de esto, se ejecuta el kernel principal del algoritmo: el método AttentionOut. Ahí es donde se pone en la cola el kernel de atención para la ejecución de las Solicitudes seleccionadas.
if(!AttentionOut()) return false;
Los resultados de la atención multidireccional se agregan usando una capa convolucional cPooling. Esta también utiliza la función de activación TANH, que aumenta la expresividad de la señal de salida.
if(!cPooling.FeedForward(cMHAttentionOut.AsObject())) return false;
Los resultados obtenidos se escalan para devolver el tensor a la dimensionalidad correspondiente a la secuencia original para que podamos utilizar correctamente las enlaces residuales.
if(!cTranspose[0].FeedForward(cPooling.AsObject())) return false; if(!cScaling.FeedForward(cTranspose[0].AsObject())) return false; if(!cTranspose[1].FeedForward(cScaling.AsObject())) return false;
La acción final del bloque de atención consiste en llamar al método SumAndNormilize, que combina los datos escalados con los datos de origen, agregando la relación residual y realizando la normalización. Esto garantiza la estabilidad del flujo de gradiente y acelera la convergencia del entrenamiento.
if(!SumAndNormilize(cTranspose[1].getOutput(), NeuronOCL.getOutput(), cTranspose[1].getOutput(), iWindow, true, 0, 0, 0, 1)) return false;
Finalmente, la salida resultante se pasa al método homónimo de la clase padre, que completa la pasada directa y devuelve el tensor de salida listo para ser pasado al siguiente módulo de red neuronal.
return CResidualConv::feedForward(cTranspose[1].AsObject()); }
De esta forma, la lógica completa de atención probabilística se implementa dentro del objeto CNeuronMHProbAttention. Este enfoque garantiza la modularidad, la reutilización y la facilidad de escalamiento del modelo.
Una vez completada la pasada directa, comienza una etapa de entrenamiento de importancia crítica: la propagación inversa del gradiente del error de optimización de los parámetros del modelo en la dirección de su disminución. Es en esta etapa que se calcula la contribución de cada elemento al error final y luego, según esta contribución, se ajustan los pesos de la red. En el caso de la atención probabilística, implementada en la clase CNeuronMHProbAttention, este proceso se vuelve particularmente complejo: solo se propagan los gradientes sobre las Consultas más importantes seleccionadas. Esto requiere una organización clara del proceso y un estricto cumplimiento de la lógica de los cálculos.
bool CNeuronMHProbAttention::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(!prevLayer) return false;
El método calcInputGradients comienza con una verificación básica para garantizar que sea válido el puntero al objeto de capa anterior obtenido en los parámetros del método. Sin el puntero actual, resulta imposible continuar propagando el gradiente de error a lo largo de la red.
A continuación, realizamos una operación preparatoria importante: la puesta a cero forzada del búfer de gradiente de Consultas.
if(!cQ.getGradient().Fill(0)) return false;
Este paso es obligatorio, ya que el algoritmo de atención en sí mismo funciona solo en un subconjunto de Consultas: las más relevantes, seleccionadas durante la pasada directa. Los elementos restantes de la matriz de gradiente pueden contener valores antiguos o aleatorios que ahora resultan irrelevantes y pueden distorsionar la actualización de los parámetros del modelo. Por consiguiente, la matriz al completo se limpia previamente, después de lo cual puede comenzar la acumulación cuidadosa de los valores correctos.
A continuación, se llama al método homónimo de la clase padre. Esta operación propaga el error a través del bloque FeedForward. De esta forma delegamos parte de la pasada inversa en la lógica ya probada.
if(!CResidualConv::calcInputGradients(cTranspose[1].AsObject())) return false;
Los siguientes pasos están orientados a propagar secuencialmente el gradiente de error a través de todos los bloques internos de atención probabilística, repitiendo exactamente la estructura de la pasada directa, pero en orden inverso. Primero, los gradientes pasan a través de un bloque de escalado.
if(!cScaling.calcHiddenGradients(cTranspose[1].AsObject())) return false; if(!cTranspose[0].calcHiddenGradients(cScaling.AsObject())) return false;
Luego, pasa a través de una capa de agregación de resultados de la atención multicabeza.
if(!cPooling.calcHiddenGradients(cTranspose[0].AsObject())) return false; if(!cMHAttentionOut.calcHiddenGradients(cPooling.AsObject())) return false;
Todos estos pasos son necesarios para reconstruir los gradientes con precisión, ya que la pasada directa realiza transformaciones en la forma y la estructura de los datos. Ahora necesitamos revertir dichas acciones.
Se presta especial atención al método AttentionInsideGradients, que propaga los gradientes dentro del propio mecanismo de atención probabilístico. Aquí es donde el error se transmite cuidadosamente por Solicitudes elegidas selectivamente. Este es el elemento más sensible de todo la pasada inversa, ya que permite mantener la precisión y la imparcialidad en la actualización de los pesos, incluso si solo una parte de la información esté involucrada en el proceso.
if(!AttentionInsideGradients()) return false;
Después de esto, sigue el procedimiento de fusión de gradientes: los gradientes de Consultas, Claves y Valores se fusionan en un único tensor QKV, que refleja la estructura original formada en la etapa de pasada directa. Si se ha utilizado una función de activación en el bloque cQKV, la corrección del gradiente se realiza calculando la derivada de esta función. Esto nos permite considerar su influencia en las señales transmitidas y garantiza la precisión matemática de todo el proceso.
if(!Concat(cQ.getGradient(), cKV.getGradient(), cQKV.getGradient(), iWindowKey * iHeads, 2 * iWindowKey * iHeads, iUnits)) return false; if(cQKV.Activation() != None) if(!DeActivation(cQKV.getOutput(), cQKV.getGradient(), cQKV.getGradient(), cQKV.Activation())) return false;
Los gradientes obtenidos en esta etapa se transmiten a la capa anterior.
if(!prevLayer.calcHiddenGradients(cQKV.AsObject())) return false;
Ahora solo queda pasar el gradiente de error a lo largo del flujo de información de los enlaces residuales. Aquí primero corregimos el gradiente de error de un flujo de información dado usando la derivada de la función de activación de la capa de datos de origen y luego sumamos los valores de las dos líneas troncales.
if(prevLayer.Activation() != None) if(!DeActivation(prevLayer.getOutput(), cTranspose[1].getGradient(), cTranspose[1].getGradient(), prevLayer.Activation())) return false; if(!SumAndNormilize(cTranspose[1].getGradient(), prevLayer.getGradient(), prevLayer.getGradient(), iWindow, false, 0, 0, 0, 1)) return false; //--- return true; }
De esta forma, el método calcInputGradients implementa un algoritmo de propagación inversa del gradiente de error completo y consistente para el módulo de atención probabilística. Este mantiene una alta precisión y consistencia en todas las transformaciones de gradiente, lo que permite utilizar el módulo de atención probabilística en modelos complejos sin riesgo de pérdida de información o distorsión de la señal de entrenamiento.
La etapa final es la actualización de los parámetros del modelo, implementada en el método updateInputWeights. Como todos los parámetros entrenables se encuentran en los objetos internos, la lógica completa se reduce a llamadas secuenciales a los métodos homónimos de estos componentes. Debido a la simplicidad de la implementación, no analizaremos este método con detalle en el artículo. El código completo para el objeto de atención probabilístico, incluido el método updateInputWeights, se ofrece en el archivo adjunto.
Arquitectura del modelo
Para concluir la revisión de la implementación de los componentes básicos, merece la pena detenerse en la arquitectura general del sistema de aprendizaje. Al igual que en varios de los artículos anteriores, seguiremos un esquema de aprendizaje jerárquico basado en el framework Actor–Director–Critic. Este enfoque permite una separación flexible de las funciones de procesamiento del entorno, la toma de decisiones y la evaluación de acciones, lo que resulta especialmente importante en el contexto de las dinámicas de mercado complejas e inestables.
En este esquema, vamos a entrenar cuatro modelos. El primero de ellos, el Codificador del estado del entorno, desempeña un papel clave. Precisamente este se encarga del análisis profundo de la situación del mercado y de la construcción de su representación latente compacta, pero informativa al máximo. Este modelo usa los enfoques del framework ACEFormer en el contexto del problema de la predicción de los estados futuros del entorno durante un horizonte de planificación determinado. Esto permite formar una representación estable y dinámicamente significativa del mercado, sobre la cual se basan posteriormente las acciones del Agente.
El segundo modelo, el Actor, trabaja con dos fuentes de información a la vez: el estado actual de la cuenta y las posiciones abiertas, por un lado, y la representación latente del entorno recibida del Codificador, por otro. El Actor aprende a elegir las acciones que mejor se adaptan a la situación comercial actual, y es su comportamiento el que se convierte en objeto de evaluación por parte de los otros dos modelos.
Los modelos del Director y el Crítico cumplen la función de evaluar las acciones elegidas por el Actor. En este caso, además, el Director introduce elementos de filtrado binario estricto que ayudan a evitar decisiones estratégicamente desfavorables, mientras que el Crítico ofrece una retroalimentación más suave y cuantitativa, determinando el grado de utilidad de las medidas adoptadas. El trabajo conjunto de estos dos componentes permite formar una estrategia sostenible y adaptativa para el comportamiento del agente en el mercado.
En este artículo, nos centraremos en la arquitectura del modelo del Codificador del estado del entorno, pues ahí se implementan los componentes clave del ACEFormer. La estructura de los modelos restantes, en general, conserva los principios constructivos expuestos en los materiales anteriores.
Como de costumbre, vamos a usar una capa completamente conectada de tamaño suficiente como capa de datos de origen.
//--- Encoder encoder.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
Aquí merece la pena señalar que no vamos a agregar valores cero para los elementos pronosticados, como sugieren los autores del framework ACEFormer.
Como etapa inicial en el procesamiento de los datos de origen, el modelo usa un módulo especializado para la preparación preliminar de características. Se construye usando un esquema que combina la normalización con la adición de ruido estocástico y la transformación convolucional de las dimensionalidades de las características.
El primer paso consiste en normalizar los datos de origen. Esto nos permite eliminar desequilibrios de escala entre diferentes características y estabilizar el entrenamiento del modelo. No obstante, para mejorar la capacidad de generalización de la red y aumentar su resiliencia ante las fluctuaciones de datos locales, en la etapa de normalización se introduce adicionalmente un nivel controlado de ruido aleatorio. Este elemento actúa como regularizador y simula la variabilidad del entorno del mercado, ayudando al modelo a adaptarse mejor a situaciones inestables o previamente no encontradas.
Luego, los datos normalizados y ruidosos se transmiten a una capa convolucional, cuya tarea es reducir la dimensionalidad de las características de entrada al formato requerido, consistente con la arquitectura de los componentes posteriores. Esta operación permite, por un lado, reducir los datos redundantes y, por otro, identificar las dependencias espaciales más relevantes entre las características dentro de una ventana temporal determinada.
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormWithNoise; descr.count = prev_count; descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; prev_count = descr.count = HistoryBars; descr.window = BarDescr; descr.step = BarDescr; int prev_out = descr.window_out = NSkills; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Después del preprocesamiento, los datos se transponen para pasar del análisis temporal al análisis de características independientes. Esto permite que el módulo de atención resalte dependencias sustanciales dentro de cada característica, mejorando la calidad de la representación y el entrenamiento del modelo.
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; prev_count = descr.window = prev_out; prev_out = descr.count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
A continuación pasamos al bloque de destilación. En la arquitectura del Codificador, se implementa la idea clave del framework ACEFormer: la extracción de las características más informativas, seguida de la agregación de información significativa. Se basa en un módulo de atención probabilística que procesa primero los datos, centrándose en las características más importantes. Esto permite que el modelo se centre en los aspectos más significativos de la señal de origen.
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMHProbAttention; descr.count = prev_count; descr.window = prev_out; descr.step = 4; descr.window_out = 32; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Después del bloque de atención viene una capa convolucional que reduce la dimensión temporal de las características. Esta se utiliza para comprimir la información y formar una representación compacta sin detalles redundantes.
//--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; prev_out = descr.count = (prev_out + 1) / 2; descr.window = 2; descr.step = 2; int filt=descr.window_out = 5; descr.layers = prev_count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
A continuación, se aplica un módulo de agregación que actúa como max-pooling: este selecciona las señales más pronunciadas de cada ventana, garantizando que se conserven las características dominantes.
//--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronProofOCL; descr.count = prev_count*prev_out; descr.window = filt; descr.step = filt; descr.layers = prev_count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
La capa de normalización final estabiliza la distribución de datos y acelera el proceso de entrenamiento.
//--- layer 7 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count * prev_out; descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
Nuestro modelo usa tres bloques de destilación consecutivos, con reducción independiente paso a paso de la dimensionalidad de series temporales unitarias de características individuales.
//--- layer 8 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMHProbAttention; descr.count = prev_count; descr.window = prev_out; descr.step = 4; descr.window_out = 32; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 9 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; prev_out = descr.count = (prev_out + 1) / 2; descr.window = 2; descr.step = 2 ; filt=descr.window_out = 3; descr.layers = prev_count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 10 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronProofOCL; descr.count = prev_count*prev_out; descr.window = filt; descr.step = filt; descr.layers = prev_count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 11 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count * prev_out; descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
//--- layer 12 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMHProbAttention; descr.count = prev_count; descr.window = prev_out; descr.step = 4; descr.window_out = 32; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 13 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; prev_out = descr.count = (prev_out + 1) / 2; descr.window = 2; descr.step = 2 ; filt=descr.window_out = 3; descr.layers = prev_count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 14 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronProofOCL; descr.count = prev_count*prev_out; descr.window = filt; descr.step = filt; descr.layers = prev_count; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 15 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count * prev_out; descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
De esta forma, el bloque de destilación no solo reduce la dimensionalidad, sino que selecciona y concentra la esencia de los datos de origen, formando una representación latente rica y resistente al ruido.
Luego, se agrega a la arquitectura del Codificador un bloque de Self-Attention de dos capas cuya tarea es identificar dependencias internas entre las características en la representación de datos comprimidos. Esto ayuda a capturar conexiones ocultas y sincronizar información entre características espaciales.
//--- layer 16 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronDMHAttention; descr.count = prev_count; descr.window = prev_out; descr.step = 4; descr.layers = 2; descr.window_out = 32; descr.batch = 1e4; descr.optimization = ADAM; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Gracias a este bloque, el modelo tiene la oportunidad de ver una imagen más amplia, no solo como un conjunto de observaciones individuales, sino como un sistema holístico de elementos que interactúan.
Para predecir secuencias unitarias de forma independiente, la arquitectura del Codificador utiliza dos capas convolucionales consecutivas. Este diseño permite que el modelo transforme eficientemente la representación latente generalizada en predicciones numéricas concretas para cada característica objetivo.
La primera capa convolucional tiene un mayor número de filtros (4 veces el número de parámetros previstos) y aplica la activación SoftPlus. Esto posibilita una transformación suave y positivamente acotada, lo que promueve la robustez del entrenamiento y el filtrado de ruido.
//--- layer 17 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = 1; descr.window = prev_out; descr.step = prev_out; prev_out = descr.window_out = 4 * NForecast; descr.layers = prev_count; descr.activation = SoftPlus; if(!encoder.Add(descr)) { delete descr; return false; }
La segunda capa completa el proceso de decodificación convirtiendo el número de salidas en un número específico de parámetros predichos (NForecast) y aplicando la función de activación TANH. Esto le permite obtener resultados dentro de un rango controlado de valores, lo cual resulta especialmente importante al trabajar con datos normalizados.
//--- layer 18 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = 1; descr.window = prev_out; descr.step = prev_out; prev_out = descr.window_out = NForecast; descr.layers = prev_count; descr.activation = TANH; if(!encoder.Add(descr)) { delete descr; return false; }
Esta estructura es simple pero efectiva: ofrece al modelo la flexibilidad necesaria a la hora de generar pronósticos, manteniendo al mismo tiempo la independencia de la evaluación de cada característica, lo cual resulta fundamental al trabajar con series temporales financieras, donde cada parámetro puede contener información única.
En la etapa final del trabajo del codificador, interpretaremos las predicciones obtenidas por el modelo, es decir, las devolveremos al espacio de los datos de origen, donde adquieren una forma significativa.
Primero transponemos el tensor. Este paso nos permite devolver el eje temporal a su posición normal y asegurar la consistencia entre los pasos temporales y los valores predichos.
//--- layer 19 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; prev_count=descr.window = prev_out; prev_out=descr.count; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
Luego, se aplica una capa convolucional cuya tarea consiste en reducir la dimensionalidad de las características a un formato que se corresponda con los datos de origen. En esencia, supone una forma de proyección: el modelo comprime o expande la representación para hacerla compatible con el espacio en el que se generaron originalmente los datos de origen.
//--- layer 20 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = prev_count; descr.window = prev_out; descr.step = prev_out; prev_out = descr.window_out = BarDescr; descr.layers = 1; descr.activation = TANH; if(!encoder.Add(descr)) { delete descr; return false; }
Finalmente, se realiza la normalización inversa, es decir, se convierten los valores de la escala interna normalizada del modelo en valores reales que resultan comprensibles para los humanos y adecuados para el análisis o el uso en decisiones comerciales.
//--- layer 21 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronRevInDenormOCL; descr.count = prev_count * prev_out; descr.layers = 1; descr.activation = None; if(!encoder.Add(descr)) { delete descr; return false; }
En conjunto, estas operaciones implementan el vínculo entre la lógica interna del modelo y el mercado real: el paso final que convierte las predicciones abstractas en resultados prácticos.
La arquitectura completa de todos los modelos entrenados se presenta en el archivo adjunto.
Simulación
Ya hemos realizado un trabajo extenso para adaptar e implementar el framework ACEFormer en el entorno MQL5. Los componentes clave del framework están integrados en la arquitectura de los modelos entrenados. Ahora viene la etapa más importante, en la que probaremos la efectividad de las soluciones usando datos históricos reales.
Como muestra de entrenamiento, hemos recopilado las pasadas utilizando políticas aleatorias de comportamiento del agente en el simulador de estrategias MetaTrader 5 con cotizaciones de EURUSD de minutos para 2024. Este enfoque abarca una amplia gama de escenarios de mercado y aumenta la versatilidad del comportamiento del modelo.
El entrenamiento se ha realizado en dos etapas. Primero, offline, sin actualizar la muestra hasta que los errores se estabilizan. Para ello, hemos adjuntado al gráfico el experto Study.mq5. Luego, online, en el simulador de estrategias con la ayuda del experto StudyOnline.mq5. En esta etapa se realiza el ajuste de los modelos en condiciones lo más cercanas posible a la realidad.
Para evaluar objetivamente los resultados, hemos realizado pruebas de los modelos entrenados con datos históricos más allá del periodo de entrenamiento (enero-marzo de 2025). Esto elimina el sobreajuste y enfatiza el valor práctico de los resultados obtenidos.
Todos los demás parámetros del entorno y los indicadores analizados se han mantenido sin cambios durante el proceso de entrenamiento y prueba, lo que nos permite evaluar la calidad de la estrategia aprendida.
Ahora le presentamos los resultados de las pruebas.

En general, durante el periodo de prueba, el modelo ha obtenido beneficios al completar 13 transacciones comerciales. Un poco más de la mitad de ellas se han cerrado con ganancias. Sin embargo, cabe señalar que 13 transacciones comerciales durante un periodo de prueba de 3 meses es un número extremadamente bajo.
Una posible explicación de esta baja actividad comercial puede ser la especificidad del uso de la atención probabilística. Este mecanismo selecciona solo las características y consultas más significativas, lo que mejora la calidad de la generalización, pero puede limitar la sensibilidad del modelo ante señales comerciales menos pronunciadas.
Además, esto puede indicar que la muestra de entrenamiento no ha sido lo suficientemente representativa: el modelo simplemente no ha encontrado una cantidad suficiente de situaciones diversas para negociar con confianza en condiciones similares durante el periodo de prueba. El aumento del volumen y la diversidad de los datos puede mejorar la flexibilidad de comportamiento del agente.
Conclusión
Hoy hemos visto el framework ACEFormer, que proporciona herramientas eficientes para extraer y comprimir características clave de las series temporales. Su arquitectura combina atención probabilística, destilación de características y mecanismos de transformación profunda, lo cual la hace particularmente atractiva para tareas de análisis de datos financieros con alto ruido e inestabilidad.
En la parte práctica, hemos implementado nuestra propia visión de los componentes principales del ACEFormer utilizando MQL5. Además, los hemos integrado en la arquitectura de los modelos entrenados dentro del enfoque Actor-Director-Crítico. El enfoque principal ha consistido en construir un modelo de Codificador del entorno que utiliza el mecanismo de procesamiento de características propuesto por el framework.
Los resultados de las pruebas han mostrado un resultado positivo: el modelo ha generado ganancias durante el periodo de prueba, lo que confirma la funcionalidad general de la arquitectura. Sin embargo, hemos topado con una baja actividad comercial, lo que puede deberse a las peculiaridades de la atención probabilística y a la muestra de entrenamiento limitada. Esto abre perspectivas para futuras investigaciones.
Enlaces
- An End-to-End Structure with Novel Position Mechanism and Improved EMD for Stock Forecasting
- Otros artículos de la serie
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 offline |
| 4 | StudyOnline.mq5 | Asesor | Asesor de entrenamiento de modelos online |
| 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/18041
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.
De principiante a experto: sistema de análisis autogeométrico
Introducción a MQL5 (Parte 16): Creación de asesores expertos utilizando patrones técnicos de gráficos
Criterio de independencia de Hilbert-Schmidt (HSIC)
Análisis cuantitativo de tendencias: Recopilamos estadísticas en Python
- 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
Neural Networks in Trading: Time Series Forecasting with Adaptive Modal Decomposition (Finalización):
Autor: Dmitriy Gizlyk