
Utilización del modelo de aprendizaje automático CatBoost como filtro para estrategias de seguimiento de tendencias
Introducción
CatBoost es un potente modelo de aprendizaje automático basado en árboles que se especializa en la toma de decisiones basada en características estacionarias. Otros modelos basados en árboles, como XGBoost y Random Forest, comparten características similares en cuanto a su solidez, capacidad para manejar patrones complejos e interpretabilidad. Estos modelos tienen una amplia gama de usos, desde el análisis de características hasta la gestión de riesgos.
En este artículo, vamos a explicar el procedimiento para utilizar un modelo CatBoost entrenado como filtro para una estrategia clásica de seguimiento de tendencias con cruce de medias móviles. Este artículo tiene como objetivo brindar información sobre el proceso de desarrollo de una estrategia y abordar los desafíos que uno puede enfrentar en el camino. Presentaré mi flujo de trabajo para obtener datos de MetaTrader 5, entrenar el modelo de aprendizaje automático en Python y volver a integrarlo con los asesores expertos de MetaTrader 5. Al final de este artículo, validaremos la estrategia a través de pruebas estadísticas y discutiremos las aspiraciones futuras que se derivan del enfoque actual.
Intuición
La regla general en la industria para desarrollar una estrategia CTA (Commodity Trading Advisor) es que es mejor tener una explicación clara e intuitiva detrás de cada idea de estrategia. Básicamente, así es como la gente piensa en las ideas de estrategia en primer lugar, sin mencionar que también evita el sobreajuste. Esta sugerencia es subordinada incluso cuando se trabaja con modelos de aprendizaje automático. Intentaremos explicar la intuición que hay detrás de esta idea.
Por qué esto podría funcionar:
El modelo CatBoost crea árboles de decisión que toman las entradas de características y generan la probabilidad de cada resultado. En este caso, solo estamos entrenando con resultados binarios (1 es ganar, 0 es perder). El modelo modificará las reglas de los árboles de decisión para minimizar la función de pérdida en el conjunto de datos de entrenamiento. Si el modelo muestra un cierto nivel de previsibilidad en los resultados de las pruebas fuera de la muestra, podríamos considerar su uso para filtrar las operaciones que tienen pocas probabilidades de éxito, lo que a su vez podría aumentar la rentabilidad general.
Una expectativa realista para los operadores minoristas como usted y yo es que los modelos que entrenamos no serán como oráculos, sino solo ligeramente mejores que un paseo aleatorio. There are plenty of ways to improve the model precision, which I will discuss later, but nevertheless it's a great endeavor for slight improvement.
Optimización de la estrategia troncal
Ya sabemos por la sección anterior que solo podemos esperar que el modelo mejore ligeramente el rendimiento, por lo que es fundamental que la estrategia principal ya tenga algún tipo de rentabilidad.
La estrategia también debe ser capaz de generar muestras abundantes porque:
- El modelo filtrará una parte de las operaciones, queremos asegurarnos de que quedan suficientes muestras para demostrar la significación estadística de las leyes de los grandes números.
- Necesitamos suficientes muestras para entrenar el modelo, de modo que minimice eficazmente la función de pérdida para los datos de la muestra.
Utilizamos una estrategia de seguimiento de tendencias históricamente probada que realiza operaciones cuando dos medias móviles de diferentes períodos se cruzan, y salimos de las operaciones cuando el precio gira hacia el lado opuesto de la media móvil. Es decir, siguiendo la tendencia. El siguiente código MQL5 es el asesor experto para esta estrategia.
#include <Trade/Trade.mqh> //XAU - 1h. CTrade trade; input ENUM_TIMEFRAMES TF = PERIOD_CURRENT; input ENUM_MA_METHOD MaMethod = MODE_SMA; input ENUM_APPLIED_PRICE MaAppPrice = PRICE_CLOSE; 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; int handleMa; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(Magic); handleMaFast =iMA(_Symbol,TF,MaPeriodsFast,0,MaMethod,MaAppPrice); handleMaSlow =iMA(_Symbol,TF,MaPeriodsSlow,0,MaMethod,MaAppPrice); handleMa = iMA(_Symbol,TF,MaPeriods,0,MaMethod,MaAppPrice); 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[]; double ma[]; CopyBuffer(handleMaFast,BASE_LINE,1,2,maFast); CopyBuffer(handleMaSlow,BASE_LINE,1,2,maSlow); CopyBuffer(handleMa,0,1,1,ma); 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(); }
Para validar su estrategia fundamental, aquí hay algunas cosas que debe tener en cuenta:
- Tamaño de muestra suficiente (la frecuencia depende de su marco temporal y de la restricción de la señal, pero sugiero un total de entre 1000 y 10.000 muestras. Cada operación es una muestra.)
- Ya muestra cierta rentabilidad, pero no demasiada (el factor de beneficio es de 1-1,15, lo que yo diría que es bastante bueno). Dado que el probador MetaTrader 5 ya tiene en cuenta los spreads, tener un factor de beneficio de 1 significa que ya tiene una ventaja estadística. Si el factor de beneficio supera 1,15, lo más probable es que la estrategia sea lo suficientemente buena como para valerse por sí misma, y probablemente no necesites más filtros para aumentar la complejidad.
- La estrategia principal no tiene demasiados parámetros. (Es mejor que la estrategia principal sea sencilla, ya que el uso de un modelo de aprendizaje automático como filtro ya aumenta considerablemente la complejidad de la estrategia. Cuanto menos filtro, menos posibilidades hay de sobreajuste.)
Esto es lo que hice para optimizar la estrategia:
- Encontrar un buen plazo. Después de ejecutar el código en diferentes marcos temporales, descubrí que esta estrategia funciona mejor en marcos temporales más amplios, pero para generar suficientes muestras, finalmente me decidí por el marco temporal de 1 hora.
- Optimización de parámetros. Optimicé el período de la media móvil lenta y el período de la media móvil rápida con el paso 5 y obtuve la configuración del código anterior.
- He intentado añadir una regla según la cual la entrada debe estar ya por encima de una media móvil de algún periodo, lo que indica que ya está siguiendo la tendencia en la dirección correspondiente. (Es importante señalar que añadir filtros también debe tener una explicación intuitiva, y validar esta hipótesis para probarla sin espiar datos). Pero finalmente descubrí que esto no mejoraba mucho el rendimiento, así que descarté esta idea para evitar complicaciones innecesarias.
Por último, este es el resultado de la prueba en el marco temporal de 1 hora del XAUUSD, del 1 de enero de 2004 al 1 de noviembre de 2024.
Obteniendo datos
Para entrenar el modelo, necesitamos los valores de las características de cada transacción y necesitamos saber el resultado de cada transacción. Mi forma más eficiente y confiable es escribir un asesor experto que almacene todas las características correspondientes en una matriz 2D y, para los datos de resultados, simplemente exportamos el informe comercial del backtest.
En primer lugar, para obtener los datos de resultados, podemos simplemente ir al backtest y hacer clic derecho, seleccionar informe y abrir XML de esta manera.
A continuación, para convertir una matriz doble en CSV, utilizaremos la clase CFileCSV explicada en este artículo.
Nos basamos en nuestro guion estratégico principal con los siguientes pasos:
1. Incluye el archivo *.mqh y crea un objeto de clase.
#include <FileCSV.mqh>
CFileCSV csvFile;
2. Declare el nombre del archivo que se va a guardar y los encabezados que contienen «índice» y todos los demás nombres de características. El «índice» aquí solo se utiliza para actualizar el índice de la matriz mientras se ejecuta el probador y se eliminará más adelante en Python.
string fileName = "ML.csv"; string headers[] = { "Index", "Accelerator Oscillator", "Average Directional Movement Index", "Average Directional Movement Index by Welles Wilder", "Average True Range", "Bears Power", "Bulls Power", "Commodity Channel Index", "Chaikin Oscillator", "DeMarker", "Force Index", "Gator", "Market Facilitation Index", "Momentum", "Money Flow Index", "Moving Average of Oscillator", "MACD", "Relative Strength Index", "Relative Vigor Index", "Standard Deviation", "Stochastic Oscillator", "Williams' Percent Range", "Variable Index Dynamic Average", "Volume", "Hour", "Stationary" }; string data[10000][26]; int indexx = 0; vector xx;
3. Escribimos una función getData() que calcula todos los valores de las características y los almacena en la matriz global. En este caso, utilizamos el tiempo, los osciladores y el precio estacionario como características. Esta función se activará cada vez que haya una señal de operación para que se alinee con tus operaciones. La selección de características se mencionará más adelante.
//+------------------------------------------------------------------+ //| Execute get data function | //+------------------------------------------------------------------+ vector getData(){ //23 oscillators double ac[]; // Accelerator Oscillator double adx[]; // Average Directional Movement Index double wilder[]; // Average Directional Movement Index by Welles Wilder double atr[]; // Average True Range double bep[]; // Bears Power double bup[]; // Bulls Power double cci[]; // Commodity Channel Index double ck[]; // Chaikin Oscillator double dm[]; // DeMarker double f[]; // Force Index double g[]; // Gator double bwmfi[]; // Market Facilitation Index double m[]; // Momentum double mfi[]; // Money Flow Index double oma[]; // Moving Average of Oscillator double macd[]; // Moving Averages Convergence/Divergence double rsi[]; // Relative Strength Index double rvi[]; // Relative Vigor Index double std[]; // Standard Deviation double sto[]; // Stochastic Oscillator double wpr[]; // Williams' Percent Range double vidya[]; // Variable Index Dynamic Average double v[]; // Volume CopyBuffer(handleAc, 0, 1, 1, ac); // Accelerator Oscillator CopyBuffer(handleAdx, 0, 1, 1, adx); // Average Directional Movement Index CopyBuffer(handleWilder, 0, 1, 1, wilder); // Average Directional Movement Index by Welles Wilder CopyBuffer(handleAtr, 0, 1, 1, atr); // Average True Range CopyBuffer(handleBep, 0, 1, 1, bep); // Bears Power CopyBuffer(handleBup, 0, 1, 1, bup); // Bulls Power CopyBuffer(handleCci, 0, 1, 1, cci); // Commodity Channel Index CopyBuffer(handleCk, 0, 1, 1, ck); // Chaikin Oscillator CopyBuffer(handleDm, 0, 1, 1, dm); // DeMarker CopyBuffer(handleF, 0, 1, 1, f); // Force Index CopyBuffer(handleG, 0, 1, 1, g); // Gator CopyBuffer(handleBwmfi, 0, 1, 1, bwmfi); // Market Facilitation Index CopyBuffer(handleM, 0, 1, 1, m); // Momentum CopyBuffer(handleMfi, 0, 1, 1, mfi); // Money Flow Index CopyBuffer(handleOma, 0, 1, 1, oma); // Moving Average of Oscillator CopyBuffer(handleMacd, 0, 1, 1, macd); // Moving Averages Convergence/Divergence CopyBuffer(handleRsi, 0, 1, 1, rsi); // Relative Strength Index CopyBuffer(handleRvi, 0, 1, 1, rvi); // Relative Vigor Index CopyBuffer(handleStd, 0, 1, 1, std); // Standard Deviation CopyBuffer(handleSto, 0, 1, 1, sto); // Stochastic Oscillator CopyBuffer(handleWpr, 0, 1, 1, wpr); // Williams' Percent Range CopyBuffer(handleVidya, 0, 1, 1, vidya); // Variable Index Dynamic Average CopyBuffer(handleV, 0, 1, 1, v); // Volume //2 means 2 decimal places data[indexx][0] = IntegerToString(indexx); data[indexx][1] = DoubleToString(ac[0], 2); // Accelerator Oscillator data[indexx][2] = DoubleToString(adx[0], 2); // Average Directional Movement Index data[indexx][3] = DoubleToString(wilder[0], 2); // Average Directional Movement Index by Welles Wilder data[indexx][4] = DoubleToString(atr[0], 2); // Average True Range data[indexx][5] = DoubleToString(bep[0], 2); // Bears Power data[indexx][6] = DoubleToString(bup[0], 2); // Bulls Power data[indexx][7] = DoubleToString(cci[0], 2); // Commodity Channel Index data[indexx][8] = DoubleToString(ck[0], 2); // Chaikin Oscillator data[indexx][9] = DoubleToString(dm[0], 2); // DeMarker data[indexx][10] = DoubleToString(f[0], 2); // Force Index data[indexx][11] = DoubleToString(g[0], 2); // Gator data[indexx][12] = DoubleToString(bwmfi[0], 2); // Market Facilitation Index data[indexx][13] = DoubleToString(m[0], 2); // Momentum data[indexx][14] = DoubleToString(mfi[0], 2); // Money Flow Index data[indexx][15] = DoubleToString(oma[0], 2); // Moving Average of Oscillator data[indexx][16] = DoubleToString(macd[0], 2); // Moving Averages Convergence/Divergence data[indexx][17] = DoubleToString(rsi[0], 2); // Relative Strength Index data[indexx][18] = DoubleToString(rvi[0], 2); // Relative Vigor Index data[indexx][19] = DoubleToString(std[0], 2); // Standard Deviation data[indexx][20] = DoubleToString(sto[0], 2); // Stochastic Oscillator data[indexx][21] = DoubleToString(wpr[0], 2); // Williams' Percent Range data[indexx][22] = DoubleToString(vidya[0], 2); // Variable Index Dynamic Average data[indexx][23] = DoubleToString(v[0], 2); // Volume datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; data[indexx][24]= IntegerToString(currentHour); double close = iClose(_Symbol,PERIOD_CURRENT,1); double open = iOpen(_Symbol,PERIOD_CURRENT,1); double stationary = MathAbs((close-open)/close)*100; data[indexx][25] = DoubleToString(stationary,2); vector features(26); for(int i = 1; i < 26; i++) { features[i] = StringToDouble(data[indexx][i]); } //A lot of the times positions may not open due to error, make sure you don't increase index blindly if(PositionsTotal()>0) indexx++; return features; }
Tenga en cuenta que hemos añadido una comprobación aquí.
if(PositionsTotal()>0) indexx++;
Esto se debe a que, cuando se produce una señal de operación, es posible que no se genere una operación porque el EA se está ejecutando durante el cierre del mercado, pero el probador no realizará ninguna operación.
4. Guardamos el archivo cuando se llama a OnDeInit(), que es cuando finaliza la prueba.
//+------------------------------------------------------------------+ //| 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!"); } }
Ejecute este asesor experto en el probador de estrategias y, a continuación, debería poder ver su archivo CSV creado en el directorio /Tester/Agent-sth000.
Limpieza y ajuste de datos
Ahora tenemos los dos archivos de datos en la bolsa, pero aún quedan muchos problemas subyacentes por resolver.
1. El informe de backtest es desordenado y está en formato *.xlsx. Solo queremos saber si ganamos o no en cada operación.
Primero, extraemos las filas donde solo se muestran los resultados comerciales. Es posible que tengas que desplazarte hacia abajo en el archivo XLSX hasta que veas algo como esto:
Recuerde el número de fila y aplíquelo al siguiente código de Python:
import pandas as pd # Replace 'your_file.xlsx' with the path to your file input_file = 'ML2.xlsx' # Load the Excel file and skip the first {skiprows} rows df = pd.read_excel(input_file, skiprows=10757) # Save the extracted content to a CSV file output_file = 'extracted_content.csv' df.to_csv(output_file, index=False) print(f"Content has been saved to {output_file}.")
Luego aplicamos este contenido extraído al siguiente código para obtener el bin procesado. Donde las operaciones ganadoras serían 1 y las operaciones perdedoras serían 0.
import pandas as pd # Load the CSV file file_path = 'extracted_content.csv' # Update with the correct file path if needed data = pd.read_csv(file_path) # Select the 'profit' column (assumed to be 'Unnamed: 10') and filter rows as per your instructions profit_data = data["Profit"][1:-1] profit_data = profit_data[profit_data.index % 2 == 0] # Filter for rows with odd indices profit_data = profit_data.reset_index(drop=True) # Reset index # Convert to float, then apply the condition to set values to 1 if > 0, otherwise to 0 profit_data = pd.to_numeric(profit_data, errors='coerce').fillna(0) # Convert to float, replacing NaN with 0 profit_data = profit_data.apply(lambda x: 1 if x > 0 else 0) # Apply condition # Save the processed data to a new CSV file with index output_csv_path = 'processed_bin.csv' profit_data.to_csv(output_csv_path, index=True, header=['bin']) print(f"Processed data saved to {output_csv_path}")
El archivo de resultados debería verse así
bin | |
---|---|
0 | 1 |
1 | 0 |
2 | 1 |
3 | 0 |
4 | 0 |
5 | 1 |
Tenga en cuenta que si todos los valores son 0, puede deberse a que las filas iniciales son incorrectas; asegúrese de verificar si su fila inicial ahora es par o impar y cámbiela según corresponda con el código de Python.
2. Los datos de las características son todos cadenas debido a la clase CFileCSV, y están agrupados en una sola columna, separados únicamente por comas.
El siguiente código Python hace el trabajo.
import pandas as pd # Load the CSV file with semicolon separator file_path = 'ML.csv' data = pd.read_csv(file_path, sep=';') # Drop rows with any missing or incomplete values data.dropna(inplace=True) # Drop any duplicate rows if present data.drop_duplicates(inplace=True) # Convert non-numeric columns to numerical format for col in data.columns: if data[col].dtype == 'object': # Convert categorical to numerical using label encoding data[col] = data[col].astype('category').cat.codes # Ensure all remaining columns are numeric and cleanly formatted for CatBoost data = data.apply(pd.to_numeric, errors='coerce') data.dropna(inplace=True) # Drop any rows that might still contain NaNs after conversion # Save the cleaned data to a new file in CatBoost-friendly format output_file_path = 'Cleaned.csv' data.to_csv(output_file_path, index=False) print(f"Data cleaned and saved to {output_file_path}")
Por último, utilizamos este código para fusionar los dos archivos, de modo que podamos acceder fácilmente a ellos como un único marco de datos en el futuro.
import pandas as pd # Load the two CSV files file1_path = 'processed_bin.csv' # Update with the correct file path if needed file2_path = 'Cleaned.csv' # Update with the correct file path if needed data1 = pd.read_csv(file1_path, index_col=0) # Load first file with index data2 = pd.read_csv(file2_path, index_col=0) # Load second file with index # Merge the two DataFrames on the index merged_data = pd.merge(data1, data2, left_index=True, right_index=True, how='inner') # Save the merged data to a new CSV file output_csv_path = 'merged_data.csv' merged_data.to_csv(output_csv_path) print(f"Merged data saved to {output_csv_path}")
Para confirmar que los dos datos se han fusionado correctamente, podemos comprobar los tres archivos CSV que acabamos de crear y ver si su índice final es el mismo. Si es así, lo más probable es que estemos relajándonos.
Modelo de entrenamiento
No profundizaremos demasiado en las explicaciones técnicas detrás de cada aspecto del aprendizaje automático. Sin embargo, te recomiendo encarecidamente que leas Advances in Financial Machine Learning, de Marcos López de Prado, si te interesa el trading con ML en general.
Nuestro objetivo para esta sección es muy claro.
Primero, usamos la biblioteca pandas para leer los datos fusionados y dividimos la columna bin como y y el resto como X.
data = pd.read_csv("merged_data.csv",index_col=0) XX = data.drop(columns=['bin']) yy = data['bin'] y = yy.values X = XX.values
Luego dividimos los datos en 80% para entrenamiento y 20% para pruebas.
Luego entrenamos. Los detalles de cada parámetro del clasificador se documentan en el sitio web de CatBoost.
from catboost import CatBoostClassifier from sklearn.ensemble import BaggingClassifier # Define the CatBoost model with initial parameters catboost_clf = CatBoostClassifier( class_weights=[10, 1], #more weights to 1 class cuz there's less correct cases iterations=20000, # Number of trees (similar to n_estimators) learning_rate=0.02, # Learning rate depth=5, # Depth of each tree l2_leaf_reg=5, bagging_temperature=1, early_stopping_rounds=50, loss_function='Logloss', # Use 'MultiClass' if it's a multi-class problem random_seed=RANDOM_STATE, verbose=1000, # Suppress output (set to a positive number if you want to see training progress) ) fit = catboost_clf.fit(X_train, y_train)
Guardamos el archivo *.cbm.
catboost_clf.save_model('catboost_test.cbm')
Lamentablemente aún no hemos terminado. MetaTrader 5 solo admite modelos en formato ONNX, por lo que utilizamos el siguiente código de este artículo para transformarlo al formato ONNX.
model_onnx = convert_sklearn( model, "catboost", [("input", FloatTensorType([None, X.shape[1]]))], target_opset={"": 12, "ai.onnx.ml": 2}, ) # And save. with open("CatBoost_test.onnx", "wb") as f: f.write(model_onnx.SerializeToString())
Pruebas estadísticas
Luego de obtener el archivo *.onnx lo arrastramos a la carpeta MQL5/Files. Ahora construimos sobre el asesor experto que utilizamos anteriormente para obtener datos. Una vez más, este artículo ya explica detalladamente el procedimiento para inicializar el modelo *.onnx en los asesores expertos, por lo que solo me centraré en cómo cambiar los criterios de entrada.
if (maFast[1]>maSlow[1]&&maFast[0]<maSlow[0]&&sellpos == buypos){ xx= getData(); prob = cat_boost.predict_proba(xx); if (prob[1]<max&&prob[1]>min)executeBuy(); } if(maFast[1]<maSlow[1]&&maFast[0]>maSlow[0]&&sellpos == buypos){ xx= getData(); prob = cat_boost.predict_proba(xx); Print(prob); if(prob[1]<max&&prob[1]>min)executeSell(); }
Aquí llamamos a getData() para almacenar la información del vector en la variable xx, y luego devolvemos la probabilidad de éxito según el modelo. Hemos añadido una instrucción de impresión para hacernos una idea del rango que va a tener. En el caso de la estrategia de seguimiento de tendencias, debido a su baja precisión y a la elevada relación entre la recompensa y el riesgo por operación, normalmente vemos que el modelo da una probabilidad inferior a 0,5.
Añadimos un umbral para filtrar las operaciones que muestran una baja probabilidad de éxito y hemos terminado la parte de codificación. Ahora probemos.
¿Recuerdas que lo dividimos en una proporción de 8 a 2? Ahora vamos a realizar una prueba fuera de la muestra con los datos no entrenados, que abarcan aproximadamente desde el 1 de enero de 2021 hasta el 1 de noviembre de 2024.
Primero realizamos la prueba dentro de la muestra con un umbral de probabilidad de 0,05 para confirmar que hemos entrenado con los datos correctos. El resultado debería ser casi perfecto, como este.
A continuación, realizamos una prueba fuera de la muestra sin umbral como referencia. Esperamos que, si aumentamos el umbral, superemos este resultado de referencia por un margen considerable.
Por último, realizamos pruebas fuera de la muestra para analizar los patrones de rentabilidad en relación con diferentes umbrales.
Resultados del umbral = 0,05:
Resultados del umbral = 0,1:
Resultados del umbral = 0,2:
Para un umbral de 0,05, el modelo filtró aproximadamente la mitad de las operaciones originales, pero esto provocó una disminución de la rentabilidad. Esto podría sugerir que el predictor está sobreajustado, que se adapta demasiado a los patrones entrenados y no logra captar los patrones similares que comparten los conjuntos de entrenamiento y prueba. En el aprendizaje automático financiero, este es un problema habitual. Sin embargo, cuando el umbral se incrementa a 0,1, el factor de beneficio mejora gradualmente, superando el de nuestra referencia.
Con un umbral de 0,2, el modelo filtra alrededor del 70 % de las operaciones originales, pero la calidad general de las operaciones restantes es significativamente más rentable que la de las originales. El análisis estadístico muestra que, dentro de este rango de umbrales, la rentabilidad global está correlacionada positivamente con el valor del umbral. Esto sugiere que, a medida que aumenta la confianza del modelo en una operación, también lo hace su rendimiento general, lo cual es un resultado favorable.
Ejecuté una validación cruzada de diez veces en Python para confirmar que la precisión del modelo es consistente.
{'score': array([-0.97148655, -1.25263677, -1.02043177, -1.06770248, -0.97339545, -0.88611439, -0.83877111, -0.95682533, -1.02443847, -1.1385681 ])}
La diferencia entre cada puntuación de validación cruzada es leve, lo que indica que la precisión del modelo se mantiene constante en los diferentes periodos de entrenamiento y prueba.
Además, con una puntuación media de pérdida logarítmica de alrededor de -1, el rendimiento del modelo puede considerarse moderadamente eficaz.
Para mejorar aún más la precisión del modelo, se pueden tener en cuenta las siguientes ideas:
1. Ingeniería de características
Trazamos la importancia de las características de la siguiente manera y eliminamos las que tienen poca importancia.
Para seleccionar características, cualquier cosa relacionada con el mercado es plausible, pero asegúrese de que los datos sean estacionarios porque los modelos basados en árboles usan reglas de valores fijos para procesar las entradas.
2. Ajuste de hiperparámetros
¿Recuerdas los parámetros de la función clasificadora de los que hablé anteriormente? Podríamos escribir una función para recorrer una cuadrícula de valores y probar qué parámetro de entrenamiento produciría los mejores puntajes de validación cruzada.
3. Selección de modelos
Podemos probar diferentes modelos de aprendizaje automático o diferentes tipos de valores para predecir. La gente ha descubierto que, si bien los modelos de aprendizaje automático son malos para predecir precios, son bastante competentes para predecir la volatilidad. Además, el modelo de Markov oculto se utiliza ampliamente para predecir tendencias ocultas. Ambos podrían ser filtros potentes para las estrategias de seguimiento de tendencias.
Animo a los lectores a que prueben estos métodos con mi código adjunto y me hagan saber si lograron mejorar el rendimiento.
Conclusión
En este artículo, recorrimos todo el flujo de trabajo del desarrollo de un filtro de aprendizaje automático CatBoost para una estrategia de seguimiento de tendencias. En el camino, destacamos diferentes aspectos a tener en cuenta al investigar estrategias de aprendizaje automático. Al final, validamos la estrategia a través de pruebas estadísticas y discutimos las aspiraciones futuras derivadas del enfoque actual.
Tabla de archivos adjuntos
Nombre del archivo | Uso |
---|---|
ML-Momentum Data.mq5 | EA para obtener datos de características. |
ML-Momentum.mq5 | Ejecución final del EA. |
CB2.ipynb | El flujo de trabajo para entrenar y probar el modelo CatBoost. |
handleMql5DealReport.py | Extraer filas útiles del informe de transacciones. |
getBinFromMql5.py | Obtener resultado binario del contenido extraído. |
clean_mql5_csv.py | Limpiar las características CSV extraídas de MT5. |
merge_data2.py | Fusionar características y resultados en un solo CSV. |
OnnxConvert.ipynb | Convertir el modelo *.cbm al formato *.onnx. |
Classic Trend Following.mq5 | Asesor experto de estrategias troncales. |
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/16487
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
Hola. Estoy jugando con CatBoost y he llegado a un punto en el que una estrategia entrenada en (todos) los datos de 2024 produce más de un 300% de rentabilidad cuando se realiza una prueba retrospectiva (en MetaTrader) en 2024, pero funciona mal en otros años. ¿Alguien tiene experiencia con esto? Intuitivamente se siente como overfitting, pero incluso si entreno con iteraciones mucho más bajas (como 1k) obtengo el mismo resultado.
Estoy entrenando con ~40 - 50 características, en datos de minutos, así que algo así como 250.000 filas por año. El tamaño del archivo .cbm tiende a ser 1000 veces el número de iteraciones (por ejemplo, 1000 iteraciones = 1MB, 10.000 iteraciones = 10MB, y así sucesivamente). El backtesting en Metatrader me limita a unos 100.000 MB antes de que el backtester se detenga. Puedo hacer backtesting con C++ hasta un tamaño arbitrariamente alto pero mis rendimientos en metatrader vs C++ son salvajemente diferentes.
Estoy entrenando con ~40 - 50 características, en datos de minutos, así que algo así como 250.000 filas por año. El tamaño del archivo .cbm tiende a ser 1000 veces el número de iteraciones (por ejemplo, 1000 iteraciones = 1MB, 10.000 iteraciones = 10MB, y así sucesivamente). El backtesting en Metatrader me limita a unos 100.000 MB antes de que el backtester se detenga. Puedo hacer backtesting con C++ hasta un tamaño arbitrariamente alto pero mis rendimientos en metatrader vs C++ son salvajemente diferentes.
Hola a todos. En primer lugar, Metatrader backtester tiene en cuenta de los diferenciales y comisiones, lo que puede explicar por qué es diferente de sus resultados en C ++. En segundo lugar, en mi opinión, el aprendizaje automático es esencialmente un proceso de sobreajuste. Hay muchas formas de reducir el sobreajuste, como el ensemble, el abandono y la ingeniería de características. Pero al fin y al cabo, dentro de la muestra siempre es mucho mejor que fuera de la muestra. El uso del aprendizaje automático para predecir series temporales financieras es un problema antiguo. Si usted está tratando de predecir el rendimiento (estoy asumiendo cuz usted está diciendo 250k filas), cuz ruido es de esperar cuz usted y otros jugadores tienen el mismo objetivo de predicción. Mientras que lo que he introducido en este artículo es un método de metalabeling donde hay menos ruido cuz su objetivo de predicción se limita a su propia estrategia, pero tendría menos muestras para aprender, haciendo restricción complejidad aún más estrictas. Yo diría que reduzcas tus expectativas con el método ML y explores formas de reducir el sobreajuste.
Gracias por responder tan rápidamente a un tema que tiene más de 6 meses. Aquí hay mucho en lo que pensar. Me estoy acostumbrando al enorme espacio de parámetros e intento encontrar formas de reducir el sobreajuste.
Gracias de nuevo.
Gracias por responder tan rápidamente a un tema que tiene más de 6 meses. Aquí hay mucho en lo que pensar. Me estoy acostumbrando al enorme espacio de parámetros e intento encontrar formas de reducir el sobreajuste.
Gracias de nuevo.
Suerte con la investigación.