
Redes neuronales en el trading: Agente multimodal con herramientas complementarias (Final)
Introducción
En el artículo anterior, nos familiarizamos con el framework FinAgent, una herramienta avanzada para el análisis de datos y el apoyo a la toma de decisiones en los mercados financieros. Su desarrollo tiene por objeto crear un mecanismo eficaz para formar estrategias comerciales y minimizar los riesgos en un entorno de mercado complejo y dinámico. La arquitectura de FinAgent consta de cinco módulos interconectados, cada uno de los cuales realiza funciones especializadas para garantizar la adaptabilidad general del sistema.
El módulo de análisis de mercado permite extraer y procesar datos de fuentes heterogéneas, como gráficos de precios, noticias de mercado y también informes. Aquí es donde se realiza la búsqueda de patrones estables que puedan utilizarse para predecir la dinámica de los precios.
Los módulos de reflexión juegan un papel importante en el proceso de adaptación y aprendizaje del modelo. El módulo de reflexión de bajo nivel analiza las interdependencias entre las señales actuales del mercado, mejorando la precisión de las previsiones a corto plazo. El módulo de alto nivel, por su parte, trabaja con las tendencias a largo plazo, incluidos los datos históricos y los resultados de las decisiones comerciales anteriores, con el fin de ajustar la estrategia utilizada según la experiencia adquirida.
El módulo de memoria permite almacenar a largo plazo grandes cantidades de datos de mercado. La aplicación de modernas técnicas de similitud vectorial puede minimizar el impacto del ruido y mejorar la precisión de la recuperación de información, lo cual resulta especialmente importante para elaborar estrategias a largo plazo e identificar relaciones complejas.
La pieza central del sistema es el módulo de toma de decisiones, que integra los resultados de los demás componentes. Basándose en la información actual e histórica, elabora recomendaciones comerciales óptimas. Y gracias a la capacidad de integrar los conocimientos de los expertos y los indicadores tradicionales, el módulo es capaz de generar recomendaciones equilibradas y fundamentadas.
A continuación le mostramos la visualización del framework FinAgent por parte del autor.
En el artículo anterior, comenzamos a aplicar los enfoques propuestos por los autores del framework FinAgent usando herramientas MQL5. Asimismo, presentamos algoritmos para los módulos de reflexión de bajo y alto nivel, implementados como objetos CNeuronLowLevelReflection y CNeuronHighLevelReflection. Estos módulos analizan las señales del mercado, el historial de decisiones comerciales tomadas y los resultados financieros reales obtenidos, lo cual permite adaptar la política de comportamiento del agente a las condiciones cambiantes del mercado. También ofrecen la flexibilidad necesaria para responder a cambios dinámicos en las tendencias e identificar patrones clave en los datos.
Una característica especial de nuestra aplicación es la integración de bloques de memoria directamente en los objetos de reflexión. Este enfoque se distingue de la arquitectura de framework del autor, en la que la memoria para todos los flujos de información se asigna en un módulo independiente. La integración de bloques de memoria simplifica la construcción de flujos de información para la interacción entre los distintos componentes del framework.
Como continuación del trabajo iniciado, hoy analizaremos la implementación de varios módulos importantes, cada uno de los cuales desempeña un papel único en la arquitectura general del sistema:
- El módulo de análisis de mercado se ha diseñado para procesar los datos procedentes de una amplia variedad de fuentes disponibles, como informes financieros, noticias y cotizaciones bursátiles. Además, pasa los datos multimodales a un formato común y pone de relieve patrones coherentes que pueden utilizarse para predecir la dinámica futura del mercado.
- Herramientas adicionales basadas en conocimientos a priori que sirven de apoyo al análisis y la toma de decisiones partiendo de patrones históricos, datos estadísticos y la opinión de expertos. Al mismo tiempo, permiten una interpretación lógica de las decisiones tomadas.
- El sistema de apoyo a la toma de decisiones combina los resultados de todos los módulos para formar una estrategia comercial adaptativa y óptima. Este sistema ofrece recomendaciones de actuación en tiempo real que permitirán a tráders y analistas reaccionar con rapidez a los cambios en las condiciones del mercado y tomar decisiones con mayor conocimiento de causa.
El módulo de análisis de mercado desempeña un papel central en el sistema, ya que se encarga del procesamiento previo y la unificación de los datos. Esto resulta especialmente importante a la hora de descubrir patrones ocultos que son difíciles de detectar con un enfoque tradicional de análisis de datos. Los autores del framework FinAgent usaban grandes modelos lingüísticos (LLM) para extraer aspectos clave de los datos y comprimirlos. En nuestra implementación, hemos abandonado el uso de LLM, haciendo hincapié en modelos especializados para el análisis de series temporales que ofrecen una gran precisión y rendimiento. En esta serie de artículos se han presentado varios frameworks para el análisis y la previsión de series temporales multivariantes. Y cualquiera de ellos puede usarse aquí. Al preparar este artículo, nuestra elección ha recaído en el transformador de atención segmentada, implementado como la clase CNeuronPSformer.
Sin embargo, esta no es la única opción. Además, el framework FinAgent propone el uso de datos de origen multimodal. Y esto nos permite no solo experimentar con distintos algoritmos de representación y análisis de series temporales, sino también combinarlos. Este planteamiento mejora considerablemente las capacidades del sistema ofreciendo una comprensión más detallada de los procesos del mercado y facilitando el desarrollo de estrategias adaptativas de gran eficacia.
El módulo de herramientas adicionales se encarga de integrar los conocimientos a priori del entorno analizado en la arquitectura global del modelo. Este componente nos ofrece la posibilidad de generar señales analíticas basadas en indicadores clásicos como medias móviles, osciladores e indicadores de volumen, que ya han demostrado su eficacia en la práctica del trading algorítmico. Sin embargo, el módulo no se limita a usar únicamente herramientas estándar.
Además, la generación de señales mediante reglas claras, basadas en el comportamiento de indicadores técnicos, mejora la interpretabilidad de las decisiones tomadas por el modelo, además de contribuir a su fiabilidad y eficacia, lo que resulta clave para la planificación estratégica y la gestión de riesgos.
Módulo de herramientas adicionales
La formación de un módulo de generación de señales basado en las lecturas de los indicadores clásicos en el framework de un modelo neuronal supone una tarea mucho más compleja de lo que pueda parecer a primera vista. La principal dificultad no reside en interpretar las señales, sino en evaluar las lecturas que llegan a la entrada del objeto.
En las estrategias clásicas, la descripción de las señales depende directamente de las lecturas reales de los indicadores. Sin embargo, sus lecturas suelen formar valores en distribuciones completamente inconexas e incomparables, lo cual crea importantes obstáculos a la hora de construir modelos. Este factor reduce seriamente la eficacia de su entrenamiento, ya que los algoritmos deben adaptarse para analizar datos heterogéneos. Y esto se traduce en tiempos de procesamiento más largos, menor precisión de las previsiones y otras consecuencias negativas. Por ello, hemos decidido de antemano usar exclusivamente datos normalizados en nuestros modelos.
El proceso de normalización de los datos de origen permite llevar todas las características analizadas a una escala única y comparable, lo que, a su vez, mejora significativamente la calidad del entrenamiento del modelo. Este enfoque minimiza el riesgo de que surjan incorrecciones derivadas de las diferentes unidades de medida de las métricas o de su variabilidad en el tiempo. Una ventaja importante de la normalización es que permite un análisis más profundo de los datos, ya que en esta forma se vuelven más predecibles para los algoritmos de aprendizaje automático.
Sin embargo, debemos considerar que la normalización complica considerablemente el proceso de generación de señales de las estrategias clásicas. Estas estrategias se desarrollaron originalmente para trabajar con datos "brutos", e implican el uso de umbrales fijos para interpretar los indicadores. Durante la normalización, los datos se transforman, lo que provoca un desplazamiento indeterminado de los valores umbral. Además, la normalización imposibilita la formación de una señal en el cruce de dos líneas de un indicador clásico, pues no se garantiza el desplazamiento sincrónico de los valores de las líneas analizadas. Como consecuencia, nos encontramos con la distorsión de las señales generadas o la ausencia de estas. Todo ello nos empuja a desarrollar nuevos enfoques para la interpretación de los indicadores.
Y aquí, en mi opinión, se ha encontrado una solución sencilla y, al mismo tiempo, conceptualmente sólida. Resumiendo su esencia, durante la normalización todas las características analizadas se llevan a la media cero y la varianza unitaria. Como resultado de este proceso, cada valor se vuelve comparable a los demás y puede interpretarse como una especie de oscilador. Este enfoque ofrece un esquema universal para interpretar las señales: los valores por encima de "0" se consideran señales de compra, mientras que los valores por debajo de "0", se consideran señales de venta. También podemos introducir valores umbral, lo que permite formar "corredores" para filtrar señales débiles o ambiguas. Esto minimiza los falsos positivos, mejora la precisión del análisis y facilita una toma de decisiones más informada.
También admitimos la probabilidad de señales inversas para algunas de las características analizadas. Este problema puede resolverse usando parámetros entrenados que se adapten a los datos históricos.
La aplicación del planteamiento propuesto sienta las bases para construir modelos que puedan adaptarse eficazmente a las condiciones cambiantes y generar señales más precisas y fiables.
Comenzaremos la implementación del enfoque propuesto para la generación de señales construyendo el kernel MoreLessEqual en el lado del programa OpenCL. En este caso, hemos aplicado el algoritmo más sencillo con un valor umbral fijo.
En los parámetros de este kernel obtendremos los punteros a los dos búferes de datos del mismo tamaño. Uno de ellos contiene los datos de origen, mientras que en el segundo escribiremos las señales como uno de los tres valores numéricos:
- -1 — venta;
- 0 — sin señal;
- 1— compra.
__kernel void MoreLessEqual(__global const float * input, __global float * output) { const size_t i = get_global_id(0); const float value = IsNaNOrInf(input[i], 0); float result = 0;
En el cuerpo del kernel, identificaremos el flujo actual de operaciones e inmediatamente leeremos el valor correspondiente del búfer de datos de origen en la variable local. Un paso obligatorio será comprobar la corrección del valor obtenido: todos los datos que no superen la validación se sustituirán automáticamente por "0" para evitar errores en las fases de procesamiento posteriores.
Aquí se introducirá una variable local para almacenar los resultados intermedios. Inicialmente, a esta variable se le asignará un valor que indica la ausencia de señal.
El siguiente pasada consistirá en comprobar el valor absoluto de la variable analizada. Este deberá superar el valor umbral especificado para generar una señal.
if(fabs(value) > 1.2e-7) { if(value > 0) result = 1; else result = -1; } output[i] = result; }
Los valores positivos por encima del umbral constituirán una señal de compra, y los negativos, una señal de venta. Escribiremos la bandera correspondiente en la variable local. Y antes de que el kernel termine, transferiremos la bandera obtenida al búfer de resultados.
El algoritmo descrito anteriormente supone un procedimiento secuencial de pasada directa en el que los datos se procesan sin utilizar parámetros entrenados. Este método se basa en cálculos estrictamente deterministas destinados a minimizar el coste de los recursos informáticos y evitar una complejidad excesiva, lo cual resulta especialmente importante en el contexto del procesamiento de grandes cantidades de información. Debemos además señalar que la distribución del gradiente de error no se proporciona en este flujo de información. Al fin y al cabo, lo importante para nosotros es identificar señales estables basadas en los indicadores, no "ajustarlas" al resultado necesario. Esto hace que el algoritmo sea especialmente atractivo para sistemas que requieren gran velocidad y precisión de procesamiento.
Después de construir el algoritmo en el lado del programa OpenCL, necesitaremos organizar el mantenimiento y la llamada del kernel presentado anteriormente en el lado del programa principal. Para ejecutar esta funcionalidad, crearemos un nuevo objeto CNeuronMoreLessEqual, cuya estructura se muestra a continuación.
class CNeuronMoreLessEqual : public CNeuronBaseOCL { protected: virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override {return true; } public: CNeuronMoreLessEqual(void) {}; ~CNeuronMoreLessEqual(void) {}; };
La estructura presentada del nuevo objeto es inusualmente sencilla. Ni siquiera tiene un método para inicializar objetos. En la práctica, toda la funcionalidad se realiza usando la clase padre. Solo redefiniremos los métodos de pasada directa e inversa.
En el método de pasada directa, solo se transmitirán los punteros a los búferes de datos a los parámetros del kernel presentado anteriormente y luego este se colocará en la cola de ejecución.
bool CNeuronMoreLessEqual::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!OpenCL || !NeuronOCL) return false; uint global_work_offset[1] = { 0 }; uint global_work_size[1] = { Neurons() }; ResetLastError(); const int kernel = def_k_MoreLessEqual; if(!OpenCL.SetArgumentBuffer(kernel, def_k_mle_inputs, NeuronOCL.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(kernel, def_k_mle_outputs, getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } //--- if(!OpenCL.Execute(kernel, 1, global_work_offset, global_work_size)) { printf("Error of execution kernel %s: %d; line %d", OpenCL.GetKernelName(kernel), GetLastError(), __LINE__); return false; } //--- return true; }
Dada la información mencionada sobre la ausencia de parámetros entrenables y el rechazo de las distribuciones de gradiente de error, la funcionalidad de los métodos de pasada inversa puede no parecer obvia a primera vista. Sin embargo, cabe destacar que en la estructura de construcción de las redes neuronales, estos métodos son obligatorios para todas las capas. De lo contrario, se llamará al método homónimo de la clase padre, que podría no funcionar correctamente en las condiciones de nuestra arquitectura. Para evitar estos problemas, el método de actualización de los parámetros entrenados se sobrescribe con un stub que retornará true.
En cuanto al rechazo de la distribución del gradiente de error, resulta lógicamente similar a la transmisión de valores nulos. Así, en el método de distribución de gradiente, simplemente pondremos a cero el búfer correspondiente en el objeto de datos de origen, lo cual garantizará que el modelo se ejecute correctamente y minimice el riesgo de errores.
bool CNeuronMoreLessEqual::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL || !NeuronOCL.getGradient()) return false; return NeuronOCL.getGradient().Fill(0); }
Con esto completaremos el trabajo con el objeto del módulo de herramientas adicionales. Encontrará el código completo de la clase CNeuronMoreLessEqual y todos sus métodos en el archivo adjunto.
En este punto, ya hemos abarcado prácticamente la implementación de todos los módulos clave del framework FinAgent. Queda por analizar el módulo de toma de decisiones, que desempeña el papel de pieza central de la arquitectura general. Este módulo permite sintetizar la información procedente de múltiples flujos de información (superiores a dos). Así que hemos decidido integrar el módulo de toma de decisiones en el objeto de framework global, sin separarlo en una entidad aparte, lo cual ha mejorado la interacción entre todos los componentes del sistema.
Creación del framework FinAgent
Ahora ha llegado el momento de combinar los módulos individuales creados anteriormente en la estructura global única del framework FinAgent para garantizar su integración y sinergia. Módulos de distinto enfoque funcional se combinan para lograr un objetivo común: crear un sistema eficaz y flexible para analizar datos de mercado complejos y elaborar estrategias que consideren la dinámica y las peculiaridades del mercado financiero. Esta funcionalidad la realizará un nuevo objeto CNeuronFinAgent, cuya estructura le mostraremos a continuación.
class CNeuronFinAgent : public CNeuronRelativeCrossAttention { protected: CNeuronTransposeOCL cTransposeState; CNeuronLowLevelReflection cLowLevelReflection[2]; CNeuronHighLevelReflection cHighLevelReflection; CNeuronMoreLessEqual cTools; CNeuronPSformer cMarketIntelligence; CNeuronMemory cMarketMemory; CNeuronRelativeCrossAttention cCrossLowLevel; CNeuronRelativeCrossAttention cMarketToLowLevel; CNeuronRelativeCrossAttention cMarketToTools; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; public: CNeuronFinAgent(void) {}; ~CNeuronFinAgent(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint account_descr, uint nactions, uint segments, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronFinAgent; } //--- virtual bool Save(int const file_handle) override; virtual bool Load(int const file_handle) override; //--- virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau) override; virtual void SetOpenCL(COpenCLMy *obj) override; //--- virtual bool Clear(void) override; };
En la estructura presentada vemos el conjunto habitual de métodos redefinidos y una serie de objetos internos, entre los que podemos encontrar fácilmente los objetos de los módulos del framework FinAgent que hemos implementado. Hablaremos de la construcción de los flujos de información de su interacción mientras discutimos los algoritmos para implementar los métodos de esta clase.
Todos los objetos internos se declararán estáticamente, lo que nos permitirá dejar vacíos el constructor y el destructor de la clase creada. La inicialización de todos los objetos declarados y heredados se realizará en el método Init. En los parámetros del método obtendremos una serie de constantes que nos permitirán definir de forma inequívoca la arquitectura del objeto a crear.
bool CNeuronFinAgent::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint account_descr, uint nactions, uint segments, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronRelativeCrossAttention::Init(numOutputs, myIndex, open_cl, 3, window_key, nactions / 3, heads, window, units_count, optimization_type, batch)) return false;
Adelantándonos un poco, nuestro bloque de decisión constará de varios bloques consecutivos de atención cruzada. Esto último lo implementaremos mediante el objeto padre. Para ello, hemos usado (y no por casualidad) la clase de atención cruzada relativa CNeuronRelativeCrossAttention.
En la salida de nuestra implementación del framework FinAgent, esperamos obtener una representación del tensor de acciones del agente como una matriz cuyas filas serán vectores que describen acciones individuales. Además, las transacciones de compra y venta estarán representadas por filas separadas de esta matriz. Cada operación se describirá mediante 3 parámetros: el volumen de las operaciones y dos niveles comerciales (stop loss y take profit). Por lo tanto, la matriz de acciones de nuestro agente contendrá 3 columnas.
Así, al llamar al método de inicialización de la clase padre, especificaremos que la ventana de análisis de datos en la línea troncal sea 3, y que el número de elementos de la secuencia analizada sea 3 veces menor que el tamaño del vector obtenido en los parámetros del tamaño del vector para describir las acciones del agente. Esto permitirá al modelo evaluar el rendimiento de cada operación individual en el contexto de un segundo flujo de información, a través del cual se prevé transmitir la información del entorno procesada. Esto se expresará mediante la transferencia de los parámetros pertinentes.
Una vez ejecutadas con éxito las operaciones del método de inicialización de la clase padre, prepararemos los objetos internos recién declarados. Y comenzaremos este trabajo inicializando los componentes del módulo de análisis de mercado. En nuestro caso, estará representado por dos objetos: un transformador con atención segmentada para buscar patrones estables en una serie temporal multidimensional de datos de origen y una unidad de memoria.
int index = 0; if(!cMarketIntelligence.Init(0, index, OpenCL, window, units_count, segments, 0.2f, optimization, iBatch)) return false; index++; if(!cMarketMemory.Init(0, index, OpenCL, window, window_key, units_count, heads, optimization, iBatch)) return false;
Para analizar exhaustivamente el estado del entorno, en nuestro trabajo utilizaremos 2 módulos de reflexión de bajo nivel que trabajarán en paralelo con un tensor de datos de origen presentados en diferentes proyecciones. Para obtener la segunda proyección de los datos analizados, aplicaremos un objeto de transposición.
index++; if(!cTransposeState.Init(0, index, OpenCL, units_count, window, optimization, iBatch)) return false;
A continuación, inicializaremos 2 objetos de reflexión de bajo nivel. Su análisis de los datos en una proyección diferente se evidenciará por la permutación de la dimensionalidad de la ventana y la longitud de la secuencia del tensor analizado.
index++; if(!cLowLevelReflection[0].Init(0, index, OpenCL, window, window_key, units_count, heads, optimization, iBatch)) return false; index++; if(!cLowLevelReflection[1].Init(0, index, OpenCL, units_count, window_key, window, heads, optimization, iBatch)) return false;
En el primer caso, analizaremos una serie temporal multivariante, en la que cada pasada temporal estará representada por un vector de datos, y compararemos estos vectores para identificar relaciones entre ellos. En el segundo caso, compararemos secuencias unitarias individuales para encontrar dependencias y regularidades en su dinámica.
A continuación, iniciaremos un módulo de reflexión de alto nivel en el que examinaremos las acciones recientes del agente en el contexto de la evolución de las condiciones del mercado y los resultados financieros.
index++; if(!cHighLevelReflection.Init(0, index, OpenCL, window, window_key, units_count, heads, account_descr, nactions, optimization, iBatch)) return false;
Y a continuación prepararemos el objeto del módulo de herramientas adicionales.
index++; if(!cTools.Init(0, index, OpenCL, window * units_count, optimization, iBatch)) return false; cTools.SetActivationFunction(None);
Los resultados de todos los módulos inicializados anteriormente se recogerán en el módulo de toma de decisiones, que, como hemos mencionado anteriormente, consta de varios bloques consecutivos de atención cruzada. En el primer paso, integraremos la información obtenida de los dos módulos de reflexión de bajo nivel.
index++; if(!cCrossLowLevel.Init(0, index, OpenCL, window, window_key, units_count, heads, units_count, window, optimization, iBatch)) return false;
Además, enriqueceremos los resultados del módulo de análisis de mercado con la información obtenida de los módulos de reflexión de bajo nivel.
index++; if(!cMarketToLowLevel.Init(0, index, OpenCL, window, window_key, units_count, heads, window, units_count, optimization, iBatch)) return false;
Y añadiremos algunos conocimientos a priori.
index++; if(!cMarketToTools.Init(0, index, OpenCL, window, window_key, units_count, heads, window, units_count, optimization, iBatch)) return false; //--- return true; }
Permítanme recordarles que ya hemos inicializado anteriormente la última capa del módulo de toma de decisiones. Estará representado por un objeto padre.
Una vez inicializados correctamente todos los objetos anidados, retornaremos el resultado lógico de las operaciones al programa que realiza la llamada y finalizaremos el método.
El siguiente paso en nuestro trabajo será construir el algoritmo de pasada hacia adelante para nuestra implementación del framework FinAgent dentro del método feedForward. En los parámetros del método obtendremos los punteros a los dos objetos de los datos de origen. Planeamos que el primero porte la información sobre el estado actual del entorno, mientras que el segundo flujo de información porte los datos sobre el estado de la cuenta y los resultados financieros actuales.
bool CNeuronFinAgent::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { if(!cMarketIntelligence.FeedForward(NeuronOCL)) return false; if(!cMarketMemory.FeedForward(cMarketIntelligence.AsObject())) return false;
La información sobre el entorno analizado se someterá a un procesamiento primario en el módulo de análisis de mercado, que incluirá la búsqueda de patrones por parte del transformador con atención segmentada y la identificación de sus combinaciones estables en el módulo de memoria.
A continuación, la información sobre los patrones encontrados en dos proyecciones se transmitirá a los módulos de reflexión de bajo nivel para realizar un análisis exhaustivo de la dinámica del mercado.
if(!cTransposeState.FeedForward(cMarketIntelligence.AsObject())) return false; if(!cLowLevelReflection[0].FeedForward(cMarketIntelligence.AsObject())) return false; if(!cLowLevelReflection[1].FeedForward(cTransposeState.AsObject())) return false;
Obsérvese que los módulos de reflexión de bajo nivel trabajan con los patrones detectados en el estado actual del entorno, sin considerar los datos del bloque de memoria que se ejecuta en la línea troncal del módulo de análisis de mercado. Esto se debe a la necesidad de analizar la reacción directa del mercado a la aparición de los patrones encontrados, lo cual permite una evaluación más precisa de los cambios y tendencias actuales sin depender de la información histórica.
La situación será similar con el módulo de reflexión de alto nivel.
if(!cHighLevelReflection.FeedForward(cMarketIntelligence.AsObject(), SecondInput)) return false;
Permítame recordarle que suministraremos a la entrada del módulo de reflexión de alto nivel la información sobre el estado actual del entorno como resultados del módulo de análisis de mercado y del vector de resultados financieros. El tensor de acciones previas del agente se utilizará recursivamente a partir del búfer de resultados del módulo de reflexión de alto nivel.
Pero el módulo de herramientas adicionales trabajará directamente con los datos de origen, ya que busca señales basadas en conocimientos a priori en los indicadores analizados.
if(!cTools.FeedForward(NeuronOCL)) return false;
A continuación organizaremos los procesos del módulo de toma de decisiones. En primer lugar, enriqueceremos los resultados de la reflexión de series temporales multivariantes integrando las dependencias reveladas en la dinámica de secuencias unitarias. Esto mejorará la precisión del análisis y la comprensión de las interacciones en el sistema, ofreciendo una evaluación más profunda y completa del estado del entorno analizado.
if(!cCrossLowLevel.FeedForward(cLowLevelReflection[0].AsObject(), cLowLevelReflection[1].getOutput())) return false;
En el siguiente paso, integraremos la información obtenida durante el proceso de reflexión de bajo nivel en la representación de patrones estables obtenida a la salida de la unidad de memoria que opera en la línea troncal del módulo de análisis de mercado. Esto permitirá aclarar y reforzar aún más los patrones ya identificados, ofreciendo una percepción más precisa y completa de la dinámica y las interacciones actuales en el sistema analizado.
if(!cMarketToLowLevel.FeedForward(cMarketMemory.AsObject(), cCrossLowLevel.getOutput())) return false;
Aquí debemos subrayar que los módulos de reflexión de bajo nivel analizarán un estado específico del entorno, identificando las reacciones del mercado a patrones individuales. Sin embargo, entre las pautas consideradas, puede haber algunas que resulten poco frecuentes, y la evaluación de la reacción del mercado ante ellas podría no ser representativa. En estos casos, almacenaremos la información en la memoria del módulo de reflexión de bajo nivel porque existe la posibilidad de que aparezcan patrones similares en el futuro, lo cual permitirá recopilar más datos sobre la respuesta del mercado a los mismos.
Sin embargo, no podemos utilizar información no confirmada para tomar decisiones. Por consiguiente, en el módulo de toma de decisiones, nos basaremos únicamente en patrones estables, solicitando información sobre la reacción del mercado a los mismos en el bloque de reflexión de bajo nivel, para una evaluación más precisa e informada.
A continuación, completaremos los resultados del análisis de la situación del mercado con conocimientos a priori.
if(!cMarketToTools.FeedForward(cMarketToLowLevel.AsObject(), cTools.getOutput())) return false;
Tenga en cuenta que no hemos añadido parámetros entrenados para interpretar las banderas generadas en el módulo de herramientas adicionales, a pesar de que ya hemos discutido anteriormente esta necesidad. Delegaremos esta funcionalidad en los parámetros de generación de las entidades Key y Value en el módulo de atención cruzada. Así, la interpretación y el procesamiento de las banderas se integran directamente en el proceso de atención cruzada. Por ello, la adición explícita de parámetros entrenables complementarios para interpretar los resultados del módulo de herramientas adicionales resulta redundante.
Al final del método de pasada directa, solo nos quedará analizar los resultados del módulo de reflexión de alto nivel en el contexto de los patrones estables identificados y la reacción del mercado ante ellos. Realizaremos esta operación con la ayuda de la clase padre.
return CNeuronRelativeCrossAttention::feedForward(cHighLevelReflection.AsObject(), cMarketToTools.getOutput());
}
Luego retornaremos el resultado lógico de las operaciones al programa que realiza la llamada y finalizaremos el método de pasada directa.
A la construcción del método de pasada directa le seguirá la organización de los procesos de pasada inversa. En el marco de este artículo, le propongo considerar con detalle el algoritmo de construcción del método calcInputGradients de distribución de gradientes de error, mientras dejamos el método de optimización de los parámetros entrenables updateInputWeights para el estudio por parte del lector.
bool CNeuronFinAgent::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = -1) { if(!NeuronOCL || !SecondGradient) return false;
En los parámetros del método obtendremos los punteros a los mismos objetos de datos de origen, solo que esta vez tendremos que transmitir en ellos el gradiente de error según la influencia de los datos de origen en el resultado final del modelo. Y en el cuerpo del método comprobaremos directamente la relevancia de los punteros recibidos, porque de lo contrario las operaciones posteriores no tendrán sentido.
Como usted ya sabe, la distribución de los gradientes de error repite por entero el flujo de información de la pasada directa, solo que en sentido contrario. El método de pasada directa terminaba llamando al método homónimo de la clase padre. En consecuencia, el método de distribución del gradiente de error comenzará llamando a un método de la clase padre. En él, distribuiremos el margen de error en el funcionamiento del modelo entre el módulo de reflexión de alto nivel y el bloque de atención cruzada precedente del módulo de toma de decisiones.
if(!CNeuronRelativeCrossAttention::calcInputGradients(cHighLevelReflection.AsObject(), cMarketToTools.getOutput(), cMarketToTools.getGradient(), (ENUM_ACTIVATION)cMarketToTools.Activation())) return false;
Luego ejecutaremos secuencialmente el gradiente de error a través de todos los bloques de atención cruzada del módulo de toma de decisiones, distribuyendo los errores por todos los flujos de información del framework según su impacto en el resultado del modelo.
if(!cMarketToLowLevel.calcHiddenGradients(cMarketToTools.AsObject(), cTools.getOutput(), cTools.getGradient(), (ENUM_ACTIVATION)cTools.Activation())) return false; //--- if(!cMarketMemory.calcHiddenGradients(cMarketToLowLevel.AsObject(), cCrossLowLevel.getOutput(), cCrossLowLevel.getGradient(), (ENUM_ACTIVATION)cCrossLowLevel.Activation())) return false;
A continuación, distribuiremos el gradiente de error entre los módulos de reflexión de bajo nivel.
if(!cLowLevelReflection[0].calcHiddenGradients(cCrossLowLevel.AsObject(), cLowLevelReflection[1].getOutput(), cLowLevelReflection[1].getGradient(), (ENUM_ACTIVATION)cLowLevelReflection[1].Activation())) return false; if(!cTransposeState.calcHiddenGradients(cLowLevelReflection[1].AsObject())) return false;
En esta fase, hemos distribuido el gradiente de error entre todos los módulos de nuestro framework. Y ahora tendremos que recopilar los datos de todos los flujos de información al nivel de los datos de origen. Permítame recordarle que todos los módulos de reflexión y la unidad de memoria del módulo de análisis de mercado trabajan con los resultados del preprocesamiento de los datos de origen en el transformador con atención segmentada. Por ello, primero recopilaremos el gradiente de error al nivel de resultados del objeto especificado.
La primera pasada consistirá en transferir el gradiente de error desde el bloque de memoria.
if(!((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cMarketMemory.AsObject())) return false;
A continuación, sustituiremos el puntero por el búfer de gradiente de error de nuestro objeto de preprocesamiento de datos de origen, lo cual nos permitirá conservar los valores derivados anteriormente.
CBufferFloat *temp = cMarketIntelligence.getGradient(); if(!cMarketIntelligence.SetGradient(cMarketIntelligence.getPrevOutput(), false) || !((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cHighLevelReflection.AsObject(), SecondInput, SecondGradient, SecondActivation) || !SumAndNormilize(temp, cMarketIntelligence.getGradient(), temp, 1, false, 0, 0, 0, 1)) return false;
Y llamaremos al método de distribución del gradiente de error desde el módulo de reflexión de alto nivel. Después, tendremos que sumar los valores obtenidos de los dos flujos de información.
Cabe señalar que el módulo de reflexión de alto nivel trabajará con los datos procedentes de dos flujos de información. De este modo, cuando se distribuyan los gradientes de error, tanto el error en el flujo principal como la línea troncal de resultados financieros se transmitirán simultáneamente a través de este módulo. Esto permitirá considerar el efecto de los errores en ambos componentes críticos del análisis, lo cual facilitará un ajuste más preciso del sistema.
La distribución del gradiente de error sobre los flujos de información de los módulos de reflexión de bajo nivel se realizará de forma similar. Sin embargo, a diferencia del módulo de reflexión de alto nivel, estos módulos trabajarán con una sola fuente de datos de origen, lo que simplificará el proceso de distribución del gradiente de error.
if(!((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cLowLevelReflection[0].AsObject()) || !SumAndNormilize(temp, cMarketIntelligence.getGradient(), temp, 1, false, 0, 0, 0, 1)) return false; if(!((CNeuronBaseOCL*)cMarketIntelligence.AsObject()).calcHiddenGradients(cTransposeState.AsObject()) || !SumAndNormilize(temp, cMarketIntelligence.getGradient(), temp, 1, false, 0, 0, 0, 1) || !cMarketIntelligence.SetGradient(temp, false)) return false;
No olvidemos que después de cada iteración deberemos añadir el gradiente de error obtenido a los valores acumulados anteriormente. De este modo, nos aseguraremos de tener en cuenta correctamente todos los errores de funcionamiento del modelo. Una vez procesados todos los flujos de información, deberemos devolver los punteros a los búferes de datos a su estado original.
Ahora solo tendremos que transmitir el gradiente de error al nivel de datos de origen de la línea troncal del flujo de información principal y finalizar el método devolviendo el resultado lógico de las operaciones al programa que realiza la llamada.
if(!NeuronOCL.calcHiddenGradients(cMarketIntelligence.AsObject())) return false; //--- return true; }
Obsérvese que en el algoritmo del método de distribución del gradiente de error no participa el módulo de herramientas adicionales. Como ya hemos comentado, no tenemos previsto transmitir gradientes de error por el flujo de información especificado. Y limpiar el búfer de gradiente de error del objeto que proporciona los datos de origen sería incluso perjudicial en este caso. Después de todo, el mismo objeto recibe el gradiente de error a lo largo de la línea troncal del flujo principal de información.
Con esto concluirá la revisión de algoritmos para construir el framework FinAgent utilizando herramientas MQL5. El código completo de todos los objetos presentados y sus métodos está disponible en el archivo adjunto para su revisión y uso posterior. También están disponibles el código de todos los programas y la arquitectura del modelo entrenado que se han utilizado en la elaboración del artículo. Todos ellos se han mantenido casi sin cambios desde el artículo sobre la construcción del agente con memoria multinivel. Los únicos cambios se han dado en la arquitectura del modelo, donde hemos sustituido una capa neuronal integrando el framework presentado anteriormente.
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronFinAgent; //--- Windows { int temp[] = {BarDescr, AccountDescr, 2 * NActions, Segments}; //Window, Account description, N Actions, Segments if(ArrayCopy(descr.windows, temp) < int(temp.Size())) return false; } descr.count = HistoryBars; descr.window_out = 32; descr.step = 4; // Heads descr.batch = 1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { delete descr; return false; }
La arquitectura del resto de capas del modelo se ha mantenido sin cambios. Continuemos. Vamos a pasar ahora a la fase final de nuestro trabajo: la evaluación de la eficacia de los enfoques aplicados sobre datos históricos reales.
Simulación
En los dos últimos artículos hemos analizado con detalle el framework FinAgent. Al hacerlo, hemos materializado nuestra propia visión de los planteamientos propuestos por sus autores. Así, hemos adaptado los algoritmos del framework a nuestras necesidades. Y ahora nos acercamos a la fase crucial: la comprobación de la eficacia de las soluciones aplicadas sobre datos históricos reales.
Cabe destacar que, durante el proceso de desarrollo, hemos introducido cambios significativos en los algoritmos subyacentes al framework FinAgent. Estas modificaciones abordan aspectos clave del funcionamiento del modelo. Por ello, en este caso, no evaluaremos la solución original, sino nuestra versión adaptada.
El modelo se ha entrenado utilizando los datos históricos del par de divisas EURUSD para 2023 en el marco temporal H1. Todos los ajustes de los indicadores utilizados por el modelo se han dejado en sus valores por defecto, lo que nos ha permitido centrarnos en evaluar el propio algoritmo y su capacidad para trabajar con los datos de origen sin ajustes adicionales.
Para la fase inicial del entrenamiento, hemos utilizado una muestra de datos de estudios anteriores. Hemos aplicado un algoritmo de aprendizaje con una formación de acciones objetivo del Agente "casi perfectas", lo que nos ha permitido entrenar el modelo sin actualizar constantemente la muestra de entrenamiento. Sin embargo, a pesar del éxito del algoritmo en este formato, creemos que para mejorar la precisión y abarcar una gama más amplia de estados de la cuenta, sería útil actualizar periódicamente la muestra de entrenamiento.
Tras varios ciclos de entrenamiento, el modelo ha demostrado una rentabilidad estable tanto en los datos de entrenamiento como en los de prueba. Las pruebas finales se han realizado con los datos históricos de enero de 2024. Todos los parámetros del modelo y los indicadores analizados se han mantenido sin cambios. Este enfoque nos permitirá obtener una evaluación objetiva del rendimiento del modelo en unas condiciones lo más semejantes posibles al mercado real. Ahora le presentaremos los resultados de las pruebas.
Durante el periodo de prueba, el modelo ha realizado 95 transacciones comerciales, lo que supera con creces los resultados de los últimos modelos para el mismo periodo. Más del 42% de las transacciones comerciales se han cerrado con beneficios. Pero debido a que la transacción rentable media supera en 1,5 veces a la transacción perdedora media, el modelo ha obtenido beneficios en general durante el periodo de prueba. El factor de beneficio ha sido de 1,09.
Cabe destacar que el modelo ha obtenido la mayor parte del beneficio en la primera mitad del mes, cuando el precio fluctuaba en un corredor bastante estrecho. Y al formarse una tendencia bajista, la línea de equilibrio se desplaza lateralmente. E incluso se observa cierta reducción.
Mi opinión personal es que las razones de este comportamiento pueden encontrarse en los algoritmos de los módulos de análisis de mercado y de herramientas adicionales. Pero hablaríamos ya de un estudio aparte.
Conclusión
Hoy nos hemos familiarizado con el framework FinAgent, una solución avanzada para el análisis complejo de la dinámica del mercado y los datos históricos. Los autores del framework combinan información textual y visual para mejorar enormemente la capacidad de tomar decisiones comerciales más informadas. Con cinco componentes clave que conforman la arquitectura del framework, FinAgent demuestra precisión y gran adaptabilidad, lo que resulta especialmente importante para la negociación en los mercados financieros, donde las condiciones cambian con frecuencia.
Debemos prestarse especial atención al hecho de que el framework no se limita únicamente al ámbito del análisis, sino que ofrece una amplia gama de herramientas que pueden trabajar eficazmente tanto con datos textuales como gráficos. Este enfoque nos permite considerar muchos factores que afectan al mercado y ofrece una comprensión más profunda de los procesos de mercado. Estas características hacen de FinAgent una herramienta prometedora para desarrollar estrategias comerciales capaces de adaptarse a las cambiantes condiciones del mercado y tener en cuenta hasta las más pequeñas fluctuaciones del mismo.
En la parte práctica de nuestro trabajo, hemos implementado nuestra propia visión de los enfoques propuestos usando MQL5. Asimismo, hemos entrenado el modelo integrando los enfoques propuestos y lo hemos puesto a prueba con datos históricos reales. Los resultados de las pruebas han demostrado la capacidad del modelo para generar beneficios. Sin embargo, los rendimientos del modelo dependen de las condiciones del mercado. También deberemos realizar experimentos adicionales para encontrar formas de mejorar la adaptabilidad del modelo a unas condiciones de mercado que cambian dinámicamente.
Enlaces
- A Multimodal Foundation Agent for Financial Trading: Tool-Augmented, Diversified, and Generalist
- 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 de recopilación de ejemplos con el método Real-ORL |
3 | Study.mq5 | Asesor | Asesor de entrenamiento de modelos |
4 | Test.mq5 | Asesor | Asesor para la prueba de modelos |
5 | Trajectory.mqh | Biblioteca de clases | Estructura de descripción del estado del sistema y la arquitectura del modelo |
6 | NeuroNet.mqh | Biblioteca de clases | Biblioteca de clases para crear una red neuronal |
7 | NeuroNet.cl | Biblioteca | Biblioteca de código del programa OpenCL |
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/16867
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.





- 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