English 日本語
preview
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

MetaTrader 5Handel |
121 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Einführung

Im vorangegangenen Artikel (Teil 8) haben wir eine Umkehr-Strategie untersucht, indem wir einen Expert Advisor in MetaQuotes Language 5 (MQL5) auf Basis des harmonischen Schmetterlingsmuster unter Verwendung präziser Fibonacci-Verhältnisse erstellt haben. In Teil 9 konzentrieren wir uns nun auf die asiatische Breakout-Strategie - eine Methode, die wichtige Hochs und Tiefs der Sitzung zur Bildung von Ausbruchszonen identifiziert, einen gleitenden Durchschnitt zur Trendfilterung einsetzt und ein dynamisches Risikomanagement integriert.

In diesem Artikel werden wir uns mit folgenden Themen beschäftigen:

  1. Blaupause der Strategie
  2. Implementation in MQL5
  3. Backtest und Optimierung
  4. Schlussfolgerung

    Am Ende werden Sie einen voll funktionsfähigen Expert Advisor haben, der die Asian Breakout Strategy automatisiert und bereit ist, für den Handel getestet und verfeinert zu werden. Lasst uns eintauchen!


    Blaupause der Strategie

    Um das Programm zu erstellen, werden wir einen Ansatz entwickeln, der die wichtige Preisspanne nutzt, die sich während der asiatischen Handelssitzung gebildet hat. Der erste Schritt besteht darin, die Box der Handelssitzung zu definieren, indem das höchste Hoch und das tiefste Tief innerhalb eines bestimmten Zeitfensters erfasst werden - in der Regel zwischen 23:00 und 03:00 Uhr Greenwich Mean Time (GMT). Diese Zeiten sind jedoch vollständig an Ihre Bedürfnisse anpassbar. Dieser definierte Bereich stellt die Konsolidierungszone dar, aus der wir einen Ausbruch erwarten.

    Als Nächstes werden wir Ausbruchsniveaus an den Grenzen dieser Spanne festlegen. Wenn die Marktbedingungen einen Aufwärtstrend bestätigen, platzieren wir eine schwebende Kauf-Stopp-Order leicht oberhalb des oberen Randes der Box - unter Verwendung eines gleitenden Durchschnitts (z. B. eines 50-Perioden-MA) zur Trendbestätigung. Umgekehrt werden wir bei einem Abwärtstrend eine Verkaufsstopp-Order knapp unter dem Boden der Box platzieren. Dieses duale Setup stellt sicher, dass unser Expert Advisor bereit ist, bedeutende Bewegungen in beide Richtungen zu erfassen, sobald der Preis ausbricht.

    Das Risikomanagement ist ein wichtiger Bestandteil unserer Strategie. Wir werden Stop-Loss-Aufträge knapp außerhalb der Box-Grenzen integrieren, um uns vor falschen Ausbrüchen oder Umkehrungen zu schützen, während Take-Profit-Levels auf der Grundlage eines vordefinierten Risiko-Ertrags-Verhältnisses festgelegt werden. Darüber hinaus werden wir eine zeitbasierte Ausstiegsstrategie implementieren, die automatisch alle offenen Handelsgeschäfte schließt, wenn sie über eine bestimmte Ausstiegszeit hinaus aktiv bleiben, z. B. um 13:00 Uhr GMT. Insgesamt kombiniert unsere Strategie präzise sitzungsbasierte Bereichserkennung, Trendfilterung und robustes Risikomanagement, um einen Expert Advisor zu entwickeln, der in der Lage ist, bedeutende Ausbruchsbewegungen im Markt zu erfassen. Kurz gesagt, hier ist eine Visualisierung der gesamten Strategie, die wir umsetzen wollen.

    STRATEGIE-ENTWURF


    Implementation in MQL5

    Um das Programm in MQL5 zu erstellen, öffnen wir den MetaEditor, gehen zum Navigator, suchen den Ordner Indikatoren, klicken auf die Registerkarte „Neu“ und folgen den Anweisungen, um die Datei zu erstellen. Sobald das erstellt ist, müssen wir in der Programmierumgebung einige globale Variablen deklarieren, die wir im gesamten Programm verwenden werden.

    //+------------------------------------------------------------------+
    //|                        Copyright 2025, Forex Algo-Trader, Allan. |
    //|                                 "https://t.me/Forex_Algo_Trader" |
    //+------------------------------------------------------------------+
    #property copyright "Forex Algo-Trader, Allan"
    #property link      "https://t.me/Forex_Algo_Trader"
    #property version   "1.00"
    #property description "This EA trades based on ASIAN BREAKOUT Strategy"
    #property strict
    
    #include <Trade\Trade.mqh>                              //--- Include trade library
    CTrade obj_Trade;                                          //--- Create global trade object
    
    //--- Global indicator handle for the moving average
    int maHandle = INVALID_HANDLE;                         //--- Global MA handle
    
    //==== Input parameters
    //--- Trade and indicator settings
    input double         LotSize              = 0.1;         //--- Trade lot size
    input double         BreakoutOffsetPips   = 10;          //--- Offset in pips for pending orders
    input ENUM_TIMEFRAMES BoxTimeframe         = PERIOD_M15;  //--- Timeframe for box calculation (15 or 30 minutes)
    input int            MA_Period            = 50;          //--- Moving average period for trend filter
    input ENUM_MA_METHOD MA_Method            = MODE_SMA;    //--- MA method (Simple Moving Average)
    input ENUM_APPLIED_PRICE MA_AppliedPrice   = PRICE_CLOSE; //--- Applied price for MA (Close price)
    input double         RiskToReward         = 1.3;         //--- Reward-to-risk multiplier (1:1.3)
    input int            MagicNumber          = 12345;       //--- Magic number (used for order identification)
    
    //--- Session timing settings (GMT) with minutes
    input int            SessionStartHour     = 23;          //--- Session start hour
    input int            SessionStartMinute   = 00;           //--- Session start minute
    input int            SessionEndHour       = 03;           //--- Session end hour
    input int            SessionEndMinute     = 00;           //--- Session end minute
    input int            TradeExitHour        = 13;          //--- Trade exit hour
    input int            TradeExitMinute      = 00;           //--- Trade exit minute
    
    //--- Global variables for storing session box data
    datetime lastBoxSessionEnd = 0;                        //--- Stores the session end time of the last computed box
    bool     boxCalculated     = false;                    //--- Flag: true if session box has been calculated
    bool     ordersPlaced      = false;                    //--- Flag: true if orders have been placed for the session
    double   BoxHigh           = 0.0;                        //--- Highest price during the session
    double   BoxLow            = 0.0;                        //--- Lowest price during the session
    //--- Variables to store the exact times when the session's high and low occurred
    datetime BoxHighTime       = 0;                          //--- Time when the highest price occurred
    datetime BoxLowTime        = 0;                          //--- Time when the lowest price occurred
    

    Hier binden wir die Handelsbibliothek mit „#include <Trade\Trade.mqh>“ ein, um auf integrierte Handelsfunktionen zuzugreifen und ein globales Handelsobjekt mit dem Namen „obj_Trade“ zu erstellen. Wir definieren ein globales Indikator-Handle „maHandle“, initialisieren es mit INVALID_HANDLE und richten Nutzereingaben für Handels- und Indikatoreinstellungen ein - wie „LotSize“, „BreakoutOffsetPips“ und „BoxTimeframe“ (die den Typ ENUM_TIMEFRAMES verwenden), sowie die Parameter für den gleitenden Durchschnitt („MA_Period“, „MA_Method“, „MA_AppliedPrice“) und das Risikomanagement („RiskToReward“, „MagicNumber“).

    Darüber hinaus können die Nutzer den Sitzungszeitpunkt in Stunden und Minuten angeben (mit Eingaben wie „SessionStartHour“, „SessionStartMinute“, „SessionEndHour“, „SessionEndMinute“, „TradeExitHour“ und „TradeExitMinute“) und deklarieren globale Variablen, um die Session-Box-Daten („BoxHigh“, „BoxLow“) und die genauen Zeiten, zu denen diese Extrema auftraten („BoxHighTime“, „BoxLowTime“), zusammen mit Flags („boxCalculated“ und „ordersPlaced“) zu speichern, um die Programmlogik zu steuern. Als Nächstes gehen wir zu OnInit und initialisieren das Handle.

    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit(){
       //--- Set the magic number for all trade operations
       obj_Trade.SetExpertMagicNumber(MagicNumber);           //--- Set magic number globally for trades
       //--- Create the Moving Average handle with user-defined parameters
       maHandle = iMA(_Symbol, 0, MA_Period, 0, MA_Method, MA_AppliedPrice); //--- Create MA handle
       if(maHandle == INVALID_HANDLE){                     //--- Check if MA handle creation failed
          Print("Failed to create MA handle.");           //--- Print error message
          return(INIT_FAILED);                             //--- Terminate initialization if error occurs
       }
       return(INIT_SUCCEEDED);                            //--- Return successful initialization
    }

    In OnInit wird die magische Zahl des Handelsobjekts durch den Aufruf der Methode „obj_Trade.SetExpertMagicNumber(MagicNumber)“ festgelegt, um sicherzustellen, dass alle Handelsgeschäfte eindeutig identifiziert werden. Als Nächstes erstellen wir für den gleitenden Durchschnitt das Handle unter Verwendung von iMA und unseren nutzerdefinierten Parametern („MA_Period“, „MA_Method“ und „MA_AppliedPrice“). Anschließend wird überprüft, ob das Handle erfolgreich erstellt wurde, indem geprüft wird, ob „maHandle“ gleich INVALID_HANDLE ist; ist dies der Fall, wird eine Fehlermeldung ausgegeben und INIT_FAILED zurückgegeben, andernfalls wird INIT_SUCCEEDED zurückgegeben, um eine erfolgreiche Initialisierung zu signalisieren. Als Nächstes müssen wir das erstellte Handle freigeben, um Ressourcen zu sparen, wenn das Programm nicht nutzt wird.

    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason){
       //--- Release the MA handle if valid
       if(maHandle != INVALID_HANDLE)                     //--- Check if MA handle exists
          IndicatorRelease(maHandle);                     //--- Release the MA handle
       //--- Drawn objects remain on the chart for historical reference
    }

    In der Funktion OnDeinit wird geprüft, ob das Handle „maHandle“ des gleitenden Durchschnitts gültig ist (d.h. nicht gleich INVALID_HANDLE). Wenn es gültig ist, geben wir das Handle frei, indem wir die Funktion IndicatorRelease aufrufen, um Ressourcen wieder freizugeben. Wir können nun zum Hauptereignisbehandlung OnTick übergehen, wo wir unsere gesamte Steuerungslogik implementieren.

    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick(){
       //--- Get the current server time (assumed GMT)
       datetime currentTime = TimeCurrent();              //--- Retrieve current time
       MqlDateTime dt;                                    //--- Declare a structure for time components
       TimeToStruct(currentTime, dt);                     //--- Convert current time to structure
       
       //--- Check if the current time is at or past the session end (using hour and minute)
       if(dt.hour > SessionEndHour || (dt.hour == SessionEndHour && dt.min >= SessionEndMinute)){
          //--- Build the session end time using today's date and user-defined session end time
          MqlDateTime sesEnd;                             //--- Declare a structure for session end time
          sesEnd.year = dt.year;                          //--- Set year
          sesEnd.mon  = dt.mon;                           //--- Set month
          sesEnd.day  = dt.day;                           //--- Set day
          sesEnd.hour = SessionEndHour;                   //--- Set session end hour
          sesEnd.min  = SessionEndMinute;                 //--- Set session end minute
          sesEnd.sec  = 0;                                //--- Set seconds to 0
          datetime sessionEnd = StructToTime(sesEnd);       //--- Convert structure to datetime
          
          //--- Determine the session start time
          datetime sessionStart;                          //--- Declare variable for session start time
          //--- If session start is later than or equal to session end, assume overnight session
          if(SessionStartHour > SessionEndHour || (SessionStartHour == SessionEndHour && SessionStartMinute >= SessionEndMinute)){
             datetime prevDay = sessionEnd - 86400;       //--- Subtract 24 hours to get previous day
             MqlDateTime dtPrev;                          //--- Declare structure for previous day time
             TimeToStruct(prevDay, dtPrev);               //--- Convert previous day time to structure
             dtPrev.hour = SessionStartHour;              //--- Set session start hour for previous day
             dtPrev.min  = SessionStartMinute;            //--- Set session start minute for previous day
             dtPrev.sec  = 0;                             //--- Set seconds to 0
             sessionStart = StructToTime(dtPrev);         //--- Convert structure back to datetime
          }
          else{
             //--- Otherwise, use today's date for session start
             MqlDateTime temp;                           //--- Declare temporary structure
             temp.year = sesEnd.year;                    //--- Set year from session end structure
             temp.mon  = sesEnd.mon;                     //--- Set month from session end structure
             temp.day  = sesEnd.day;                     //--- Set day from session end structure
             temp.hour = SessionStartHour;               //--- Set session start hour
             temp.min  = SessionStartMinute;             //--- Set session start minute
             temp.sec  = 0;                              //--- Set seconds to 0
             sessionStart = StructToTime(temp);          //--- Convert structure to datetime
          }
          
          //--- Recalculate the session box only if this session hasn't been processed before
          if(sessionEnd != lastBoxSessionEnd){
             ComputeBox(sessionStart, sessionEnd);       //--- Compute session box using start and end times
             lastBoxSessionEnd = sessionEnd;              //--- Update last processed session end time
             boxCalculated   = true;                      //--- Set flag indicating the box has been calculated
             ordersPlaced    = false;                     //--- Reset flag for order placement for the new session
          }
       }
    }

    In der Funktion OnTick des Expert Advisors rufen wir zunächst TimeCurrent auf, um die aktuelle Serverzeit abzurufen, und konvertieren sie dann mit der Funktion TimeToStruct in eine MqlDateTime-Struktur, damit wir auf ihre Komponenten zugreifen können. Wir vergleichen die aktuelle Stunde und Minute mit den nutzerdefinierten „SessionEndHour“ und „SessionEndMinute“; wenn die aktuelle Zeit am oder nach dem Ende der Sitzung liegt, erstellen wir eine „sesEnd“-Struktur und konvertieren sie mit StructToTime in eine Datetime.

    Je nachdem, ob der Sitzungsbeginn vor oder nach dem Sitzungsende liegt, bestimmen wir die richtige „sessionStart“-Zeit (unter Verwendung des heutigen Datums oder durch Anpassung für eine Nachtsitzung), und wenn sich dieses „sessionEnd“ von „lastBoxSessionEnd“ abweicht, rufen wir die Funktion „ComputeBox“ auf, um die Sitzungsbox neu zu berechnen und gleichzeitig „lastBoxSessionEnd“ zu aktualisieren und unsere Flags „boxCalculated“ und „ordersPlaced“ zurückzusetzen. Wir verwenden eine nutzerdefinierte Funktion, um die Box-Eigenschaften zu berechnen, und hier ist ihr Code-Fragment.

    //+------------------------------------------------------------------+
    //| Function: ComputeBox                                             |
    //| Purpose: Calculate the session's highest high and lowest low, and|
    //|          record the times these extremes occurred, using the     |
    //|          specified session start and end times.                  |
    //+------------------------------------------------------------------+
    void ComputeBox(datetime sessionStart, datetime sessionEnd){
       int totalBars = Bars(_Symbol, BoxTimeframe);       //--- Get total number of bars on the specified timeframe
       if(totalBars <= 0){
          Print("No bars available on timeframe ", EnumToString(BoxTimeframe)); //--- Print error if no bars available
          return;                                        //--- Exit if no bars are found
       }
         
       MqlRates rates[];                                 //--- Declare an array to hold bar data
       ArraySetAsSeries(rates, false);                   //--- Set array to non-series order (oldest first)
       int copied = CopyRates(_Symbol, BoxTimeframe, 0, totalBars, rates); //--- Copy bar data into array
       if(copied <= 0){
          Print("Failed to copy rates for box calculation."); //--- Print error if copying fails
          return;                                        //--- Exit if error occurs
       }
         
       double highVal = -DBL_MAX;                        //--- Initialize high value to the lowest possible
       double lowVal  = DBL_MAX;                         //--- Initialize low value to the highest possible
       //--- Reset the times for the session extremes
       BoxHighTime = 0;                                  //--- Reset stored high time
       BoxLowTime  = 0;                                  //--- Reset stored low time
       
       //--- Loop through each bar within the session period to find the extremes
       for(int i = 0; i < copied; i++){
          if(rates[i].time >= sessionStart && rates[i].time <= sessionEnd){
             if(rates[i].high > highVal){
                highVal = rates[i].high;                //--- Update highest price
                BoxHighTime = rates[i].time;            //--- Record time of highest price
             }
             if(rates[i].low < lowVal){
                lowVal = rates[i].low;                  //--- Update lowest price
                BoxLowTime = rates[i].time;             //--- Record time of lowest price
             }
          }
       }
       if(highVal == -DBL_MAX || lowVal == DBL_MAX){
          Print("No valid bars found within the session time range."); //--- Print error if no valid bars found
          return;                                        //--- Exit if invalid data
       }
       BoxHigh = highVal;                                //--- Store final highest price
       BoxLow  = lowVal;                                 //--- Store final lowest price
       Print("Session box computed: High = ", BoxHigh, " at ", TimeToString(BoxHighTime),
             ", Low = ", BoxLow, " at ", TimeToString(BoxLowTime)); //--- Output computed session box data
       
       //--- Draw all session objects (rectangle, horizontal lines, and price labels)
       DrawSessionObjects(sessionStart, sessionEnd);    //--- Call function to draw objects using computed values
    }

    Hier definieren wir eine Funktion „ComputeBox“ ohne Rückgabewert zur Berechnung der Sitzungsextreme. Zunächst wird mit der Funktion Bars die Gesamtzahl der Balken des angegebenen Zeitrahmens ermittelt. Anschließend werden die Balkendaten mit der Funktion CopyRates in ein MqlRates-Array kopiert. Wir initialisieren die Variable „highVal“ mit -DBL_MAX und „lowVal“ mit DBL_MAX, um sicherzustellen, dass jeder gültige Preis diese Extremwerte aktualisiert. Wenn wir in einer Schleife jeden Balken durchgehen, der in den Sitzungszeitraum fällt, aktualisieren wir „highVal“, wenn der „high“ eines Balkens „highVal“ übersteigt, und zeichnen die Zeit dieses Balkens in „BoxHighTime“ auf.Wenn der Tiefstwert eines Balkens unter „lowVal“ liegt, wird „lowVal“ aktualisiert und die Zeit in „BoxLowTime“ aufgezeichnet.

    Wenn nach der Verarbeitung der Daten „highVal“ weiterhin „-DBL_MAX“ oder „lowVal“ immer noch DBL_MAX ist, geben wir eine Fehlermeldung aus, die besagt, dass keine gültigen Balken gefunden wurden; andernfalls weisen wir „BoxHigh“ und „BoxLow“ die berechneten Werte zu und verwenden die Funktion TimeToString, um die aufgezeichneten Zeiten in einem lesbaren Format auszudrucken. Zum Schluss rufen wir die Funktion „DrawSessionObjects“ mit den Start- und Endzeiten der Sitzung auf, um die Sitzungsbox und die zugehörigen Objekte im Chart visuell darzustellen. Die Funktion ist wie folgt implementiert.

    //+----------------------------------------------------------------------+
    //| Function: DrawSessionObjects                                         |
    //| Purpose: Draw a filled rectangle spanning from the session's high    |
    //|          point to its low point (using exact times), then draw       |
    //|          horizontal lines at the high and low (from sessionStart to  |
    //|          sessionEnd) with price labels at the right. Dynamic styling |
    //|          for font size and line width is based on the current chart  |
    //|          scale.                                                      |
    //+----------------------------------------------------------------------+
    void DrawSessionObjects(datetime sessionStart, datetime sessionEnd){
       int chartScale = (int)ChartGetInteger(0, CHART_SCALE, 0); //--- Retrieve the chart scale (0 to 5)
       int dynamicFontSize = 7 + chartScale * 1;        //--- Base 7, increase by 2 per scale level
       int dynamicLineWidth = (int)MathRound(1 + (chartScale * 2.0 / 5)); //--- Linear interpolation
       
       //--- Create a unique session identifier using the session end time
       string sessionID = "Sess_" + IntegerToString(lastBoxSessionEnd);
       
       //--- Draw the filled rectangle (box) using the recorded high/low times and prices
       string rectName = "SessionRect_" + sessionID;       //--- Unique name for the rectangle
       if(!ObjectCreate(0, rectName, OBJ_RECTANGLE, 0, BoxHighTime, BoxHigh, BoxLowTime, BoxLow))
          Print("Failed to create rectangle: ", rectName); //--- Print error if creation fails
       ObjectSetInteger(0, rectName, OBJPROP_COLOR, clrThistle); //--- Set rectangle color to blue
       ObjectSetInteger(0, rectName, OBJPROP_FILL, true);       //--- Enable filling of the rectangle
       ObjectSetInteger(0, rectName, OBJPROP_BACK, true);       //--- Draw rectangle in background
       
       //--- Draw the top horizontal line spanning from sessionStart to sessionEnd at the session high
       string topLineName = "SessionTopLine_" + sessionID; //--- Unique name for the top line
       if(!ObjectCreate(0, topLineName, OBJ_TREND, 0, sessionStart, BoxHigh, sessionEnd, BoxHigh))
          Print("Failed to create top line: ", topLineName); //--- Print error if creation fails
       ObjectSetInteger(0, topLineName, OBJPROP_COLOR, clrBlue); //--- Set line color to blue
       ObjectSetInteger(0, topLineName, OBJPROP_WIDTH, dynamicLineWidth); //--- Set line width dynamically
       ObjectSetInteger(0, topLineName, OBJPROP_RAY_RIGHT, false); //--- Do not extend line infinitely
       
       //--- Draw the bottom horizontal line spanning from sessionStart to sessionEnd at the session low
       string bottomLineName = "SessionBottomLine_" + sessionID; //--- Unique name for the bottom line
       if(!ObjectCreate(0, bottomLineName, OBJ_TREND, 0, sessionStart, BoxLow, sessionEnd, BoxLow))
          Print("Failed to create bottom line: ", bottomLineName); //--- Print error if creation fails
       ObjectSetInteger(0, bottomLineName, OBJPROP_COLOR, clrRed); //--- Set line color to blue
       ObjectSetInteger(0, bottomLineName, OBJPROP_WIDTH, dynamicLineWidth); //--- Set line width dynamically
       ObjectSetInteger(0, bottomLineName, OBJPROP_RAY_RIGHT, false); //--- Do not extend line infinitely
       
       //--- Create the top price label at the right edge of the top horizontal line
       string topLabelName = "SessionTopLabel_" + sessionID; //--- Unique name for the top label
       if(!ObjectCreate(0, topLabelName, OBJ_TEXT, 0, sessionEnd, BoxHigh))
          Print("Failed to create top label: ", topLabelName); //--- Print error if creation fails
       ObjectSetString(0, topLabelName, OBJPROP_TEXT," "+DoubleToString(BoxHigh, _Digits)); //--- Set label text to session high price
       ObjectSetInteger(0, topLabelName, OBJPROP_COLOR, clrBlack); //--- Set label color to blue
       ObjectSetInteger(0, topLabelName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Set dynamic font size for label
       ObjectSetInteger(0, topLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Anchor label to the left so text appears to right
       
       //--- Create the bottom price label at the right edge of the bottom horizontal line
       string bottomLabelName = "SessionBottomLabel_" + sessionID; //--- Unique name for the bottom label
       if(!ObjectCreate(0, bottomLabelName, OBJ_TEXT, 0, sessionEnd, BoxLow))
          Print("Failed to create bottom label: ", bottomLabelName); //--- Print error if creation fails
       ObjectSetString(0, bottomLabelName, OBJPROP_TEXT," "+DoubleToString(BoxLow, _Digits)); //--- Set label text to session low price
       ObjectSetInteger(0, bottomLabelName, OBJPROP_COLOR, clrBlack); //--- Set label color to blue
       ObjectSetInteger(0, bottomLabelName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Set dynamic font size for label
       ObjectSetInteger(0, bottomLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Anchor label to the left so text appears to right
    }

    In der Funktion „DrawSessionObjects“ wird zunächst die aktuelle Chartskala von der Funktion ChartGetInteger mit CHART_SCALE abgerufen (die einen Wert von 0 bis 5 zurückgibt) und dann die dynamischen Styling-Parameter berechnet: eine dynamische Schriftgröße, die als „7 + chartScale * 1“ berechnet wird (mit einer Basisgröße von 7, die sich pro Skalierungsstufe um 1 erhöht), und eine dynamische Linienbreite, die mit MathRound linear interpoliert wird, sodass bei einer Chartskala von 5 die Breite 3 beträgt. Als Nächstes erstellen wir einen eindeutigen Sitzungsbezeichner, indem wir „lastBoxSessionEnd“ in eine Zeichenkette mit vorangestelltem „Sess_“ umwandeln, wodurch sichergestellt wird, dass die Objekte jeder Sitzung eindeutige Namen haben. Dann zeichnen wir ein ausgefülltes Rechteck mit ObjectCreate, wobei wir den Typ OBJ_RECTANGLE mit den genauen Zeiten und Preisen des Hochs („BoxHighTime“, „BoxHigh“) und des Tiefs („BoxLowTime“, „BoxLow“). Seine Farbe setzen wir auf „clrThistle“, aktivieren seine Füllung mit OBJPROP_FILL und platzieren es mit OBJPROP_BACK im Hintergrund.

    Anschließend zeichnen wir zwei horizontale Trendlinien - eine am Sitzungshoch und eine am Sitzungstief, die sich von „sessionStart“ bis „sessionEnd“ erstrecken; wir setzen die Farbe der oberen Linie auf „clrBlue“ und die Farbe der unteren Linie auf „clrRed“.Wir setzen die Farbe der oberen Linie auf „clrBlue“ und die Farbe der unteren Linie auf „clrRed“, und beide Linien verwenden die dynamische Linienbreite und werden nicht unendlich verlängert („OBJPROP_RAY_RIGHT“ ist auf false gesetzt). Schließlich erstellen wir Textobjekte für die oberen und unteren Preisekennzeichnungen am rechten Rand (bei „sessionEnd“) und setzen ihren Text auf den Höchst- und Tiefstwert der Sitzung (formatiert mit DoubleToString unter Verwendung der Dezimalen des Symbols, _Digits), wobei ihre Farbe auf „clrBlack“ gesetzt und die dynamische Schriftgröße angewendet wird, und wir verankern sie links, damit der Text rechts vom Anker erscheint. Nach der Kompilierung erhalten wir das folgende Ergebnis.

    IDENTIFIKATION DER ASIATISCHE BOX

    Anhand des Bildes können wir sehen, dass wir die Box identifizieren und in das Chart einzeichnen können. Wir können nun damit fortfahren, die ausstehenden Aufträge in der Nähe der identifizierten Bereichsgrenzen zu eröffnen. Um dies zu erreichen, gehen wir nach der folgenden Logik vor.

    //--- Build the trade exit time using user-defined hour and minute for today
    MqlDateTime exitTimeStruct;                        //--- Declare a structure for exit time
    TimeToStruct(currentTime, exitTimeStruct);         //--- Use current time's date components
    exitTimeStruct.hour = TradeExitHour;               //--- Set trade exit hour
    exitTimeStruct.min  = TradeExitMinute;             //--- Set trade exit minute
    exitTimeStruct.sec  = 0;                           //--- Set seconds to 0
    datetime tradeExitTime = StructToTime(exitTimeStruct); //--- Convert exit time structure to datetime
    
    //--- If the session box is calculated, orders are not placed yet, and current time is before trade exit time, place orders
    if(boxCalculated && !ordersPlaced && currentTime < tradeExitTime){
       double maBuffer[];                           //--- Declare array to hold MA values
       ArraySetAsSeries(maBuffer, true);            //--- Set the array as series (newest first)
       if(CopyBuffer(maHandle, 0, 0, 1, maBuffer) <= 0){  //--- Copy 1 value from the MA buffer
          Print("Failed to copy MA buffer.");       //--- Print error if buffer copy fails
          return;                                   //--- Exit the function if error occurs
       }
       double maValue = maBuffer[0];                 //--- Retrieve the current MA value
       
       double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get current bid price
       bool bullish = (currentPrice > maValue);      //--- Determine bullish condition
       bool bearish = (currentPrice < maValue);       //--- Determine bearish condition
       
       double offsetPrice = BreakoutOffsetPips * _Point; //--- Convert pips to price units
       
       //--- If bullish, place a Buy Stop order
       if(bullish){
          double entryPrice = BoxHigh + offsetPrice; //--- Set entry price just above the session high
          double stopLoss   = BoxLow - offsetPrice;    //--- Set stop loss below the session low
          double risk       = entryPrice - stopLoss;     //--- Calculate risk per unit
          double takeProfit = entryPrice + risk * RiskToReward; //--- Calculate take profit using risk/reward ratio
          if(obj_Trade.BuyStop(LotSize, entryPrice, _Symbol, stopLoss, takeProfit, ORDER_TIME_GTC, 0, "Asian Breakout EA")){
             Print("Placed Buy Stop order at ", entryPrice); //--- Print order confirmation
             ordersPlaced = true;                        //--- Set flag indicating an order has been placed
          }
          else{
             Print("Buy Stop order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Print error if order fails
          }
       }
       //--- If bearish, place a Sell Stop order
       else if(bearish){
          double entryPrice = BoxLow - offsetPrice;  //--- Set entry price just below the session low
          double stopLoss   = BoxHigh + offsetPrice;   //--- Set stop loss above the session high
          double risk       = stopLoss - entryPrice;    //--- Calculate risk per unit
          double takeProfit = entryPrice - risk * RiskToReward; //--- Calculate take profit using risk/reward ratio
          if(obj_Trade.SellStop(LotSize, entryPrice, _Symbol, stopLoss, takeProfit, ORDER_TIME_GTC, 0, "Asian Breakout EA")){
             Print("Placed Sell Stop order at ", entryPrice); //--- Print order confirmation
             ordersPlaced = true;                       //--- Set flag indicating an order has been placed
          }
          else{
             Print("Sell Stop order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Print error if order fails
          }
       }
    }

    Hier erstellen wir den Zeitpunkt des Ausstiegs aus dem Handel, indem wir eine MqlDateTime-Struktur namens „exitTimeStruct“ deklarieren. Anschließend zerlegen wir mit der Funktion TimeToStruct die aktuelle Zeit in ihre Bestandteile und weisen „exitTimeStruct“ die nutzerdefinierten „TradeExitHour“ und „TradeExitMinute“ (mit Sekunden auf 0 gesetzt) zu. Anschließend wird diese Struktur durch Aufruf der Funktion StructToTime wieder in einen Datumswert umgewandelt, was zu „tradeExitTime“ führt. Danach, wenn die Session Box berechnet wurde, keine Aufträge erteilt wurden und die aktuelle Zeit vor der „tradeExitTime“ liegt, werden die Aufträge erteilt.

    Wir deklarieren ein Array „maBuffer“, um gleitende Durchschnittswerte zu speichern, und rufen die Funktion ArraySetAsSeries auf, um sicherzustellen, dass das Array mit den neuesten Daten zuerst indiziert wird. Dann verwenden wir die Funktion CopyBuffer, um den letzten Wert des gleitenden Durchschnittsindikators (mit „maHandle“) in „maBuffer“ abzurufen. Wir vergleichen diesen gleitenden Durchschnittswert mit dem aktuellen Geldkurs (den wir über die Funktion SymbolInfoDouble erhalten), um festzustellen, ob der Markt steigt oder fällt. Basierend auf dieser Bedingung berechnen wir mit dem Parameter „BreakoutOffsetPips“ den passenden Einstiegskurs, Stop Loss und Take Profit und platzieren dann entweder eine Buy Stop Order mit der Methode „obj_Trade.BuyStop“ oder eine Sell Stop Order mit der Methode „obj_Trade.SellStop“.

    Schließlich drucken wir eine Bestätigungsmeldung, wenn die Bestellung erfolgreich platziert wurde, oder eine Fehlermeldung, wenn sie fehlgeschlagen ist, und setzen das Flag „ordersPlaced“ entsprechend. Wenn wir das Programm ausführen, erhalten wir das folgende Ergebnis.

    BESTÄTIGUNG DES SCHWEBENDEN AUFTRAGS

    Aus der Funktion können wir sehen, dass wir, sobald wir einen Ausbruch haben, der schwebende Auftrag in Bezug auf die Richtung des gleitenden Durchschnittsfilters platzieren, ebenso wie die Stop-Orders. Das Einzige, was bleibt, ist das Schließen der Positionen oder das Löschen der schwebenden Aufträge, sobald die Handelszeit nicht mehr innerhalb der Handelszeit liegt.

    //--- If current time is at or past trade exit time, close positions and cancel pending orders
    if(currentTime >= tradeExitTime){
       CloseOpenPositions();                          //--- Close all open positions for this EA
       CancelPendingOrders();                         //--- Cancel all pending orders for this EA
       boxCalculated = false;                         //--- Reset session box calculated flag
       ordersPlaced  = false;                         //--- Reset order placed flag
    }
    

    Hier wird geprüft, ob die aktuelle Zeit den Zeitpunkt der Beendigung des Geschäfts erreicht oder überschritten hat. Wenn dies der Fall ist, rufen wir die Funktion „CloseOpenPositions“ auf, um alle mit dem EA verbundenen offenen Positionen zu schließen, und rufen dann die Funktion „CancelPendingOrders“ auf, um alle ausstehenden Aufträge zu stornieren. Nachdem diese Funktionen ausgeführt wurden, setzen wir die Flags „boxCalculated“ und „ordersPlaced“ auf false zurück und bereiten das Programm auf eine neue Sitzung vor. Die nutzerdefinierten Funktionen, die wir verwenden, sind wie folgt.

    //+------------------------------------------------------------------+
    //| Function: CloseOpenPositions                                     |
    //| Purpose: Close all open positions with the set magic number      |
    //+------------------------------------------------------------------+
    void CloseOpenPositions(){
       int totalPositions = PositionsTotal();           //--- Get total number of open positions
       for(int i = totalPositions - 1; i >= 0; i--){      //--- Loop through positions in reverse order
          ulong ticket = PositionGetTicket(i);           //--- Get ticket number for each position
          if(PositionSelectByTicket(ticket)){            //--- Select position by ticket
             if(PositionGetInteger(POSITION_MAGIC) == MagicNumber){ //--- Check if position belongs to this EA
                if(!obj_Trade.PositionClose(ticket))        //--- Attempt to close position
                  Print("Failed to close position ", ticket, ": ", obj_Trade.ResultRetcodeDescription()); //--- Print error if closing fails
                else
                  Print("Closed position ", ticket);    //--- Confirm position closed
             }
          }
       }
    }
      
    //+------------------------------------------------------------------+
    //| Function: CancelPendingOrders                                    |
    //| Purpose: Cancel all pending orders with the set magic number     |
    //+------------------------------------------------------------------+
    void CancelPendingOrders(){
       int totalOrders = OrdersTotal();                 //--- Get total number of pending orders
       for(int i = totalOrders - 1; i >= 0; i--){         //--- Loop through orders in reverse order
          ulong ticket = OrderGetTicket(i);              //--- Get ticket number for each order
          if(OrderSelect(ticket)){                       //--- Select order by ticket
             int type = (int)OrderGetInteger(ORDER_TYPE); //--- Retrieve order type
             if(OrderGetInteger(ORDER_MAGIC) == MagicNumber && //--- Check if order belongs to this EA
                (type == ORDER_TYPE_BUY_STOP || type == ORDER_TYPE_SELL_STOP)){
                if(!obj_Trade.OrderDelete(ticket))         //--- Attempt to delete pending order
                  Print("Failed to cancel pending order ", ticket); //--- Print error if deletion fails
                else
                  Print("Canceled pending order ", ticket); //--- Confirm pending order canceled
             }
          }
       }
    }
    

    Hier wird in der Funktion „CloseOpenPositions“ zunächst die Gesamtzahl der offenen Positionen mit der Funktion PositionsTotal ermittelt und dann eine Schleife durch jede Position in umgekehrter Reihenfolge gebildet. Für jede Position erhalten wir die Ticketnummer mit PositionGetTicket und wählen die Position mit PositionSelectByTicket aus. Wir prüfen dann, ob der Wert von POSITION_MAGIC der Position mit unserer nutzerdefinierten „MagicNumber“ übereinstimmt, um sicherzustellen, dass sie zu unserem EA gehört. Wenn dies der Fall ist, versuchen wir, die Position mit der Funktion „obj_Trade.PositionClose“ zu schließen und drucken je nach Ergebnis eine Bestätigungs- oder eine Fehlermeldung (mit „obj_Trade.ResultRetcodeDescription“).

    In der Funktion „CancelPendingOrders“ wird zunächst die Gesamtzahl der ausstehenden Aufträge mit der Funktion OrdersTotal ermittelt und in umgekehrter Reihenfolge durchlaufen. Für jeden Auftrag erhalten wir mit OrderGetTicket das Ticket und wählen es mit OrderSelect aus. Dann prüfen wir, ob ORDER_MAGIC des Auftrags mit unserer „MagicNumber“ übereinstimmt und ob sein Typ entweder „ORDER_TYPE_BUY_STOP“ oder ORDER_TYPE_SELL_STOP ist. Wenn beide Bedingungen erfüllt sind, versuchen wir, den Auftrag mit der Funktion „obj_Trade.OrderDelete“ zu stornieren, wobei je nach Erfolg entweder eine Erfolgs- oder eine Fehlermeldung ausgegeben wird. Wenn wir das Programm ausführen, erhalten wir folgende Ergebnisse.

    GIF DER STRATEGIE

    Anhand der Visualisierung können wir sehen, dass wir die asiatische Sitzung identifizieren, sie auf dem Chart darstellen, schwebende Aufträge in Bezug auf die Richtung des gleitenden Durchschnitts platzieren und die Aufträge oder aktivierten Positionen stornieren, wenn sie noch bestehen, sobald wir die vom Nutzer definierte Handelszeit überschritten haben, und somit unser Ziel erreichen. Bleiben nur noch die Backtests des Programms, und das wird im nächsten Abschnitt behandelt.


    Backtest und Optimierung

    Nach einem gründlichen Backtests über ein Jahr, 2023, mit den Standardeinstellungen haben wir die folgenden Ergebnisse.

    Backtest-Grafik:

    GRAFIK 1

    Aus dem Bild können wir ersehen, dass die Grafik recht gut ist, aber wir können sie verbessern, indem wir einen Trailing-Stop-Mechanismus anwenden, und wir haben dies mit der folgenden Logik erreicht.

    //+------------------------------------------------------------------+
    //|        FUNCTION TO APPLY TRAILING STOP                           |
    //+------------------------------------------------------------------+
    void applyTrailingSTOP(double slPoints, CTrade &trade_object,int magicNo=0){
       double buySL = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID)-slPoints,_Digits); //--- Calculate SL for buy positions
       double sellSL = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK)+slPoints,_Digits); //--- Calculate SL for sell positions
    
       for (int i = PositionsTotal() - 1; i >= 0; i--){ //--- Iterate through all open positions
          ulong ticket = PositionGetTicket(i);          //--- Get position ticket
          if (ticket > 0){                              //--- If ticket is valid
             if (PositionGetString(POSITION_SYMBOL) == _Symbol &&
                (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)){ //--- Check symbol and magic number
                if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY &&
                   buySL > PositionGetDouble(POSITION_PRICE_OPEN) &&
                   (buySL > PositionGetDouble(POSITION_SL) ||
                   PositionGetDouble(POSITION_SL) == 0)){ //--- Modify SL for buy position if conditions are met
                   trade_object.PositionModify(ticket,buySL,PositionGetDouble(POSITION_TP));
                }
                else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL &&
                   sellSL < PositionGetDouble(POSITION_PRICE_OPEN) &&
                   (sellSL < PositionGetDouble(POSITION_SL) ||
                   PositionGetDouble(POSITION_SL) == 0)){ //--- Modify SL for sell position if conditions are met
                   trade_object.PositionModify(ticket,sellSL,PositionGetDouble(POSITION_TP));
                }
             }
          }
       }
    }
    
    //---- CALL THE FUNCTION IN THE TICK EVENT HANDLER
    
    if (PositionsTotal() > 0){                       //--- If there are open positions
       applyTrailingSTOP(30*_Point,obj_Trade,0);  //--- Apply a trailing stop
    }
    

    Nach der Anwendung der Funktion und dem Test sind die neuen Ergebnisse wie folgt.

    Backtest-Grafik:

    BACKTEST-GRAFIK

    Backtest-Bericht:

    BACKTEST-ERGEBNISSE


    Schlussfolgerung

    Zusammenfassend lässt sich sagen, dass wir erfolgreich einen MQL5 Expert Advisor entwickelt haben, der die Asian Breakout Strategy mit Präzision automatisiert. Durch die Nutzung von sitzungsbasierter Bereichserkennung, Trendfilterung über einen gleitenden Durchschnitt und dynamisches Risikomanagement haben wir ein System entwickelt, das wichtige Konsolidierungszonen identifiziert und Ausbruchsgeschäfte effizient ausführt.

    Haftungsausschluss: Dieser Artikel ist nur für Bildungszwecke gedacht. Der Handel ist mit erheblichen finanziellen Risiken verbunden, und die Marktbedingungen können unvorhersehbar sein. Obwohl die skizzierte Strategie einen strukturierten Ansatz für den Breakout-Handel bietet, ist sie keine Garantie für Rentabilität. Umfassende Backtests und ein angemessenes Risikomanagement sind unerlässlich, bevor dieses Programm in einer Live-Umgebung eingesetzt wird.

    Durch die Anwendung dieser Techniken können Sie Ihre algorithmischen Handelsfähigkeiten verbessern, Ihre technischen Analysefähigkeiten verfeinern und Ihre Handelsstrategie weiterentwickeln. Viel Glück auf Ihrer Handelsreise!

    Übersetzt aus dem Englischen von MetaQuotes Ltd.
    Originalartikel: https://www.mql5.com/en/articles/17239

    Beigefügte Dateien |
    MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 55): SAC mit priorisierter Erfahrungswiederholung MQL5-Assistenten-Techniken, die Sie kennen sollten (Teil 55): SAC mit priorisierter Erfahrungswiederholung
    Replay-Puffer sind beim Reinforcement Learning besonders wichtig bei Off-Policy-Algorithmen wie DQN oder SAC. Damit wird das Sampling-Verfahren dieses Speicherpuffers in den Mittelpunkt gerückt. Während bei den Standardoptionen von SAC beispielsweise eine zufällige Auswahl aus diesem Puffer verwendet wird, wird bei den priorisierten Erfahrungswiederholungspuffern eine Feinabstimmung vorgenommen, indem eine Auswahl aus dem Puffer auf der Grundlage eines TD-Scores erfolgt. Wir gehen auf die Bedeutung des Reinforcement Learning ein und untersuchen wie immer nur diese Hypothese (nicht die Kreuzvalidierung) in einem von einem Assistenten zusammengestellten Expert Advisor.
    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
    Schließen Sie sich unserer heutigen Diskussion an, wenn wir nach einem algorithmischen Verfahren suchen, mit dem wir die Gesamtzahl der Ausstiege aus Gewinngeschäften minimieren können. Das Problem, mit dem wir konfrontiert waren, ist sehr schwierig, und die meisten Lösungen, die in den Diskussionen in der Gemeinschaft genannt wurden, haben keine festen Regeln. Unser algorithmischer Ansatz zur Lösung des Problems erhöhte die Rentabilität unserer Handelsgeschäft und reduzierte den durchschnittlichen Verlust pro Handelsgeschäft. Es müssen jedoch noch weitere Fortschritte gemacht werden, um alle Handelsgeschäfte, die ausgestoppt werden, vollständig herauszufiltern, aber unsere Lösung ist ein guter erster Schritt, den jeder ausprobieren kann.
    Automatisieren von Handelsstrategien in MQL5 (Teil 10): Entwicklung der Strategie Trend Flat Momentum Automatisieren von Handelsstrategien in MQL5 (Teil 10): Entwicklung der Strategie Trend Flat Momentum
    In diesem Artikel entwickeln wir einen Expert Advisor in MQL5 für die Strategie Trend Flat Momentum. Wir kombinieren das Kreuzen zweier gleitender Durchschnitte, gefiltert mit dem Momentum von RSI und CCI, um Handelssignale zu generieren. Wir befassen uns auch mit Backtests und möglichen Verbesserungen für die reale Leistung.
    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.