English 日本語
preview
Erstellen von selbstoptimierenden Expert Advisor in MQL5 (Teil 6): Stop-Out-Prävention

Erstellen von selbstoptimierenden Expert Advisor in MQL5 (Teil 6): Stop-Out-Prävention

MetaTrader 5Beispiele | 20 Juni 2025, 12:21
22 0
Gamuchirai Zororo Ndawana
Gamuchirai Zororo Ndawana

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.

Bildschirmfoto 1

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_score
Wir 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
 01234567891011121314 
 Train19675.49249619765.297106
19609.764419511.588484
19859.734807
19942.30371
18831.617167
10703.554068
 4930.771654
1639.952482
1389.6150522938.371438
1.5367652.193895
30.553918
 Test13171.51913714113.25299414428.15920313649.15752513655.64306612919.77334611472.7707295878.96456411293.444345
3788.388634 2545.3684193599.3640282240.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 NameDatei Beschreibung
Baseline Model.mq5Unsere ursprüngliche Handelsstrategie, mit der wir eine Outperformance erzielen wollten.
Stop Out Prevention Model.mq5Unsere verfeinerte Version der Handelsstrategie, die auf einem tiefen neuronalen Netzwerk basiert.
EURUSD Stop Out Moving Average Model.ipynbDas Jupyter-Notebook, das wir zur Analyse der von unserem MetaTrader 5 Terminal extrahierten Finanzdaten verwendet haben.
EURUSD Stop Out Prevention Model.onnxUnser tiefes neuronales Netzwerk.
Fetch Data MA.mq5Das 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

Automatisieren von Handelsstrategien in MQL5 (Teil 9): Aufbau eines Expert Advisors für die asiatische Breakout-Strategie Automatisieren von Handelsstrategien in MQL5 (Teil 9): Aufbau eines Expert Advisors für die asiatische Breakout-Strategie
In diesem Artikel erstellen wir einen Expert Advisor in MQL5 für die Asian Breakout Strategy, indem wir das Hoch und das Tief der Sitzung berechnen und die Trendfilterung mit einem gleitenden Durchschnitt anwenden. Wir implementieren ein dynamisches Objekt-Styling, nutzerdefinierte Zeitangaben und ein robustes Risikomanagement. Schließlich demonstrieren wir Techniken für Backtests und Optimierung zur Verfeinerung des Programms.
Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 16): Einführung in die Quarters Theory (II) - Intrusion Detector EA Entwicklung eines Toolkit zur Analyse von Preisaktionen (Teil 16): Einführung in die Quarters Theory (II) - Intrusion Detector EA
In unserem letzten Artikel haben wir ein einfaches Skript namens „Quarters Drawer“ vorgestellt. Auf dieser Grundlage gehen wir nun den nächsten Schritt und erstellen einen Monitor Expert Advisor (EA), der diese Quarter verfolgt und einen Überblick über mögliche Marktreaktionen auf diesen Niveaus bietet. Begleiten Sie uns in diesem Artikel bei der Entwicklung eines Tools zur Zonenerkennung.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Erstellen eines Handelsadministrator-Panels in MQL5 (Teil IX): Code Organisation (III): Kommunikationsmodul Erstellen eines Handelsadministrator-Panels in MQL5 (Teil IX): Code Organisation (III): Kommunikationsmodul
Nehmen Sie an einer ausführlichen Diskussion über die neuesten Fortschritte im MQL5-Schnittstellendesign teil, wenn wir das neu gestaltete Kommunikations-Panel vorstellen und unsere Serie über den Aufbau des neuen Admin-Panels unter Verwendung von Modularisierungsprinzipien fortsetzen. Wir werden die Klasse CommunicationsDialog Schritt für Schritt entwickeln und ausführlich erklären, wie man sie von der Klasse Dialog erbt. Außerdem werden wir Arrays und die ListView-Klasse in unserer Entwicklung nutzen. Gewinnen Sie umsetzbare Erkenntnisse, um Ihre MQL5-Entwicklungsfähigkeiten zu verbessern - lesen Sie den Artikel und beteiligen Sie sich an der Diskussion im Kommentarbereich!