
Erstellen von selbstoptimierenden Expert Advisor in MQL5 (Teil 6): Stop-Out-Prävention
Es ist möglich, dass Händler die künftige Kursentwicklung eines Marktes richtig vorhersagen, ihre Positionen aber mit Verlust schließen. In Handelskreisen wird dies gemeinhin als „Ausgestoppt werden“ bezeichnet. Dieses Problem ergibt sich aus der Tatsache, dass sich das Preisniveau nicht in geraden und vorhersehbaren Linien verändert.
In Abbildung 1 sehen Sie eine Momentaufnahme der stündlichen Kursveränderung des EURUSD-Paares. Die weißen, gestrichelten vertikalen Linien markieren den Beginn und das Ende des Handelstages. Ein Händler, der davon überzeugt war, dass die Preise an diesem Tag fallen würden, hätte die künftige Preisänderung richtig vorhergesagt. Leider stiegen die Kurse zunächst deutlich an, bevor sie fielen. Das bedeutet, dass unser Händler seine Position mit Verlust geschlossen hätte, wenn sein Stop-Loss innerhalb der in Abb. 1 hervorgehobenen roten Zone gelegen hätte, nachdem er die künftige Kursentwicklung korrekt vorhergesagt hatte.
Abb. 1: Visualisierung von Situationen, in denen Händler typischerweise ausgestoppt werden
Im Laufe der Jahre haben Händler verschiedene Lösungen vorgeschlagen, um dieses Problem zu lösen, aber wie wir in unserer Diskussion sehen werden, sind die meisten dieser Lösungen nicht gültig. Die am häufigsten genannte Lösung besteht darin, einfach „die Stopps auszuweiten“.
Dies bedeutet, dass Händler an besonders volatilen Handelstagen ihre Stop-Loss weiter fassen sollten, um nicht ausgestoppt zu werden. Dies ist jedoch ein schlechter Ratschlag, da er die Händler dazu ermutigt, sich anzugewöhnen, bei ähnlichen Handelsgeschäften unterschiedliche Risikobeträge einzugehen, ohne dass es feste und klar definierte Regeln gibt, die ihre Entscheidungen leiten.
Eine andere häufig zitierte Lösung ist, „auf die Bestätigung zu warten, bevor Sie Ihre Handelsgeschäfte abschließen“. Auch dies ist angesichts der Art des Problems, auf das wir uns konzentrieren, ein schlechter Rat. Es kann sich herausstellen, dass das Warten auf die Bestätigung den Prozess des Ausstiegs nur hinauszögert und das Problem nicht vollständig löst.
Kurz gesagt, das Problem, ausgestoppt zu werden, macht es den Händlern schwer, solide Risikomanagementprinzipien zu befolgen, während es gleichzeitig die Rentabilität der Handelssitzungen verringert, neben anderen Punkten, die den Händlern Sorgen bereiten.
Deshalb werden wir es uns zum Ziel machen, Sie, den Leser, mit vernünftigen und gut definierten Regeln auszustatten, um die Häufigkeit zu minimieren, mit der Sie aus Ihren gewinnbringenden Handelsgeschäften ausgestoppt werden.
Die von uns vorgeschlagene Lösung wird gegen den Strich der allgemein zitierten Lösungen gehen und Sie dazu ermutigen, solide Handelsgewohnheiten zu entwickeln, wie z. B. die Beibehaltung der Größe Ihres Stop-Loss, im Gegensatz zu der allgemein zitierten Lösung „erweitern Sie Ihren Stop-Loss“.
Überblick über die Handelsstrategie
Unsere Handelsstrategie wird eine Strategie der Rückkehr zu Mitte (mean reverting) sein, die aus einer Kombination von Unterstützungs- und Widerstandsniveaus und technischer Analyse besteht. Zunächst markieren wir die für uns interessanten Kursniveaus anhand des Höchst- und Tiefstkurses vom Vortag. Von dort aus werden wir abwarten, ob die Preisniveaus des Vortages im Laufe des heutigen Tages durchbrochen werden. Wenn zum Beispiel der Höchstkurs des Vortages durch einen neuen Höchstkurs im Laufe des Tages gebrochen wird, suchen wir nach Möglichkeiten, Verkaufspositionen im Markt einzunehmen, indem wir darauf wetten, dass die Kurse zu ihrem Durchschnitt zurückkehren werden. Das Signal zum Eingehen von Verkaufspositionen wird für uns klar, wenn wir beobachten, dass die Kurse über dem gleitenden Durchschnittsindikator schließen, nachdem sie erfolgreich über dem vorherigen Hoch geschlossen haben.
Abb. 2: Visualisierung unserer Handelsstrategie in Aktion
Überblick über den Zeitraum des Backtests
Um die Wirksamkeit der von uns vorgeschlagenen Änderungen an der Handelsstrategie zu analysieren, benötigen wir zunächst einen festen Zeitraum, in dem wir die Änderungen an unserem System vergleichen. Für diese Diskussion beginnen wir unseren Test vom 1. Januar 2022 bis zum 1. Januar 2025. Der betreffende Zeitraum ist in Abb. 1 hervorgehoben, wobei zu beachten ist, dass wir den EURUSD auf dem monatlichen Zeitrahmen beobachten.Abb. 3: Der Zeitraum, über den wir unseren Backtest durchführen werden
Unsere eigentlichen Tests werden im Zeitrahmen M30 durchgeführt. In Abb. 2 haben wir den Markt, den wir für unsere Tests nutzen wollen, sowie den Zeitraum, den wir zuvor besprochen haben, hervorgehoben. Diese Einstellungen werden im weiteren Verlauf des Artikels festgelegt, weshalb wir sie hier besprechen müssen. Für alle folgenden Tests werden wir diese Einstellungen unverändert lassen. Stellen Sie außerdem sicher, dass Sie den EURUSD auswählen, wenn Sie uns folgen möchten, oder welches Symbol Sie auch immer handeln möchten.
Abb. 4: Der Zeitraum unseres Backtests
Wählen Sie außerdem „Jeder Tick auf Basis von realen Ticks“, um die genaueste Nachbildung historischer Marktereignisse zu erhalten, die wir erstellen können. Beachten Sie, dass diese Einstellung die relevanten Daten von Ihrem Broker abruft, was je nach Netzversorgung eine beträchtliche Zeit in Anspruch nehmen kann.
Abb. 5: Die Kontoeinstellungen, die wir für unseren Backtest verwenden werden
Erste Schritte in MQL5
Nachdem wir uns nun mit dem Zeitraum für unseren heutigen Test vertraut gemacht haben, wollen wir zunächst einen Ausgangswert festlegen, den wir übertreffen werden. Wir beginnen mit dem Aufbau einer Handelsanwendung zur Umsetzung einer Unterstützungs- und Widerstands-Handelsstrategie, die auf den Handel mit Ausbrüchen abzielt. Zunächst importieren wir die Handelsbibliothek.
//+------------------------------------------------------------------+ //| Baseline Model.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| Libraries | //+------------------------------------------------------------------+ #include <Trade/Trade.mqh> CTrade Trade;
Definition der Systemkonstanten. Mit diesen Konstanten können wir sicherstellen, dass wir das Verhalten unserer Anwendung über alle Tests hinweg genau kontrollieren können.
//+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define MA_PERIOD 14 //--- Moving Average Period #define MA_TYPE MODE_EMA //--- Type of moving average we have #define MA_PRICE PRICE_CLOSE //---- Applied Price of Moving Average #define TF_1 PERIOD_D1 //--- Our time frame for technical analysis #define TF_2 PERIOD_M30 //--- Our time frame for managing positions #define VOL 0.1 //--- Our trading volume #define SL_SIZE 1e3 * _Point //--- The size of our stop loss
Außerdem benötigen wir einige globale Variablen, die uns helfen, das gestrige Preisniveau von Interesse im Auge zu behalten.
//+------------------------------------------------------------------+ //| Our global variables | //+------------------------------------------------------------------+ int ma_handler,system_state; double ma[]; double bid,ask,yesterday_high,yesterday_low; const string last_high = "LAST_HIGH"; const string last_low = "LAST_LOW";
Wenn unsere Anwendung zum ersten Mal geladen wird, richten wir alle unsere technischen Indikatoren ein.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- setup(); //--- return(INIT_SUCCEEDED); }
Wenn unsere Anwendung nicht mehr gebraucht wird, geben wir die technischen Indikatoren frei, die wir nicht mehr verwenden.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- release(); }
Immer wenn wir aktualisierte Preise erhalten, speichern wir sie und berechnen auch unsere Indikatorwerte neu.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- update(); }
Wir werden nun jede der Funktionen definieren, die wir in unserem Ereigniszyklus aufgerufen haben. Zunächst wird unsere Aktualisierungsfunktion mit 2 verschiedenen Zeitrahmen arbeiten. Es gibt Routinen und Verfahren, die einmal am Tag durchgeführt werden müssen, und andere, die in viel kürzeren Abständen durchgeführt werden müssen. Diese Trennung wird für uns durch die beiden von uns definierten Systemkonstanten TF_1 (Tageszeitrahmen) und TF_2 (M30-Zeitrahmen) gewährleistet. Aufgaben wie die Abfrage des Vortags, des Hochs und des Tiefs, müssen nur einmal am Tag erledigt werden. Andererseits müssen Aufgaben, wie z. B. die Suche nach Positionen, nur einmal pro neuer 30-Minuten-Kerze durchgeführt werden.
//+------------------------------------------------------------------+ //| Perform our update routines | //+------------------------------------------------------------------+ void update() { //--- Daily procedures { static datetime time_stamp; datetime current_time = iTime(Symbol(),TF_1,0); if(time_stamp != current_time) { yesterday_high = iHigh(Symbol(),TF_1,1); yesterday_low = iLow(Symbol(),TF_1,1); //--- Mark yesterday's levels ObjectDelete(0,last_high); ObjectDelete(0,last_low); ObjectCreate(0,last_high,OBJ_HLINE,0,0,yesterday_high); ObjectCreate(0,last_low,OBJ_HLINE,0,0,yesterday_low); } } //--- M30 procedures { static datetime time_stamp; datetime current_time = iTime(Symbol(),TF_2,0); if(time_stamp != current_time) { time_stamp = current_time; //--- Get updated prices bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); //--- Update our technical indicators CopyBuffer(ma_handler,0,0,1,ma); //--- Check for a setup if(PositionsTotal()==0) find_setup(); } } }
Diese spezielle Anwendung stützt sich nur auf einen technischen Indikator. Daher ist unser Verfahren zur Definition der Einrichtungsfunktion einfach.
//+------------------------------------------------------------------+ //| Custom functions | //+------------------------------------------------------------------+ void setup(void) { ma_handler = iMA(Symbol(),TF_2,MA_PERIOD,0,MA_TYPE,MA_PRICE); };
Unsere Bedingungen für die Eröffnung einer Position sind erfüllt, wenn wir feststellen, dass unser aktueller Extremkurs den entgegengesetzten Extremkurs, den wir am Vortag beobachtet haben, übersteigt. Neben dem Durchbrechen der am Vortag festgelegten Niveaus möchten wir auch eine zusätzliche Bestätigung durch das Verhältnis des Kurses zu seinem gleitenden Durchschnitt erhalten.
//+------------------------------------------------------------------+ //| Check if we have any trading setups | //+------------------------------------------------------------------+ void find_setup(void) { if(iHigh(Symbol(),TF_2,1) < yesterday_low) { if(iClose(Symbol(),TF_2,1) < ma[0]) { Trade.Buy(VOL,Symbol(),ask,(bid - (SL_SIZE)),(bid + (SL_SIZE))); } } if(iLow(Symbol(),TF_2,1) > yesterday_high) { if(iClose(Symbol(),TF_2,1) > ma[0]) { Trade.Sell(VOL,Symbol(),bid,(ask + (SL_SIZE)),(ask - (SL_SIZE))); } } }
Wenn wir unseren Expert Advisor nicht mehr verwenden, sollten wir die Systemressourcen freigeben, die wir nicht mehr benötigen.
//+------------------------------------------------------------------+ //| Free resources we are no longer using up | //+------------------------------------------------------------------+ void release(void) { IndicatorRelease(ma_handler); }
Schließlich werden wir am Ende des Ausführungszyklus unseres Programms die zuvor definierten Systemkonstanten löschen.
//+------------------------------------------------------------------+ //| Undefine the system constants we created | //+------------------------------------------------------------------+ #undef TF_1 #undef TF_2 #undef VOL #undef SL_SIZE #undef MA_PERIOD #undef MA_PRICE #undef MA_TYPE
Die durch unsere derzeitige Handelsstrategie erzeugte Kapitalkurve ist nicht stabil. Das Gleichgewicht, das unser derzeitiges System herstellt, zeigt die Tendenz, im Laufe der Zeit immer weiter zu sinken. Wir wünschen uns eine Strategie, die gelegentlich fällt, aber die Tendenz hat, im Laufe der Zeit weiter zu steigen. Daher werden wir die Regeln, die wir zur Eröffnung unserer Positionen verwendet haben, beibehalten und versuchen, die Handelsgeschäfte herauszufiltern, von denen wir glauben, dass sie den Stop-Loss erreichen werden. Diese Übung wird definitiv eine Herausforderung sein. Es ist jedoch besser, jede Lösung anzuwenden als keine.
Abb. 6: Die Kapitalkurve, die sich aus unserer aktuellen Version der Handelsstrategie ergibt
Wenn wir die detaillierten Ergebnisse unserer Handelsstrategie analysieren, können wir feststellen, dass unser Algorithmus während unseres 3 Jahre zurückliegenden Testzeitraums mehr als $1000 verloren hat. Diese Informationen sind alles andere als ermutigend. Außerdem übersteigt unser durchschnittlicher und größter Verlust unseren durchschnittlichen und größten Gewinn. Daraus ergeben sich negative Erwartungen an die künftige Leistung der Strategie. Daher würden wir die Strategie in ihrer jetzigen Form nicht für den Handel mit einem Konto mit realem Kapital verwenden wollen.
Abb. 7: Analyse der detaillierten Ergebnisse unserer Handelsstrategie
Verbesserung der Ausgangssituation
Der Grundstein für unsere Präventionsstrategie liegt in einer Beobachtung, die wir zu einem früheren Zeitpunkt in unserer Diskussionsreihe gemacht haben. Leser, die die frühere Diskussion nachlesen möchten, können sie hier nachlesen. Zusammenfassend haben wir festgestellt, dass bei über 200 verschiedenen Symbolen auf unserem MetaTrader 5-Terminal der technische Indikator des gleitenden Durchschnitts durchweg leichter zu prognostizieren war als der direkte Preis.
Wir können unsere Beobachtungen nutzen, indem wir vorhersagen, ob der künftige Wert des gleitenden Durchschnitts unser Stop-Loss voraussichtlich übersteigen wird. Wenn unsere Anwendung davon ausgeht, dass dies der Fall ist, sollte sie keine Handelsgeschäfte tätigen, solange sie erwartet, dass der gleitende Durchschnitt unseren Stop-Loss erreicht, andernfalls kann unsere Anwendung ihre Handelsgeschäfte tätigen.
Das ist die Essenz unserer Lösung. Sie ist von Anfang bis Ende klar definiert und beruht auf soliden Grundsätzen und objektiven Überlegungen. Wir können sogar noch spezifischer sein und verlangen, dass unser Computer nicht nur nicht erwartet, dass unser Stop-Loss erreicht wird, sondern auch, dass der gleitende Durchschnitt unser Take-Profit-Niveau übersteigt, bevor wir einen Handel eingehen. Warum sollte sonst jemand einen Handel eingehen, wenn er keinen Grund zur Annahme hat, dass sein Take-Profit-Auftrag ausgeführt wird?
Um zu beginnen, müssen wir zunächst die relevanten Marktdaten von unserem MetaTrader 5-Terminal mithilfe eines MQL5-Skripts abrufen.
//+------------------------------------------------------------------+ //| ProjectName | //| Copyright 2020, CompanyName | //| http://www.companyname.net | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs //--- Define our moving average indicator #define MA_PERIOD 14 //--- Moving Average Period #define MA_TYPE MODE_EMA //--- Type of moving average we have #define MA_PRICE PRICE_CLOSE //---- Applied Price of Moving Average //--- Our handlers for our indicators int ma_handle; //--- Data structures to store the readings from our indicators double ma_reading[]; //--- File name string file_name = Symbol() + " Stop Out Prevention Market Data.csv"; //--- Amount of data requested input int size = 3000; //+------------------------------------------------------------------+ //| Our script execution | //+------------------------------------------------------------------+ void OnStart() { //---Setup our technical indicators ma_handle = iMA(_Symbol,PERIOD_M30,MA_PERIOD,0,MA_TYPE,MA_PRICE); //---Set the values as series CopyBuffer(ma_handle,0,0,size,ma_reading); ArraySetAsSeries(ma_reading,true); //---Write to file int file_handle=FileOpen(file_name,FILE_WRITE|FILE_ANSI|FILE_CSV,","); for(int i=size;i>=1;i--) { if(i == size) { FileWrite(file_handle,"Time","Open","High","Low","Close","MA 14"); } else { FileWrite(file_handle, iTime(_Symbol,PERIOD_CURRENT,i), iOpen(_Symbol,PERIOD_CURRENT,i), iHigh(_Symbol,PERIOD_CURRENT,i), iLow(_Symbol,PERIOD_CURRENT,i), iClose(_Symbol,PERIOD_CURRENT,i), ma_reading[i]); } } //--- Close the file FileClose(file_handle); } //+------------------------------------------------------------------+
Analysieren unserer Daten in Python
Nachdem Sie das Skript auf den Markt Ihrer Wahl angewendet haben, können wir mit der Analyse unserer Finanzdaten mithilfe der Python-Bibliotheken beginnen. Unser Ziel ist es, ein neuronales Netzwerk aufzubauen, das uns hilft, den zukünftigen Wert des gleitenden Durchschnittsindikators zu prognostizieren und uns möglicherweise vor Verlustgeschäften zu bewahren.
import pandas as pd import numpy as np import seaborn as sns import matplotlib.pyplot as plt
Lesen Sie nun die Daten ein, die wir aus unserem Terminal extrahiert haben.
data = pd.read_csv("EURUSD Stop Out Prevention Market Data.csv")
data
Kennzeichnung der Daten:
LOOK_AHEAD = 48 data['Target'] = data['MA 14'].shift(-LOOK_AHEAD) data.dropna(inplace=True) data.reset_index(drop=True,inplace=True)
Streichen wir die Zeiträume, die sich mit unserem Backtest überschneiden.
#Let's entirely drop off the last 2 years of data data.iloc[-((48 * 365 * 2) + (48 * 31 * 2) + (48 * 14) - (3)):,:]
Wir überschreiben die ursprünglichen Marktdaten mit den neuen Daten, die nicht die Beobachtungen im Zeitraum unseres Backtests enthalten.
#Let's entirely drop off the last 2 years of data _ = data.iloc[-((48 * 365 * 2) + (48 * 31 * 2) + (48 * 14) - (3)):,:] data = data.iloc[:-((48 * 365 * 2) + (48 * 31 * 2) + (48 * 14) - (3)),:] data
Nun laden wir unsere Bibliotheken für maschinelles Lernen.
from sklearn.neural_network import MLPRegressor from sklearn.model_selection import train_test_split,TimeSeriesSplit,cross_val_scoreWir erstellen ein geteiltes Zeitreihenobjekt, damit wir unser Modell schnell überprüfen können.
tscv = TimeSeriesSplit(n_splits=5,gap=LOOK_AHEAD)
Wir geben die Eingaben und das Ziel an
X = data.columns[1:-1] y = data.columns[-1:]und teilen die Daten in zwei Hälften, für das Training und den Test des neuen Modells.
train , test = train_test_split(data,test_size=0.5,shuffle=False)
Bereiten wir die zu normalisierenden und zu skalierenden Zug- und Testpartitionen vor,
train_X = train.loc[:,X] train_y = train.loc[:,y] test_X = test.loc[:,X] test_y = test.loc[:,y]
berechnen die Parameter für unsere z-Scores
mean_scores = train_X.mean() std_scores = train_X.std()und normalisieren die Eingabedaten des Modells.
train_X = ((train_X - mean_scores) / std_scores) test_X = ((test_X - mean_scores) / std_scores)
Wir wollen eine Zeilensuche nach der optimalen Anzahl von Trainingsiterationen für unser tiefes neuronales Netz durchführen. Wir werden durch aufsteigende Potenzen von 2 iterieren. Beginnend mit 2 hoch 0 bis 2 hoch 14.
MAX_POWER = 15 results = pd.DataFrame(index=["Train","Test"],columns=[np.arange(0,MAX_POWER)])
Wir definieren eine for-Schleife, die uns hilft, die optimale Anzahl von Trainingsiterationen abzuschätzen, die notwendig sind, um unser tiefes neuronales Netzwerkmodell an die vorhandenen Daten anzupassen.
#Classical Inputs for i in np.arange(0,MAX_POWER): print(i) model = MLPRegressor(hidden_layer_sizes=(5,10,4,2),solver="adam",activation="relu",max_iter=(2**i),early_stopping=False) results.iloc[0,i] = np.mean(np.abs(cross_val_score(model,train_X.loc[:,:],train_y.values.ravel(),cv=tscv))) results.iloc[1,i] = np.mean(np.abs(cross_val_score(model,test_X.loc[:,:],test_y.values.ravel(),cv=tscv))) results
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Train | 19675.492496 | 19765.297106 | 19609.7644 | 19511.588484 | 19859.734807 | 19942.30371 | 18831.617167 | 10703.554068 | 4930.771654 | 1639.952482 | 1389.615052 | 2938.371438 | 1.536765 | 2.193895 | 30.553918 |
Test | 13171.519137 | 14113.252994 | 14428.159203 | 13649.157525 | 13655.643066 | 12919.773346 | 11472.770729 | 5878.964564 | 11293.444345 | 3788.388634 | 2545.368419 | 3599.364028 | 2240.598518 | 1041.641869 | 882.696622 |
Die visuelle Darstellung der Daten zeigt, dass wir die maximale Anzahl von Iterationen benötigen, um die optimale Ausgabe unseres Modells zu erhalten. Allerdings sollte der Leser auch offen sein für die Möglichkeit, dass unser Suchverfahren vorzeitig beendet wurde. Das bedeutet, dass es möglich ist, dass wir bessere Ergebnisse erhalten hätten, wenn wir 2er-Potenzen größer als 14 verwendet hätten. Aufgrund des hohen Rechenaufwands für das Training dieser Modelle ging unsere Suche jedoch nicht über 2 hoch 14 hinaus.
plt.title("Neural Network RMSE Forecasting 14 Period MA") plt.ylabel("5 CV RMSE") plt.xlabel("Training Iterations As Powers of 2") plt.grid() sns.lineplot(np.array(results.iloc[1,:]).transpose()) plt.axhline(results.min(1)[1],linestyle='--',color='red') plt.axvline(14,linestyle='--',color='red')
Abb. 8: Die Ergebnisse der Suche nach der optimalen Anzahl von Trainingsiterationen für unser tiefes neuronales Netzmodell
Nachdem unser Modell nun trainiert wurde, können wir es in das ONNX-Format exportieren.
import onnx import skl2onnx from skl2onnx.common.data_types import FloatTensorType
Bereiten wir die Anpassung des Modells unter Verwendung der von uns geschätzten optimalen Anzahl von Trainingswiederholungen vor.
model = MLPRegressor(hidden_layer_sizes=(5,10,4,2),solver="adam",activation="relu",max_iter=(2**14),early_stopping=False)
Wir laden die z-Scores für den gesamten Datensatz.
mean_scores = data.loc[:,X].mean() std_scores = data.loc[:,X].std() mean_scores.to_csv("EURUSD StopOut Mean.csv") std_scores.to_csv("EURUSD StopOut Std.csv")
Wir transformieren den gesamten Datensatz
data[X] = ((data.loc[:,X] - mean_scores) / std_scores)
und passen unser Modell an alle Daten an, die wir haben, mit Ausnahme der Testdaten.
model.fit(data.loc[:,X],data.loc[:,'Target'].values.ravel())
Wir geben die Eingabeform unseres Modells an
initial_types = [("float_input",FloatTensorType([1,5]))]
und bereiten die Konvertierung des Modells in das ONNX-Format vor.
model_proto = skl2onnx.convert_sklearn(model,initial_types=initial_types,target_opset=12)
Speichern wir das Modell als ONNX-Datei.
onnx.save(model_proto,"EURUSD StopOut Prevention Model.onnx")
Aufbau einer verfeinerten Version unserer Strategie
Beginnen wir mit dem Aufbau unserer neuen, verfeinerten Version der Handelsstrategie. Zunächst laden wir zunächst das ONNX-Modell, das wir gerade erstellt haben.
//+------------------------------------------------------------------+ //| Baseline Model.mq5 | //| Gamuchirai Ndawana | //| https://www.mql5.com/en/users/gamuchiraindawa | //+------------------------------------------------------------------+ #property copyright "Gamuchirai Ndawana" #property link "https://www.mql5.com/en/users/gamuchiraindawa" #property version "1.00" //+------------------------------------------------------------------+ //| Resources | //+------------------------------------------------------------------+ #resource "\\Files\\EURUSD StopOut Prevention Model.onnx" as uchar onnx_model_buffer[];
Wir werden einige zusätzliche Systemkonstanten für diese Version unserer Anwendung erstellen.
//+------------------------------------------------------------------+ //| System constants | //+------------------------------------------------------------------+ #define MA_PERIOD 14 //--- Moving Average Period #define MA_TYPE MODE_EMA //--- Type of moving average we have #define MA_PRICE PRICE_CLOSE //---- Applied Price of Moving Average #define TF_1 PERIOD_D1 //--- Our time frame for technical analysis #define TF_2 PERIOD_M30 //--- Our time frame for managing positions #define VOL 0.1 //--- Our trading volume #define SL_SIZE 1e3 * _Point //--- The size of our stop loss #define SL_ADJUSTMENT 1e-5 * _Point //--- The step size for our trailing stop #define ONNX_MODEL_INPUTS 5 //---- Total model inputs for our ONNX model
Außerdem müssen unsere globalen z-Scores in Arrays geladen werden.
//+------------------------------------------------------------------+ //| Our global variables | //+------------------------------------------------------------------+ int ma_handler,system_state; double ma[]; double mean_values[ONNX_MODEL_INPUTS] = {1.157641086508574,1.1581085911361018,1.1571729541088953,1.1576420747040126,1.157640521193191}; double std_values[ONNX_MODEL_INPUTS] = {0.04070388112283021,0.040730761156963606,0.04067819202368064,0.040703752648947544,0.040684857239172416}; double bid,ask,yesterday_high,yesterday_low; const string last_high = "LAST_HIGH"; const string last_low = "LAST_LOW"; long onnx_model; vectorf model_forecast = vectorf::Zeros(1);
Bevor wir unsere ONNX-Modelle verwenden können, müssen wir die Modelle zunächst entsprechend einstellen und überprüfen, ob sie korrekt konfiguriert wurden.
//+------------------------------------------------------------------+ //| Prepare the resources our EA requires | //+------------------------------------------------------------------+ bool setup(void) { onnx_model = OnnxCreateFromBuffer(onnx_model_buffer,ONNX_DEFAULT); if(onnx_model == INVALID_HANDLE) { Comment("Failed to create ONNX model: ",GetLastError()); return(false); } ulong input_shape[] = {1,ONNX_MODEL_INPUTS}; ulong output_shape[] = {1,1}; if(!OnnxSetInputShape(onnx_model,0,input_shape)) { Comment("Failed to set ONNX model input shape: ",GetLastError()); return(false); } if(!OnnxSetOutputShape(onnx_model,0,output_shape)) { Comment("Failed to set ONNX model output shape: ",GetLastError()); return(false); } ma_handler = iMA(Symbol(),TF_2,MA_PERIOD,0,MA_TYPE,MA_PRICE); if(ma_handler == INVALID_HANDLE) { Comment("Failed to load technical indicator: ",GetLastError()); return(false); } return(true); };
Unsere Vorgehensweise bei der Suche nach einem Handelsaufbau wird sich leicht ändern. Zunächst holen wir uns eine Vorhersage von unserem Modell. Danach bleiben unsere Bedingungen für die Eröffnung und Schließung von Positionen gleich. Zusätzlich zur Erfüllung dieser Bedingungen werden wir jedoch auch prüfen, ob neue Bedingungen erfüllt sind.
//+------------------------------------------------------------------+ //| Check if we have any trading setups | //+------------------------------------------------------------------+ void find_setup(void) { if(!model_predict()) { Comment("Failed to get a forecast from our model"); return; } if((iHigh(Symbol(),TF_2,1) < yesterday_low) && (iHigh(Symbol(),TF_2,2) < yesterday_low)) { if(iClose(Symbol(),TF_2,1) > ma[0]) { check_buy(); } } if((iLow(Symbol(),TF_2,1) > yesterday_high) && (iLow(Symbol(),TF_2,2) > yesterday_high)) { if(iClose(Symbol(),TF_2,1) < ma[0]) { check_sell(); } } }
Die neuen Bedingungen, die wir angeben müssen, gelten sowohl für unsere Kauf- als auch für unsere Verkaufspositionen. Zunächst prüfen wir, ob unsere Prognose des gleitenden Durchschnitts größer ist als der aktuelle Wert des uns vorliegenden Indikators für den gleitenden Durchschnitt. Darüber hinaus wollen wir auch prüfen, ob der erwartete zukünftige Wert des gleitenden Durchschnittsindikators größer ist als der aktuell angebotene Kurs.
Das bedeutet, dass unser Computer vermutet, dass sich der Trend in eine Richtung fortsetzen wird. Schließlich prüfen wir, ob unser Computer erwartet, dass der gleitende Durchschnitt unter dem Stop-Loss bleibt. Wenn alle unsere Bedingungen erfüllt sind, eröffnen wir sofort eine Position auf dem Markt.
//+------------------------------------------------------------------+ //| Check if we have a valid buy setup | //+------------------------------------------------------------------+ void check_buy(void) { if((model_forecast[0] > ma[0]) && (model_forecast[0] > iClose(Symbol(),TF_2,0))) { if(model_forecast[0] > (bid - (SL_SIZE))) Trade.Buy(VOL,Symbol(),ask,(bid - (SL_SIZE)),(bid + (SL_SIZE))); } }
Unsere Bedingungen für die Eröffnung von Verkaufspositionen sind die gleichen wie die für Kaufpositionen, aber sie funktionieren in umgekehrter Reihenfolge.
//+------------------------------------------------------------------+ //| Check if we have a valid sell setup | //+------------------------------------------------------------------+ void check_sell(void) { if((model_forecast[0] < ma[0]) && (model_forecast[0] < iClose(Symbol(),TF_2,0))) { if(model_forecast[0] < (ask + (SL_SIZE))) Trade.Sell(VOL,Symbol(),bid,(ask + (SL_SIZE)),(ask - (SL_SIZE))); } }
Sobald wir eine Position eröffnet haben, müssen wir sie weiter beobachten. Unsere Funktion für das Update der Stop-Loss dient 2 Zwecken, je nachdem, wie sie aufgerufen wird. Sie nimmt einen Flag-Parameter an, der ihr Verhalten ändert. Wenn die Flagge auf 0 gesetzt ist, suchen wir einfach nach einer Gelegenheit, unsere Stopp-Levels in Richtung profitablerer Kurse zu verschieben. Andernfalls, wenn das Flag auf 1 gesetzt ist, wollen wir zuerst eine neue Prognose von unserem Modell abrufen und prüfen, ob der zukünftige Wert des gleitenden Durchschnitts unser aktuellen Stop-Loss überschreiten könnte.
Wenn erwartet wird, dass der gleitende Durchschnitt unseren Stop-Loss übersteigt, aber dennoch eine gewinnbringende Bewegung bildet, passen wir unseren Stop-Loss an das Niveau an, von dem wir erwarten, dass der gleitende Durchschnitt seinen Höhepunkt erreicht. Andernfalls, wenn erwartet wird, dass der Handel unter den Eröffnungskurs fällt, wollen wir unseren Computer anweisen, bei solchen Handelsgeschäften, die wenig Gewinnpotenzial aufweisen, weniger zu riskieren.
//+------------------------------------------------------------------+ //| Update our stop loss | //+------------------------------------------------------------------+ void update_sl(int flag) { //--- First find our open position if(PositionSelect(Symbol())) { double current_sl = PositionGetDouble(POSITION_SL); double current_tp = PositionGetDouble(POSITION_TP); double open_price = PositionGetDouble(POSITION_PRICE_OPEN); //--- Flag 0 means we just want to push the stop loss and take profit forward if its possible if(flag == 0) { //--- Buy Setup if(current_tp > current_sl) { if((bid - SL_SIZE) > current_sl) Trade.PositionModify(Symbol(),(bid - SL_SIZE),(bid + SL_SIZE)); } //--- Sell setup if(current_tp < current_sl) { if((ask + SL_SIZE) < current_sl) Trade.PositionModify(Symbol(),(ask + SL_SIZE),(ask - SL_SIZE)); } } //--- Flag 1 means we want to check if the stop loss may be hit soon, and act accordingly if(flag == 1) { model_predict(); //--- Buy setup if(current_tp > current_sl) { if(model_forecast[0] < current_sl) { if((model_forecast[0] > ma[0]) && (model_forecast[0] > yesterday_low)) Trade.PositionModify(Symbol(),model_forecast[0],current_tp); } if(model_forecast[0] < open_price) Trade.PositionModify(Symbol(),model_forecast[0] * 1.5,current_tp); } //--- Sell setup if(current_tp < current_sl) { if(model_forecast[0] > current_sl) { if((model_forecast[0] < ma[0]) && (model_forecast[0] < yesterday_high)) Trade.PositionModify(Symbol(),model_forecast[0],current_tp); } if(model_forecast[0] > open_price) Trade.PositionModify(Symbol(),model_forecast[0] * 0.5,current_tp); } } } }
Unser Aktualisierungsverfahren wird leicht modifiziert, um die Funktion des Aktualisierungsstopps aufzurufen.
//+------------------------------------------------------------------+ //| Perform our update routines | //+------------------------------------------------------------------+ void update() { //--- Daily procedures { static datetime time_stamp; datetime current_time = iTime(Symbol(),TF_1,0); if(time_stamp != current_time) { yesterday_high = iHigh(Symbol(),TF_1,1); yesterday_low = iLow(Symbol(),TF_1,1); //--- Mark yesterday's levels ObjectDelete(0,last_high); ObjectDelete(0,last_low); ObjectCreate(0,last_high,OBJ_HLINE,0,0,yesterday_high); ObjectCreate(0,last_low,OBJ_HLINE,0,0,yesterday_low); } } //--- M30 procedures { static datetime time_stamp; datetime current_time = iTime(Symbol(),TF_2,0); if(time_stamp != current_time) { time_stamp = current_time; //--- Get updated prices bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); //--- Update our technical indicators CopyBuffer(ma_handler,0,0,1,ma); //--- Check for a setup if(PositionsTotal()==0) find_setup(); //--- Check for a setup if(PositionsTotal() > 0) update_sl(1); } } //--- Per tick procedures { //--- These function calls can become expensive and may slow down the speed of your back tests //--- Be thoughtful when placing any function calls in this scope update_sl(0); } }
Außerdem benötigen wir eine spezielle Funktion, die für das Abrufen von Vorhersagen aus unserem neuronalen Netzmodell zuständig ist. Wir werden zunächst die Eingaben in einen Float-Vektor aufbereiten und dann die Eingaben standardisieren, damit wir eine Vorhersage von unserem Modell abrufen können.
//+------------------------------------------------------------------+ //| Get a forecast from our deep neural network | //+------------------------------------------------------------------+ bool model_predict(void) { double ma_input[] = {0}; CopyBuffer(ma_handler,0,1,1,ma_input); vectorf model_inputs = { (float) iOpen(Symbol(),TF_2,1), (float) iHigh(Symbol(),TF_2,1), (float) iLow(Symbol(),TF_2,1), (float) iClose(Symbol(),TF_2,1), (float) ma_input[0] }; for(int i = 0; i < ONNX_MODEL_INPUTS;i++) { model_inputs[i] = (float)((model_inputs[i] - mean_values[i]) / std_values[i]); } if(!OnnxRun(onnx_model,ONNX_DEFAULT,model_inputs,model_forecast)) { Comment("Failed to obtain forecast: ",GetLastError()); return(false); } Comment(StringFormat("Expected MA Value: %f",model_forecast[0])); return(true); }
Wenn unsere Anwendung nicht mehr verwendet wird, werden wir den Indikator und das ONNX-Modell freigeben.
//+------------------------------------------------------------------+ //| Free resources we are no longer using up | //+------------------------------------------------------------------+ void release(void) { OnnxRelease(onnx_model); IndicatorRelease(ma_handler); } //+------------------------------------------------------------------+
Wenn wir die Kapitalkurve analysieren, die von unserer neuen, verfeinerten Version unseres Handelsalgorithmus erzeugt wird, können wir schnell feststellen, dass die charakteristische negative Steigung, die wir bei der ersten Implementierung der Strategie beobachten konnten, korrigiert wurde und unsere Strategie nun einen positiven Trend aufweist, mit gelegentlichen Einbrüchen. Dies ist wünschenswerter als der Ausgangszustand unserer Strategie.
Abb. 9: Veranschaulichung der Gewinnkurve, die sich aus unserer neuen, verfeinerten Version des Stopp- und Präventionsalgorithmus ergibt
Bei näherer Betrachtung stellen wir fest, dass unsere neue Strategie jetzt rentabel ist. Die erste Version unserer Strategie verlor etwa 1000 Dollar, und unsere aktuelle Version hat etwas mehr als 1000 Dollar eingebracht. Dies ist eine wesentliche Verbesserung. Unsere ursprüngliche Sharpe Ratio lag bei -0,39 und unsere neue Sharpe Ratio beträgt 0,79. Dem Leser wird auch auffallen, dass unser durchschnittliches Gewinngeschäft von 98 $ auf 130 $ gestiegen ist, während das durchschnittliche Verlustgeschäft von 102 $ auf 63 $ gefallen ist. Dies zeigt, dass unsere durchschnittlichen Gewinne deutlich schneller wachsen als unsere durchschnittlichen Verluste. Diese Kennzahlen lassen uns positive Erwartungen hegen, wenn wir diese Version unserer Handelsstrategie in Betracht ziehen.
Obwohl wir erhebliche Fortschritte gemacht haben, ist das Problem des „Ausgestoppt-Werdens“ sicherlich immer noch schwierig zu lösen. Das sehen wir daran, dass ca. 60 % aller von uns eröffneten Positionen Verlustgeschäfte waren. Es ist eine Herausforderung, alle Handelsgeschäfte herauszufiltern, bei denen ein Händler ausgestoppt wird. Heute ist es uns gelungen, die meisten großen und unrentablen Handelsgeschäfte herauszufiltern.
Abb. 10: Eine detaillierte Analyse der Ergebnisse, die wir mit unserem neuen Algorithmus zur Verhinderung von Ausfällen erzielt haben
Schlussfolgerung
In diesem Artikel haben wir dem Leser eine mögliche Lösung für das seit langem bestehende Problem des Ausstiegs aus gewinnbringenden Handelsgeschäften aufgezeigt. Dieses Problem ist der Kern des erfolgreichen Handels und wird vielleicht nie ganz gelöst werden. Jede neue Lösung bringt eine Reihe von Schwachstellen in unsere Strategie ein. Nach der Lektüre dieses Artikels verfügt der Leser über einen quantitativen Rahmen für die Verwaltung seiner Stop-Loss-Niveaus. Das Erkennen und Herausfiltern von Handelsgeschäften, die Ihr Konto unnötig belasten, ist eine entscheidende Komponente jeder Handelsstrategie.
Datei Name | Datei Beschreibung |
---|---|
Baseline Model.mq5 | Unsere ursprüngliche Handelsstrategie, mit der wir eine Outperformance erzielen wollten. |
Stop Out Prevention Model.mq5 | Unsere verfeinerte Version der Handelsstrategie, die auf einem tiefen neuronalen Netzwerk basiert. |
EURUSD Stop Out Moving Average Model.ipynb | Das Jupyter-Notebook, das wir zur Analyse der von unserem MetaTrader 5 Terminal extrahierten Finanzdaten verwendet haben. |
EURUSD Stop Out Prevention Model.onnx | Unser tiefes neuronales Netzwerk. |
Fetch Data MA.mq5 | Das MQL5-Skript, das wir zum Abrufen der erforderlichen Marktdaten verwendet haben. |
Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/17213





- 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.