
Erforschung fortgeschrittener maschineller Lerntechniken bei der Darvas Box Breakout Strategie
Einführung
Die von Nicolas Darvas entwickelte Darvas-Box-Breakout-Strategie ist ein technischer Handelsansatz, der potenzielle Kaufsignale erkennt, wenn der Kurs einer Aktie über einen festgelegten Bereich der „Box“ ansteigt, was auf eine starke Aufwärtsdynamik hindeutet. In diesem Artikel werden wir dieses Strategiekonzept als Beispiel anwenden, um drei fortgeschrittene Techniken des maschinellen Lernens zu untersuchen. Dazu gehören die Verwendung eines maschinellen Lernmodells zur Generierung von Signalen anstelle von Handelsfiltern, die Verwendung von kontinuierlichen Signalen anstelle von diskreten Signalen und die Verwendung von Modellen, die auf verschiedenen Zeitrahmen trainiert wurden, um Handelsgeschäfte zu bestätigen. Diese Methoden bieten neue Perspektiven, wie maschinelles Lernen den algorithmischen Handel über die traditionellen Praktiken hinaus verbessern kann.
Dieser Artikel befasst sich eingehend mit den Merkmalen und der Theorie hinter drei fortgeschrittenen Techniken, die von Pädagogen nur selten behandelt werden, da sie im Vergleich zu traditionellen Methoden innovativ sind. Außerdem werden Einblicke in fortgeschrittene Themen wie Feature-Engineering und Hyperparameter-Tuning während des Modelltrainings gewährt. Es wird jedoch nicht jeder Schritt des Arbeitsablaufs beim Training von Modellen für maschinelles Lernen im Detail behandelt. Leser, die sich für die übersprungenen Verfahren interessieren, finden unter diesem Artikel den vollständigen Implementierungsprozess.
Signalerzeugung
Beim maschinellen Lernen gibt es drei Haupttypen: überwachtes Lernen, unüberwachtes Lernen und Verstärkungslernen. Im quantitativen Handel verwenden Händler meist das überwachte Lernen, und zwar aus zwei Hauptgründen. - Unüberwachtes Lernen ist oft zu einfach, um die komplexen Beziehungen zwischen Handelsergebnissen und Marktmerkmalen zu erfassen. Ohne Kennzeichnungen ist es schwierig, die Vorhersageziele zu erreichen, und es eignet sich besser für die Vorhersage indirekter Daten als für die direkten Ergebnisse einer Handelsstrategie.
- Verstärkungslernen erfordert die Einrichtung einer Trainingsumgebung mit einer Belohnungsfunktion, die auf die Maximierung des langfristigen Gewinns abzielt, anstatt sich auf genaue Einzelvorhersagen zu konzentrieren. Dieser Ansatz erfordert ein kompliziertes Setup für die einfache Aufgabe, Ergebnisse vorherzusagen, was ihn für Kleinhändler weniger kosteneffizient macht.
Dennoch bietet das überwachte Lernen zahlreiche Anwendungen für den algorithmischen Handel. Eine gängige Methode ist die Verwendung als Filter: Sie beginnen mit einer ursprünglichen Strategie, die viele Stichproben erzeugt, und trainieren dann ein Modell, um zu erkennen, wann die Strategie wahrscheinlich erfolgreich ist oder scheitert. Das Konfidenzniveau des Modells trägt dazu bei, die von ihm vorhergesagten schlechten Abschlüsse herauszufiltern.
Ein anderer Ansatz, den wir in diesem Artikel untersuchen werden, ist die Verwendung von überwachtem Lernen zur Erzeugung von Signalen. Bei typischen Regressionsaufgaben wie der Preisvorhersage ist es ganz einfach: Kaufen Sie, wenn das Modell einen Preisanstieg vorhersagt, und verkaufen Sie, wenn es einen Rückgang vorhersagt. Doch wie lässt sich dies mit einer Kernstrategie wie dem Darvas Box Breakout verbinden?
Zunächst werden wir einen EA entwickeln, um die erforderlichen Merkmalsdaten und Beschriftungsdaten für das spätere Training des Modells in Python zu sammeln.
Die Darvas Box Breakout-Strategie definiert eine Box mit einer Reihe von Ablehnungskerzen nach einem Hoch oder Tief und löst einen Handel aus, wenn der Preis aus dieser Spanne ausbricht. In jedem Fall brauchen wir ein Signal, um mit der Sammlung von Merkmalsdaten und der Vorhersage künftiger Ergebnisse zu beginnen. Als Auslöser setzen wir also den Moment, in dem der Kurs aus der unteren oder oberen Bereich ausbricht. Diese Funktion erkennt, ob es eine Darvas-Box für einen bestimmten Rückblickzeitraum und eine bestimmte Anzahl von Bestätigungskerzen gibt, weist den Variablen den Wert der Hoch-/Tiefspanne zu und stellt die Box auf dem Chart dar.
double high; double low; bool boxFormed = false; bool DetectDarvasBox(int n = 100, int M = 3) { // Clear previous Darvas box objects for (int k = ObjectsTotal(0, 0, -1) - 1; k >= 0; k--) { string name = ObjectName(0, k); if (StringFind(name, "DarvasBox_") == 0) ObjectDelete(0, name); } bool current_box_active = false; // Start checking from the oldest bar within the lookback period for (int i = M+1; i <= n; i++) { // Get high of current bar and previous bar double high_current = iHigh(_Symbol, PERIOD_CURRENT, i); double high_prev = iHigh(_Symbol, PERIOD_CURRENT, i + 1); // Check for a new high if (high_current > high_prev) { // Check if the next M bars do not exceed the high bool pullback = true; for (int k = 1; k <= M; k++) { if (i - k < 0) // Ensure we don't go beyond available bars { pullback = false; break; } double high_next = iHigh(_Symbol, PERIOD_CURRENT, i - k); if (high_next > high_current) { pullback = false; break; } } // If pullback condition is met, define the box if (pullback) { double top = high_current; double bottom = iLow(_Symbol, PERIOD_CURRENT, i); // Find the lowest low over the bar and the next M bars for (int k = 1; k <= M; k++) { double low_next = iLow(_Symbol, PERIOD_CURRENT, i - k); if (low_next < bottom) bottom = low_next; } // Check for breakout from i - M - 1 to the current bar (index 0) int j = i - M - 1; while (j >= 0) { double close_j = iClose(_Symbol, PERIOD_CURRENT, j); if (close_j > top || close_j < bottom) break; // Breakout found j--; } j++; // Adjust to the bar after breakout (or 0 if no breakout) // Create a unique object name string obj_name = "DarvasBox_" + IntegerToString(i); // Plot the box datetime time_start = iTime(_Symbol, PERIOD_CURRENT, i); datetime time_end; if (j > 0) { // Historical box: ends at breakout time_end = iTime(_Symbol, PERIOD_CURRENT, j); } else { // Current box: extends to the current bar time_end = iTime(_Symbol, PERIOD_CURRENT, 0); current_box_active = true; } high = top; low = bottom; ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, time_start, top, time_end, bottom); ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue); ObjectSetInteger(0, obj_name, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 1); boxFormed = true; // Since we're only plotting the most recent box, break after finding it break; } } } return current_box_active; }
Hier sehen Sie einige Beispiele für die Darvas-Box in einem Chart:
Im Vergleich zur Verwendung als Filter hat diese Methode jedoch Nachteile. Wir müssten ausgewogene Ergebnisse mit gleichen Wahrscheinlichkeiten vorhersagen, z. B. ob die nächsten 10 Balken höher oder niedriger ausfallen oder ob der Kurs zuerst 10 Pips nach oben oder unten geht. Ein weiterer Nachteil ist, dass wir den eingebauten Vorteil einer Backbone-Strategie verlieren - der Vorteil hängt ganz von der Vorhersagekraft des Modells ab. Positiv ist, dass wir nicht durch die Stichproben beschränkt sind, die eine Backbone-Strategie nur bei Auslösung liefert, was Ihnen eine größere anfängliche Stichprobengröße und ein größeres Potenzial bietet. Wir implementieren die Handelslogik in der Funktion onTick() wie folgt:
input int checkBar = 30; input int lookBack = 100; input int countMax = 10; void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; boxFormed = false; bool NotInPosition = true; lastClose = iClose(_Symbol, PERIOD_CURRENT, 1); lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2); for(int i = 0; i<PositionsTotal(); i++){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;} /*count++; if(count >=countMax ){ trade.PositionClose(pos); count = 0;} }}*/ DetectDarvasBox(lookBack,checkBar); if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low)))executeBuy(); } }
Bei dieser Strategie ist die Verwendung der gleichen Take-Profit- und Stop-Loss-Größe konsistenter als die Verfolgung der Ergebnisse der nächsten 10 Bars. Ersteres bindet unsere Vorhersage direkt an den endgültigen Gewinn, während letzteres die Unsicherheit durch schwankende Renditen in jeder 10-Bar-Periode erhöht. Es ist erwähnenswert, dass wir Take-Profit und Stop-Loss als Prozentsatz des Preises verwenden, was es anpassungsfähiger für verschiedene Vermögenswerte macht und besser geeignet ist für tendenzielle Vermögenswerte wie Gold oder Indizes. Leser können die Alternative testen, indem sie den kommentierten Code auskommentieren und Take Profit und Stop Loss aus der Buy-Funktion entfernen.
Für die zur Vorhersage der Ergebnisse verwendeten Merkmalsdaten wählte ich die letzten drei normalisierten Renditen, den normalisierten Abstand vom Höchst- und Tiefstwert der Spanne sowie einige gängige stationäre Indikatoren. Wir speichern diese Daten in einem Multi-Array, das dann mit der Klasse CFileCSV aus einer mitgelieferten Datei in eine CSV-Datei gespeichert wird. Stellen Sie sicher, dass alle Zeitrahmen und Symbole wie unten aufgeführt eingestellt sind, damit Sie problemlos zwischen den Zeitrahmen und Assets wechseln können.string data[50000][12]; int indexx = 0; void getData(){ double close = iClose(_Symbol,PERIOD_CURRENT,1); double close2 = iClose(_Symbol,PERIOD_CURRENT,2); double close3 = iClose(_Symbol,PERIOD_CURRENT,3); double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close; double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2; double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3; double highDistance = 1000*(close-high)/close; double lowDistance = 1000*(close-low)/close; double boxSize = 1000*(high-low)/close; double adx[]; // Average Directional Movement Index double wilder[]; // Average Directional Movement Index by Welles Wilder double dm[]; // DeMarker double rsi[]; // Relative Strength Index double rvi[]; // Relative Vigor Index double sto[]; // Stochastic 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(handleDm, 0, 1, 1, dm); // DeMarker CopyBuffer(handleRsi, 0, 1, 1, rsi); // Relative Strength Index CopyBuffer(handleRvi, 0, 1, 1, rvi); // Relative Vigor Index CopyBuffer(handleSto, 0, 1, 1, sto); // Stochastic Oscillator //2 means 2 decimal places data[indexx][0] = DoubleToString(adx[0], 2); // Average Directional Movement Index data[indexx][1] = DoubleToString(wilder[0], 2); // Average Directional Movement Index by Welles Wilder data[indexx][2] = DoubleToString(dm[0], 2); // DeMarker data[indexx][3] = DoubleToString(rsi[0], 2); // Relative Strength Index data[indexx][4] = DoubleToString(rvi[0], 2); // Relative Vigor Index data[indexx][5] = DoubleToString(sto[0], 2); // Stochastic Oscillator data[indexx][6] = DoubleToString(stationary,2); data[indexx][7] = DoubleToString(boxSize,2); data[indexx][8] = DoubleToString(stationary2,2); data[indexx][9] = DoubleToString(stationary3,2); data[indexx][10] = DoubleToString(highDistance,2); data[indexx][11] = DoubleToString(lowDistance,2); indexx++; }
Der endgültige Code für den Expertenberater, der die Daten abruft, sieht folgendermaßen aus:
#include <Trade/Trade.mqh> CTrade trade; #include <FileCSV.mqh> CFileCSV csvFile; string fileName = "box.csv"; string headers[] = { "Average Directional Movement Index", "Average Directional Movement Index by Welles Wilder", "DeMarker", "Relative Strength Index", "Relative Vigor Index", "Stochastic Oscillator", "Stationary", "Box Size", "Stationary2", "Stationary3", "Distance High", "Distance Low" }; input double lott = 0.01; input int Magic = 0; input int checkBar = 30; input int lookBack = 100; input int countMax = 10; input double slp = 0.003; input double tpp = 0.003; input bool saveData = true; string data[50000][12]; int indexx = 0; int barsTotal = 0; int count = 0; double high; double low; bool boxFormed = false; double lastClose; double lastlastClose; int handleAdx; // Average Directional Movement Index - 3 int handleWilder; // Average Directional Movement Index by Welles Wilder - 3 int handleDm; // DeMarker - 1 int handleRsi; // Relative Strength Index - 1 int handleRvi; // Relative Vigor Index - 2 int handleSto; // Stochastic Oscillator - 2 int OnInit() { trade.SetExpertMagicNumber(Magic); handleAdx=iADX(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index - 3 handleWilder=iADXWilder(_Symbol,PERIOD_CURRENT,14);//Average Directional Movement Index by Welles Wilder - 3 handleDm=iDeMarker(_Symbol,PERIOD_CURRENT,14);//DeMarker - 1 handleRsi=iRSI(_Symbol,PERIOD_CURRENT,14,PRICE_CLOSE);//Relative Strength Index - 1 handleRvi=iRVI(_Symbol,PERIOD_CURRENT,10);//Relative Vigor Index - 2 handleSto=iStochastic(_Symbol,PERIOD_CURRENT,5,3,3,MODE_SMA,STO_LOWHIGH);//Stochastic Oscillator - 2 return(INIT_SUCCEEDED); } 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!"); } } void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; boxFormed = false; bool NotInPosition = true; lastClose = iClose(_Symbol, PERIOD_CURRENT, 1); lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2); for(int i = 0; i<PositionsTotal(); i++){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;} /*count++; if(count >=countMax ){ trade.PositionClose(pos); count = 0;} }}*/ DetectDarvasBox(lookBack,checkBar); if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low)))executeBuy(); } } void executeBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); double sl = lastClose*(1-slp); double tp = lastClose*(1+tpp); trade.Buy(lott,_Symbol,ask,sl,tp); if(PositionsTotal()>0)getData(); } bool DetectDarvasBox(int n = 100, int M = 3) { // Clear previous Darvas box objects for (int k = ObjectsTotal(0, 0, -1) - 1; k >= 0; k--) { string name = ObjectName(0, k); if (StringFind(name, "DarvasBox_") == 0) ObjectDelete(0, name); } bool current_box_active = false; // Start checking from the oldest bar within the lookback period for (int i = M+1; i <= n; i++) { // Get high of current bar and previous bar double high_current = iHigh(_Symbol, PERIOD_CURRENT, i); double high_prev = iHigh(_Symbol, PERIOD_CURRENT, i + 1); // Check for a new high if (high_current > high_prev) { // Check if the next M bars do not exceed the high bool pullback = true; for (int k = 1; k <= M; k++) { if (i - k < 0) // Ensure we don't go beyond available bars { pullback = false; break; } double high_next = iHigh(_Symbol, PERIOD_CURRENT, i - k); if (high_next > high_current) { pullback = false; break; } } // If pullback condition is met, define the box if (pullback) { double top = high_current; double bottom = iLow(_Symbol, PERIOD_CURRENT, i); // Find the lowest low over the bar and the next M bars for (int k = 1; k <= M; k++) { double low_next = iLow(_Symbol, PERIOD_CURRENT, i - k); if (low_next < bottom) bottom = low_next; } // Check for breakout from i - M - 1 to the current bar (index 0) int j = i - M - 1; while (j >= 0) { double close_j = iClose(_Symbol, PERIOD_CURRENT, j); if (close_j > top || close_j < bottom) break; // Breakout found j--; } j++; // Adjust to the bar after breakout (or 0 if no breakout) // Create a unique object name string obj_name = "DarvasBox_" + IntegerToString(i); // Plot the box datetime time_start = iTime(_Symbol, PERIOD_CURRENT, i); datetime time_end; if (j > 0) { // Historical box: ends at breakout time_end = iTime(_Symbol, PERIOD_CURRENT, j); } else { // Current box: extends to the current bar time_end = iTime(_Symbol, PERIOD_CURRENT, 0); current_box_active = true; } high = top; low = bottom; ObjectCreate(0, obj_name, OBJ_RECTANGLE, 0, time_start, top, time_end, bottom); ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue); ObjectSetInteger(0, obj_name, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 1); boxFormed = true; // Since we're only plotting the most recent box, break after finding it break; } } } return current_box_active; } void getData(){ double close = iClose(_Symbol,PERIOD_CURRENT,1); double close2 = iClose(_Symbol,PERIOD_CURRENT,2); double close3 = iClose(_Symbol,PERIOD_CURRENT,3); double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close; double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2; double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3; double highDistance = 1000*(close-high)/close; double lowDistance = 1000*(close-low)/close; double boxSize = 1000*(high-low)/close; double adx[]; // Average Directional Movement Index double wilder[]; // Average Directional Movement Index by Welles Wilder double dm[]; // DeMarker double rsi[]; // Relative Strength Index double rvi[]; // Relative Vigor Index double sto[]; // Stochastic 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(handleDm, 0, 1, 1, dm); // DeMarker CopyBuffer(handleRsi, 0, 1, 1, rsi); // Relative Strength Index CopyBuffer(handleRvi, 0, 1, 1, rvi); // Relative Vigor Index CopyBuffer(handleSto, 0, 1, 1, sto); // Stochastic Oscillator //2 means 2 decimal places data[indexx][0] = DoubleToString(adx[0], 2); // Average Directional Movement Index data[indexx][1] = DoubleToString(wilder[0], 2); // Average Directional Movement Index by Welles Wilder data[indexx][2] = DoubleToString(dm[0], 2); // DeMarker data[indexx][3] = DoubleToString(rsi[0], 2); // Relative Strength Index data[indexx][4] = DoubleToString(rvi[0], 2); // Relative Vigor Index data[indexx][5] = DoubleToString(sto[0], 2); // Stochastic Oscillator data[indexx][6] = DoubleToString(stationary,2); data[indexx][7] = DoubleToString(boxSize,2); data[indexx][8] = DoubleToString(stationary2,2); data[indexx][9] = DoubleToString(stationary3,2); data[indexx][10] = DoubleToString(highDistance,2); data[indexx][11] = DoubleToString(lowDistance,2); indexx++; }
Wir beabsichtigen, diese Strategie auf dem 15-Minuten-Zeitrahmen des XAUUSD zu handeln, da der Vermögenswert eine solide Volatilität aufweist und 15 Minuten ein Gleichgewicht zwischen geringerem Rauschen und einer höheren Anzahl von Stichproben darstellen. Ein typischer Handel würde folgendermaßen aussehen:
Wir verwenden die Daten von 2020-2024 als Trainings- und Validierungsdaten und werden das Ergebnis später auf dem MetaTrader 5-Terminal für 2024-2025 testen. Nachdem wir diesen EA im Strategietester ausgeführt haben, wird die CSV-Datei bei der Deinitialisierung des EAs im Verzeichnis /Tester/Agent-sth000 gespeichert.
Wir klicken außerdem mit der rechten Maustaste, um den Backtest-Excel-Bericht wie folgt zu erhalten:
Bitte notieren Sie sich die Zeilennummer der Zeile „Deals“, die wir später als Eingabe verwenden werden.
Danach trainieren wir unser Modell in Python.
Das Modell, das wir für diesen Artikel ausgewählt haben, ist ein entscheidungsbaumbasiertes Modell, das sich ideal für Klassifizierungsprobleme eignet, genau wie das Modell, das wir in diesem Artikel verwendet haben.
import pandas as pd # Replace 'your_file.xlsx' with the path to your file input_file = 'box.xlsx' # Load the Excel file and skip the first {skiprows} rows data1 = pd.read_excel(input_file, skiprows=4417) # Select the 'profit' column (assumed to be 'Unnamed: 10') and filter rows as per your instructions profit_data = data1["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 # Load the CSV file with semicolon separator file_path = 'box.csv' data2 = pd.read_csv(file_path, sep=';') # Drop rows with any missing or incomplete values data2.dropna(inplace=True) # Drop any duplicate rows if present data2.drop_duplicates(inplace=True) # Convert non-numeric columns to numerical format for col in data2.columns: if data2[col].dtype == 'object': # Convert categorical to numerical using label encoding data2[col] = data2[col].astype('category').cat.codes # Ensure all remaining columns are numeric and cleanly formatted for CatBoost data2 = data2.apply(pd.to_numeric, errors='coerce') data2.dropna(inplace=True) # Drop any rows that might still contain NaNs after conversion # Merge the two DataFrames on the index merged_data = pd.merge(profit_data, 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}")Wir verwenden diesen Code, um den Excel-Bericht zu kennzeichnen, indem wir den Handelsgeschäften mit positivem Gewinn eine 1 zuweisen und den Handelsgeschäften ohne Gewinn eine 0. Dann kombinieren wir sie mit den Merkmalsdaten aus der CSV-Datei des datenabrufenden EA. Beachten Sie, dass der Wert für „Skiprow“ mit der Zeilennummer von „Deals“ übereinstimmt.
import numpy as np import pandas as pd import warnings warnings.filterwarnings("ignore") data = pd.read_csv("merged_data.csv",index_col=0) XX = data.drop(columns=['Profit']) yy = data['Profit'] y = yy.values X = XX.values pd.DataFrame(X,y)
Als Nächstes weisen wir das Label-Array der y-Variablen und den Merkmalsdatenrahmen der X-Variablen zu.
import numpy as np import pandas as pd import warnings import seaborn as sns warnings.filterwarnings("ignore") from sklearn.model_selection import train_test_split import catboost as cb from sklearn.metrics import roc_curve, roc_auc_score import matplotlib.pyplot as plt # Split data X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, shuffle=False) # Identify categorical features cat_feature_indices = [i for i, col in enumerate(XX.columns) if XX[col].dtype == 'object'] # Train CatBoost classifier model = cb.CatBoostClassifier( iterations=5000, # 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 verbose=1000) model.fit(X_train, y_train, cat_features=cat_feature_indices)
Dann teilen wir die Daten im Verhältnis 9:1 in Trainings- und Validierungsdatensätze auf und beginnen mit dem Training des Modells. Die Standardeinstellung für die Funktion „train-test split“ in sklearn beinhaltet Shuffling, was für Zeitreihendaten nicht ideal ist. Stellen Sie daher sicher, dass Sie shuffle=False in den Parametern einstellen. Es ist eine gute Idee, die Hyperparameter so zu verändern, dass je nach Stichprobengröße eine Über- oder Unteranpassung vermieden wird. Ich persönlich habe festgestellt, dass das Anhalten der Iteration bei 0,1 log Verlust gut funktioniert.
import numpy as np from sklearn.metrics import roc_curve, roc_auc_score import matplotlib.pyplot as plt # Assuming you already have y_test, X_test, and model defined # Predict probabilities y_prob = model.predict_proba(X_test)[:, 1] # Probability for positive class # Compute ROC curve and AUC (for reference) fpr, tpr, thresholds = roc_curve(y_test, y_prob) auc_score = roc_auc_score(y_test, y_prob) print(f"AUC Score: {auc_score:.2f}") # Define confidence thresholds to test (e.g., 50%, 60%, 70%, etc.) confidence_thresholds = np.arange(0.5, 1.0, 0.05) # From 50% to 95% in steps of 5% accuracies = [] coverage = [] # Fraction of samples classified at each threshold for thresh in confidence_thresholds: # Classify only when probability is >= thresh (positive) or <= (1 - thresh) (negative) y_pred_confident = np.where(y_prob >= thresh, 1, np.where(y_prob <= (1 - thresh), 0, -1)) # Filter out unclassified samples (where y_pred_confident == -1) mask = y_pred_confident != -1 y_test_confident = y_test[mask] y_pred_confident = y_pred_confident[mask] # Calculate accuracy and coverage if len(y_test_confident) > 0: # Avoid division by zero acc = np.mean(y_pred_confident == y_test_confident) cov = len(y_test_confident) / len(y_test) else: acc = 0 cov = 0 accuracies.append(acc) coverage.append(cov) # Plot Accuracy vs Confidence Threshold plt.figure(figsize=(10, 6)) plt.plot(confidence_thresholds, accuracies, marker='o', label='Accuracy', color='blue') plt.plot(confidence_thresholds, coverage, marker='s', label='Coverage', color='green') plt.xlabel('Confidence Threshold') plt.ylabel('Metric Value') plt.title('Accuracy and Coverage vs Confidence Threshold') plt.legend(loc='best') plt.grid(True) plt.show() # Also show the original ROC curve for reference plt.figure(figsize=(8, 6)) plt.plot(fpr, tpr, label=f'ROC Curve (AUC = {auc_score:.2f})', color='blue') plt.plot([0, 1], [0, 1], 'k--', label='Random Guess') plt.xlabel('False Positive Rate') plt.ylabel('True Positive Rate') plt.title('Receiver Operating Characteristic (ROC) Curve') plt.legend(loc='lower right') plt.grid(True) plt.show()
Anschließend werden die Ergebnisse visualisiert, um den Validierungstest zu überprüfen. Beim typischen Test-Ansatz von Training und Validierung hilft der Validierungsschritt bei der Auswahl der besten Hyperparameter und bewertet zunächst, ob das trainierte Modell Vorhersagekraft hat. Er dient als Puffer, bevor der letzte Test durchgeführt wird.
# Feature importance feature_importance = model.get_feature_importance() importance_df = pd.DataFrame({ 'feature': XX.columns, 'importance': feature_importance }).sort_values('importance', ascending=False) print("Feature Importances:") print(importance_df) plt.figure(figsize=(10, 6)) sns.barplot(x='importance', y='feature', data=importance_df) plt.title(' Feature Importance') plt.xlabel('Importance') plt.ylabel('Feature') x = 100/len(XX.columns) plt.axvline(x,color = 'red', linestyle = '--') plt.show()
Dieser Codeblock zeichnet die Bedeutung des Merkmals und die Medianlinie auf. Es gibt viele Möglichkeiten, die Bedeutung von Merkmalen im Bereich des maschinellen Lernens zu definieren, z. B:
- Baum-basierte Bedeutung: Misst die Reduktion von Unreinheiten (z.B. Gini) in Entscheidungsbäumen oder Ensembles wie Random Forest und XGBoost.
- Bedeutung der Permutation: Bewertet den Leistungsabfall, wenn die Werte eines Merkmals gemischt werden.
- SHAP-Werte: Berechnet den Beitrag eines Merkmals zu Vorhersagen auf der Grundlage von Shapley-Werten.
- Größe des Koeffizienten: Verwendet den absoluten Wert der Koeffizienten in linearen Modellen.
In unserem Beispiel verwenden wir CatBoost, ein entscheidungsbaumbasiertes Modell. Die Merkmalsbedeutung zeigt an, wie viel Unordnung (Unreinheit) jedes Merkmal reduziert, wenn es zur Aufteilung des Entscheidungsbaums in den In-Sample-Daten verwendet wird. Es ist wichtig, sich darüber im Klaren zu sein, dass die Auswahl der wichtigsten Merkmale für die endgültige Zusammenstellung zwar oft die Effizienz des Modells steigern kann, aber aus diesen Gründen nicht immer die Vorhersagbarkeit verbessert:
- Die Wichtigkeit von Merkmalen wird anhand der In-Sample-Daten berechnet, ohne dass die Out-of-Sample-Daten berücksichtigt werden.
- Die Wichtigkeit eines Merkmals hängt von den anderen zu berücksichtigenden Merkmalen ab. Wenn die meisten der von Ihnen ausgewählten Merkmale nicht aussagekräftig genug sind, hilft es nicht, die schwächsten zu streichen.
- Die Wichtigkeit spiegelt wider, wie effektiv ein Merkmal den Baum aufteilt, und nicht unbedingt, wie entscheidend es für das endgültige Entscheidungsergebnis ist.
Diese Erkenntnisse trafen mich, als ich unerwartet entdeckte, dass die Auswahl der am wenigsten wichtigen Merkmale tatsächlich die Genauigkeit außerhalb der Stichprobe erhöhte. Aber im Allgemeinen trägt die Auswahl der wichtigsten Merkmale und die Kürzung der weniger wichtigen dazu bei, das Modell zu vereinfachen und wahrscheinlich die Genauigkeit insgesamt zu verbessern.
from onnx.helper import get_attribute_value import onnxruntime as rt from skl2onnx import convert_sklearn, update_registered_converter from skl2onnx.common.shape_calculator import ( calculate_linear_classifier_output_shapes, ) # noqa from skl2onnx.common.data_types import ( FloatTensorType, Int64TensorType, guess_tensor_type, ) from skl2onnx._parse import _apply_zipmap, _get_sklearn_operator_name from catboost import CatBoostClassifier from catboost.utils import convert_to_onnx_object def skl2onnx_parser_castboost_classifier(scope, model, inputs, custom_parsers=None): options = scope.get_options(model, dict(zipmap=True)) no_zipmap = isinstance(options["zipmap"], bool) and not options["zipmap"] alias = _get_sklearn_operator_name(type(model)) this_operator = scope.declare_local_operator(alias, model) this_operator.inputs = inputs label_variable = scope.declare_local_variable("label", Int64TensorType()) prob_dtype = guess_tensor_type(inputs[0].type) probability_tensor_variable = scope.declare_local_variable( "probabilities", prob_dtype ) this_operator.outputs.append(label_variable) this_operator.outputs.append(probability_tensor_variable) probability_tensor = this_operator.outputs if no_zipmap: return probability_tensor return _apply_zipmap( options["zipmap"], scope, model, inputs[0].type, probability_tensor ) def skl2onnx_convert_catboost(scope, operator, container): """ CatBoost returns an ONNX graph with a single node. This function adds it to the main graph. """ onx = convert_to_onnx_object(operator.raw_operator) opsets = {d.domain: d.version for d in onx.opset_import} if "" in opsets and opsets[""] >= container.target_opset: raise RuntimeError("CatBoost uses an opset more recent than the target one.") if len(onx.graph.initializer) > 0 or len(onx.graph.sparse_initializer) > 0: raise NotImplementedError( "CatBoost returns a model initializers. This option is not implemented yet." ) if ( len(onx.graph.node) not in (1, 2) or not onx.graph.node[0].op_type.startswith("TreeEnsemble") or (len(onx.graph.node) == 2 and onx.graph.node[1].op_type != "ZipMap") ): types = ", ".join(map(lambda n: n.op_type, onx.graph.node)) raise NotImplementedError( f"CatBoost returns {len(onx.graph.node)} != 1 (types={types}). " f"This option is not implemented yet." ) node = onx.graph.node[0] atts = {} for att in node.attribute: atts[att.name] = get_attribute_value(att) container.add_node( node.op_type, [operator.inputs[0].full_name], [operator.outputs[0].full_name, operator.outputs[1].full_name], op_domain=node.domain, op_version=opsets.get(node.domain, None), **atts, ) update_registered_converter( CatBoostClassifier, "CatBoostCatBoostClassifier", calculate_linear_classifier_output_shapes, skl2onnx_convert_catboost, parser=skl2onnx_parser_castboost_classifier, options={"nocl": [True, False], "zipmap": [True, False, "columns"]}, ) model_onnx = convert_sklearn( model, "catboost", [("input", FloatTensorType([None, X.shape[1]]))], target_opset={"": 12, "ai.onnx.ml": 2}, ) # And save. with open("box2024.onnx", "wb") as f: f.write(model_onnx.SerializeToString())
Schließlich exportieren wir die ONNX-Datei und speichern sie im Verzeichnis MQL5/Files.
Kehren wir nun zum MetaTrader 5 Code-Editor zurück, um den Trading EA zu erstellen.
Wir müssen nur den ursprünglichen Datenabruf-EA anpassen, indem wir einige Include-Dateien einbinden, um das CatBoost-Modell zu verarbeiten.
#resource "\\Files\\box2024.onnx" as uchar catboost_onnx[] #include <CatOnnx.mqh> CCatBoost cat_boost; string data[1][12]; vector xx; vector prob;
Dann passen wir die Funktion getData() so an, dass sie einen Vektor zurückgibt.
vector getData(){ double close = iClose(_Symbol,PERIOD_CURRENT,1); double close2 = iClose(_Symbol,PERIOD_CURRENT,2); double close3 = iClose(_Symbol,PERIOD_CURRENT,3); double stationary = 1000*(close-iOpen(_Symbol,PERIOD_CURRENT,1))/close; double stationary2 = 1000*(close2-iOpen(_Symbol,PERIOD_CURRENT,2))/close2; double stationary3 = 1000*(close3-iOpen(_Symbol,PERIOD_CURRENT,3))/close3; double highDistance = 1000*(close-high)/close; double lowDistance = 1000*(close-low)/close; double boxSize = 1000*(high-low)/close; double adx[]; // Average Directional Movement Index double wilder[]; // Average Directional Movement Index by Welles Wilder double dm[]; // DeMarker double rsi[]; // Relative Strength Index double rvi[]; // Relative Vigor Index double sto[]; // Stochastic 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(handleDm, 0, 1, 1, dm); // DeMarker CopyBuffer(handleRsi, 0, 1, 1, rsi); // Relative Strength Index CopyBuffer(handleRvi, 0, 1, 1, rvi); // Relative Vigor Index CopyBuffer(handleSto, 0, 1, 1, sto); // Stochastic Oscillator data[0][0] = DoubleToString(adx[0], 2); // Average Directional Movement Index data[0][1] = DoubleToString(wilder[0], 2); // Average Directional Movement Index by Welles Wilder data[0][2] = DoubleToString(dm[0], 2); // DeMarker data[0][3] = DoubleToString(rsi[0], 2); // Relative Strength Index data[0][4] = DoubleToString(rvi[0], 2); // Relative Vigor Index data[0][5] = DoubleToString(sto[0], 2); // Stochastic Oscillator data[0][6] = DoubleToString(stationary,2); data[0][7] = DoubleToString(boxSize,2); data[0][8] = DoubleToString(stationary2,2); data[0][9] = DoubleToString(stationary3,2); data[0][10] = DoubleToString(highDistance,2); data[0][11] = DoubleToString(lowDistance,2); vector features(12); for(int i = 0; i < 12; i++) { features[i] = StringToDouble(data[0][i]); } return features; }
Die endgültige Handelslogik in der Funktion OnTick() sieht dann so aus:
void OnTick() { int bars = iBars(_Symbol,PERIOD_CURRENT); if (barsTotal!= bars){ barsTotal = bars; boxFormed = false; bool NotInPosition = true; lastClose = iClose(_Symbol, PERIOD_CURRENT, 1); lastlastClose = iClose(_Symbol,PERIOD_CURRENT,2); for(int i = 0; i<PositionsTotal(); i++){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)NotInPosition = false;} /*count++; if(count >=countMax){ trade.PositionClose(pos); count = 0;} }}*/ DetectDarvasBox(lookBack,checkBar); if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low))){ xx = getData(); prob = cat_boost.predict_proba(xx); if(prob[1]>threshold)executeBuy(); if(prob[0]>threshold)executeSell(); } } }
In der Signallogik wird zunächst geprüft, ob derzeit keine Position offen ist, um zu gewährleisten, dass jeweils nur ein Handel stattfindet. Dann wird festgestellt, ob es einen Ausbruch auf beiden Seiten des Bereichs gibt. Danach ruft es die Funktion getData() auf, um den Merkmalsvektor zu erhalten. Dieser Vektor wird dem CatBoost-Modell als Eingabe übergeben, und das Modell gibt die Vorhersagekonfidenz für jedes Ergebnis in das prob-Array aus. Auf der Grundlage der Konfidenzniveaus für jedes Ergebnis platzieren wir einen Handel, der auf das vorhergesagte Ergebnis wettet. Im Wesentlichen verwenden wir das Modell, um die Kauf- oder Verkaufssignale zu generieren.
Wir führten einen Backtest auf dem MetaTrader 5-Strategietester durch, bei dem wir In-Sample-Daten von 2020 bis 2024 verwendeten, um zu überprüfen, dass unsere Trainingsdaten keine Fehler aufwiesen und dass die Zusammenführung von Merkmalen und Ergebnissen korrekt war. Wenn alles korrekt ist, sollte die Kapitalkurve nahezu perfekt aussehen, etwa so:
Anschließend führen wir einen Backtest des Out-of-Sample-Tests von 2024 bis 2025 durch, um festzustellen, ob die Strategie im letzten Zeitraum rentabel war. Wir setzen den Schwellenwert auf 0,7, sodass das Modell nur dann einen Handel in einer Richtung tätigt, wenn das Konfidenzniveau für das Erreichen des Take-Profits in dieser Richtung auf der Grundlage der Trainingsdaten 70 % oder mehr beträgt.
Es ist zu erkennen, dass das Modell in der ersten Jahreshälfte außerordentlich gut, im weiteren Verlauf jedoch nicht mehr so gut abschnitt. Dies ist bei Modellen des maschinellen Lernens häufig der Fall, da der aus früheren Daten gewonnene Vorteil oft nur vorübergehend ist und mit der Zeit abnimmt. Dies deutet darauf hin, dass ein kleineres Verhältnis von Test-Train für künftige Live-Trading-Implementierungen besser geeignet sein könnte. Insgesamt zeigt das Modell eine gewisse Vorhersagbarkeit, da es auch nach Berücksichtigung der Handelskosten profitabel blieb.
Kontinuierliches Signal
Beim algorithmischen Handel halten sich die Händler in der Regel an eine einfache Methode, bei der sie diskrete Signale verwenden - entweder kaufen oder verkaufen mit einem festen Risiko pro Handel. Das macht die Sache überschaubar und erleichtert die Analyse der Strategieleistung. Einige Händler haben versucht, diese diskrete Signalmethode zu verfeinern, indem sie ein additives Signal verwenden, bei dem sie das Risiko des Handels auf der Grundlage der Erfüllung der Bedingungen der Strategie anpassen. Kontinuierliche Signale führen diesen additiven Ansatz weiter, indem sie ihn auf abstraktere Strategiebedingungen anwenden und ein Risikoniveau zwischen null und eins erzeugen.
Der Grundgedanke dahinter ist, dass nicht alle Handelsgeschäfte, die die Einstiegskriterien erfüllen, gleich sind. Einige scheinen erfolgreicher zu sein, weil ihre Signale stärker sind und auf nicht-linearen Regeln beruhen, die mit der Strategie verbunden sind. Dies kann als ein Instrument des Risikomanagements angesehen werden - setzen Sie mehr, wenn das Vertrauen groß ist, und schränken Sie es ein, wenn ein Handel weniger vielversprechend erscheint, selbst wenn er immer noch eine positive erwartete Rendite hat. Wir müssen jedoch bedenken, dass dies einen weiteren Faktor für die Leistung der Strategie darstellt und dass die Risiken der Vorausschau und der Überanpassung immer noch problematisch sind, wenn wir bei der Umsetzung nicht vorsichtig sind.
Um dieses Konzept in unserem Handels-EA anzuwenden, müssen wir zunächst die Kauf-/Verkaufsfunktionen so anpassen, dass die Losgröße auf der Grundlage des Risikos berechnet wird, das wir bereit sind zu verlieren, wenn der Stop-Loss erreicht wird. Die Funktion der Losgrößenberechnung sieht folgendermaßen aus:
double calclots(double slpoints, string symbol, double risk) { double balance = AccountInfoDouble(ACCOUNT_BALANCE); double riskAmount = balance* risk / 100; double ticksize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE); double tickvalue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE); double lotstep = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep; double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep; lots = MathMin(lots, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX)); lots = MathMax(lots, SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN)); return lots; }Anschließend aktualisieren wir die Kauf-/Verkaufsfunktionen so, dass sie diese Funktion calclots() aufrufen und den Risikomultiplikator als Eingabe verwenden:
void executeSell(double riskMultiplier) { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); double sl = lastClose*(1+slp); double tp = lastClose*(1-tpp); double lots = 0.1; lots = calclots(slp*lastClose,_Symbol,risks*riskMultiplier); trade.Sell(lots,_Symbol,bid,sl,tp); } void executeBuy(double riskMultiplier) { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); double sl = lastClose*(1-slp); double tp = lastClose*(1+tpp); double lots = 0.1; lots = calclots(slp*lastClose,_Symbol,risks*riskMultiplier); trade.Buy(lots,_Symbol,ask,sl,tp); }Da unser maschinelles Lernmodell bereits das Konfidenzniveau ausgibt, können wir es direkt als Eingabe für den Risikomultiplikator verwenden. Wenn wir den Einfluss des Konfidenzniveaus auf das Risiko für jeden Handel anpassen wollen, können wir das Konfidenzniveau einfach nach oben oder unten skalieren.
if(prob[1]>threshold)executeBuy(prob[1]); if(prob[0]>threshold)executeSell(prob[0]);
Wenn wir zum Beispiel die Signifikanz der Konfidenzniveauunterschiede verstärken wollen, können wir die Wahrscheinlichkeit dreimal mit sich selbst multiplizieren. Dadurch würde sich die Verhältnisdifferenz zwischen den Wahrscheinlichkeiten erhöhen, wodurch die Auswirkungen der Konfidenzniveaus stärker zum Tragen kämen.
if(prob[1]>threshold)executeBuy(prob[1]*prob[1]*prob[1]); if(prob[0]>threshold)executeSell(prob[0]*prob[0]*prob[0]);
Jetzt versuchen wir, das Ergebnis im Backtest zu sehen.
Die eingegangenen Handelsgeschäfte sind immer noch dieselben wie bei der Version mit diskreten Signalen, aber der Gewinnfaktor und das Sharpe-Ratio haben sich leicht verbessert. Dies deutet darauf hin, dass das kontinuierliche Signal in diesem speziellen Szenario die Gesamtleistung des Out-of-Sample-Tests verbessert hat, der frei von Verzerrungen durch Vorausschau ist, da wir nur einmal getestet haben. Es ist jedoch zu beachten, dass dieser Ansatz die Methode mit festem Risiko nur dann übertrifft, wenn die Vorhersagegenauigkeit des Modells höher ist, wenn sein Konfidenzniveau höher ist. Andernfalls könnte der ursprüngliche Ansatz mit festem Risiko besser sein. Da wir die durchschnittliche Losgröße durch die Anwendung von Risikomultiplikatoren zwischen null und eins reduziert haben, müssen wir außerdem den Wert der Risikovariablen erhöhen, wenn wir einen ähnlichen Gesamtgewinn wie zuvor erzielen wollen.
Validierung in anderen Zeitrahmen
Das Training separater maschineller Lernmodelle, die jeweils einen anderen Zeitrahmen von Merkmalen zur Vorhersage desselben Ergebnisses verwenden, kann eine leistungsstarke Methode zur Verbesserung der Handelsfilterung und Signalgenerierung darstellen. Indem Sie ein Modell auf kurzfristige Daten, ein anderes auf mittelfristige und vielleicht ein drittes auf langfristige Trends konzentrieren, gewinnen Sie spezialisierte Erkenntnisse, die, wenn sie kombiniert werden, Vorhersagen zuverlässiger bestätigen können als ein einzelnes Modell. Dieser Multi-Modell-Ansatz kann das Vertrauen in Handelsentscheidungen stärken, indem er Signale gegeneinander abgleicht und das Risiko reduziert, aufgrund von Rauschen in einem bestimmten Zeitrahmen zu handeln.
Auf der anderen Seite kann diese Strategie das System verkomplizieren, insbesondere wenn Sie den Vorhersagen mehrerer Modelle unterschiedliche Gewichtungen zuweisen. Dies kann zu eigenen Verzerrungen oder Fehlern führen, wenn es nicht sorgfältig abgestimmt wird. Jedes Modell könnte auch zu sehr auf seinen spezifischen Zeitrahmen zugeschnitten sein und die breitere Marktdynamik übersehen, und Diskrepanzen zwischen ihren Vorhersagen könnten zu Verwirrung führen, Entscheidungen verzögern oder das Vertrauen untergraben.
Dieser Ansatz beruht auf zwei Grundannahmen: Im höheren Zeitrahmen gibt es keine Verzerrung durch die Vorausschau (wir müssen den Wert des letzten Balkens verwenden, nicht den aktuellen), und das Modell des höheren Zeitrahmens hat seine eigene Vorhersagbarkeit (es schneidet besser ab als das zufällige Raten bei Out-of-Sample-Tests).
Um dies umzusetzen, ändern wir zunächst den Code im Datenabruf-EA, indem wir alle Zeitrahmen, die sich auf die Merkmalsextraktion beziehen, auf einen höheren Zeitrahmen, z. B. 1 Stunde, ändern. Dazu gehören Indikatoren, Preisberechnungen und alle anderen verwendeten Funktionen.
int OnInit() { trade.SetExpertMagicNumber(Magic); handleAdx = iADX(_Symbol, PERIOD_H1, 14); // Average Directional Movement Index - 3 handleWilder = iADXWilder(_Symbol, PERIOD_H1, 14); // Average Directional Movement Index by Welles Wilder - 3 handleDm = iDeMarker(_Symbol, PERIOD_H1, 14); // DeMarker - 1 handleRsi = iRSI(_Symbol, PERIOD_H1, 14, PRICE_CLOSE); // Relative Strength Index - 1 handleRvi = iRVI(_Symbol, PERIOD_H1, 10); // Relative Vigor Index - 2 handleSto = iStochastic(_Symbol, PERIOD_H1, 5, 3, 3, MODE_SMA, STO_LOWHIGH); // Stochastic Oscillator - 2 return(INIT_SUCCEEDED); } void getData() { double close = iClose(_Symbol, PERIOD_H1, 1); double close2 = iClose(_Symbol, PERIOD_H1, 2); double close3 = iClose(_Symbol, PERIOD_H1, 3); double stationary = 1000 * (close - iOpen(_Symbol, PERIOD_H1, 1)) / close; double stationary2 = 1000 * (close2 - iOpen(_Symbol, PERIOD_H1, 2)) / close2; double stationary3 = 1000 * (close3 - iOpen(_Symbol, PERIOD_H1, 3)) / close3; }
Danach folgen die gleichen Schritte wie zuvor: Abrufen der Daten, Trainieren des Modells und Exportieren des Modells, genau wie zuvor beschrieben.
Dann, in den Handel EA, machen wir eine zweite Funktion für die Abholung der Features Eingabe, werden wir diese Funktion auf die zweite ML-Modell, das wir importiert, um die Konfidenzniveau Ausgabe zu erhalten Feed.
vector getData2() { double close = iClose(_Symbol, PERIOD_H1, 1); double close2 = iClose(_Symbol, PERIOD_H1, 2); double close3 = iClose(_Symbol, PERIOD_H1, 3); double stationary = 1000 * (close - iOpen(_Symbol, PERIOD_H1, 1)) / close; double stationary2 = 1000 * (close2 - iOpen(_Symbol, PERIOD_H1, 2)) / close2; double stationary3 = 1000 * (close3 - iOpen(_Symbol, PERIOD_H1, 3)) / close3; double highDistance = 1000 * (close - high) / close; double lowDistance = 1000 * (close - low) / close; double boxSize = 1000 * (high - low) / close; double adx[]; // Average Directional Movement Index double wilder[]; // Average Directional Movement Index by Welles Wilder double dm[]; // DeMarker double rsi[]; // Relative Strength Index double rvi[]; // Relative Vigor Index double sto[]; // Stochastic 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(handleDm, 0, 1, 1, dm); // DeMarker CopyBuffer(handleRsi, 0, 1, 1, rsi); // Relative Strength Index CopyBuffer(handleRvi, 0, 1, 1, rvi); // Relative Vigor Index CopyBuffer(handleSto, 0, 1, 1, sto); // Stochastic Oscillator data[0][0] = DoubleToString(adx[0], 2); // Average Directional Movement Index data[0][1] = DoubleToString(wilder[0], 2); // Average Directional Movement Index by Welles Wilder data[0][2] = DoubleToString(dm[0], 2); // DeMarker data[0][3] = DoubleToString(rsi[0], 2); // Relative Strength Index data[0][4] = DoubleToString(rvi[0], 2); // Relative Vigor Index data[0][5] = DoubleToString(sto[0], 2); // Stochastic Oscillator data[0][6] = DoubleToString(stationary, 2); data[0][7] = DoubleToString(boxSize, 2); data[0][8] = DoubleToString(stationary2, 2); data[0][9] = DoubleToString(stationary3, 2); data[0][10] = DoubleToString(highDistance, 2); data[0][11] = DoubleToString(lowDistance, 2); vector features(12); for(int i = 0; i < 12; i++) { features[i] = StringToDouble(data[0][i]); } return features; }
Angenommen, wir wollen den beiden Modellen die gleiche Gewichtung zuweisen, dann nehmen wir einfach den Durchschnitt ihrer Ergebnisse und betrachten ihn als das einzige Ergebnis, das wir zuvor verwendet haben.
if (NotInPosition&&boxFormed&&((lastlastClose<high&&lastClose>high)||(lastClose<low&&lastlastClose>low))){ xx = getData(); xx2 = getData2(); prob = cat_boost.predict_proba(xx); prob2 = cat_boost.predict_proba(xx2); double probability_buy = (prob[1]+prob2[1])/2; double probability_sell = (prob[0]+prob2[0])/2; if(probability_buy>threshold)executeBuy(probability_buy); if(probability_sell>threshold)executeSell(probability_sell); } }
Mit diesen beiden wie oben berechneten Variablen können wir sie nun zu einem einzigen Konfidenzniveau zusammenfassen und für die Validierung verwenden, wobei wir denselben Ansatz wie zuvor verwenden.
Schlussfolgerung
In diesem Artikel haben wir uns zunächst mit der Idee beschäftigt, ein maschinelles Lernmodell als Signalgenerator anstelle eines Filters zu verwenden, und dies anhand einer Darvas-Box-Breakout-Strategie demonstriert. Wir gingen kurz den Prozess der Modellschulung durch und erörterten die Bedeutung von Schwellenwerten für das Konfidenzniveau und die Signifikanz von Merkmalen. Als Nächstes haben wir das Konzept der kontinuierlichen Signale eingeführt und ihre Leistung mit der von diskreten Signalen verglichen. In diesem Beispiel haben wir festgestellt, dass kontinuierliche Signale die Backtest-Performance verbessern, da das Modell mit steigendem Konfidenzniveau tendenziell eine höhere Vorhersagegenauigkeit aufweist. Schließlich haben wir den Entwurf für die Verwendung mehrerer Modelle des maschinellen Lernens, die auf unterschiedlichen Zeitrahmen trainiert wurden, um Signale gemeinsam zu validieren, angesprochen.
Insgesamt zielte dieser Artikel darauf ab, unkonventionelle Ideen zur Anwendung von maschinellen Lernmodellen im überwachten Lernen für den CTA-Handel vorzustellen. Ziel ist es nicht, eine endgültige Aussage darüber zu treffen, welcher Ansatz am besten funktioniert, da alles vom jeweiligen Szenario abhängt, sondern die Leser zu kreativem Denken anzuregen und einfache Ausgangskonzepte zu erweitern. Letztendlich ist nichts völlig neu - Innovation entsteht oft durch die Kombination bestehender Ideen, um etwas Neues zu schaffen.
Datei-Tabelle
Datei Name | Dateiverwendung |
---|---|
Darvas_Box.ipynb | Die Jupyter Notebook-Datei für das Training des ML-Modells |
Darvas Box Data.mq5 | Der EA zum Abrufen von Daten für das Modelltraining |
Darvas Box EA.mq5 | Der Handels-EA aus dem Artikel |
CatOnnx.mqh | Eine Include-Datei für die Verarbeitung des CatBoost-Modells |
FileCSV.mqh | Eine Include-Datei zum Speichern von Daten in CSV |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/17466





- 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.
Danke für den Hinweis. Der Pip-Installationsteil wurde ignoriert, aber die Benutzer müssen die zugehörige Bibliothek installieren, wenn sie es nicht schon getan haben.
Ihr Fehler kann dadurch verursacht werden, dass die Dimensionen, die Sie beim Training Ihres Modells verwendet haben, sich von denen unterscheiden, die Sie in Ihrem EA verwenden. Wenn Sie zum Beispiel ein Modell mit 5 Merkmalen trainiert haben, sollten Sie auch 5 Merkmale in Ihren EA eingeben, nicht 4 oder 6. Eine detailliertere Anleitung finden Sie in diesem Artikel. Hoffentlich hilft das. Wenn nicht, geben Sie bitte mehr Kontext an.