English Русский 中文 Español 日本語 Português
Entwicklung eines plattformübergreifenden Grid-EAs

Entwicklung eines plattformübergreifenden Grid-EAs

MetaTrader 5Handelssysteme | 15 Mai 2019, 10:49
833 0
Roman Klymenko
Roman Klymenko

Einführung

Die häufigsten Nutzer dieser Website wissen ziemlich genau, dass MQL5 die beste Option für die Entwicklung von kundenspezifischen EAs ist. Leider erlauben nicht alle Broker die Erstellung von Konten, die in MetaTrader 5 verfügbar sind. Selbst wenn Sie derzeit mit einem Broker zusammenarbeiten, der dies zulässt, könnten Sie in Zukunft zu einem Broker wechseln, der nur MetaTrader 4 anbietet. Was werden Sie in diesem Fall mit all den MQL5 EAs machen, die Sie entwickelt haben? Werden Sie viel Zeit damit verbringen, sie zu überarbeiten, um sie in MQL4 zu übersetzen? Vielleicht wäre es vernünftiger, ein EA zu entwickeln, das sowohl in MetaTrader 5 als auch in MetaTrader 4 arbeiten kann?

In diesem Artikel werden wir, ein solchen EA entwickeln und prüfen, ob ein Handelssystem, das auf einem Auftragsraster basiert, verwendbar ist.

Ein paar Worte zur bedingten Kompilierung

Das bedingte Kompilieren wird es uns ermöglichen, ein EA zu entwickeln, der sowohl in MetaTrader 4 als auch in MetaTrader 5 funktioniert. Die angewandte Syntax ist wie folgt:

   #ifdef __MQL5__ 
      // MQL5 Code
   #else 
      // MQL4 Code
   #endif 

Das bedingte Kompilieren erlaubt es uns, festzulegen, dass ein bestimmter Block nur dann kompiliert werden soll, wenn die Kompilierung in einem MQL5 EA durchgeführt wird. Beim Kompilieren in MQL4 und anderen Sprachversionen wird dieser Codeblock einfach ignoriert. Stattdessen wird der Codeblock nach dem Operator #else verwendet (falls vorhanden).

Wenn also einige Funktionen in MQL4 und MQL5 unterschiedlich implementiert sind, sollen wir sie für beide Situationen implementieren, während die bedingte Kompilierung die Auswahl der Option ermöglicht, die für eine bestimmte Sprache notwendig ist.

In anderen Fällen werden wir die Syntax verwenden, die sowohl in MQL4 als auch in MQL5 funktioniert.

Das Grid-Handelssystem

Bevor wir mit der EA-Entwicklung beginnen, beschreiben wir die Grundlagen der Grid-Handelsstrategien.

Raster-EAs oder Grider sind EAs, die mehrere Limit-Orders über dem aktuellen Preis und gleichzeitig die gleiche Anzahl von Limit-Orders unter ihm platzieren.

Limit-Orders werden in einer bestimmten Schrittweite und nicht auf einen einzigen Preis festgelegt. Das heißt, die erste Limit-Order wird in einem bestimmten Abstand über dem aktuellen Preis platziert. Die zweite Limit-Order wird über der ersten im gleichen Abstand eingestellt. Und so weiter. Die Anzahl der Orders und die verwendete Schrittweite variieren.

Orders in eine Richtung werden über dem aktuellen Preis platziert, während Orders in die andere Richtung unter dem aktuellen Preis platziert werden. Es wird davon ausgegangen, dass:

  • Während eines Trends Kaufaufträge über dem aktuellen Kurs platziert werden, während Verkaufsaufträge unter diesem Kurs platziert werden;
  • Während einer Seitwärtsbewegung, sollten die Verkaufsaufträge über dem aktuellen Preis platziert werden, während die Kaufaufträge unter diesem platziert werden.

Sie können entweder Stopps verwenden oder ohne sie arbeiten.

Wenn Sie den Verlust-Stop nicht nutzen und Gewinn erzielen, existieren alle offenen Positionen, sowohl profitable als auch verlustbringende, bis der Gesamtgewinn ein bestimmtes Niveau erreicht. Danach werden alle offenen Positionen sowie nicht vom Preis betroffenen Limit-Orders geschlossen und ein neues Raster gesetzt.

Der folgende Screenshot zeigt ein offenes Gitter:

Beispiel eines Grids

Theoretisch können Sie also mit Grid-Handelssystemen in jedem Markt einen Gewinn zu erzielen, ohne auf einzigartige Einstiegspunkte zu warten und ohne Indikatoren zu verwenden.

Wenn ein Verlust-Stop und ein Gewinnziel verwendet werden, dann wird der Gewinn dadurch erzielt, dass der Verlust auf einer Position durch den Gesamtgewinn auf der anderen Seite abgedeckt wird, wenn sich der Kurs in eine Richtung bewegt.

Ohne Stopps wird der Gewinn durch die Eröffnung einer größeren Anzahl von Orders in die richtige Richtung erzielt. Selbst wenn der Preis zuerst die Positionen in eine Richtung berührt und sich dann umkehrt, decken neue Positionen in die richtige Richtung den Verlust der zuvor geöffneten Positionen, da es am Ende mehr davon geben wird.

Die Arbeitsprinzipien unserer Grid-EAs

Wir haben oben das Funktionsprinzip des einfachsten Grid-EAs beschrieben. Sie können sich Ihre eigenen Optionen für Gitter ausdenken, die die Richtung der Eröffnung von Aufträgen ändern, die Möglichkeit hinzufügen, mehrere Aufträge zum gleichen Preis zu öffnen, Indikatoren hinzufügen, etc.

In diesem Artikel werden wir versuchen, die einfachste Grid-Version ohne Verlust-Stop zu implementieren, da die Idee, auf der sie basiert, sehr verlockend ist.

Tatsächlich erscheint die Vorstellung, dass der Preis früher oder später den Gewinn erreicht, wenn er sich in eine Richtung bewegt, auch wenn Positionen zunächst in die falsche Richtung eröffnet wurden, vernünftig. Angenommen, der Preis hat zu Beginn eine Korrektur erfahren und zwei Aufträge erreicht. Danach begann sich der Preis in die entgegengesetzte Richtung (Haupttrend) zu bewegen. In diesem Fall werden früher oder später mehr als zwei Aufträge in die richtige Richtung eröffnet, und unser Anfangsverlust wird sich nach einiger Zeit in einen Gewinn verwandeln.

Es scheint, dass der einzige Fall, in dem das Handelssystem einen Verlust verursachen kann, darin besteht, dass der Preis zuerst eine Order berührt, dann zurückkehrt und das Gegenteil berührt, dann wieder die Richtung ändert und eine andere Order berührt und seine Richtung immer wieder ändert, indem er immer weiter entfernte Orders berührt. Aber ist ein solches Preisverhalten unter realen Bedingungen überhaupt möglich?

Eine EA-Vorlage

Wir werden den EA aus der Vorlage entwickeln. So können wir sofort erkennen, welche Standard-MQL-Funktionen beteiligt sein sollen.

#property copyright "Klymenko Roman (needtome@icloud.com)"
#property link      "https://www.mql5.com/en/users/needtome"
#property version   "1.00"
#property strict

//+------------------------------------------------------------------+
//| Initialisierungsfunktion des Experten                            |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Deinitialisierungsfunktion des Experten                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
  }
//+------------------------------------------------------------------+
//| Experten Funktion OnTick                                         |
//+------------------------------------------------------------------+
void OnTick()
  {
  }

void OnChartEvent(const int id,         // Ereignis-ID   
                  const long& lparam,   // Ereignisparameter vom Typ long
                  const double& dparam, // Ereignisparameter vom Typ double
                  const string& sparam) // Ereignisparameter vom Typ string
   {
   }

Der einzige Unterschied zur Standardvorlage, die bei der Erstellung des EA mit dem MQL5-Assistenten generiert wurde, ist die Zeile #property strict. Wir fügen es so hinzu, damit der EA auch in MQL4 funktioniert.

Wir benötigen die Funktion OnChartEvent(), um auf das Anklicken der Schaltflächen reagieren zu können. Als Nächstes werden wir die Schaltfläche Close All implementieren, um alle Symbolpositionen und Aufträge manuell schließen zu können, wenn wir den gewünschten Wert erreicht haben oder einfach nur den EA stoppen wollen.

Funktion zum Öffnen von Positionen

Die wahrscheinlich wichtigste Funktion eines jeden EAs ist die Möglichkeit, einen Auftrag abzuschicken. Hier warten die ersten Herausforderungen auf uns. Bei MQL5 und MQL4 sind die Aufträge sehr unterschiedlich. Um diese Funktionen irgendwie zu vereinheitlichen, müssen wir eine eigene Funktion zur Auftragserteilung entwickeln.

Jede Order hat ihren eigenen Typ: Kaufauftrag, Verkaufsauftrag, Limit-Order Kauf oder -Verkauf. Die Variable, in der dieser Typ bei der Bestellung gesetzt wird, ist auch bei MQL5 und MQL4 unterschiedlich.

In MQL4 wird ein Auftragstyp durch eine Variable vom Typ int angegeben, während in MQL5 die Enumeration ENUM_ORDER_TYPE verwendet wird. Eine solche Enumeration gibt es in MQL4 nicht. Um beide Methoden zu kombinieren, sollten wir daher eine benutzerdefinierte Enumeration zur Festlegung einer Auftragsart erstellen. Aus diesem Grund wird die Funktion, die wir in Zukunft erstellen werden, nicht von der MQL-Version abhängen:

enum TypeOfPos{
   MY_BUY,
   MY_SELL,
   MY_BUYSTOP,
   MY_BUYLIMIT,
   MY_SELLSTOP,
   MY_SELLLIMIT,
}; 

Jetzt können wir eine benutzerdefinierte Funktion zur Auftragserteilung erstellen. Nennen wir sie pdxSendOrder(). Wir übergeben alles, was für die Auftragserteilung benötigt wird: Auftragsart, offener Preis, Stop-Loss (0, wenn nicht gesetzt), Profit-Target (0, wenn nicht gesetzt), Volumen, Ticket offener Positionen (wenn eine offene Position in MQL5 geändert werden soll), Kommentar und Symbol (wenn Sie eine Order für ein anderes als das aktuell geöffnete Symbol öffnen müssen):

// Funktion zum Senden der Aufträge
bool pdxSendOrder(TypeOfPos mytype, double price, double sl, double tp, double volume, ulong position=0, string comment="", string sym=""){
   // check passed values
   if( !StringLen(sym) ){
      sym=_Symbol;
   }
   int curDigits=(int) SymbolInfoInteger(sym, SYMBOL_DIGITS);
   if(sl>0){
      sl=NormalizeDouble(sl,curDigits);
   }
   if(tp>0){
      tp=NormalizeDouble(tp,curDigits);
   }
   if(price>0){
      price=NormalizeDouble(price,curDigits);
   }
   
   #ifdef __MQL5__ 
      ENUM_TRADE_REQUEST_ACTIONS action=TRADE_ACTION_DEAL;
      ENUM_ORDER_TYPE type=ORDER_TYPE_BUY;
      switch(mytype){
         case MY_BUY:
            action=TRADE_ACTION_DEAL;
            type=ORDER_TYPE_BUY;
            break;
         case MY_BUYSTOP:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_BUY_STOP;
            break;
         case MY_BUYLIMIT:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_BUY_LIMIT;
            break;
         case MY_SELL:
            action=TRADE_ACTION_DEAL;
            type=ORDER_TYPE_SELL;
            break;
         case MY_SELLSTOP:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_SELL_STOP;
            break;
         case MY_SELLLIMIT:
            action=TRADE_ACTION_PENDING;
            type=ORDER_TYPE_SELL_LIMIT;
            break;
      }
      
      MqlTradeRequest mrequest;
      MqlTradeResult mresult;
      ZeroMemory(mrequest);
      
      mrequest.action = action;
      mrequest.sl = sl;
      mrequest.tp = tp;
      mrequest.symbol = sym;
      if(position>0){
         mrequest.position = position;
      }
      if(StringLen(comment)){
         mrequest.comment=comment;
      }
      if(action!=TRADE_ACTION_SLTP){
         if(price>0){
            mrequest.price = price;
         }
         if(volume>0){
            mrequest.volume = volume;
         }
         mrequest.type = type;
         mrequest.magic = EA_Magic;
         switch(useORDER_FILLING_RETURN){
            case FOK:
               mrequest.type_filling = ORDER_FILLING_FOK;
               break;
            case RETURN:
               mrequest.type_filling = ORDER_FILLING_RETURN;
               break;
            case IOC:
               mrequest.type_filling = ORDER_FILLING_IOC;
               break;
         }
         mrequest.deviation=100;
      }
      if(OrderSend(mrequest,mresult)){
         if(mresult.retcode==10009 || mresult.retcode==10008){
            return true;
         }else{
            msgErr(GetLastError(), mresult.retcode);
         }
      }
   #else 
      int type=OP_BUY;
      switch(mytype){
         case MY_BUY:
            type=OP_BUY;
            break;
         case MY_BUYSTOP:
            type=OP_BUYSTOP;
            break;
         case MY_BUYLIMIT:
            type=OP_BUYLIMIT;
            break;
         case MY_SELL:
            type=OP_SELL;
            break;
         case MY_SELLSTOP:
            type=OP_SELLSTOP;
            break;
         case MY_SELLLIMIT:
            type=OP_SELLLIMIT;
            break;
      }
      if(OrderSend(sym, type, volume, price, 100, sl, tp, comment, EA_Magic, 0)<0){
         msgErr(GetLastError());
      }else{
         return true;
      }
   
   #endif 
   return false;
}

Überprüfen Sie zunächst die der Funktion übergebenen Werte und normalisieren Sie die Preise.

Eingabeparameter. Verwenden Sie anschließend die bedingte Kompilierung, um die aktuelle MQL-Version zu definieren und eine Reihenfolge nach deren Regeln festzulegen. Der zusätzliche Eingabeparameter useORDER_FILLING_RETURN wird für MQL5 verwendet. Mit seiner Hilfe konfigurieren wir den Ausführungsmodus der Aufträge entsprechend den vom Broker unterstützten Modi. Da der Eingabeparameter useORDER_FILLING_RETURN nur für die MQL5 EA notwendig ist, verwenden Sie die bedingte Kompilierung erneut, um sie hinzuzufügen:

#ifdef __MQL5__ 
   enum TypeOfFilling //Füll-Modus
     {
      FOK,//ORDER_FILLING_FOK
      RETURN,// ORDER_FILLING_RETURN
      IOC,//ORDER_FILLING_IOC
     }; 
   input TypeOfFilling  useORDER_FILLING_RETURN=FOK; //Füll-Modus
#endif 

Außerdem wird bei der Auftragserteilung der eingehende Parameter EA_Magic verwendet, der die Magicnummer des EA enthält.

Wenn dieser Parameter in den EA-Einstellungen nicht eingestellt ist, gelten alle Positionen auf einem Symbol, an dem das EA gestartet wurde, als Eigentum des EA. Somit übernimmt das EA die volle Kontrolle über sie.

Wenn die Magicnummer eingestellt ist, berücksichtigt der EA nur Positionen, die diese Magicnummer haben.

Fehler anzeigen. Wenn ein Auftrag erfolgreich erteilt wurde, wird true zurückgegeben. Andernfalls wird der entsprechende Fehlercode an die Funktion msgErr() zur weiteren Analyse und Anzeige einer verständlichen Fehlermeldung übergeben. Die Funktion zeigt eine lokalisierte Meldung mit einer detaillierten Fehlerbeschreibung an. Es hat keinen Sinn, hier seinen vollständigen Code aufzuführen. Deshalb werde ich nur einen Teil davon zeigen:

void msgErr(int err, int retcode=0){
   string curErr="";
   switch(err){
      case 1:
         curErr=langs.err1;
         break;
//      case N:
//         curErr=langs.errN;
//         break;
      default:
         curErr=langs.err0+": "+(string) err;
   }
   if(retcode>0){
      curErr+=" ";
      switch(retcode){
         case 10004:
            curErr+=langs.retcode10004;
            break;
//         case N:
//            curErr+=langs.retcodeN;
//            break;
      }
   }
   
   Alert(curErr);
}

Wir werden im nächsten Abschnitt näher auf die Lokalisierung eingehen.

EA-Lokalisierung

Bevor wir die EA-Entwicklung wieder aufnehmen, lassen Sie uns diese zweisprachig gestalten. Lassen Sie uns die Möglichkeit hinzufügen, die Sprache der EA-Nachrichten auszuwählen. Wir werden zwei Sprachen anbieten: Englisch und Russisch.

Erstellen Sie die Aufzählung mit möglichen Sprachoptionen und fügen Sie einen geeigneten Parameter zur Auswahl einer Sprache hinzu:

enum TypeOfLang{
   MY_ENG, // Englisch
   MY_RUS, // Russisch
}; 

input TypeOfLang  LANG=MY_RUS; // Sprache

Wir erstellen anschließend eine Struktur, in der alle im EA verwendeten Textelemente gespeichert werden. Danach deklarieren wir die Variable des von uns erstellten Typs:

struct translate{
   string err1;
   string err2;
//   ... mehr Text
};
translate langs;

Wir haben bereits die Variable, die die Zeichenketten enthält. Obwohl es dort noch keine Texte gibt. Erstellen wir die Funktion, die sie mit den Texten in der Sprache füllt, die in der Eingabe Language ausgewählt wurde. Nennen wir die Funktion init_lang(). Ein Teil des Codes wird unten angezeigt:

void init_lang(){
   switch(LANG){
      case MY_ENG:
         langs.err1="No error, but unknown result. (1)";
         langs.err2="General error (2)";
         langs.err3="Incorrect parameters (3)";
//         ... mehr Text
         break;
      case MY_RUS:
         langs.err0="Во время выполнения запроса произошла ошибка";
         langs.err1="Нет ошибки, но результат неизвестен (1)";
         langs.err2="Общая ошибка (2)";
         langs.err3="Неправильные параметры (3)";
//         ... mehr Text
         break;
   }
}

Es bleibt nur noch, die Funktion init_lang() aufzurufen, damit die Texte mit den notwendigen Werten gefüllt werden. Der perfekte Ort, um es zu nennen, ist eine Standard OnInit() Funktion, da es während des EA-Starts aufgerufen wird, und das ist genau das, was wir brauchen.

Haupteingabeparameter

Es ist an der Zeit, die wichtigsten Eingabeparameter unserem EA hinzuzufügen. Neben den bereits beschriebenen EA_Magic und LANG sind dies:

input double      Lot=0.01;     //Losgröße
input uint        maxLimits=7;  //Anzahl der Limit-Orders im Raster in eine Richtung
input int         Step=10;      //Schrittweite des Rasters in Punkte 
input double      takeProfit=1; //Alles Schließen, wenn das angegebene Gewinnziel erreicht wurde, $

Mit anderen Worten, wir werden maxLimits Aufträge in eine Richtung und die gleiche Anzahl von Aufträgen in die andere Richtung öffnen. Der erste Auftrag befindet sich im Abstand von Step Punkten vom aktuellen Preis. Während der Zweite wiederum im Abstand Step Punkte vom ersten Auftrag und so weiter.

Ein Gewinn wird realisiert, sobald er den Wert takeProfit erreicht (in $). In diesem Fall werden alle offenen Positionen geschlossen und alle platzierten Aufträge gelöscht. Danach setzt der EA sein Grid zurück.

Wir halten die Möglichkeit, überhaupt zu verlieren, nicht für möglich, daher ist das Gewinnziel die einzige Bedingung für das Schließen von Positionen.

Die Funktion OnInit

Wie bereits erwähnt, wird die Funktion OnInit() einmalig beim ersten EA-Start aufgerufen. Wir haben bereits den Funktionsaufruf init_lang() hinzugefügt. Schreiben wir sie fertig, um nicht mehr darauf zurückkommen zu müssen.

Im Rahmen unseres EA ist das einzige Ziel der Funktion OnInit() die Korrektur der Eingabe Step, wenn der Preis 3 oder 5 Nachkommastellen hat. Mit anderen Worten, wenn eine einzige zusätzliche Nachkommastelle vom Broker für das Symbol genutzt wird:

   ST=Step;
   if(_Digits==5 || _Digits==3){
      ST*=10;
   }

Daher werden wir den korrigierten ST-Parameter anstelle der Eingabe Step im EA selbst verwenden. Deklarieren wir sie, bevor wir die Funktionen aufrufen, indem wir den Typ double vereinbaren.

Da wir den Abstand im Symbolpreis und nicht in Punkten benötigen, um ein Grid zu bilden, sollten wir die Konvertierung sofort durchführen:

   ST*=SymbolInfoDouble(_Symbol, SYMBOL_POINT);

Auch in dieser Funktion können wir überprüfen, ob der Handel für unseren EA erlaubt ist. Wenn der Handel deaktiviert ist, ist es besser, die Nutzer sofort darüber zu informieren, damit er dies korrigieren kann.

Die Überprüfung kann mit diesem kleinen Codestück durchgeführt werden:

   if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){
      Alert(langs.noBuy+" ("+(string) EA_Magic+")");
      ExpertRemove();
   }   

Wenn der Handel deaktiviert ist, informieren wir den Nutzer in der von ihm eingestellten Sprache. Danach beendet sich der EA.

Im Ergebnis schaut den endgültige Funktion OnInit() so aus:

//+------------------------------------------------------------------+
//| Initialisierungsfunktion des Experten                            |
//+------------------------------------------------------------------+
int OnInit()
  {
   init_lang();
   
   if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){
      Alert(langs.noBuy+" ("+(string) EA_Magic+")");
      ExpertRemove();
   }

   ST=Step;
   if(_Digits==5 || _Digits==3){
      ST*=10;
   }
   ST*=SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   
   return(INIT_SUCCEEDED);
  }

Hinzufügen der Schaltfläche "Close All" (Alles Schließen)

Die Nutzerfreundlichkeit bei der Arbeit mit einem EA ist ebenso wichtig wie die Einhaltung einer ausgewählten Handelsstrategie.

In unserem Fall drückt sich die Bequemlichkeit in der Möglichkeit aus, auf einen Blick zu sehen, wie viele Kauf- und Verkaufs-Positionen bereits offen sind, und auch den Gesamtgewinn für alle derzeit offenen Positionen anzuzeigen.

Wir sollten auch in der Lage sein, alle offenen Aufträge und Positionen schnell zu schließen, wenn wir mit dem Gewinn zufrieden sind oder etwas schief geht.

Deshalb fügen wir die Schaltfläche mit allen notwendigen Daten hinzu, damit alle Positionen geschlossen und Aufträge gelöscht werden, wenn wir darauf klicken.

Grafisches Objektpräfix. Jedes grafische Objekt in MetaTrader sollte einen Namen haben. Die Namen der von einem EA erstellten Objekte sollten nicht mit den Namen der Objekte übereinstimmen, die auf einem Diagramm manuell oder von anderen EAs erstellt wurden. Daher definieren wir zunächst das Präfix, das den Namen aller grafischen Objekte hinzugefügt werden soll:

string prefix_graph="grider_";

Positionen und Gewinn berechnen. Jetzt können wir eine Funktion erstellen, die die Anzahl der offenen Kauf- und Verkaufspositionen sowie deren Gesamtgewinn berechnet und die Schaltfläche mit den erhaltenen Daten anzeigt oder den Text aktualisiert, wenn eine solche Schaltfläche bereits existiert. Nennen wir die Funktion getmeinfo_btn():

void getmeinfo_btn(string symname){
   double posPlus=0;
   double posMinus=0;
   double profit=0;
   double positionExist=false;

   // Zählen der offenen Kauf- und Verkaufsposition,
   // und des Gesamtgewinns von allen
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=symname) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         positionExist=true;
         
         profit+=PositionGetDouble(POSITION_PROFIT);
         profit+=PositionGetDouble(POSITION_SWAP);
         
         if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY){
            posPlus+=PositionGetDouble(POSITION_VOLUME);
         }else{
            posMinus+=PositionGetDouble(POSITION_VOLUME);
         }
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; }
            if(OrderSymbol()!=symname) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            positionExist=true;
            
            profit+=OrderCommission();
            profit+=OrderProfit();
            profit+=OrderSwap();
            
            if(OrderType()==OP_BUY){
               posPlus+=OrderLots();
            }else{
               posMinus+=OrderLots();
            }
         }
      }
   #endif 
   
   // Wenn es offene Positionen gibt,
   // hinzufügen der Schaltfläche zum Schließen
   if(positionExist){
      createObject(prefix_graph+"delall", 233, langs.closeAll+" ("+DoubleToString(profit, 2)+") L: "+(string) posPlus+" S: "+(string) posMinus);
   }else{
      // andernfalls wird die Schaltfläche zum Schließen der Positionen gelöscht
      if(ObjectFind(0, prefix_graph+"delall")>0){
         ObjectDelete(0, prefix_graph+"delall");
      }
   }
   
   // aktualisieren der Anzeige des aktuellen Charts
   // die implementierten Änderungen
   ChartRedraw(0);
}

Hier haben wir die bedingte Kompilierung zum zweiten Mal verwendet, da sich die Funktionen für die Arbeit mit offenen Positionen in MQL5 von der von MQL4 unterscheiden. Aus dem gleichen Grund werden wir die bedingte Kompilierung mehr als einmal später im Artikel verwenden.

Anzeige der Schaltfläche. Beachten Sie auch, dass wir, um die Schaltfläche in einem Diagramm anzuzeigen, die nutzerdefinierte Funktion createObject() verwenden. Die Funktion prüft, ob die Schaltfläche mit dem als erstes Funktionsargument übergebenen Namen im Diagramm vorhanden ist.

Wenn die Schaltfläche bereits erstellt wurde, aktualisieren Sie einfach den Text entsprechend dem Text, der als drittes Argument der Funktion übergeben wird.

Wenn es keine Schaltfläche gibt, erstellen Sie diese in der oberen rechten Ecke des Diagramms. In diesem Fall setzt das zweite Funktionsargument die Breite der Schaltfläche:

void createObject(string name, int weight, string title){
   // falls es keine benannte Schaltfläche auf dem Chart gibt, wird eine errstellt
   if(ObjectFind(0, name)<0){
      // Definieren des Abstands der Schaltfläche abhängig von der Ecke, in der sie erscheinen soll.
      long offset= ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-87;
      long offsetY=0;
      for(int ti=0; ti<ObjectsTotal((long) 0); ti++){
         string objName= ObjectName(0, ti);
         if( StringFind(objName, prefix_graph)<0 ){
            continue;
         }
         long tmpOffset=ObjectGetInteger(0, objName, OBJPROP_YDISTANCE);
         if( tmpOffset>offsetY){
            offsetY=tmpOffset;
         }
      }
      
      for(int ti=0; ti<ObjectsTotal((long) 0); ti++){
         string objName= ObjectName(0, ti);
         if( StringFind(objName, prefix_graph)<0 ){
            continue;
         }
         long tmpOffset=ObjectGetInteger(0, objName, OBJPROP_YDISTANCE);
         if( tmpOffset!=offsetY ){
            continue;
         }
         
         tmpOffset=ObjectGetInteger(0, objName, OBJPROP_XDISTANCE);
         if( tmpOffset>0 && tmpOffset<offset){
            offset=tmpOffset;
         }
      }
      offset-=(weight+1);
      if(offset<0){
         offset=ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-87;
         offsetY+=25;
         offset-=(weight+1);
      }
  
     ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0);
     ObjectSetInteger(0,name,OBJPROP_XDISTANCE,offset); 
     ObjectSetInteger(0,name,OBJPROP_YDISTANCE,offsetY); 
     ObjectSetString(0,name,OBJPROP_TEXT, title); 
     ObjectSetInteger(0,name,OBJPROP_XSIZE,weight); 
     ObjectSetInteger(0,name,OBJPROP_FONTSIZE, 8);
     ObjectSetInteger(0,name,OBJPROP_COLOR, clrBlack);
     ObjectSetInteger(0,name,OBJPROP_YSIZE,25); 
     ObjectSetInteger(0,name,OBJPROP_BGCOLOR, clrLightGray);
     ChartRedraw(0);
  }else{
     ObjectSetString(0,name,OBJPROP_TEXT, title);
  }
}

Reaktion auf das Anklicken der Schaltfläche. Wenn wir nun die Funktion getmeinfo_btn() aufrufen, erscheint die Schaltfläche Close All auf dem Chart (wenn wir offene Positionen haben). Beim Anklicken dieser Schaltfläche passiert jedoch noch nichts.

Um eine Antwort auf das Klicken auf die Schaltfläche hinzuzufügen, müssen wir das Klick-Ereignis in der Standardfunktion OnChartEvent() hinzufügen. Da dies das einzige Ziel der Funktion OnChartEvent() ist, können wir ihren endgültigen Code hier aufführen:

void OnChartEvent(const int id,         // Ereignis-ID   
                  const long& lparam,   // Ereignisparameter vom Typ long
                  const double& dparam, // Ereignisparameter vom Typ double
                  const string& sparam) // Ereignisparameter vom Typ string
{
   string text="";
   switch(id){
      case CHARTEVENT_OBJECT_CLICK:
         // falls der Name der angeklickten Schaltfläche lautet prefix_graph+"delall", dann
         if (sparam==prefix_graph+"delall"){
            closeAllPos();
         }
         break;
   }
}

Wenn Sie nun auf die Schaltfläche "Close All" klicken, wird die Funktion closeAllPos() aufgerufen. Diese Funktion ist noch nicht implementiert. Wir werden das im nächsten Abschnitt tun.

Zusätzliche Aktionen. Wir haben bereits die Funktion getmeinfo_btn(), die die notwendigen Daten berechnet und die Schaltfläche für das Schließen der Position anzeigt. Außerdem haben wir die Aktion implementiert, die beim Anklicken der Schaltfläche auftritt. Die Funktion getmeinfo_btn() selbst wird jedoch noch nirgendwo im EA aufgerufen. Daher wird sie vorerst nicht auf dem Chart angezeigt.

Wir werden die Funktion getmeinfo_btn() verwenden, wenn wir den Code der Standardfunktion OnTick() behandeln.

In der Zwischenzeit sollten wir unsere Aufmerksamkeit auf die Standardfunktion OnDeInit() richten. Da unser EA ein grafisches Objekt erstellt, stellen Sie sicher, dass alle von ihm erstellten grafischen Objekte beim Schließen des EA aus dem Diagramm entfernt werden. Deshalb benötigen wir die Funktion OnDeInit(). Sie wird automatisch beim Schließen eines EAs aufgerufen.

Dadurch sieht der Funktionskörper OnDeInit() wie folgt aus:

void OnDeinit(const int reason)
  {
      ObjectsDeleteAll(0, prefix_graph);
  }

Diese Zeile entfernt alle grafischen Objekte, die beim Schließen des EAs das angegebene Präfix in ihrem Namen enthalten. Wir haben bisher nur ein einziges solches Objekt.

Implementierung der Funktion zum Schließen aller Positionen

Da wir bereits mit der Funktion closeAllPos() begonnen haben, implementieren wir den Code jetzt.

Die Funktion closeAllPos() schließt alle aktuell offenen Positionen und entfernt alle platzierten Aufträge.

Aber es ist nicht so einfach. Die Funktion löscht nicht nur alle aktuell offenen Positionen. Wenn wir eine offene Kaufposition und die gleiche Verkaufsposition haben, werden wir versuchen, eine dieser Positionen durch eine entgegengesetzte zu schließen. Wenn Ihr Broker diese Operation mit dem aktuellen Finanzinstrument unterstützt, erhalten wir den Spread, den wir für die Eröffnung von zwei Positionen bezahlt haben, zurück. Dies verbessert die Rentabilität unseres EA. Wenn wir alle Positionen bei erreichtem Gewinnziel schließen, werden wir tatsächlich einen Gewinn erzielen, der leicht über demjenigen liegt, der im Eingangsparameter takeProfit angegeben ist.

Somit enthält die erste Zeichenkette der Funktion closeAllPos() den Aufruf einer weiteren Funktion: closeByPos().

Die Funktion closeByPos() versucht, Positionen mit entgegengesetzten Positionen zu schließen. Nachdem alle entgegengesetzten Positionen geschlossen sind, schließt die Funktion closeAllPos() die restlichen Positionen wie gewohnt. Danach löscht sie die platzierten Aufträge.

Ich verwende normalerweise das Objekt CTrade, um Positionen in MQL5 zu schließen. Bevor wir also die beiden benutzerdefinierten Funktionen implementieren, lassen Sie uns die Klasse aufnehmen und ihr Objekt sofort erstellen:

#ifdef __MQL5__ 
   #include <Trade\Trade.mqh>
   CTrade Trade;
#endif 

Jetzt können wir mit der Entwicklung der Funktion beginnen, die alle Positionen durch entgegengesetzte schließt:

void closeByPos(){
   bool repeatOpen=false;
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         if( PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY ){
            long closefirst=PositionGetInteger(POSITION_TICKET);
            double closeLots=PositionGetDouble(POSITION_VOLUME);
            
            for(int ti2=cntMyPos-1; ti2>=0; ti2--){
               if(PositionGetSymbol(ti2)!=_Symbol) continue;
               if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
               if( PositionGetInteger(POSITION_TYPE)!=POSITION_TYPE_SELL ) continue;
               if( PositionGetDouble(POSITION_VOLUME)!=closeLots ) continue;
               
               MqlTradeRequest request;
               MqlTradeResult  result;
               ZeroMemory(request);
               ZeroMemory(result);
               request.action=TRADE_ACTION_CLOSE_BY;
               request.position=closefirst;
               request.position_by=PositionGetInteger(POSITION_TICKET);
               if(EA_Magic>0) request.magic=EA_Magic;
               if(OrderSend(request,result)){
                  repeatOpen=true;
                  break;
               }
            }
            if(repeatOpen){
               break;
            }
         }
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; 
            if( OrderSymbol()!=_Symbol ) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            if( OrderType()==OP_BUY ){
               int closefirst=OrderTicket();
               double closeLots=OrderLots();
               
               for(int ti2=cntMyPos-1; ti2>=0; ti2--){
                  if(OrderSelect(ti2,SELECT_BY_POS,MODE_TRADES)==false) continue; 
                  if( OrderSymbol()!=_Symbol ) continue;
                  if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
                  if( OrderType()!=OP_SELL ) continue;
                  if( OrderLots()<closeLots ) continue;
                  
                  if( OrderCloseBy(closefirst, OrderTicket()) ){
                     repeatOpen=true;
                     break;
                  }
               }
               if(repeatOpen){
                  break;
               }
            }
                        
         }
      }
   #endif 
   // wenn eine Position durch eine andere geschlossen wurde,
   // wird die Funktion closeByPos erneut aufgerufen
   if(repeatOpen){
      closeByPos();
   }
}

Die Funktion ruft sich selbst auf, wenn ein "Close By" erfolgreich war. Dies ist notwendig, da Positionen unterschiedliche Volumina haben können, was bedeutet, dass das Schließen von zwei Positionen nicht immer die angestrebten Ergebnisse liefert. Wenn die Volumina unterschiedlich sind, verringert sich das Volumen einer der Positionen einfach, so dass sie beim nächsten Funktionstart von einer entgegengesetzten Position geschlossen werden kann.

Nach dem Schließen aller entgegengesetzten Positionen schließt die Funktion closeAllPos() die restlichen:

void closeAllPos(){
   closeByPos();
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;

         Trade.PositionClose(PositionGetInteger(POSITION_TICKET));
      }
      int cntMyPosO=OrdersTotal();
      for(int ti=cntMyPosO-1; ti>=0; ti--){
         ulong orderTicket=OrderGetTicket(ti);
         if(OrderGetString(ORDER_SYMBOL)!=_Symbol) continue;
         if(EA_Magic>0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) continue;
         
         Trade.OrderDelete(orderTicket);
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; 
            if( OrderSymbol()!=_Symbol ) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            if( OrderType()==OP_BUY ){
               MqlTick latest_price;
               if(!SymbolInfoTick(OrderSymbol(),latest_price)){
                  Alert(GetLastError());
                  return;
               }
               if(!OrderClose(OrderTicket(), OrderLots(),latest_price.bid,100)){
               }
            }else if(OrderType()==OP_SELL){
               MqlTick latest_price;
               if(!SymbolInfoTick(OrderSymbol(),latest_price)){
                  Alert(GetLastError());
                  return;
               }
               if(!OrderClose(OrderTicket(), OrderLots(),latest_price.ask,100)){
               }
            }else{
               if(!OrderDelete(OrderTicket())){
               }
            }
                        
         }
      }
   #endif 
   // Löschen der Schaltfläche zum Schließen der Positionen
   if(ObjectFind(0, prefix_graph+"delall")>0){
      ObjectDelete(0, prefix_graph+"delall");
   }

}

Implementierung der Funktion OnTick

Wir haben bereits fast alle EA-Funktionen implementiert. Jetzt ist es an der Zeit, den wichtigsten Teil zu entwickeln — die Platzierung eines Auftragsrasters (Order Grid).

Die Standardfunktion OnTick() wird bei jedem neuen Tick des Symbols aufgerufen. Wir werden die Funktion verwenden, um zu überprüfen, ob die Gitterordnung vorhanden ist, und sie zu erstellen, wenn nicht.

Prüfung einer neuen Bar. Eine Überprüfung bei jedem Tick wäre jedoch redundant. Es wäre ausreichend, das Vorhandensein des Rasters zu überprüfen, z.B. alle 5 Minuten. Dazu fügen Sie den Code der Prüfung einer neuen Bar der Funktion OnTick() hinzu. Wenn dies nicht der erste Tick einer neuen Bar ist, beenden Sie die Funktionsausführung, ohne etwas zu tun:

   if( !pdxIsNewBar() ){
      return;
   }

Die Funktion pdxIsNewBar() schaut wie folgt aus:

bool pdxIsNewBar(){
   static datetime Old_Time;
   datetime New_Time[1];

   if(CopyTime(_Symbol,_Period,0,1,New_Time)>0){
      if(Old_Time!=New_Time[0]){
         Old_Time=New_Time[0];
         return true;
      }
   }
   return false;
}

Damit das EA unsere Bedingungen alle fünf Minuten überprüfen kann, sollte es auf dem Zeitrahmen M5 gestartet werden.

Prüfen des Gewinnziels. Vor der Überprüfung des Rasters sollten wir prüfen, ob das Gewinnziel von allen aktuell offenen Positionen des Rasters erreicht wird. Wenn das Gewinnziel erreicht ist, rufen Sie die oben beschriebene Funktion closeAllPos() auf.

   if(checkTakeProfit()){
      closeAllPos();
   }

Um nach dem Gewinnziel zu suchen, rufen Sie die Funktion checkTakeProfit() auf. Sie berechnet den Gewinn aller aktuell offenen Positionen und vergleicht ihn mit dem Wert des Eingabeparameters takeProfit:

bool checkTakeProfit(){
   if( takeProfit<=0 ) return false;
   double curProfit=0;
   double profit=0;
   
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         profit+=PositionGetDouble(POSITION_PROFIT);
         profit+=PositionGetDouble(POSITION_SWAP);
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; }
            if(OrderSymbol()!=_Symbol) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            profit+=OrderCommission();
            profit+=OrderProfit();
            profit+=OrderSwap();
         }
      }
   #endif 
   if(profit>takeProfit){
      return true;
   }
   return false;
}

Displaying the Close All button. Vergessen Sie nicht die Schaltfläche Close All, die wir implementiert haben, die aber noch nicht angezeigt wird. Es ist an der Zeit, deren Funktionsaufruf hinzuzufügen:

getmeinfo_btn(_Symbol);

Sie wird so aussehen:

Die Schaltfläche "Close All"

Platzieren eines Rasters. Langsam nähern wir uns dem wichtigsten Teil unseres EAs. Er sieht ganz einfach aus, da der gesamte Code wieder in Funktionen verborgen ist:

   // Wenn es für eine Symbol offene Positionen oder platzierte Aufträge gibt, dann
   if( existLimits() ){
   }else{
   // andernfalls wird das Raster (grid) platziert
      initLimits();
   }

Die Funktion existLimits() liefert den Rückgabewert 'true', wenn es für das Symbol offene Positionen oder platzierte Aufträge gibt:

bool existLimits(){
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         return true;
      }
      int cntMyPosO=OrdersTotal();
      for(int ti=cntMyPosO-1; ti>=0; ti--){
         ulong orderTicket=OrderGetTicket(ti);
         if(OrderGetString(ORDER_SYMBOL)!=_Symbol) continue;
         if(EA_Magic>0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) continue;
         return true;
      }
   #else 
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if(OrderSymbol()!=_Symbol) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            return true;
         }
      }
   #endif 
   
   return false;
}

Wenn die Funktion 'true' zurückgibt, tun wir nichts. Andernfalls platzieren wir ein neues Auftragsraster mit der Funktion initLimits():

void initLimits(){
   // Preis für das Platzieren der Rasteraufträge
   double curPrice;
   // aktueller Preis des Symbols
   MqlTick lastme;
   SymbolInfoTick(_Symbol, lastme);
   // wenn es keinen aktuellen Preis gibt, wird das Platzieren des Rasters abgebrochen
   if( lastme.bid==0 ){
      return;
   }

   // kleinst möglicher Abstand vom Preis für das Platzieren von Stop-Loss und,
   // höchstwahrscheinlich, Pending-Orders
   double minStop=SymbolInfoDouble(_Symbol, SYMBOL_POINT)*SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   
   // Platzieren der Kaufaufträge
   curPrice=lastme.bid;
   for(uint i=0; i<maxLimits; i++){
      curPrice+=ST;

      if( curPrice-lastme.ask < minStop ) continue;
      if(!pdxSendOrder(MY_BUYSTOP, curPrice, 0, 0, Lot, 0, "", _Symbol)){
      }
   }
   // Platzieren der Verkaufsaufträge
   curPrice=lastme.ask;
   for(uint i=0; i<maxLimits; i++){
      curPrice-=ST;
      if( lastme.bid-curPrice < minStop ) continue;
      if(!pdxSendOrder(MY_SELLSTOP, curPrice, 0, 0, Lot, 0, "", _Symbol)){
      }
   }
}

Testen des EAs

Unser EA ist fertig. Nun sollten wir ihn testen und Schlussfolgerungen über die Performance der Handelsstrategie ziehen.

Da unser EA sowohl im MetaTrader 4 als auch in MetaTrader 5 arbeitet, können wir die Terminalversion auswählen, in der der Test durchgeführt werden soll. Obwohl die Wahl hier ganz offensichtlich ist. MetaTrader 5 gilt als leichter verständlich und besser.

Zuerst führen wir einen Test ohne jegliche Optimierung durch. Unser EA sollte bei der Verwendung vernünftiger Werte nicht vollständig von den Werten der Eingabeparameter abhängen. Verwenden wir:

  • EURUSD-Symbol;
  • M5 Zeitrahmen;
  • Zeitraum vom 1. August 2018 bis zum 1. Januar 2019;
  • Testmodus 1 Minute OHLC.

Die Standardwerte der Eingaben bleiben erhalten (lot 0.01, step 10 points, 7 orders per grid, take profit $1).

Das Ergebnis ist unten dargestellt:

Saldenkurve des ersten Tests

Wie aus der Grafik ersichtlich ist, lief er einen ganzen Monat und eine Woche lang gut. Wir haben es geschafft, fast $100 bei einem Drawdown von $30 zu verdienen. Dann geschieht das scheinbar unmögliche Ereignis. Schauen Sie sich das im visuellen Modus des Testers an, um zu sehen, wie sich der Preis im September entwickelt hat:

Ergebnis im visuellen Modus

Es begann am 13. September, kurz nach 16:15. Zuerst berührte der Preis einen Kaufauftrag. Dann wurden 2 Verkaufsaufträge, 2 weitere Kaufaufträge und schließlich die restlichen 5 Verkaufsaufträge aktiviert. Infolgedessen haben wir 3 Kaufaufträge und 7 Verkaufsaufträge.

Dies ist auf dem Bild nicht zu sehen, aber der Preis bewegte sich nicht weiter nach unten. Am 20. September kehrte er zum obersten Punkt zurück und aktivierte die restlichen 4 Kaufaufträge.

Infolgedessen wurden alle 7 Verkaufs- und 7 Kaufaufträge eröffnet. Das bedeutet, dass wir so nie das Gewinnziel erzielen können.

Wenn wir uns die weitere Preisentwicklung ansehen, wird sie um etwa 80 Punkte weiter steigen. Wenn wir beispielsweise 13 Aufträge in unserer Kette hätten, dann könnten wir die Situation wahrscheinlich umkehren und Gewinne erzielen.

Selbst wenn dies nicht genug wäre, würde der Preis später um 200 Punkte sinken, so dass wir bei 30 Aufträgen in der Kette theoretisch ein Gewinnziel erzielen könnten. Obwohl dies wahrscheinlich Monate dauern würde, und der Drawdown enorm wäre.

Test mit einer neuen Anzahl von Aufträgen im Raster. Überprüfen wir unsere Annahmen. 13 Aufträge im Raster änderten nichts, während 20 Aufträge es uns erlaubten, unbeschadet davonzukommen:

Test EURUSD, 20 Rasteraufträge

Der Drawdown betrug jedoch etwa 300 US-Dollar, während der Gesamtgewinn etwas über 100 US-Dollar liegt. Vielleicht ist unsere Handelsstrategie kein völliger Misserfolg, aber sie bedarf definitiv dramatischer Verbesserungen.

Daher ist es sinnlos, sie jetzt nur zu optimieren. Aber versuchen wir es trotzdem.

Optimierung. Die Optimierung erfolgt mit den folgenden Parametern:

  • Anzahl der Aufträge eines Rasters: 4-21;
  • Schrittweite (step) des Rasters: 10-20 Punkte;
  • Gewinnziel (take profit) bleibt gleich ($1).

Die Schrittweite von 13 Punkten hat sich als der beste erwiesen, während die Anzahl der Aufträge im Raster 16 beträgt:

Test EURUSD, 16 Rasteraufträge, Schrittweite 13 Punkte

Dies ist das Testergebnis im Modus "Jeder Tick anhand realer Ticks". Trotz der Tatsache, dass das Ergebnis positiv ist, ist $119 für 5 Monate bei einem Drawdown von $221 nicht das beste Ergebnis. Das bedeutet, dass unsere Handelsstrategie wirklich verbessert werden muss.

Mögliche Wege zur Verbesserung der Handelsstrategie

Anscheinend ist die Verwendung von einem Gewinnziel unzureichend. Von Zeit zu Zeit gibt es Situationen, in denen der Preis alle oder die meisten Aufträge in beide Richtungen trifft. In diesem Fall können wir monatelang auf den Gewinn warten, wenn nicht sogar unendlich.

Lasst uns darüber nachdenken, was wir tun können, um das erkannte Problem zu lösen.

Manuelle Kontrolle. Natürlich ist der einfachste Weg, den EA von Zeit zu Zeit manuell zu steuern. Wenn sich ein potenzielles Problem abzeichnet, können wir zusätzliche Aufträge erteilen oder einfach alle Positionen schließen.

Ein zusätzliches Raster einstellen. Wir können versuchen, ein anderes Raster zu setzen, wenn beispielsweise 70% der Aufträge in eine Richtung und 70% der Aufträge in eine andere Richtung betroffen sind. Orders aus dem zusätzlichen Raster können eine schnelle Erhöhung der Anzahl der offenen Positionen in eine Richtung ermöglichen und so das Gewinnziel schneller erreichen.

Abgesehen von der Anzahl der offenen Positionen können wir das Datum der letzten offenen Position überprüfen. Wenn beispielsweise mehr als eine Woche seit dem Öffnen der letzten Position vergangen ist, wird ein neues Raster gesetzt.

Bei beiden Optionen besteht das Risiko, dass sich die Situation weiter verschärft und der ohnehin schon hohe Drawdown zunimmt.

Schließen des gesamten Rasters und ein neues platzieren. Abgesehen davon, dass wir ein weiteres Raster setzen, können wir alle Positionen schließen und Aufträge erteilen, die zum aktuellen Raster gehören, was bedeutet, dass wir die Schlacht verloren haben, aber nicht den Krieg.

Es gibt mehrere Fälle, in denen wir das tun können:

  • wenn mehr als N% der Aufträge in beide Richtungen ausgelöst wurden,
  • wenn seit dem Auslösen der letzten Position N Tage vergangen sind,
  • wenn der Verlust aller offenen Positionen $N erreicht hat.

Als Beispiel versuchen wir, das letzte Element aus der Liste zu implementieren. Wir fügen einen Eingabeparameter hinzu, bei dem wir die Größe des Verlusts in $ festlegen, bei dem wir Positionen des aktuellen Rasters schließen und ein neues öffnen. Für die Einstellung eines Verlustes ist eine Zahl kleiner als 0 zu verwenden:

input double      takeLoss=0; //Schließen im Falle eines Verlustes, $

Jetzt müssen wir die Funktion checkTakeProfit() neu schreiben, so dass sie den Gewinn für alle offenen Positionen zurückgibt und nicht 'true' oder 'false':

double checkTakeProfit(){
   double curProfit=0;
   double profit=0;
   
   #ifdef __MQL5__ 
      int cntMyPos=PositionsTotal();
      for(int ti=cntMyPos-1; ti>=0; ti--){
         if(PositionGetSymbol(ti)!=_Symbol) continue;
         if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue;
         
         profit+=PositionGetDouble(POSITION_PROFIT);
         profit+=PositionGetDouble(POSITION_SWAP);
      }
   #else
      int cntMyPos=OrdersTotal();
      if(cntMyPos>0){
         for(int ti=cntMyPos-1; ti>=0; ti--){
            if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue;
            if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; }
            if(OrderSymbol()!=_Symbol) continue;
            if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue;
            
            profit+=OrderCommission();
            profit+=OrderProfit();
            profit+=OrderSwap();
         }
      }
   #endif 
   return profit;
}

Die Änderungen sind gelb markiert.

Jetzt können wir die Funktion OnTick() so überarbeiten, dass sie neben dem Gewinnziel auch einen Stop-Loss auf allen Positionen überprüft:

   if(takeProfit>0 && checkTakeProfit()>takeProfit){
      closeAllPos();
   }else if(takeLoss<0 && checkTakeProfit()<takeLoss){
      closeAllPos();
   }

Zusätzliche Prüfungen

Mal sehen, ob diese Verbesserungen von Nutzen waren.

Wir werden nur den Stop-Loss in $ im Bereich von -$5 bis -$100 optimieren. Die übrigen Parameter bleiben auf den Ebenen, die während des letzten Tests ausgewählt wurden (Schrittweite 13 Punkte, 16 Rasteraufträge).

Der höchste Gewinn wird mit dem Stop-Loss von -56$ erzielt. Der Gewinn innerhalb von 5 Monaten umfasst 156 $ mit einem maximalen Drawdown von 83 $:

Test EURUSD, Stop-Loss bei -$56

Bei der Analyse des Charts können wir sehen, dass der Stop-Loss nur einmal innerhalb von fünf Monaten aktiviert wurde. Das Ergebnis ist natürlich besser, was das Verhältnis von Gewinn zu Drawdown betrifft.

Bevor wir jedoch endgültige Schlussfolgerungen ziehen, sollten wir prüfen, ob unser EA mit den ausgewählten Parametern langfristig zumindest einen gewissen Gewinn erzielen kann. Versuchen wir es mit dem Zeitraum der letzten fünf Jahre:

Testergebnis mit EURUSD und einem Stop-Loss über 5 Jahre

Die Ergebnisse sind entmutigend. Vielleicht könnte eine zusätzliche Optimierung diese verbessern. In jedem Fall bedarf der Einsatz dieser Raster-Handelsstrategie einer radikalen Verbesserung. Die Vorstellung, dass zusätzliche offene Positionen früher oder später die Verluste überwinden werden, ist im Hinblick auf den langfristigen automatisierten Handel falsch.

Hinzufügen von Stop-Losses und Gewinnzielen für Aufträge

Leider führen auch andere oben aufgeführte EA-Verbesserungsoptionen nicht zu besseren Ergebnissen. Aber was ist mit Stop-Losses für einzelne Positionen? Vielleicht verbessert das Hinzufügen von Stop-Loss unseren EA für den langfristigen automatisierten Handel.

Die Optimierung der fünfjährigen Geschichte zeigte im Vergleich zu den oben genannten Ergebnissen bessere Ergebnisse.

Der Stop-Loss von 140 Punkten und der Take-Profit von 50 Punkten waren am effizientesten. Wenn nicht innerhalb von 30 Tagen eine einzelne Position des aktuellen Rasters geöffnet wird, wird es geschlossen und ein neues Ratsre geöffnet.

Das Endergebnis ist unten dargestellt:

Nutzung von Stop-Loss und Take-Profit für die Aufträge

Der Gewinn beträgt 351$ bei einem Drawdown von 226$. Natürlich ist dies besser als ein Handelsergebnis ohne Stop-Loss. Wir können jedoch nicht umhin festzustellen, dass alle Ergebnisse, die beim Schließen des aktuellen Netzes in weniger als 30 Tagen nach Abschluss des letzten Geschäfts erzielt wurden, Verluste verursachen. Außerdem führt die Anzahl der Tage von über 30 meist auch zu Verlusten. Dieses Ergebnis ist also eher ein Zufall als eine Regel.

Schlussfolgerung

Das Hauptziel dieses Artikels war es, einen Handels-EA zu schreiben, der sowohl im MetaTrader 4 als auch im MetaTrader 5 arbeitet. Das ist uns gelungen.

Auch noch einmal, sahen wir, dass das Testen eines EA auf mehrere Monate der Geschichte unzureichend ist, es sei denn, Sie sind bereit, seine Parameter jede Woche anzupassen.

Leider sind Ideen, die auf einfachen Rastern basieren, nicht realisierbar. Aber vielleicht haben wir etwas übersehen. Wenn Sie wissen, wie man einen grundlegenden Raster-EA entwickelt, der tatsächlich profitabel ist, schreiben Sie bitte Ihre Vorschläge in die Kommentare.

Unsere Ergebnisse bedeuten jedoch nicht, dass rasterbasierte Handelsstrategien nicht profitabel sein können. Betrachten Sie zum Beispiel diese Signale:

Die Signale basieren auf einem einzigen Ratsre-EA, der komplexer ist als das hier beschriebene. Dieser Raster-EA kann tatsächlich bis zu 100% des Gewinns pro Monat erwirtschaften. Wir werden im nächsten Artikel über Raster-EA darauf näher eingehen.

Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/5596

Beigefügte Dateien |
griderEA.ex5 (112.85 KB)
griderEA.mq5 (77.8 KB)
griderEA.ex4 (36.54 KB)
griderEA.mq4 (77.8 KB)
MTF-Indikatoren als Werkzeuge der technischen Analyse MTF-Indikatoren als Werkzeuge der technischen Analyse
Die meisten Händler sind sich einig, dass eine Zustandsanalyse des aktuellen Marktes mit der Bewertung höherer Zeitrahmen beginnt. Die Analyse wird nach unten auf niedrigere Zeitrahmen bis zu demjenigen durchgeführt, in dem der Handel durchgeführt wird. Diese Analysemethode scheint ein obligatorischer Bestandteil des professionellen Ansatzes für einen erfolgreichen Handel zu sein. In diesem Artikel werden wir Indikatoren auf mehreren Zeitrahmen (multi-timeframe, MTF) und ihre Erstellungswege diskutieren, sowie Codebeispiele für MQL5 liefern. Neben der allgemeinen Bewertung von Vor- und Nachteilen werden wir einen neuen Indikatoransatz mit dem MTF-Modus vorschlagen.
Die rechnerische Fähigkeiten von MATLAB 2018 im MetaTrader 5 nutzen Die rechnerische Fähigkeiten von MATLAB 2018 im MetaTrader 5 nutzen
Nach dem Upgrade des MATLAB-Pakets im Jahr 2015 ist es notwendig, auf eine moderne Art der Erstellung von DLL-Bibliotheken umzustellen. Der Artikel veranschaulicht anhand eines exemplarischen prädiktiven Indikators die Besonderheiten der Verknüpfung von MetaTrader 5 und MATLAB mit modernen 64-Bit-Versionen der Plattformen, die heute genutzt werden. Mit der gesamten Verbindungssequenz von MATLAB können MQL5-Entwickler Anwendungen mit erweiterten Rechenfunktionen viel schneller erstellen und so "Fallstricke" vermeiden.
Untersuchung von Techniken zur Analyse der Kerzen (Teil III): Eine Bibliothek für die Musterbearbeitung Untersuchung von Techniken zur Analyse der Kerzen (Teil III): Eine Bibliothek für die Musterbearbeitung
Der Zweck dieses Artikels ist es, ein benutzerdefiniertes Werkzeug zu erstellen, das es den Benutzern ermöglichen würde, die gesamte Bandbreite an Informationen über die zuvor diskutierten Muster zu erhalten und zu nutzen. Wir erstellen eine Bibliothek mit musterbezogenen Funktionen, die Sie in Ihren eigenen Indikatoren, Handelspanels, Expert Advisors usw. verwenden können.
Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil III). Erhebung (Collection) von Marktorders und Positionen Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil III). Erhebung (Collection) von Marktorders und Positionen
Im ersten Teil begannen wir mit der Erstellung einer großen plattformübergreifenden Bibliothek, die die Entwicklung von Programmen für MetaTrader 5 und MetaTrader 4 Plattformen vereinfacht. Danach haben wir die Collection (Sammlung bzw. Liste) von historischen Aufträgen und Deals implementiert. Unser nächster Schritt ist das Erstellen einer Klasse für eine komfortable Auswahl und Sortierung von Aufträgen, Deals und Positionen in Collections. Wir werden das Basis-Bibliotheksobjekt Engine implementieren und der Bibliothek die Collection von Marktorders und Positionen hinzufügen.