Predicción de tendencias con LSTM para estrategias de seguimiento de tendencias
Introducción
La memoria a corto y largo plazo (Long Short-Term Memory, LSTM) es un tipo de red neuronal recurrente (Recurrent Neural Network, RNN) diseñada para modelar datos secuenciales capturando de manera efectiva las dependencias a largo plazo y abordando el problema del gradiente que se desvanece. En este artículo, exploraremos cómo utilizar LSTM para predecir tendencias futuras, mejorando el rendimiento de las estrategias de seguimiento de tendencias. El artículo tratará sobre la introducción de conceptos clave y la motivación detrás del desarrollo, la obtención de datos de MetaTrader 5, el uso de esos datos para entrenar el modelo en Python, la integración del modelo de aprendizaje automático en MQL5 y la reflexión sobre los resultados y las aspiraciones futuras basadas en pruebas estadísticas retrospectivas.
Motivación
Intuitivamente, las estrategias de seguimiento de tendencias capitalizan las ganancias en mercados con tendencia, pero tienen un rendimiento deficiente en mercados volátiles, donde la estrategia termina comprando con una prima y vendiendo con un descuento. La investigación académica ha demostrado que las estrategias clásicas de seguimiento de tendencias, como la cruz dorada, funcionan en múltiples mercados y plazos a lo largo de extensos periodos históricos. Si bien estas estrategias pueden no ser altamente rentables, han demostrado generar ganancias constantes. Las estrategias de seguimiento de tendencias suelen beneficiarse de valores atípicos extremos, que generan ganancias significativamente mayores que la pérdida promedio. La estrategia, con su estricto límite de pérdidas y su enfoque de "dejar correr las ganancias", da como resultado una baja tasa de aciertos pero una alta relación recompensa-riesgo por operación.
LSTM (Long Short-Term Memory) es un tipo especializado de red neuronal recurrente (RNN) diseñada para capturar dependencias de largo alcance en datos secuenciales. Utiliza células de memoria que pueden mantener la información durante largos períodos, superando el problema de la desaparición del gradiente que suele afectar a las RNN tradicionales. Esta capacidad de almacenar y acceder a información de momentos anteriores de la secuencia hace que LSTM sea particularmente eficaz para tareas como la previsión de series temporales y la predicción de tendencias. Para problemas de regresión, LSTM puede modelar las relaciones temporales entre las características de entrada y predecir salidas continuas con alta precisión, lo que la hace ideal para aplicaciones de pronóstico.
La motivación de este artículo es aprovechar el poder de LSTM para la regresión de tendencias, prediciendo tendencias futuras y potencialmente filtrando malas operaciones que resultan de una baja tendencia. Esta motivación se basa en la hipótesis de que las estrategias de seguimiento de tendencias funcionan mejor en mercados con tendencia que en mercados sin tendencia.
Utilizaremos el ADX (Average Directional Index) para indicar la fuerza de la tendencia, ya que es uno de los indicadores más populares para evaluar la tendencia actual. Nuestro objetivo es predecir su valor futuro en lugar de utilizar su valor actual, ya que un ADX alto generalmente indica que una tendencia ya ha ocurrido o está terminando, lo que hace que nuestro punto de entrada sea demasiado tarde para obtener beneficios.
El ADX se calcula mediante:

Preparación y preprocesamiento de datos
Antes de obtener los datos, primero debemos aclarar qué datos se requieren. Planeamos utilizar varias características para entrenar un modelo de regresión que prediga los valores futuros de ADX. Estas características incluyen el RSI, que indica la fuerza relativa actual del mercado, el porcentaje de retorno de la última vela que sirve como valor estacionario del precio de cierre, y el propio ADX, que es directamente relevante para el valor que pretendemos predecir. Ten en cuenta que acabamos de explicar la intuición detrás de la elección de estas características. Puedes decidir tú mismo las características, pero asegúrate de que sean razonables y fijas. Planeamos entrenar el modelo utilizando datos por hora desde el 1 de enero de 2020 hasta el 1 de enero de 2024 y probar el rendimiento del modelo desde el 1 de enero de 2024 hasta el 1 de enero de 2025 como una prueba fuera de muestra.
Ahora que hemos aclarado los datos que queremos obtener, construyamos un asesor experto para recuperar estos datos.
Utilizaremos la clase CFileCSV, introducida en este artículo, para guardar la matriz como una cadena en un archivo CSV. El código para este proceso es bastante simple, como se muestra a continuación.
#include <FileCSV.mqh> CFileCSV csvFile; int barsTotal = 0; int handleRsi; int handleAdx; string headers[] = { "time", "ADX", "RSI", "Stationary" }; string data[1000000][4]; int indexx = 0; vector xx; input string fileName = "XAU-1h-2020-2024.csv"; input bool SaveData = true; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() {//Initialize model handleRsi = iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE); handleAdx = iADX(_Symbol,PERIOD_CURRENT,14); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if (!SaveData) return; if(csvFile.Open(fileName, FILE_WRITE|FILE_ANSI)) { //Write the header csvFile.WriteHeader(headers); //Write data rows csvFile.WriteLine(data); //Close the file csvFile.Close(); } else { Print("File opening error!"); } } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; double rsi[]; double adx[]; CopyBuffer(handleAdx,0,1,1,adx); CopyBuffer(handleRsi,0,1,1,rsi); data[indexx][0] =(string)TimeTradeServer(); data[indexx][1] = DoubleToString(adx[0], 2); data[indexx][2] = DoubleToString(rsi[0], 2); data[indexx][3] = DoubleToString((iClose(_Symbol,PERIOD_CURRENT,1)-iOpen(_Symbol,PERIOD_CURRENT,1))/iClose(_Symbol,PERIOD_CURRENT,1),3); indexx++; } }
Este asesor experto (Expert Advisor, EA) está diseñado para rastrear y registrar los valores del Índice de Fuerza Relativa (Relative Strength Index, RSI) y el Índice Direccional Promedio (Average Directional Index, ADX) para un símbolo determinado. El EA utiliza las funciones iRSI e iADX para obtener los valores actuales de RSI y ADX, y los almacena junto con una marca de tiempo en un archivo CSV. El archivo CSV se crea con encabezados para «tiempo», «ADX», «RSI» y «Estacionario».. Si la opción SaveData está habilitada, escribe los datos en un archivo (especificado por fileName) al desinicializar. Realiza un seguimiento de los nuevos datos en cada tick y los almacena cuando se produce un cambio en el número de barras.
Ejecute el Asesor Experto (EA) en el Probador de Estrategias con una sola prueba. Después de ejecutar la prueba, el archivo debe guardarse en la siguiente ruta: /Tester/Agent-sth000/MQL5/Files.
A continuación, pasamos a Python para el preprocesamiento de datos en preparación para el entrenamiento de nuestro modelo de aprendizaje automático.
Planeamos utilizar un enfoque de aprendizaje supervisado, donde el modelo se entrena para predecir un resultado deseado basándose en datos etiquetados. El proceso de entrenamiento implica ajustar los pesos de varias operaciones aplicadas a las características, con el fin de minimizar la pérdida por error y producir el resultado final.
Para la etiqueta, sugiero utilizar el valor ADX medio de los próximos 10 valores ADX. Este enfoque garantiza que la tendencia no esté ya completamente establecida cuando entremos en la operación, al tiempo que evita que la tendencia se aleje demasiado de la señal actual. Utilizar la media de los próximos 10 valores ADX es una excelente manera de asegurar que la tendencia se mantenga activa durante las próximas barras, lo que permite que nuestra entrada capture las ganancias de los próximos movimientos direccionales.
import pandas as pd data = pd.read_csv('XAU-1h-2020-2024.csv', sep=';') data= data.dropna().set_index('time') data['output'] = data['ADX'].shift(-10) data = data[:-10] data['output']= data['output'].rolling(window=10).mean() data = data[9:]
Este código lee el archivo CSV y separa los datos en diferentes columnas, ya que los valores están agrupados por un punto y coma (';'). A continuación, elimina las filas vacías y establece la columna 'time' como índice para garantizar que los datos estén ordenados cronológicamente para el entrenamiento. A continuación, se crea una nueva columna llamada "output", que calcula la media de los próximos 10 valores ADX. Después de esto, el código elimina las filas vacías restantes, ya que algunas filas pueden no tener suficientes valores ADX futuros para calcular la salida.
Entrenamiento de modelos

Este diagrama ilustra lo que LSTM intenta lograr durante nuestro proceso de entrenamiento. Queremos que la entrada tenga la forma (sample_amount, time_steps, feature_amount), donde time_step representa cuántos puntos temporales anteriores de datos queremos utilizar para predecir el siguiente valor. Por ejemplo, podríamos utilizar los datos del lunes al jueves para predecir algún resultado para el viernes. LSTM utiliza algoritmos para identificar patrones en las series temporales y la relación entre las características y el resultado. Crea una o más capas de redes neuronales, cada una de las cuales consta de muchas unidades de peso (neuronas), que aplican pesos a cada característica y cada paso temporal para, en última instancia, generar la predicción final.
Para simplificar, basta con ejecutar el siguiente código y este se encargará del proceso de entrenamiento por ti.
import numpy as np import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense # Assume data is your DataFrame already loaded with the specified columns and a time-based index # data.columns should include ['ADX', 'RSI', 'Stationary', 'output'] # --- Step 1: Data Preparation --- time_step = 5 # Select features and target features = ['ADX', 'RSI', 'Stationary'] target = 'output' # --- Step 2: Create sequences for LSTM input --- def create_sequences(data, target_col, time_step): """ Create sequences of length time_step from the DataFrame. data: DataFrame of input features and target. target_col: Name of the target column. Returns: X, y arrays suitable for LSTM. """ X, y = [], [] feature_cols = data.columns.drop(target_col) for i in range(len(data) - time_step): seq_x = data.iloc[i:i+time_step][feature_cols].values # predict target at the next time step after the sequence seq_y = data.iloc[i+time_step][target_col] X.append(seq_x) y.append(seq_y) return np.array(X), np.array(y) # Create sequences X, y = create_sequences(data, target_col=target, time_step=time_step) # --- Step 3: Split into training and evaluation sets --- # Use a simple 80/20 split for training and evaluation X_train, X_eval, y_train, y_eval = train_test_split(X, y, test_size=0.2, shuffle=False) # --- Step 4: Build the LSTM model --- n_features = len(features) # number of features per time step model = Sequential() model.add(LSTM(50, input_shape=(time_step, n_features))) # LSTM layer with 50 units model.add(Dense(1)) # output layer for regression model.compile(optimizer='adam', loss='mse') model.summary() # --- Step 5: Train the model --- epochs = 50 batch_size = 100 history = model.fit( X_train, y_train, epochs=epochs, batch_size=batch_size, validation_data=(X_eval, y_eval) )
A continuación se indican algunas consideraciones importantes a tener en cuenta al entrenar con el código anterior:
-
Preprocesamiento de datos: Asegúrese de que sus datos estén ordenados cronológicamente durante la fase de preprocesamiento. Si no se hace así, puede haber un sesgo de anticipación al dividir los datos en trozos de time_steps.
-
División entre entrenamiento y prueba: al dividir los datos en conjuntos de entrenamiento y prueba, no mezcle los datos. Se debe preservar el orden temporal para evitar el sesgo de anticipación.
-
Complejidad del modelo: para el análisis de series temporales, especialmente con puntos de datos limitados, no es necesario construir demasiadas capas, neuronas o épocas. Una excesiva complejidad del modelo podría conducir a un sobreajuste o a parámetros con un sesgo elevado. La configuración utilizada en el ejemplo debería ser suficiente.
# --- Step 6: Evaluate the model --- eval_loss = model.evaluate(X_eval, y_eval) print(f"Evaluation Loss: {eval_loss}") # --- Step 7: Generate Predictions and Plot --- # Generate predictions on the evaluation set predictions = model.predict(X_eval).flatten() # Create a plot for predictions vs actual values plt.figure(figsize=(12, 6)) plt.plot(predictions, label='Predicted Output', color='red') plt.plot(y_eval, label='Actual Output', color='blue') plt.title('LSTM Predictions vs Actual Output') plt.xlabel('Sample Index') plt.ylabel('Output Value') plt.legend() plt.show()
Este código debería generar el error cuadrático medio del conjunto de evaluación comparado con la predicción del modelo, de la siguiente manera.

Evaluation Loss: 57.405677795410156 Se calcula mediante:

Donde n es el tamaño de la muestra, yi es el valor previsto para cada muestra y y^i es el valor real para cada resultado de la evaluación.
Como se puede observar en el cálculo, se puede comparar la pérdida del modelo con el cuadrado de los valores medios de los elementos que se están prediciendo para comprobar si la pérdida relativa es excesivamente alta. Además, asegúrese de que la pérdida sea similar a la de los conjuntos de entrenamiento, lo que indica que el modelo no está sobreajustado a los datos de entrenamiento.
Finalmente, para que el modelo sea compatible con MQL5, queremos guardarlo en formato ONNX. Como los modelos LSTM no admiten directamente las transiciones ONNX, primero debemos guardar el modelo como funcional, definiendo explícitamente el formato de su entrada y salida. Después de eso, podemos guardarlo como un archivo ONNX, lo que lo hará adecuado para su uso futuro con MQL5.
import tensorflow as tf import tf2onnx # Define the input shape based on your LSTM requirements: (time_step, n_features) time_step = 5 n_features = 3 # Create a new Keras Input layer matching the shape of your data inputs = tf.keras.Input(shape=(time_step, n_features), name="input") # Pass the input through your existing sequential model outputs = model(inputs) functional_model = tf.keras.Model(inputs=inputs, outputs=outputs) # Create an input signature that matches the defined input shape input_signature = ( tf.TensorSpec((None, time_step, n_features), dtype=tf.float32, name="input"), ) output_path = "regression2024.onnx" # Convert the functional model to ONNX format onnx_model, _ = tf2onnx.convert.from_keras( functional_model, input_signature=input_signature, # matching the input signature opset=15, output_path=output_path ) print(f"Model successfully converted to ONNX at {output_path}")
Tenga en cuenta que "None" como formato de entrada aquí significa que el modelo puede aceptar cualquier número de muestras. Generará automáticamente las predicciones correspondientes para cada muestra, lo que lo hace flexible para diferentes tamaños de lote.
Construyendo un asesor experto
Ahora que hemos guardado el archivo del modelo ONNX, queremos copiarlo en el directorio /MQL5/Files para su uso posterior.
Volvemos a MetaEditor. Nos basaremos en una estrategia clásica de seguimiento de tendencias basada en la lógica de la señal de la cruz dorada. Es el mismo que implementé en mi artículo anterior sobre aprendizaje automático. La lógica básica implica dos medias móviles: una rápida y otra lenta. Se genera una señal de operación cuando las dos medias móviles se cruzan, y la dirección de la operación sigue la media móvil rápida, de ahí el término «seguimiento de tendencias». La señal de salida se produce cuando el precio cruza la media móvil lenta, lo que deja más margen para los stops dinámicos. El código completo es el siguiente:
#include <Trade/Trade.mqh> //XAU - 1h. CTrade trade; input int MaPeriodsFast = 15; input int MaPeriodsSlow = 25; input int MaPeriods = 200; input double lott = 0.01; ulong buypos = 0, sellpos = 0; input int Magic = 0; int barsTotal = 0; int handleMaFast; int handleMaSlow; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(Magic); handleMaFast =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsFast,0,MODE_SMA,PRICE_CLOSE); handleMaSlow =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsSlow,0,MODE_SMA,PRICE_CLOSE); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); //Beware, the last element of the buffer list is the most recent data, not [0] if (barsTotal!= bars){ barsTotal = bars; double maFast[]; double maSlow[]; CopyBuffer(handleMaFast,BASE_LINE,1,2,maFast); CopyBuffer(handleMaSlow,BASE_LINE,1,2,maSlow); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double lastClose = iClose(_Symbol, PERIOD_CURRENT, 1); //The order below matters if(buypos>0&& lastClose<maSlow[1]) trade.PositionClose(buypos); if(sellpos>0 &&lastClose>maSlow[1])trade.PositionClose(sellpos); if (maFast[1]>maSlow[1]&&maFast[0]<maSlow[0]&&buypos ==sellpos)executeBuy(); if(maFast[1]<maSlow[1]&&maFast[0]>maSlow[0]&&sellpos ==buypos) executeSell(); if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; } } } //+------------------------------------------------------------------+ //| Expert trade transaction handling function | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction& trans, const MqlTradeRequest& request, const MqlTradeResult& result) { if (trans.type == TRADE_TRANSACTION_ORDER_ADD) { COrderInfo order; if (order.Select(trans.order)) { if (order.Magic() == Magic) { if (order.OrderType() == ORDER_TYPE_BUY) { buypos = order.Ticket(); } else if (order.OrderType() == ORDER_TYPE_SELL) { sellpos = order.Ticket(); } } } } } //+------------------------------------------------------------------+ //| Execute sell trade function | //+------------------------------------------------------------------+ void executeSell() { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); trade.Sell(lott,_Symbol,bid); sellpos = trade.ResultOrder(); } //+------------------------------------------------------------------+ //| Execute buy trade function | //+------------------------------------------------------------------+ void executeBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); trade.Buy(lott,_Symbol,ask); buypos = trade.ResultOrder(); }
No voy a entrar en más detalles sobre la validación y las sugerencias para seleccionar su estrategia de backtest. Para más detalles, consulte mi artículo anterior sobre aprendizaje automático, cuyo enlace se encuentra aquí.
Ahora, intentaremos ejecutar nuestro modelo LSTM basándonos en este marco.
En primer lugar, declaramos las variables globales que especifican la forma de nuestra entrada y salida, así como dos matrices múltiples para almacenar los datos de entrada y salida. Además, declaramos un identificador de modelo que gestionará el proceso de obtención de datos para el modelo y la extracción de predicciones del mismo. Esta configuración garantiza un flujo de datos adecuado y una interacción correcta entre el modelo y las variables de entrada/salida.
#resource "\\Files\\regression2024.onnx" as uchar lstm_onnx[] float data[1][5][3]; float out[1][1]; long lstmHandle = INVALID_HANDLE; const long input_shape[] = {1,5,3}; const long output_shape[]={1,1};
A continuación, en la función OnInit(), inicializamos los indicadores relevantes, como RSI y ADX, así como el modelo ONNX. Durante esta inicialización, verificamos que la forma de entrada y la forma de salida declaradas en MQL5 coincidan con las especificadas anteriormente en el modelo funcional de Python. Este paso garantiza la coherencia y evita errores durante la inicialización del modelo, asegurando que el modelo pueda procesar correctamente los datos en el formato esperado.
int handleMaFast; int handleMaSlow; int handleAdx; // Average Directional Movement Index - 3 int handleRsi; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() {//Initialize model trade.SetExpertMagicNumber(Magic); handleMaFast =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsFast,0,MODE_SMA,PRICE_CLOSE); handleMaSlow =iMA(_Symbol,PERIOD_CURRENT,MaPeriodsSlow,0,MODE_SMA,PRICE_CLOSE); handleAdx=iADX(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index - 3 handleRsi = iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE); // Load the ONNX model lstmHandle = OnnxCreateFromBuffer(lstm_onnx, ONNX_DEFAULT); //--- specify the shape of the input data if(!OnnxSetInputShape(lstmHandle,0,input_shape)) { Print("OnnxSetInputShape failed, error ",GetLastError()); OnnxRelease(lstmHandle); return(-1); } //--- specify the shape of the output data if(!OnnxSetOutputShape(lstmHandle,0,output_shape)) { Print("OnnxSetOutputShape failed, error ",GetLastError()); OnnxRelease(lstmHandle); return(-1); } if (lstmHandle == INVALID_HANDLE) { Print("Error creating model OnnxCreateFromBuffer ", GetLastError()); return(INIT_FAILED); } return(INIT_SUCCEEDED); }
A continuación, declaramos una función para actualizar los datos de entrada con cada nueva barra. Esta función recorre el time_step (en este caso, 5) para almacenar los datos correspondientes en la matriz global. Convierte los datos a tipo flotante para garantizar que cumplan con el requisito de 32 bits que espera el modelo ONNX. Además, la función garantiza que el orden de la matriz múltiple sea correcto, con los datos más antiguos primero y los datos más recientes añadidos en secuencia. Esto garantiza que los datos se introduzcan en el modelo en el orden cronológico correcto.
void getData(){ double rsi[]; double adx[]; CopyBuffer(handleAdx,0,1,5,adx); CopyBuffer(handleRsi,0,1,5,rsi); for (int i =0; i<5; i++){ data[0][i][0] = (float)adx[i]; data[0][i][1] = (float)rsi[i]; data[0][i][2] = (float)((iClose(_Symbol,PERIOD_CURRENT,5-i)-iOpen(_Symbol,PERIOD_CURRENT,5-i))/iClose(_Symbol,PERIOD_CURRENT,5-i)); } }
Por último, en la función OnTick(), implementamos la lógica de negociación.
Esta función garantiza que la lógica de negociación posterior solo se compruebe cuando se haya formado una nueva barra. Esto evita recálculos innecesarios o acciones comerciales durante la misma barra y garantiza que las predicciones del modelo se basen en datos completos para cada nuevo intervalo de tiempo.
int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars;
Este código restablece las variables buypos y sellpos a 0 cuando no quedan posiciones con el número mágico del EA. Las variables buypos y sellpos se utilizan para garantizar que tanto las posiciones de compra como las de venta estén vacías antes de generar una señal de entrada. Al restablecer estas variables cuando no hay posiciones abiertas, nos aseguramos de que el sistema no intente abrir accidentalmente nuevas posiciones si ya existe una.
if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; }
Utilizamos esta línea de código para ejecutar el modelo ONNX, donde toma los datos de entrada y envía la predicción a la matriz de salida. Esta operación se ejecuta solo cuando se forma la señal de entrada inicial, en lugar de en cada nueva barra. Este enfoque ayuda a conservar la potencia de cálculo y hace que la prueba retrospectiva sea más eficiente, ya que evitamos evaluaciones innecesarias del modelo durante los periodos en los que no hay señales de entrada.
OnnxRun(lstmHandle, ONNX_NO_CONVERSION, data, out);
La lógica de negociación ahora es la siguiente: cuando se produce el cruce de la media móvil y no hay ninguna posición abierta, ejecutamos el modelo para obtener el valor ADX previsto. Si el valor es inferior a un umbral determinado, lo consideramos una tendencia baja y evitamos la operación, y si es superior, entramos. Aquí está la función OnTick() completa:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; double maFast[]; double maSlow[]; double adx[]; CopyBuffer(handleMaFast,BASE_LINE,1,2,maFast); CopyBuffer(handleMaSlow,BASE_LINE,1,2,maSlow); CopyBuffer(handleAdx,0,1,1,adx); double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double lastClose = iClose(_Symbol, PERIOD_CURRENT, 1); //The order below matters if(buypos>0&& lastClose<maSlow[1]) trade.PositionClose(buypos); if(sellpos>0 &&lastClose>maSlow[1])trade.PositionClose(sellpos); if(maFast[1]<maSlow[1]&&maFast[0]>maSlow[0]&&sellpos == buypos){ getData(); OnnxRun(lstmHandle, ONNX_NO_CONVERSION, data, out); if(out[0][0]>threshold)executeSell();} if(maFast[1]>maSlow[1]&&maFast[0]<maSlow[0]&&sellpos == buypos){ getData(); OnnxRun(lstmHandle, ONNX_NO_CONVERSION, data, out); if(out[0][0]>threshold)executeBuy();} if(buypos>0&&(!PositionSelectByTicket(buypos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ buypos = 0; } if(sellpos>0&&(!PositionSelectByTicket(sellpos)|| PositionGetInteger(POSITION_MAGIC) != Magic)){ sellpos = 0; } } }
Prueba retrospectiva estadística
Tras implementar todo, ya podemos compilar el EA y probar los resultados en el probador de estrategias. Realizaremos una prueba fuera de muestra para XAUUSD en el marco temporal de 1 hora desde el 1 de enero de 2024 hasta el 1 de enero de 2025. En primer lugar, ejecutaremos nuestra estrategia de prueba retrospectiva original como referencia. Esperamos que el EA con la implementación LSTM supere al modelo de referencia durante este periodo.




Ahora, ejecutemos la prueba retrospectiva en el EA con la implementación LSTM, utilizando un umbral de 30, ya que un ADX de 30 es ampliamente reconocido como indicativo de una fuerte tendencia.




Al comparar los dos resultados, observamos que la implementación de LSTM filtró alrededor del 70 % de las operaciones originales y mejoró el factor de beneficio de 1,48 a 1,52. También mostró una correlación LR más alta que la línea de base, lo que sugiere que contribuyó a un rendimiento general más estable.
Al realizar pruebas retrospectivas de modelos de aprendizaje automático, es importante reconocer que los parámetros internos del modelo son determinantes clave, a diferencia de las estrategias más simples donde los parámetros tienen menos impacto. En consecuencia, diferentes datos de entrenamiento pueden conducir a resultados de parámetros muy diferentes. Además, entrenar con todo el conjunto de datos históricos a la vez no es lo ideal, ya que daría como resultado demasiadas muestras, la mayoría de las cuales carecerían de actualidad. Por este motivo, recomiendo utilizar el método de ventana deslizante para realizar pruebas retrospectivas en estos casos. Si disponemos de muestras limitadas a lo largo de todo el historial de backtesting, tal y como se explica en mi artículo anterior sobre el modelo CatBoost, es más adecuado realizar un backtesting con ventana expansiva.
Aquí están las imágenes de demostración:


La prueba retrospectiva con ventana expansible comienza con una ventana de datos de tamaño fijo inicial, pero a medida que se dispone de nuevos puntos de datos, la ventana se expande para incluir los nuevos datos, probando la estrategia en un conjunto de datos cada vez mayor a lo largo del tiempo.
Para realizar la prueba retrospectiva deslizante, simplemente repetimos el proceso descrito en este artículo y fusionamos los resultados en un único conjunto de datos. A continuación se muestra el rendimiento del backtest deslizante desde el 1 de enero de 2015 hasta el 1 de enero de 2025:

Métricas:
Profit Factor: 1.24 Maximum Drawdown: -250.56 Average Win: 12.02 Average Loss: -5.20 Win Rate: 34.81%
El resultado es impresionante, con margen para seguir mejorando.
Reflexión
El rendimiento del algoritmo evolutivo se correlaciona directamente con la predictibilidad que muestra el modelo. Para mejorar tu EA, hay algunos factores clave a considerar:
- La ventaja de su estrategia en pruebas retrospectivas: en última instancia, la mayoría de sus señales originales deben tener una ventaja que justifique un filtrado adicional.
- Los datos utilizados: las ineficiencias del mercado se revelan analizando la importancia de cada variable y identificando aquellas menos conocidas que podrían proporcionar una ventaja.
- El modelo que utilizas para entrenar: piensa si lo que intentas resolver es un problema de clasificación o de regresión. Y elegir los parámetros de entrenamiento adecuados también es fundamental.
- Las cosas que intentas predecir: en lugar de predecir directamente el resultado de una operación, céntrate en algo relacionado indirectamente con el resultado final, tal y como demuestro en este artículo.
A lo largo de mis artículos anteriores, he experimentado con diversas técnicas de aprendizaje automático que están al alcance de los operadores minoristas. Mi objetivo es inspirar a los lectores para que adopten estas ideas y desarrollen sus propios enfoques innovadores, ya que la creatividad en este campo no tiene límites. El aprendizaje automático no es intrínsecamente complejo ni inalcanzable: es una forma de pensar. Se trata de comprender los límites, construir modelos predictivos y comprobar hipótesis de forma rigurosa. A medida que continúes experimentando, esta comprensión se irá aclarando gradualmente.
Conclusión
En este artículo, primero presentamos la motivación para usar LSTM en la predicción de tendencias, explicando los conceptos detrás de ADX y LSTM. A continuación, obtuvimos datos de MetaTrader 5, los procesamos y entrenamos el modelo en Python. Luego, repasamos el proceso de construcción del Asesor Experto y revisamos los resultados de las pruebas retrospectivas. Por último, introdujimos los conceptos de pruebas retrospectivas con ventana deslizante y pruebas retrospectivas con ventana expansiva, y concluimos el artículo con algunas reflexiones.
Tabla de archivos
| Nombre del archivo | Uso del archivo |
|---|---|
| FileCSV.mqh | El archivo de inclusión para almacenar datos en formato CSV. |
| LSTM_Demonstration.ipynb | El archivo Python para entrenar el modelo LSTM. |
| LSTM-TF-XAU.mq5 | El EA de trading con implementación LSTM. |
| OHLC Getter.mq5 | El EA para obtener datos. |
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/16940
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 11): EA de señales Heikin Ashi
Características del Wizard MQL5 que debe conocer (Parte 53): Market Facilitation Index (MFI)
Ingeniería de características con Python y MQL5 (Parte III): El ángulo del precio (2) Coordenadas polares
Automatización de estrategias de trading en MQL5 (Parte 5): Desarrollo de la estrategia Adaptive Crossover RSI Trading Suite
- 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
No lo entiendo: ¿dónde está el propio modelo regression2024.onnx en el archivo zip?
Hola an_tar.
Como se menciona en el artículo, este tipo de sistema debe ser validado a través de backtest rolling-window. No quería incluir todo mi modelo entrenado desde 2008 para hacer el archivo pesado.
Se aconseja utilizar el marco introducido en el artículo para entrenar su propio modelo para ser compatible con su método de validación personal.