
Nutzung des CatBoost Machine Learning Modells als Filter für Trendfolgestrategien
Einführung
CatBoost ist ein leistungsfähiges, baumbasiertes, maschinelles Lernmodell, das auf die Entscheidungsfindung auf der Grundlage stationärer Merkmale spezialisiert ist. Andere baumbasierte Modelle wie XGBoost und Random Forest haben ähnliche Eigenschaften in Bezug auf ihre Robustheit, ihre Fähigkeit, komplexe Muster zu verarbeiten, und ihre Interpretierbarkeit. Diese Modelle haben ein breites Anwendungsspektrum, das von der Merkmalsanalyse bis zum Risikomanagement reicht.
In diesem Artikel werden wir das Verfahren zur Verwendung eines trainierten CatBoost-Modells als Filter für eine klassische Trendfolgestrategie mit gleitendem Durchschnitt erläutern. Dieser Artikel soll einen Einblick in den Strategieentwicklungsprozess geben und gleichzeitig auf die Herausforderungen eingehen, denen man sich auf diesem Weg stellen kann. Ich werde meinen Arbeitsablauf vorstellen, der darin besteht, Daten aus dem MetaTrader 5 zu holen, ein maschinelles Lernmodell in Python zu trainieren und es wieder in die MetaTrader 5 Expert Advisors zu integrieren. Am Ende dieses Artikels werden wir die Strategie durch statistische Tests validieren und zukünftige Bestrebungen erörtern, die über den derzeitigen Ansatz hinausgehen.
Intuition
In der Branche gilt als Faustregel für die Entwicklung von CTA-Strategien (Commodity Trading Advisor), dass hinter jeder Strategieidee eine klare, intuitive Erklärung stehen sollte. Dies ist im Grunde genommen die Art und Weise, wie Menschen überhaupt über Strategieideen nachdenken, ganz zu schweigen davon, dass dadurch auch eine Überanpassung vermieden wird. Dieser Vorschlag ist sogar für die Arbeit mit maschinellen Lernmodellen geeignet. Wir werden versuchen, die Intuition hinter dieser Idee zu erklären.
Warum das funktionieren könnte:
Das CatBoost-Modell erstellt Entscheidungsbäume, die die Eingaben der Merkmale aufnehmen und die Wahrscheinlichkeit der einzelnen Ergebnisse ausgeben. In diesem Fall trainieren wir nur für binäre Ergebnisse (1 ist ein Gewinn, 0 ein Verlust). Das Modell ändert die Regeln in den Entscheidungsbäumen so, dass es die Verlustfunktion im Trainingsdatensatz minimiert. Wenn das Modell ein gewisses Maß an Vorhersagbarkeit des Ergebnisses der Out-of-Sample-Tests aufweist, können wir in Erwägung ziehen, es zum Herausfiltern von Geschäften mit geringer Gewinnwahrscheinlichkeit zu verwenden, was wiederum die Gesamtrentabilität steigern könnte.
Eine realistische Erwartung für Händler wie Sie und ich ist, dass die Modelle, die wir trainieren, nicht wie Orakel sein werden, sondern nur geringfügig besser als Random Walk. Es gibt viele Möglichkeiten, die Präzision des Modells zu verbessern, auf die ich später noch eingehen werde, aber dennoch ist es ein großartiges Unterfangen für leichte Verbesserungen.
Optimierung der Backbone-Strategie
Wir wissen bereits aus dem obigen Abschnitt, dass wir von dem Modell nur eine geringe Leistungssteigerung erwarten können, und daher ist es entscheidend, dass die Backbone-Strategie bereits eine gewisse Rentabilität aufweist.
Die Strategie muss auch in der Lage sein, eine große Anzahl von Proben zu erzeugen, denn:
- Das Modell wird einen Teil der Trades herausfiltern. Wir wollen sicherstellen, dass genügend Stichproben übrig bleiben, um die statistische Signifikanz das Gesetz der großen Zahl zu zeigen.
- Wir benötigen eine ausreichende Anzahl von Stichproben, auf denen das Modell trainiert werden kann, damit es die Verlustfunktion für die Daten in der Stichprobe effektiv minimiert.
Wir verwenden eine historisch bewährte Trendfolgestrategie, bei der wir einsteigen, wenn sich zwei gleitende Durchschnitte verschiedener Perioden kreuzen, und aussteigen, wenn der Kurs auf die entgegengesetzte Seite des gleitenden Durchschnitts dreht, d.h. wir folgen dem Trend. Der folgende MQL5-Code ist der Expert Advisor für diese Strategie.
#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(); }
Bei der Validierung unserer Backbone-Strategie sind einige Dinge zu beachten:
- Ausreichender Stichprobenumfang (die Häufigkeit hängt von Ihrem Zeitrahmen und der Signalbeschränkung ab, aber insgesamt schlage ich 1000-10000 Stichproben vor. Jedes Geschäft ist ein Muster).
- Sie weist bereits eine gewisse Rentabilität auf, aber nicht zu viel (Gewinnfaktor von 1-1,15 würde ich sagen, ist gut genug. Da der MetaTrader 5-Tester die Spreads bereits berücksichtigt, bedeutet ein Gewinnfaktor von 1, dass er bereits einen statistischen Vorteil hat. Wenn der Gewinnfaktor 1,15 übersteigt, ist die Strategie höchstwahrscheinlich gut genug, um für sich selbst zu stehen, und Sie brauchen wahrscheinlich keine weiteren Filter, um die Komplexität zu erhöhen).
- Die Backbone-Strategie hat nicht allzu viele Parameter. (Die Backbone-Strategie sollte besser einfach sein, da die Verwendung eines maschinellen Lernmodells als Filter Ihre Strategie bereits sehr komplex macht. Je weniger Filter, desto geringer die Wahrscheinlichkeit einer Überanpassung).
Das habe ich getan, um die Strategie zu optimieren:
- Einen guten Zeitrahmen zu finden. Nachdem ich den Code in verschiedenen Zeitrahmen ausgeführt hatte, stellte ich fest, dass diese Strategie am besten auf höheren Zeitrahmen funktioniert, aber um genügend Stichproben zu generieren, blieb ich schließlich beim 1-Stunden-Zeitrahmen.
- Parameteroptimierung. Ich habe die langsame MA-Periode und die schnelle MA-Periode mit Schritt 5 optimiert und die Einstellungen im obigen Code erhalten.
- Ich habe versucht, eine Regel hinzuzufügen, nach der der Einstieg bereits über einem gleitenden Durchschnitt einer bestimmten Periode liegen muss, was bedeutet, dass er bereits in die entsprechende Richtung tendiert. (Es ist wichtig zu beachten, dass das Hinzufügen von Filtern auch eine intuitive Erklärung haben muss, und diese Hypothese zu überprüfen, ohne Daten zu schnüffeln). Ich stellte jedoch fest, dass dies die Leistung nicht wesentlich verbesserte, sodass ich diese Idee wieder verwarf, um übermäßige Komplikationen zu vermeiden.
Schließlich ist dies das Ergebnis des Tests auf XAUUSD 1h Zeitrahmen, 2004.1.1 - 2024.11.1
Abrufen von Daten
Um das Modell zu trainieren, benötigen wir die Merkmalswerte bei jedem Handel, und wir müssen das Ergebnis jedes Handels kennen. Meine effizienteste und zuverlässigste Methode ist es, einen Expert Advisor zu schreiben, der alle entsprechenden Merkmale in einem 2D-Array speichert, und für die Ergebnisdaten exportieren wir einfach den Handelsbericht aus dem Backtest.
Um die Ergebnisdaten zu erhalten, können wir einfach zum Backtest gehen und mit der rechten Maustaste auf Bericht wählen und XML wie folgt öffnen.
Um ein Double-Array in CSV umzuwandeln, verwenden wir die Klasse CFileCSV, die in diesem Artikel erklärt wird.
Wir bauen auf unserem Backbone-Strategie-Skript mit den folgenden Schritten auf:
1. Wir binden die mqh-Datei ein und erstellen ein Klassenobjekt.
#include <FileCSV.mqh>
CFileCSV csvFile;
2. Wir geben den zu speichernden Dateinamen und die Kopfzeilen an, die "index" und alle anderen Merkmalsnamen enthalten. Der "index" wird hier lediglich zur Aktualisierung des Array-Indexes während der Ausführung des Testers verwendet und wird später in Python gelöscht.
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. Wir schreiben eine getData()-Funktion, die alle Merkmalswerte berechnet und sie im globalen Array speichert. In diesem Fall verwenden wir die Zeit, die Oszillatoren und den stationären Preis als Merkmale. Diese Funktion wird jedes Mal aufgerufen, wenn es ein Handelssignal gibt, damit es mit Ihren Handelsgeschäften übereinstimmt. Auf die Auswahl der Merkmale wird später noch eingegangen.
//+------------------------------------------------------------------+ //| 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; }
Beachten Sie, dass wir hier einen Haken gesetzt haben.
if(PositionsTotal()>0) indexx++;
Denn wenn Ihr Handelssignal auftritt, kann es sein, dass es nicht zu einem Handel führt, weil der EA während der Marktschlusszeit läuft, aber der Tester keine Geschäfte tätigt.
4. Wir speichern die Datei, wenn OnDeInit() aufgerufen wird, d. h. wenn der Test beendet ist.
//+------------------------------------------------------------------+ //| 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!"); } }
Führen Sie diesen Expert Advisor im Strategietester aus. Danach sollten Sie Ihre csv-Datei im Verzeichnis /Tester/Agent-sth000 sehen können.
Bereinigen und Anpassen von Daten
Jetzt haben wir die beiden Dateien in der Tasche, aber es bleiben noch viele grundlegende Probleme zu lösen.
1. Der Backtest-Bericht ist unübersichtlich und liegt im .xlsx-Format vor. Wir wollen nur wissen, ob wir bei jedem Geschäft gewonnen haben oder nicht.
Zunächst extrahieren wir die Zeilen, in denen nur die Ergebnisse des Handels angezeigt werden. Möglicherweise müssen Sie in Ihrer XLSX-Datei nach unten blättern, bis Sie etwas wie dieses sehen:
Merken Sie sich die Zeilennummer und wenden Sie sie auf den folgenden Python-Code an:
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}.")
Dann wenden wir diesen extrahierten Inhalt auf den folgenden Code an, um die verarbeitete Tonne zu erhalten. Handelsgeschäfte mit Gewinn würden mit 1 und die mit Verlust mit 0 bewertet.
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}")
Die Ergebnisdatei sollte etwa so aussehen
bin | |
---|---|
0 | 1 |
1 | 0 |
2 | 1 |
3 | 0 |
4 | 0 |
5 | 1 |
Wenn alle Werte 0 sind, kann das daran liegen, dass Ihre Startzeilen falsch sind. Überprüfen Sie, ob Ihre Startzeile gerade oder ungerade ist und ändern Sie sie entsprechend mit dem Python-Code.
2. Die Merkmalsdaten sind dank der Klasse CFileCSV allesamt Zeichenketten, die in einer Spalte zusammenhängen und nur durch Kommata getrennt sind.
Der folgende Python-Code erledigt diese Aufgabe.
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}")
Schließlich verwenden wir diesen Code, um die beiden Dateien zusammenzuführen, damit wir in Zukunft problemlos auf sie als einen einzigen Datenrahmen zugreifen können.
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}")
Um zu bestätigen, dass zwei Daten korrekt zusammengeführt wurden, können wir die drei soeben erstellten CSV-Dateien daraufhin überprüfen, ob ihr endgültiger Index derselbe ist. Wenn ja, sind wir höchstwahrscheinlich am Chillen.
Trainingsmodell
Wir werden nicht zu sehr in die technischen Erklärungen hinter jedem Aspekt des maschinellen Lernens gehen. Ich empfehle Ihnen jedoch dringend einen Blick auf Advances in Financial Machine Learning von Marcos López de Prado, wenn Sie sich für den ML-Handel als Ganzes interessieren.
Unser Ziel für diesen Abschnitt ist ganz klar.
Zunächst verwenden wir die Pandas-Bibliothek, um die zusammengeführten Daten zu lesen und die bin-Spalte als y und den Rest als X aufzuteilen.
data = pd.read_csv("merged_data.csv",index_col=0) XX = data.drop(columns=['bin']) yy = data['bin'] y = yy.values X = XX.values
Dann teilen wir die Daten in 80 % für das Training und 20 % für den Test auf.
Danach trainieren wir. Die Einzelheiten der einzelnen Parameter des Klassifikators sind auf der Website dokumentiert.
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)
Wir speichern die .cbm-Datei.
catboost_clf.save_model('catboost_test.cbm')
Leider sind wir noch nicht fertig. Da MetaTrader 5 nur das ONNX-Format unterstützt, verwenden wir den folgenden Code aus diesem Artikel, um ihn in das ONNX-Format umzuwandeln.
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())
Statistische Prüfung
Nachdem wir die .onnx-Datei erhalten haben, ziehen wir sie in den Ordner MQL5/Files. Wir erweitern nun auf dem Expert Advisor, den wir zuvor zum Abrufen von Daten verwendet haben. Auch dieser Artikel erklärt bereits das Verfahren der Initialisierung .onnx Modell in Expert Advisors im Detail, würde ich nur auf, wie wir die Eingabekriterien ändern betonen.
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(); }
Hier rufen wir getData() auf, um die Vektorinformationen in der Variablen xx zu speichern, und geben dann die Erfolgswahrscheinlichkeit gemäß dem Modell zurück. Wir haben eine Druckanweisung hinzugefügt, damit wir ein Gefühl dafür bekommen, welcher Bereich es sein wird. Bei der Trendfolgestrategie liegt die Wahrscheinlichkeit des Modells aufgrund der geringen Genauigkeit und des hohen Rendite-Risiko-Verhältnisses pro Handel normalerweise unter 0,5.
Wir fügen einen Schwellenwert hinzu, um Handelsgeschäfte mit geringer Erfolgswahrscheinlichkeit herauszufiltern, und schon ist der Codierungsteil abgeschlossen. Nun wollen wir es testen.
Erinnern Sie sich, dass wir das Verhältnis 8:2 geteilt haben? Jetzt werden wir einen Test außerhalb der Stichprobe mit den untrainierten Daten durchführen, die ungefähr 2021.1.1-2024.11.1 umfassen.
Zunächst führen wir den stichprobeninternen Test mit einer Wahrscheinlichkeitsschwelle von 0,05 durch, um sicherzustellen, dass wir mit den richtigen Daten trainiert haben. So sollte das Ergebnis nahezu perfekt sein.
Dann führen wir einen Out-of-Sample-Test ohne Schwellenwert als Basislinie durch. Wir gehen davon aus, dass wir dieses Ergebnis deutlich übertreffen werden, wenn wir den Schwellenwert heraufsetzen.
Schließlich führen wir Out-of-Sample-Tests durch, um die Rentabilitätsmuster in Bezug auf die verschiedenen Schwellenwerte zu analysieren.
Ergebnisse der Schwelle = 0,05:
Ergebnisse für Schwellenwert = 0,1:
Ergebnisse für Schwellenwert = 0,2:
Bei einem Schwellenwert von 0,05 filterte das Modell etwa die Hälfte der ursprünglichen Handelsgeschäfte heraus, was jedoch zu einem Rückgang der Rentabilität führte. Dies könnte darauf hindeuten, dass der Prädiktor zu sehr an die trainierten Muster angepasst ist und es versäumt, die ähnlichen Muster zu erfassen, die zwischen den Trainings- und den Testmengen bestehen. Beim maschinellen Lernen im Finanzbereich ist dies ein häufiges Problem. Wenn der Schwellenwert jedoch auf 0,1 erhöht wird, verbessert sich der Gewinnfaktor allmählich und übertrifft den unserer Basislösung.
Bei einem Schwellenwert von 0,2 filtert das Modell etwa 70 % der ursprünglichen Abschlüsse heraus, aber die Gesamtqualität der verbleibenden Abschlüsse ist deutlich profitabler als die der ursprünglichen. Die statistische Analyse zeigt, dass die Gesamtrentabilität innerhalb dieses Schwellenwerts positiv mit dem Schwellenwert korreliert ist. Dies deutet darauf hin, dass mit zunehmendem Vertrauen des Modells in einen Handel auch seine Gesamtleistung steigt, was ein positives Ergebnis ist.
Ich habe eine zehnfache Kreuz-Validierung in Python durchgeführt, um zu bestätigen, dass die Modellgenauigkeit konsistent ist.
{'score': array([-0.97148655, -1.25263677, -1.02043177, -1.06770248, -0.97339545, -0.88611439, -0.83877111, -0.95682533, -1.02443847, -1.1385681 ])}
Der Unterschied zwischen den einzelnen Ergebnissen der Kreuz-Validierung ist gering, was darauf hindeutet, dass die Genauigkeit des Modells über verschiedene Trainings- und Testperioden hinweg konstant bleibt.
Darüber hinaus kann die Leistung des Modells mit einem durchschnittlichen Log-Loss-Wert von etwa -1 als mäßig effektiv angesehen werden.
Um die Genauigkeit des Modells weiter zu verbessern, können die folgenden Ideen aufgegriffen werden:
1. Technische Merkmale
Wir stellen die Wichtigkeit der Merkmale auf diese Weise dar und entfernen diejenigen, die nur eine geringe Bedeutung haben.
Für die Auswahl von Merkmalen ist alles, was mit dem Markt zu tun hat, plausibel, aber stellen Sie sicher, dass Sie die Daten stationär machen, da baumbasierte Modelle feste Wertregeln zur Verarbeitung von Eingaben verwenden.
2. Abstimmung der Hyperparameter
Erinnern Sie sich an die Parameter in der Klassifizierungsfunktion, über die ich vorhin gesprochen habe? Wir könnten eine Funktion schreiben, die ein Raster von Werten durchläuft und testet, welcher Trainingsparameter die besten Ergebnisse bei der Kreuz-Validierung liefert.
3. Auswahl des Modells
Wir können verschiedene Modelle des maschinellen Lernens oder verschiedene Arten von Werten zur Vorhersage ausprobieren. Es hat sich gezeigt, dass Modelle des maschinellen Lernens zwar schlecht bei der Vorhersage von Kursen sind, aber recht gut bei der Vorhersage der Volatilität. Außerdem wird das Hidden-Markov-Modell häufig zur Vorhersage verborgener Trends verwendet. Beides könnten starke Filter für Trendfolgestrategien sein.
Ich möchte die Leser ermutigen, diese Methoden mit dem beigefügten Code auszuprobieren, und mir mitzuteilen, ob Sie Erfolg bei der Verbesserung der Leistung hatten.
Schlussfolgerung
In diesem Artikel haben wir den gesamten Arbeitsablauf bei der Entwicklung eines CatBoost-Filters für maschinelles Lernen für eine Trendfolgestrategie durchlaufen. Auf dem Weg dorthin haben wir verschiedene Aspekte beleuchtet, die bei der Erforschung von Strategien für maschinelles Lernen zu beachten sind. Abschließend haben wir die Strategie durch statistische Tests validiert und künftige Bestrebungen diskutiert, die auf dem derzeitigen Ansatz aufbauen.
Tabelle der beigefügten Dateien
Dateiname | Verwendung |
---|---|
ML-Momentum Data.mq5 | Der EA zum Abrufen von Merkmalsdaten |
ML-Momentum.mq5 | Endgültiger EA |
CB2.ipynb | Der Arbeitsablauf zum Trainieren und Testen des CatBoost-Modells. |
handleMql5DealReport.py | nützliche Zeilen aus dem Geschäftsbericht extrahieren. |
getBinFromMql5.py | ein binäres Ergebnis aus dem extrahierten Inhalt abrufen. |
clean_mql5_csv.py | Bereinigen der aus Mt5 extrahierten CSV-Merkmale. |
merge_data2.py | Zusammenführen von Merkmalen und Ergebnissen in eine CSV-Datei. |
OnnxConvert.ipynb | Konvertieren eines .cbm-Modells in das .onnx-Format. |
Classic Trend Following.mq5 | Die Backbone-Strategie Expert Advisor. |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/16487





- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.
Hallo. Ich spiele mit CatBoost herum und bin zu dem Punkt gekommen, an dem eine Strategie, die auf (allen) Daten aus dem Jahr 2024 trainiert wurde, beim Backtesting (im MetaTrader) für das Jahr 2024 eine Rendite von mehr als 300 % erzielt, aber in anderen Jahren schlecht abschneidet. Hat jemand Erfahrung mit diesem Problem? Intuitiv fühlt es sich wie eine Überanpassung an, aber selbst wenn ich mit viel niedrigeren Iterationen (wie 1k) trainiere, erhalte ich das gleiche Ergebnis.
Ich trainiere mit ca. 40 - 50 Merkmalen, auf Minutendaten, also etwa 250.000 Zeilen pro Jahr. Die Größe der .cbm-Datei entspricht in der Regel dem 1000-fachen der Anzahl der Iterationen (z. B. 1000 Iterationen = 1 MB, 10.000 Iterationen = 10 MB usw.). Beim Backtesting mit Metatrader kann ich nur etwa 100.000 MB speichern, bevor der Backtester zum Stillstand kommt. Ich kann Backtests mit C++ bis zu einer beliebig hohen Größe durchführen, aber meine Ergebnisse in Metatrader und C++ sind völlig unterschiedlich.
Ich trainiere mit ca. 40 - 50 Merkmalen, auf Minutendaten, also etwa 250.000 Zeilen pro Jahr. Die Größe der .cbm-Datei entspricht in der Regel dem 1000-fachen der Anzahl der Iterationen (z. B. 1000 Iterationen = 1 MB, 10.000 Iterationen = 10 MB usw.). Beim Backtesting mit Metatrader kann ich nur etwa 100.000 MB speichern, bevor der Backtester zum Stillstand kommt. Ich kann Backtests mit C++ bis zu einer beliebig hohen Größe durchführen, aber meine Ergebnisse in Metatrader und C++ sind völlig unterschiedlich.
Hallo zusammen. Zunächst einmal berücksichtigt der Metatrader-Backtester Spreads und Provisionen, was erklären könnte, warum er sich von Ihren Ergebnissen in C++ unterscheidet. Zweitens ist maschinelles Lernen meiner Meinung nach im Wesentlichen ein Prozess der Überanpassung. Es gibt viele Möglichkeiten, die Überanpassung zu reduzieren, z. B. Ensemble, Dropout und Feature Engineering. Aber letzten Endes ist In-Sample immer besser als Out-of-Sample. Der Einsatz von maschinellem Lernen bei der Vorhersage von Finanzzeitreihen ist ein uraltes Problem. Wenn Sie versuchen, die Rendite vorherzusagen (ich nehme an, weil Sie von 250k Zeilen sprechen), ist Rauschen zu erwarten, weil Sie und andere Spieler das gleiche Vorhersageziel haben. Was ich in diesem Artikel vorstelle, ist eine Methode des Metalabeling, bei der es weniger Rauschen gibt, weil Ihr Vorhersageziel auf Ihre eigene Strategie eingegrenzt ist, aber es hätte weniger Stichproben zum Lernen, was die Komplexitätseinschränkung noch strenger macht. Ich würde sagen, senken Sie Ihre Erwartungen mit der ML-Methode und suchen Sie nach Möglichkeiten, die Überanpassung zu reduzieren.
Danke, dass Sie so schnell auf ein Thema geantwortet haben, das mehr als 6 Monate alt ist. Hier gibt es eine Menge zu bedenken. Ich gewöhne mich gerade an den riesigen Parameterraum und versuche, Wege zu finden, die Überanpassung zu reduzieren.
Nochmals vielen Dank!
Danke, dass Sie so schnell auf ein Thema geantwortet haben, das mehr als 6 Monate alt ist. Hier gibt es eine Menge zu bedenken. Ich gewöhne mich gerade an den riesigen Parameterraum und versuche, Wege zu finden, die Überanpassung zu reduzieren.
Nochmals vielen Dank!
Viel Glück bei Ihrer Forschung!