English Русский 日本語
preview
Nutzung des CatBoost Machine Learning Modells als Filter für Trendfolgestrategien

Nutzung des CatBoost Machine Learning Modells als Filter für Trendfolgestrategien

MetaTrader 5Integration |
123 12
Zhuo Kai Chen
Zhuo Kai Chen

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:

  1. 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. 
  2. 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:

  1. 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).
  2. 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).
  3. 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:

  1. 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.
  2. Parameteroptimierung. Ich habe die langsame MA-Periode und die schnelle MA-Periode mit Schritt 5 optimiert und die Einstellungen im obigen Code erhalten.  
  3. 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

Einstellung

Parameter

Kurve1

Ergebnis1


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.

excel-bericht

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:

Zeile finden

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.

In-Sample-Kurve

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.

Baseline-Kurve

Baseline-Ergebnis

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:

0,05-Kurve

0,05 Ergebnis

Ergebnisse für Schwellenwert = 0,1:

0,1 Kurve

0,1 Ergebnis

Ergebnisse für Schwellenwert = 0,2:

0,2 Kurve

0,2 Ergebnis

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.

Bedeutung des Merkmals

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

Beigefügte Dateien |
ML-TF-Project.zip (186.72 KB)
Letzte Kommentare | Zur Diskussion im Händlerforum (12)
johnboy85
johnboy85 | 7 Juni 2025 in 10:01

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.

Zhuo Kai Chen
Zhuo Kai Chen | 8 Juni 2025 in 10:23
johnboy85 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.

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.

johnboy85
johnboy85 | 8 Juni 2025 in 11:29

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!

Zhuo Kai Chen
Zhuo Kai Chen | 8 Juni 2025 in 11:40
johnboy85 #:

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!

Maxim Dmitrievsky
Maxim Dmitrievsky | 4 Juli 2025 in 11:19
Der Hype um MO und die Qualität des Materials ist einfach deprimierend.
Integration von MQL5 mit Datenverarbeitungspaketen (Teil 4): Umgang mit großen Daten Integration von MQL5 mit Datenverarbeitungspaketen (Teil 4): Umgang mit großen Daten
Dieser Teil befasst sich mit fortgeschrittenen Techniken zur Integration von MQL5 mit leistungsstarken Datenverarbeitungswerkzeugen und konzentriert sich auf den effizienten Umgang mit Big Data zur Verbesserung der Handelsanalyse und Entscheidungsfindung.
Erstellen eines Handelsadministrator-Panels in MQL5 (Teil VIII): Das Analytics Panel Erstellen eines Handelsadministrator-Panels in MQL5 (Teil VIII): Das Analytics Panel
Heute befassen wir uns mit dem Einbinden nützlicher Handelsmetriken in ein spezielles Fenster, das in den Admin Panel EA integriert ist. Diese Diskussion konzentriert sich auf die Implementierung von MQL5 zur Entwicklung des „Analytics Panel“ und hebt den Wert der Daten hervor, die es den Handelsadministratoren liefert. Die Auswirkungen sind weitgehend lehrreich, da aus dem Entwicklungsprozess wertvolle Lehren gezogen werden, von denen sowohl angehende als auch erfahrene Entwickler profitieren. Diese Funktion zeigt die grenzenlosen Möglichkeiten, die diese Entwicklungsreihe für die Ausstattung von Handelsmanagern mit fortschrittlichen Softwaretools bietet. Darüber hinaus werden wir die Implementierung der Klassen PieChart und ChartCanvas als Teil der kontinuierlichen Erweiterung der Funktionen des Trading Administrator-Panels untersuchen.
Handel mit dem MQL5 Wirtschaftskalender (Teil 5): Verbessern des Dashboards mit reaktionsschnellen Steuerelementen und Filterschaltflächen Handel mit dem MQL5 Wirtschaftskalender (Teil 5): Verbessern des Dashboards mit reaktionsschnellen Steuerelementen und Filterschaltflächen
In diesem Artikel erstellen wir Schaltflächen für die Filter von Währungspaar, Wichtigkeitsstufen, Zeitspannen und eine Abbruchoption, um die Kontrolle über das Dashboard zu verbessern. Diese Tasten sind so programmiert, dass sie dynamisch auf Nutzeraktionen reagieren und eine nahtlose Interaktion ermöglichen. Außerdem automatisieren wir ihr Verhalten, um Änderungen in Echtzeit auf dem Dashboard anzuzeigen. Dies verbessert die allgemeine Funktionsweise, Mobilität und Reaktionsfähigkeit des Panels.
Handel mit dem MQL5 Wirtschaftskalender (Teil 4): Implementierung von Echtzeit-Nachrichtenaktualisierungen im Dashboard Handel mit dem MQL5 Wirtschaftskalender (Teil 4): Implementierung von Echtzeit-Nachrichtenaktualisierungen im Dashboard
Dieser Artikel erweitert unser Wirtschaftskalender-Dashboard durch die Implementierung von Echtzeit-Nachrichten-Updates, um Marktinformationen aktuell und umsetzbar zu halten. Wir integrieren Techniken zum Abrufen von Live-Daten in MQL5, um Ereignisse auf dem Dashboard kontinuierlich zu aktualisieren und die Reaktionsfähigkeit der Schnittstelle zu verbessern. Dieses Update stellt sicher, dass wir direkt über das Dashboard auf die neuesten Wirtschaftsnachrichten zugreifen können, um unsere Handelsentscheidungen auf der Grundlage der aktuellsten Daten zu optimieren.