
Integración de MQL5 con paquetes de procesamiento de datos (Parte 2): Aprendizaje automático (Machine Learning, ML) y análisis predictivo
Introducción
En este artículo nos centramos específicamente en el aprendizaje automático (Machine Learning, ML) y el análisis predictivo. Los paquetes de procesamiento de datos abren nuevas fronteras para los operadores cuantitativos y los analistas financieros. Al incorporar capacidades de aprendizaje automático en MQL5, los operadores pueden elevar sus estrategias de negociación de sistemas tradicionales basados en reglas a sofisticados modelos basados en datos que se adaptan continuamente a la evolución de las condiciones del mercado.
El proceso implica el uso de las potentes bibliotecas de procesamiento de datos y aprendizaje automático de Python, como Scikit-learn, junto con MQL5. Esta integración permite a los operadores entrenar modelos predictivos a partir de datos históricos, comprobar su eficacia mediante técnicas de pruebas retrospectivas y, a continuación, desplegar esos modelos para tomar decisiones de negociación en tiempo real. La flexibilidad para combinar estas herramientas permite crear estrategias que van más allá de los indicadores técnicos típicos, incorporando análisis predictivos y reconocimiento de patrones que pueden mejorar significativamente los resultados de las operaciones.
Recopilación de datos históricos
Para empezar necesitamos datos históricos de MetaTrader 5 guardados en formato CSV, así que simplemente inicie su plataforma MetaTrader y en la parte superior de su panel MetaTrader 5 navegue a > Herramientas y luego > Opciones y llegará a las opciones de Gráficos. A continuación, deberá seleccionar la cantidad de barras del gráfico que desea descargar. Es mejor elegir la opción de barras ilimitadas ya que trabajaremos con fecha y no sabríamos cuántas barras hay en un periodo de tiempo determinado.
Después tendrás que descargar los datos reales. Para ello tendrás que navegar a > Ver y luego a > Símbolos y aterrizarás en la pestaña Especificaciones simplemente navega a > Barras o Ticks dependiendo del tipo de datos que quieras descargar. Proceda e introduzca el periodo de fechas de inicio y fin de los datos históricos que desea descargar, después haga clic en el botón de solicitud para descargar los datos y guardarlos en formato CSV.
Después de todos estos pasos, habrá descargado con éxito los datos históricos de MetaTrader. Ahora necesitas descargar y configurar el entorno Jupyter Lab para el análisis. Para descargar y configurar Jupyter Lab puedes dirigirte a su página web oficial y seguir unos sencillos pasos para descargarlo. Dependiendo del tipo de sistema operativo que utilices, tendrás variedad de opciones de si instalar utilizando pip, conda o brew.
Procesar datos históricos de MetaTrader 5 en Jupyter Lab
Para cargar con éxito sus datos históricos de MetaTrader 5 en Jupyter Lab tendrá que conocer la carpeta que había seleccionado para descargar los datos, y luego en Jupyter Lab simplemente navegar a esa carpeta. Para empezar tendrá que cargar los datos e inspeccionar los nombres de las columnas. Tenemos que inspeccionar los nombres de las columnas para manejarlas correctamente y evitar errores que podrían surgir si utilizamos un nombre de columna incorrecto. Código Python:
import pandas as pd # assign variable to the historical data file_path = '/home/int_junkie/Documents/ML/predi/XAUUSD.m_H1_201510010000_202408052300.csv' data = pd.read_csv(file_path, delimiter='\t') # Display the first few rows and column names print(data.head()) print(data.columns)
Salida:
Estamos trabajando con datos históricos de MetaTrader 5 del año 2015 al 2024, es decir, aproximadamente 9 años de datos históricos. Este tipo de datos ayuda a capturar ciclos amplios del mercado. Es probable que el conjunto de datos capture diferentes fases del mercado, lo que permitirá una mejor comprensión y modelado de estos ciclos. Los conjuntos de datos más largos reducen la probabilidad de sobreajuste al proporcionar una gama más completa de escenarios.
Es más probable que un modelo entrenado en un conjunto de datos más amplio se generalice bien a datos no vistos, especialmente si el conjunto de datos corresponde a un período de tiempo menor, como H1. Esto es particularmente importante en el análisis de series de tiempo, donde tener más observaciones mejora la confiabilidad de los resultados. Por ejemplo, puede detectar tendencias seculares (direcciones del mercado a largo plazo) o efectos estacionales recurrentes que son relevantes para la previsión.
Gráfico de líneas a partir de datos históricos
data.plot.line(y = "<CLOSE>", x = "<DATE>", use_index = True)
Salida:
Utilizamos el código anterior al visualizar datos de series de tiempo, como activos financieros a lo largo del tiempo. Si el índice de su marco de datos ya contiene fechas, puede omitir la especificación 'x="<DATE>"' y simplemente use 'use_index=True'.
del data["<VOL>"] del data["<SPREAD>"]
Luego eliminamos las columnas especificadas de nuestros datos históricos, usamos la biblioteca pandas para eliminar estas columnas.
data.head()
Salida:
De la salida anterior podemos ver que efectivamente se eliminaron las columnas especificadas.
# We add a colunm for tommorows price data["<NexH>"] = data["<CLOSE>"].shift(-1)
1. 'data["<NexH>"]':
- Esto añade una nueva columna llamada '<NexH>' (SIGUIENTE HORA) al marco de datos (Data-Frame) 'data'. El valor de esta columna representará los precios de cierre para la próxima hora en relación con cada fila.
2. 'data["<CLOSE>"].shift(-1)':
- 'data["<CLOSE>"]' se refiere a la columna existente en el Data-Frame que contiene los precios de cierre de para cada fecha y hora.
- El método '.shift(-1)' desplaza la fecha y hora de la columna '<CLOSE>' una fila hacia arriba (porque el argumento es '-1'), lo que efectivamente desplaza cada valor a la fila anterior.
- Como resultado, el valor que originalmente correspondía a una fecha determinada ahora aparecerá en la fila correspondiente a la fecha anterior.
Salida:
data["<TRGT>"] = (data["<NexH>"] > data["<CLOSE>"]).astype(int) data
A continuación, utilizaremos el código anterior para crear una nueva columna en el Data-Frame '<data>' que contenga valores binarios (0 ó 1) que indiquen si el precio máximo del siguiente periodo (NexH) es mayor que el precio de cierre actual (<CLOSE>).
1. 'data["<TRGT>"]':
- Esta es la nueva columna llamada '"<TRGT> "' (TARGET) en el marco de datos 'data'. Esta columna almacenará los valores objetivo binarios (0 o 1) según una condición.
2. '(data["<NexH>"] > data["<CLOSE>"])':
- Esta expresión compara el valor de la columna '<NexH>' (precio máximo del período siguiente) con el valor de la columna '<CLOSE>' (precio de cierre actual) de cada fila.
- Se trata de una serie booleana, donde cada valor es 'True' (si el siguiente máximo es mayor que el cierre actual) o 'False'.
3. '.astype(int)':
- Esta función convierte los valores booleanos ('True' o 'False') en enteros ('1' o '0', respectivamente).
- 'True' se convierte en “1” y 'Falso' en “0”.
Salida:
from sklearn.ensemble import RandomForestClassifier model = RandomForestClassifier(n_estimators = 50, min_samples_split = 50, random_state = 1) train = data.iloc[:-50] test = data.iloc[-50:] predictors = ["<CLOSE>","<TICKVOL>", "<OPEN>", "<HIGH>", "<LOW>"] model.fit(train[predictors], train["<TRGT>"])
Salida:
1. Importación del clasificador Random Forest
- El 'Random-Forest-Classifier' es un modelo de aprendizaje automático ensamblado que crea múltiples árboles de decisión y fusiona sus resultados para mejorar la precisión predictiva y controlar el sobreajuste.
2. Inicialización del modelo:
- 'estimator' especifica el número de árboles de decisión en el bosque. En este caso, el modelo construirá 50 árboles.
- 'min_sample_split' establece el mínimo de muestras necesarias para dividir un nodo interno. Un valor más alto reduce el sobreajuste al garantizar que las divisiones solo ocurran cuando haya suficientes datos disponibles.
- 'random_state' corrige la semilla aleatoria para garantizar la reproducibilidad insegura de los resultados. Usar la misma semilla (por ejemplo, '1') producirá los mismos resultados cada vez que se ejecute el código.
3. División de los datos en conjuntos de entrenamiento y prueba:
- 'data.iloc[:-50]' selecciona todas las filas excepto las últimas 50 como datos de entrenamiento.
- 'data.iloc[-50:]' selecciona las últimas 50 filas como datos de prueba
- Esta división se utiliza comúnmente en datos de series de tiempo, donde el modelo se entrena con datos históricos y se prueba con los datos más recientes para evaluar el rendimiento en predicciones futuras.
4. Especificación de las variables predictoras:
- La lista 'predictors' contiene los nombres de las columnas que representan las características utilizadas por el modelo para realizar predicciones. Entre ellos se incluyen (''«<CLOSE>», '«<TICKVOL>»', '«<OPEN>»', '«<HIGH>»', y '«<LOW>»').
El código prepara un clasificador Random Forest para predecir el comportamiento futuro del mercado a partir de datos pasados. El modelo se entrena utilizando características como precio de cierre, volumen de ticks y otras. Después de dividir los datos en conjuntos de entrenamiento y prueba, el modelo se ajusta a los datos de entrenamiento, aprendiendo de los patrones históricos para hacer predicciones futuras.
Medir la precisión del modelo
from sklearn.metrics import precision_score prcsn = model.predict(test[predictors])
Importamos la función 'precision-score' del módulo 'sklearn.metrics'. La puntuación de precisión es una métrica utilizada para evaluar modelos de clasificación, particularmente útil cuando las clases están desequilibradas. Mide cuántos de los resultados positivos previstos son realmente positivos. Una alta precisión indica tasas positivas bajas.
prcsn = pd.Series(prcsn, index = test.index)
Luego convertimos las predicciones ('prcsn') en una 'Series' de Pandas mientras preservamos el índice del conjunto de datos de prueba.
precision_score(test["<TRGT>"], prcsn)
Obtenemos la precisión de las predicciones del modelo comparando los valores objetivo reales en el conjunto de prueba con los valores previstos.
Salida:
cmbnd = pd.concat([test["<TRGT>"], prcsn], axis = 1) cmbnd.plot()
Combinamos los valores objetivo reales y los valores previstos del modelo en un único marco de datos para facilitar el análisis.
Salida:
def predors(train, test, predictors, model): model.fit(train[predictors], train["<TRGT>"]) prcsn = model.predict(test[predictors]) prcsn = pd.Series(prcsn, index = test.index, name = "Predictions") cmbnd = pd.concat([test["<TRGT>"], prcsn], axis = 1) return cmbnd
Esta función toma un conjunto de datos de entrenamiento y prueba, una lista de variables predictoras y un modelo de aprendizaje automático. La función entrena el modelo con los datos de entrenamiento, hace predicciones sobre los datos de prueba y devuelve un marco de datos que contiene los valores objetivo reales y los valores predichos uno al lado del otro.
def backtestor(data, model, predictors, start = 2500, step = 250): all_predictions = [] for i in range(start, data.shape[0], step): train = data.iloc[0:i].copy() test = data.iloc[i:(i + step)].copy() predictions = predors(train, test, predictors, model) all_predictions.append(predictions) return pd.concat(all_predictions)
Esta función realiza una prueba retrospectiva en un conjunto de datos de series temporales utilizando un modelo de aprendizaje automático. La prueba retrospectiva evalúa el rendimiento del modelo simulando predicciones como si se hicieran en un entorno comercial del mundo real, donde los datos se revelan gradualmente con el tiempo.
predictions = backtestor(data, model, predictors)
Ejecuta la función 'backtestor' utilizando el conjunto de datos especificado ('data'), el modelo de aprendizaje automático ('model') y las variables predictoras ('predictors'). Realiza una prueba retrospectiva y las predicciones resultantes se almacenan en la variable 'predicitions'.
predictions["Predictions"].value_counts()
Contamos el número de apariciones de cada valor único en la columna «Predictions» del marco de datos «prediction».
Salida:
precision_score(predictions["<TRGT>"], predictions["Predictions"])
Calcula la precisión de la predicción del modelo. La precisión es una métrica que mide la exactitud de las predicciones positivas.
Salida:
predictions["<TRGT>"].value_counts() / predictions.shape[0]
Calcula la proporción de cada valor único en la columna '<TRGT>' en relación con el número total de predicciones.
Salida:
horizons = [2, 5, 55, 125, 750] new_predictors = [] # Ensure only numeric columns are used for rolling calculations numeric_columns = data.select_dtypes(include=[float, int]).columns for i in horizons: # Calculate rolling averages for numeric columns only rolling_averages = data[numeric_columns].rolling(i).mean() # Generate the ratio column ratio_column = f"Close_Ratio_{i}" data[ratio_column] = data["<CLOSE>"] / rolling_averages["<CLOSE>"] # Generate the trend column trend_column = f"Trend_{i}" data[trend_column] = data["<TRGT>"].shift(1).rolling(i).sum() new_predictors += [ratio_column, trend_column] data
Genera nuevas funciones basadas en promedios móviles y tendencias en diferentes horizontes temporales. Los predictores adicionales ayudan a mejorar el rendimiento de los modelos al proporcionarles más información sobre el mercado durante períodos variables.
Salida:
data = data.dropna()
Eliminamos cualquier fila del Data-Frame con valores faltantes.
def predict(train, test, predictors, model): model.fit(train[predictors], train["<TRGT>"]) prcsn = model.predict_proba(test[predictors])[:1] prcsn[prcsn >= .6] = 1 prcsn[prcsn < .6] = 0 prcsn = pd.Series(prcsn, index = test.index, name = "Predictions") cmbnd = pd.concat([test["<TRGT>"], prcsn], axis = 1) return cmbnd
El modelo se entrena en el conjunto de datos de entrenamiento utilizando los predictores seleccionados y la variable objetivo. Para el umbral y las predicciones, aplicamos un umbral personalizado de 0,6. Si la probabilidad de la clase 1 es 0,6 o superior, el modelo predice "1". De lo contrario, predice "0". Este ajuste permite que el modelo sea más conservador, requiriendo una mayor confianza antes de señalar una operación.
predictions = backtestor(data, model, new_predictors)
predictions["Predictions"].value_counts()
precision_score(predictions["<TRGT>"], predictions["Predictions"])
Salida:
Nuestra puntuación de precisión aumentó ligeramente, 0,52 si redondeamos hacia arriba.
Entrena el modelo y expórtalo a ONNX
import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split import onnx import skl2onnx from skl2onnx import convert_sklearn from skl2onnx.common.data_types import FloatTensorType # Load and preprocess your data (example) # Replace this with your actual data loading process #data = pd.read_csv('your_data.csv') # Replace with your actual data source #data = data.dropna() # Define predictors and target predictors = ["<CLOSE>", "<TICKVOL>", "<OPEN>", "<HIGH>", "<LOW>"] target = "<TRGT>" # Split data into train and test sets train, test = train_test_split(data, test_size=0.2, shuffle=False) # Define and train the model model = RandomForestClassifier(n_estimators=50, min_samples_split=50, random_state=1) model.fit(train[predictors], train[target]) # Export the trained model to ONNX format initial_type = [('float_input', FloatTensorType([None, len(predictors)]))] onnx_model = convert_sklearn(model, initial_types=initial_type) # Save the ONNX model to a file with open("random_forest_model.onnx", "wb") as f: f.write(onnx_model.SerializeToString())
Entrenamos nuestro modelo, y lo convertimos, exportamos y guardamos en formato ONNX usando `skl2onnx`. Copiamos nuestro modelo guardado en la carpeta 'Files' de MQL5 donde podremos acceder a él.
Poniéndolo todo junto en MQL5
Cargue el modelo en Oninit().
#include <Trade/Trade.mqh> #define ModelName "RandomForestClassifier" #define ONNXFilename "random_forest_model.onnx" // Single ONNX model resource #resource "\\Files\\random_forest_model.onnx" as const uchar ExtModelDouble[]; input double lotsize = 0.1; // Trade lot size input double stoploss = 20; // Stop loss in points input double takeprofit = 50; // Take profit in points // Trading functions CTrade m_trade;
Las variables 'includes' y globales en el ámbito global. También especificamos que el modelo ONNX está integrado en MQL5 como un recurso binario. El '#resource' se utiliza para incluir archivos externos.
//+------------------------------------------------------------------+ //| Run classification using double values | //+------------------------------------------------------------------+ bool RunModel(long model, vector &input_vector, vector &output_vector) { ulong batch_size = input_vector.Size() / 5; // Assuming 5 input features if (batch_size == 0) return (false); output_vector.Resize((int)batch_size); // Prepare input tensor double input_data[]; ArrayResize(input_data, input_vector.Size()); for (int k = 0; k < input_vector.Size(); k++) input_data[k] = input_vector[k]; // Set input shape ulong input_shape[] = {batch_size, 5}; // 5 input features for each prediction OnnxSetInputShape(model, 0, input_shape); // Prepare output tensor double output_data[]; ArrayResize(output_data, (int)batch_size); // Set output shape (binary classification) ulong output_shape[] = {batch_size, 2}; // Output shape for probability (0 or 1) OnnxSetOutputShape(model, 0, output_shape); // Run the model bool res = OnnxRun(model, ONNX_DEBUG_LOGS, input_data, output_data); if (res) { // Copy output to vector (only keeping the class with highest probability) for (int k = 0; k < batch_size; k++) output_vector[k] = (output_data[2 * k] < output_data[2 * k + 1]) ? 1.0 : 0.0; } return (res); }
Esta función 'RunModel' es un modelo ONNX que hemos entrenado para realizar clasificaciones binarias. La función determina la clase prevista (0 o 1) en función de qué clase tiene la mayor probabilidad y almacena los resultados en un vector de salida.
//+------------------------------------------------------------------+ //| Generate input data for prediction | //+------------------------------------------------------------------+ vector input_data() { vector input_vector; MqlRates rates[]; // Get the last 5 bars of data if (CopyRates(Symbol(), PERIOD_H1, 5, 1, rates) > 0) { input_vector.Resize(5 * 5); // 5 input features for each bar for (int i = 0; i < 5; i++) { input_vector[i * 5] = rates[i].open; input_vector[i * 5 + 1] = rates[i].high; input_vector[i * 5 + 2] = rates[i].low; input_vector[i * 5 + 3] = rates[i].close; input_vector[i * 5 + 4] = rates[i].tick_volume; } } return (input_vector); } //+------------------------------------------------------------------+ //| Check if there is a new bar | //+------------------------------------------------------------------+ bool NewBar() { static datetime last_time = 0; datetime current_time = iTime(Symbol(), Period(), 0); if (current_time != last_time) { last_time = current_time; return (true); } return (false); } //+------------------------------------------------------------------+ //| Check if a position of a certain type exists | //+------------------------------------------------------------------+ bool PosExists(int type) { for (int i = PositionsTotal() - 1; i >= 0; i--) { if (PositionGetInteger(POSITION_TYPE) == type && PositionGetString(POSITION_SYMBOL) == Symbol()) return (true); } return (false); } //+------------------------------------------------------------------+ //| Script program initialization | //+------------------------------------------------------------------+ int OnInit() { Print("Initializing ONNX model..."); // Initialize the ONNX model long model = OnnxCreateFromBuffer(ExtModelDouble, ONNX_DEFAULT); if (model == INVALID_HANDLE) { Print("Error loading ONNX model: ", GetLastError()); return INIT_FAILED; } // Store the model handle for further use GlobalVariableSet("model_handle", model); return (INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { if (NewBar()) // Trade at the opening of a new candle { vector input_vector = input_data(); vector output_vector; // Retrieve the model handle long model = GlobalVariableGet("model_handle"); if (model == INVALID_HANDLE) { Print("Invalid model handle."); return; } bool prediction_success = RunModel(model, input_vector, output_vector); if (!prediction_success || output_vector.Size() == 0) { Print("Prediction failed."); return; } long signal = output_vector[0]; // The predicted class (0 or 1) MqlTick ticks; if (!SymbolInfoTick(Symbol(), ticks)) return; if (signal == 1) // Bullish signal { if (!PosExists(POSITION_TYPE_BUY)) // No buy positions exist { if (!m_trade.Buy(lotsize, Symbol(), ticks.ask, ticks.bid - stoploss * Point(), ticks.ask + takeprofit * Point())) // Open a buy trade Print("Failed to open a buy position, error = ", GetLastError()); } } else if (signal == 0) // Bearish signal { if (!PosExists(POSITION_TYPE_SELL)) // No sell positions exist { if (!m_trade.Sell(lotsize, Symbol(), ticks.bid, ticks.ask + stoploss * Point(), ticks.bid - takeprofit * Point())) // Open a sell trade Print("Failed to open a sell position, error = ", GetLastError()); } } } } //+------------------------------------------------------------------+ //| Script program deinitialization | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { // Release the ONNX model long model = GlobalVariableGet("model_handle"); if (model != INVALID_HANDLE) { OnnxRelease(model); } }Durante la inicialización, se carga el modelo y se almacena su identificador para su uso posterior. En la función 'OnTick()', el script ejecuta el modelo cuando se detecta una nueva barra. Utilizamos la predicción del modelo (0 para bajista y 1 para alcista) y ejecutamos operaciones en consecuencia. Las operaciones de compra se realizan cuando el modelo predice una tendencia alcista, mientras que las operaciones de venta se realizan cuando una tendencia bajista.
Conclusión
En resumen, hemos utilizado un paquete de procesamiento de datos (Jupyter Lab) para procesar datos históricos, desarrollado y entrenado el modelo usando aprendizaje automático para poder hacer predicciones. Hemos explorado los pasos cruciales que son necesarios para una integración y un funcionamiento perfectos. Luego nos centramos en cargar y gestionar el modelo dentro de MQL5 incorporándolo como un recurso y garantizando que el modelo esté correctamente inicializado y disponible durante el tiempo de ejecución.
En conclusión, hemos integrado un modelo ONNX en un entorno comercial MQL5 para mejorar la toma de decisiones mediante el aprendizaje automático. El proceso comenzó con la carga del modelo en el entorno MQL5. Luego configuramos el Asesor Experto para recopilar datos relevantes del mercado, preprocesarlos en vectores de características y alimentarlos al modelo para realizar predicciones. La lógica fue diseñada para ejecutar transacciones basadas en el resultado del modelo. Las posiciones solo se abren cuando se detecta una nueva barra y no hay operaciones conflictivas activas. Además, el sistema gestiona la comprobación de posiciones, la gestión de errores y la desasignación de recursos para garantizar una solución comercial sólida y eficiente. Esta implementación demuestra una combinación perfecta de análisis financiero y conocimientos impulsados por inteligencia artificial, lo que permite estrategias comerciales automatizadas que se adaptan a las condiciones del mercado en tiempo real.
Referencias
Manual de referencia de MQL5 > Modelos ONNX > Creación de un modelo
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/15578





- 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