Fertige Expert Advisors von MQL5 Wizard laufen auf MetaTrader 4

Stanislav Korotky | 26 April, 2017

Die MetaTrader 4 und MetaTrader 5 Kundenterminals bieten ihren Nutzern die Möglichkeit, Prototypen ihrer Programme in der MQL Programmiersprache im eingebauten MQL Wizard ganz einfach zu entwickeln. Die Wizards der beiden Versionen des Terminals sind sehr ähnlich, weisen aber einen wichtigen Unterschied auf. Im MetaTrader 5 Wizard gibt es die Option der Generierung fertiger Expert Advisors, und in MetaTrader 4 nicht. Solche Expert Advisors basieren auf Klassen der MQL Standardbibliothek, d.h. auf den Sets von Header-Dateien, die im Terminal enthalten sind. MetaTrader 4 beinhaltet diese Bibliothek auch, sie umfasst aber die Handelsklassen aus MQL5 nicht. Unter anderem gibt es da keine Klassen, die für die Vorbereitung und Sendung von Handelsanfragen, Berechnung von Signalen anhand von Indikatorwerten oder Preisstruktur, Trailing, Money Management verantwortlich sind, und all das ist die notwendige Grundlage für die Erstellung automatisch generierter Expert Advisors.‌

Das hat sich historisch infolge einer sukzessiven Entwicklung der MQL5 Sprache ergeben. Die neue Sprache erschien ursprünglich in MetaTrader 5, und gerade für dieses Terminal wurde die Standardbibliothek von Klassen entwickelt. Erst eine Weile später wurde MQL5 in MetaTrader 4 integriert, aber da sich die Handelsfunktionen in API der zwei Terminals stark voneinander unterschieden, wurde die Standardbibliothek ins frühere Produkt nicht vollständig übertragen — ohne Handelsklassen. Infolgedessen gibt es im MetaTrader 4 Wizard keine Option für die Generierung fertiger Expert Advisors.

Dabei ist aber MetaTrader 4 immer noch populär, und die Option der Generierung fertiger Experten wäre sehr praktisch. Da dem MetaTrader 4 keine neuen Funktionen mehr hinzugefügt werden, nur Fehler behoben werden, ist es eher unwahrscheinlich, dass sein Wizard vervollkomnet wird. Es gibt aber keine Hindernisse dafür, MetaTrader 5 Wizard zu nutzen und anschließend den erhaltenen Code auf MetaTrader 4 zu übertragen. Damit dieser Code da funktioniert, wird eine Kleinigkeit benötigt — ein Set von Handelsklassen der Standardbibliothek, angepasst für MQL API MetaTrader 4. Mit anderen Worten müssen aus der MetaTrader 5 Standardbibliothek die Klassen kopiert werden, die in MetaTrader 4 fehlen, und für diese Klasse muss die Handelsumgebung des MetaTrader 5 emuliert werden.

Für das Verständnis des Beitrags muss man die Grundsätze der Transaktionen in MetaTrader 5 kennen: das Wesen von Orders (Aufträgen), Abschlüssen und Positionen. Wenn Sie mit dieser Version des Terminals nicht vertraut sind, ist es empfehlenswert, den Artikel "Orders, Positions und Abschlüsse in MetaTrader 5" zu lesen.

Planung

Bei jeder Arbeit ist besser, sich an einen Plan zu halten. Als Beispiel für diesen Ansatz, der bei der Entwicklung von Programmen verwendet wird, ist das so genannte Wasserfallmodell anzuführen. Es passt sehr gut für unseren Fall, weil wir nicht nur entwickeln, sondern den Prozess auch beschreiben. Aber in der Praxis ist es effektiver, einen Code aus MQL5 in MQL4 (oder umgekehrt) mithilfe eines der flexiblen Ansätze zu portieren, wie z.B. Extreme Programming. Das Motto des Ansatzes: weniger planen, mehr umsetzen. Das heißt, man kann einen Quellcode nehmen, versuchen ihn zu kompilieren und dann alle auftretenden Fehler konsequent zu beheben. Der Plan, den ich in diesem Artikel anbiete, wurde konsequent erarbeitet, und basiert gerade auf den Hinweisen des unzufriedenen Compilers.‌

Beim Vergleich der Bibliotheken zweier Terminals fällt einem sofort auf, dass die Ordner Trade, Expert und Models in der vierten Version fehlen. Das heißt, die Hauptarbeit besteht in der Portierung aller in diesen Ordnern vorhandenen Klassen auf den MetaTrader 4. Darüber hinaus müssen wir offensichtlich auch etwas im Ordner Indicators korrigieren. Er ist in der Bibliothek des MetaTrader 4 enthalten, die Grundsätze der Arbeit mit Indikatoren unterscheiden sich allerdings in den zwei Terminals voneinander. Man sollte allerdings möglichst wenig Dateien der Bibliothek ändern, denn sie wird immer wieder aktualisiert, und dann müssen wir unsere Änderungen mit den offiziellen Aktualisierungen synchronisieren.

Alle kopierten Dateien beziehen sich mehr oder weniger auf MQL API der fünften Version. Deswegen müssen wir einen vollständigen Set von Definitionen und Funktionen erarbeiten, die diese Programmschnittstelle beibehalten und alle Aufrufe von vererbten MQL API der vierten Version umwandeln werden. Gehen wir genauer darauf ein, was die zu emulierende Handelsumgebung beinhalten muss. Fangen wir mit Typen an, denn das sind Bauteile, auf welchen der Algorithmus und das ganze Programm basieren.

Zu den einfachsten Typen gehören Aufzählungen. Sie werden in den meisten Funktionen direkt oder indirekt durch Strukturen verwendet. Deswegen ist die Reihenfolge der Anpassung wie folgt: Aufzählungen, Strukturen, Konstanten und Funktionen.

Aufzählungen

Eine Reihe der notwendigen Aufzählungen wurde bereits auf MetaTrader 4 übertragen. Dazu gehören zum Beispiel die Eigenschaften der Orders: ENUM_ORDER_TYPE, ENUM_ORDER_PROPERTY_INTEGER, ENUM_ORDER_PROPERTY_DOUBLE und ENUM_ORDER_PROPERTY_STRING. Von einer Seite ist das praktisch, von der anderen Seite sind nicht alle diese Aufzählungen definiert wie in MetaTrader 5, und dies bereitet Schwierigkeiten.

ENUM_ORDER_TYPE enthält zum Beispiel in MetaTrader 5 mehr Ordertypen als in MetaTrader 4. Wenn man ENUM_ORDER_TYPE so lässt wie sie ist, erhalten wir Kompilierungsfehler, denn der kopierte Code bezieht sich auf nicht vorhandene Elemente. Es ist unmöglich, die Aufzählung weder umzudefinieren noch nachzudefinieren. Deswegen ist hier die einfachste Lösung — die Makrodefinition für den Präprozessor, zum Beispiel:

// ENUM_ORDER_TYPE extension
#define ORDER_TYPE_BUY_STOP_LIMIT ((ENUM_ORDER_TYPE)6)
#define ORDER_TYPE_SELL_STOP_LIMIT ((ENUM_ORDER_TYPE)7)
#define ORDER_TYPE_CLOSE_BY ((ENUM_ORDER_TYPE)8)

Die anderen Aufzählungen, die es in MetaTrader 4 nicht gibt, können analog zu den Aufzählungen in MetaTrader 5 definiert werden, zum Beispiel:

enum ENUM_ORDER_TYPE_FILLING
{
  ORDER_FILLING_FOK,
  ORDER_FILLING_IOC,
  ORDER_FILLING_RETURN
};

Auf diese Weise sollten wir die unten angeführten Aufzählungen definieren (oder durch Konstanten ergänzen). Auf den ersten Blick ist ihre Anzahl sehr groß, dennoch handelt es sich um eine einfache Arbeit: es reicht sie aus der Dokumentation zu kopieren (die Links zu den entsprechenden Bereichen sind unten angeführt; mit dem Sternchen sind die vorhandenen Aufzählungen markiert, die einen "Schliff" benötigen).

  • Order (Order)
    • ENUM_ORDER_TYPE_TIME
    • ENUM_ORDER_STATE
    • ENUM_ORDER_TYPE_FILLING
    • ENUM_ORDER_TYPE (*)
    • ENUM_ORDER_PROPERTY_INTEGER (*)
    • ENUM_ORDER_PROPERTY_STRING (*)
  • Position (Position)
    • ENUM_POSITION_TYPE
    • ENUM_POSITION_PROPERTY_INTEGER
    • ENUM_POSITION_PROPERTY_DOUBLE
    • ENUM_POSITION_PROPERTY_STRING
  • Abschluss (Deal)
    • ENUM_DEAL_ENTRY
    • ENUM_DEAL_TYPE
    • ENUM_DEAL_PROPERTY_INTEGER
    • ENUM_DEAL_PROPERTY_DOUBLE
    • ENUM_DEAL_PROPERTY_STRING
  • Typen der Transaktionen
    • ENUM_TRADE_REQUEST_ACTIONS

MetaTrader 4 umfasst bereits die Definitionen der Aufzählungen, die Symbole beschreiben, solche wie ENUM_SYMBOL_INFO_INTEGER, ENUM_SYMBOL_INFO_DOUBLE und ENUM_SYMBOL_INFO_STRING. Einige Elemente sind nur reserviert, funktionieren aber nicht (s. Dokumentation). Das sind die Beschränkungen der MetaTrader 4 Plattform, und wir müssen das hinnehmen. Für uns ist es wichtig, dass diese Aufzählung nicht im Projekt definiert werden müssen.‌

Strukturen

Neben den Aufzählungen werden in Handelsfunktionen des MetaTrader 5 Strukturen verwendet. Ihre Definitionen können auch der Dokumentation entnommen werden (Links auf die entsprechenden Bereiche sind unten angeführt).

Makrodefinitionen

In Ergänzung zu den oben angeführten Typen verwenden Quellcodes des MetaTrader 5 eine Vielzahl von Konstanten, die man im Rahmen dieses Projekts am einfachsten durch die Anweisung #define des Präprozessors definieren kann.

Handelsfunktionen

Den letzten und den wichtigsten Punkt unseres Plans bilden Handelsfunktionen selbst. Wir können sie erst dann umsetzen, nachdem alle oben aufgezählten Typen und Konstanten definiert worden sind.

Die Liste der Handelsfunktionen ist sehr umfassend. Sie können in 4 Gruppen unterteilt werden:

  • Orders
  • Positionen
  • Historie der Orders
  • Historie der Abschlüsse

Schließlich benötigen wir solche einfachen Permutationen wie:

#define MQL5InfoInteger MQLInfoInteger
#define MQL5InfoString  MQLInfoString

Das sind quasi die gleichen Funktionen des Kerns des Terminals, ihre Namen unterscheiden sich aber etwas in MQL5 und MQL4 voneinander.

Bevor wir mit der Implementierung anfangen, müssen wir uns einfallen lassen, wie das MetaTrader 5 Handelsmodell auf dem MetaTrader 4 Handelsmodell anzuzeigen.

Anzeige

Versuchen wir eine Parallele zwischen MetaTrader 5 und MetaTrader 4 zu ziehen. Es ist einfacher, mit Metatrader 4 zu beginnen. Da wird ein universeller Begriff "Order" quasi für alles verwendet: für Marktorders, Pending Orders, Historie von Transaktionen. In all den Fällen hat eine Order unterschiedliche Status. In MetaTrader 5 sind Marktorders — Positionen, Pending Orders — einfache Orders, und die Historie von Transaktionen wird mithilfe von Abschlüssen geschrieben.‌

Im einfachsten Fall funktioniert MetaTrader 5 ungefähr so. Für das Eröffnen eine Position wird eine Anweisung zum Einstieg in den Markt an den Handelsserver gesendet. Für das Schließen einer Position trifft eine andere Anweisung ein, die Anweisung zum Ausstieg. Die Ausführung jeder Anweisung erfolgt im Rahmen eines Abschlusses, der in der Handelshistorie gespeichert wird. Auf diese Weise muss eine Marktorder des MetaTrader 4 in der emulierten Handelsumgebung des MetaTrader 5 wie folgt angezeigt werden:

  • Einstiegsorder
  • Abschluss des Einstiegs
  • Position
  • Ausstiegsorder
  • Abschluss des Ausstiegs

Es ist anzumerken, dass MetaTrader 5 ursprünglich eine Netting-Plattform war, d.h. es konnte nur eine Position pro Symbol vorhanden sein. Jede Anweisung auf einem Symbols erhöhte, reduzierte oder löschte das Gesamtvolumen des Symbols sowie änderte Stop Loss und Take Profit Levels für dieses Symbol. Diesen Modus gibt es nicht in MetaTrader 4, und die Umsetzung dieses Projektes wäre für uns sehr kompliziert gewesen, wäre dem MetaTrader 5 die Unterstützung für den Hedging-Modus nicht hinzugefügt. Das ist gerade der Modus, zu welchem sich MetaTrader 4 "bekennt": die Ausführung jeder Anweisung bildet eine separate "Position" (nach dem Begriff des MetaTrader 5), so dass es mehrere offene Orders pro Symbol existieren können, inklusive der entgegengesetzen.

Achtung! Wenn Sie erzeugte Expert Advisors in MetaTrader 5 und MetaTrader 4 vergleichen möchten, vergessen Sie nicht, den Kontotyp mit dem Hedging-Modus in MetaTrader 5 zu aktivieren. Für den Vergleich sind am besten Servers eines Brokers zu nutzen.

Umsetzung

Emulation der Handelsumgebung des MetaTrader 5

Die ganze Umgebung, die emuliert werden muss, inklusive Typen, Konstanten und Funktionen, platzieren wir einfachheitshalber in einer einzigen Header-Datei MT5Bridge.mqh. Ein guter Stil würde wahrscheinlich verlangen, alles in separaten Dateien zu speichern. Solche Strukturierung ist für große Projekte besonders wichtig sowie für Projekte, an welchen eine Gruppe von Menschen arbeitet. Aber von der Verbreitung und Installation her ist eine Datei praktischer.

Definieren wir laut dem Plan alle Aufzählungen, Konstanten und Strukturen. Es geht um ein einfaches Kopieren, ohne jegliche Schwierigkeiten. Diese Operation muss wohl nicht ausführlicher erklärt werden, darauf sind wir in den Anmerkungen bei der Planung eingegangen. Blicken wir wieder auf die Dokumentation über Handelsfunktionen und fangen wir mit dem kreativen Teil, dem Schreiben des Codes aller dieser Funktionen.

Fangen wir mit den aktuellen Operationen, zu denen die Bearbeitung von Marktorders und Pending Orders sowie Positionen gehört.

Dafür wird die universelle Funktion des MetaTrader 5 OrderSend benötigt.

bool OrderSend(MqlTradeRequest &request, MqlTradeResult &result)
{

Je nach dem Anfragetyp muss einer der Ordertypen des MetaTrader 4 verwendet werden.

  int cmd;   
  result.retcode = 0;
  switch(request.type)
  {
    case ORDER_TYPE_BUY:
      cmd = OP_BUY;
      break;
    case ORDER_TYPE_SELL:
      cmd = OP_SELL;
      break;
    case ORDER_TYPE_BUY_LIMIT:
      cmd = OP_BUYLIMIT;
      break;
    case ORDER_TYPE_SELL_LIMIT:
      cmd = OP_SELLLIMIT;
      break;
    case ORDER_TYPE_BUY_STOP:
      cmd = OP_BUYSTOP;
      break;
    case ORDER_TYPE_SELL_STOP:
      cmd = OP_SELLSTOP;
      break;
    default:
      Print("Unsupported request type:", request.type);
      return false;
  }

Der in das Feld action übergebene Code der Operation ermöglicht es, die Installation, das Löschen und die Modifikation von Orders zu bearbeiten. Zum Beispiel kann die Eröffnung einer Marktorder oder das Platzieren einer Pending Order wie folgt umgesetzt werden.

  ResetLastError();
  if(request.action == TRADE_ACTION_DEAL || request.action == TRADE_ACTION_PENDING)
  {
    if(request.price == 0)
    {
      if(cmd == OP_BUY)
      {
        request.price = MarketInfo(request.symbol, MODE_ASK);
      }
      else
      if(cmd == OP_SELL)
      {
        request.price = MarketInfo(request.symbol, MODE_BID);
      }
    }
    if(request.position > 0)
    {
      if(!OrderClose((int)request.position, request.volume, request.price, (int)request.deviation))
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = request.position | 0x8000000000000000;
        result.order = request.position | 0x8000000000000000;
        result.volume = request.volume;
        result.price = request.price;
      }
    }
    else
    {
      int ticket = OrderSend(request.symbol, cmd, request.volume, request.price, (int)request.deviation, request.sl, request.tp, request.comment, (int)request.magic, request.expiration);
      if(ticket == -1)
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = ticket;
        result.order = ticket;
        result.request_id = ticket;
        if(OrderSelect(ticket, SELECT_BY_TICKET))
        {
          result.volume = OrderLots();
          result.price = OrderOpenPrice() > 0 ? OrderOpenPrice() : request.price;
          result.comment = OrderComment();
          result.ask = MarketInfo(OrderSymbol(), MODE_ASK);
          result.bid = MarketInfo(OrderSymbol(), MODE_BID);
        }
        else
        {
          result.volume = request.volume;
          result.price = request.price;
          result.comment = "";
        }
      }
    }
  }

Die Hauptarbeit führt die übliche Funktion des MetaTrader 4 OrderSend mit einer Vielzahl von Parametern durch. Nach ihrem Aufruf werden die Ergebnisse in die Output-Struktur geschrieben.

In MetaTrader 5 wird eine Marktorder durch die Eröffnung einer anderen gegenläufigen Order geschlossen, und dem Feld position wird der Identifier der zu schließenden Position übergeben. In diesem Fall, also wenn das Feld position nicht leer ist, versucht der oben angeführte Code die Order mithilfe der Funktion OrderClose zu schließen. Dabei wird als Identifier der Position das Ticket der Order selbst verwendet. Es ist logisch, weil jede Order in MetaTrader 4 eine eigene Position erstellt. Der Abschluss bekommt das gleiche Ticket.

Was die virtuelle Order zum Schließen einer Position betrifft (sie gibt es in der Wirklichkeit nicht), wird als ihr Ticket die Ausgangsnummer verwendet, ergänzt durch das auf 1 gesetzte höchstwertige Bit. Dies wird später bei der Aufzählung von Orders und Abschlüssen verwendet.

Schauen wir nun, wie man die Änderung der Levels in einer offenen Position umsetzen kann.

  else if(request.action == TRADE_ACTION_SLTP) // change opened position
  {
    if(OrderSelect((int)request.position, SELECT_BY_TICKET))
    {
      if(!OrderModify((int)request.position, OrderOpenPrice(), request.sl, request.tp, 0))
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = OrderTicket();
        result.order = OrderTicket();
        result.request_id = OrderTicket();
        result.volume = OrderLots();
        result.comment = OrderComment();
      }
    }
    else
    {
      result.retcode = TRADE_RETCODE_POSITION_CLOSED;
    }
  }

Es ist offensichtlich, dass dafür OrderModify verwendet wird.

Diese Funktion wird auch für die Modifizierung einer Pending Order verwendet.‌

  else if(request.action == TRADE_ACTION_MODIFY) // change pending order
  {
    if(OrderSelect((int)request.order, SELECT_BY_TICKET))
    {
      if(!OrderModify((int)request.order, request.price, request.sl, request.tp, request.expiration))
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = OrderTicket();
        result.order = OrderTicket();
        result.request_id = OrderTicket();
        result.price = request.price;
        result.volume = OrderLots();
        result.comment = OrderComment();
      }
    }
    else
    {
      result.retcode = TRADE_RETCODE_INVALID_ORDER;
    }
  }

Die Löschung der Pending Order führt die Standardfunktion OrderDelete aus.

  else if(request.action == TRADE_ACTION_REMOVE)
  {
    if(!OrderDelete((int)request.order))
    {
      result.retcode = GetLastError();
    }
    else
    {
      result.retcode = TRADE_RETCODE_DONE;
    }
  }

Letztendlich ist das Schließen einer Positionen zur Gegenposition im Kontext des MetaTrader 4 gleich dem Schließen von Gegenorders.

  else if(request.action == TRADE_ACTION_CLOSE_BY)
  {
    if(!OrderCloseBy((int)request.position, (int)request.position_by))
    {
      result.retcode = GetLastError();
    }
    else
    {
      result.retcode = TRADE_RETCODE_DONE;
    }
  }
  return true;
}

Neben OrderSend stellt MetaTrader 5 die asynchrone Funktion OrderSendAsync bereit. Wir werden sie nicht umsetzen, und werden alle Fälle der Verwendung des asynchronen Modus in der Bibliothek deaktivieren, d.h. wir ersetzen sie durch eine synchrone Variante.

Das Platzieren von Orders geht häufig mit dem Aufruf drei anderen Funktion einher: OrderCalcMargin, OrderCalcProfit und OrderCheck.‌

Hier ist eine der Varianten, wie man sie mit den in MetaTrader 4 verfügbaren Mitteln implementieren kann.

int EnumOrderType2Code(int action)

{   // ORDER_TYPE_BUY/ORDER_TYPE_SELL and derivatives   return (action % 2 == 0) ? OP_BUY : OP_SELL; }

bool OrderCalcMargin(   ENUM_ORDER_TYPE action,   string          symbol,   double          volume,   double          price,   double         &margin   ) {   int cmd = EnumOrderType2Code(action);   double m = AccountFreeMarginCheck(symbol, cmd, volume);   if(m <= 0 || GetLastError() == ERR_NOT_ENOUGH_MONEY)   {     return false;   }   margin = AccountFreeMargin() - m;   return true; }

bool OrderCalcProfit(   ENUM_ORDER_TYPE action,   string          symbol,   double          volume,   double          price_open,   double          price_close,   double         &profit   ) {   int cmd = EnumOrderType2Code(action);   if(cmd > -1)   {     int points = (int)((price_close - price_open) / MarketInfo(symbol, MODE_POINT));     if(cmd == OP_SELL) points = -points;     profit = points * volume * MarketInfo(symbol, MODE_TICKVALUE) / (MarketInfo(symbol, MODE_TICKSIZE) / MarketInfo(symbol, MODE_POINT));     return true;   }   return false; } bool OrderCheck(const MqlTradeRequest &request, MqlTradeCheckResult &result) {   if(request.volume > MarketInfo(request.symbol, MODE_MAXLOT)   || request.volume < MarketInfo(request.symbol, MODE_MINLOT)   || request.volume != MathFloor(request.volume / MarketInfo(request.symbol, MODE_LOTSTEP)) * MarketInfo(request.symbol, MODE_LOTSTEP))   {     result.retcode = TRADE_RETCODE_INVALID_VOLUME;     return false;   }   double margin;   if(!OrderCalcMargin(request.type, request.symbol, request.volume, request.price, margin))   {     result.retcode = TRADE_RETCODE_NO_MONEY;     return false;   }   if((request.action == TRADE_ACTION_DEAL || request.action == TRADE_ACTION_PENDING)   && SymbolInfoInteger(request.symbol, SYMBOL_TRADE_MODE) == SYMBOL_TRADE_EXECUTION_MARKET   && (request.sl != 0 || request.tp != 0))   {     result.retcode = TRADE_RETCODE_INVALID_STOPS;     return false;   }   result.balance = AccountBalance();   result.equity = AccountEquity();   result.profit = AccountEquity() - AccountBalance();   result.margin = margin;   result.margin_free = AccountFreeMargin();   result.margin_level = 0;   result.comment = "";   return true; }

Hier werden die eingebauten Funktionen AccountEquity, AccountFreeMargin und AccountFreeMarginCheck verwendet sowie der Punktpreis eines Symbols und weitere Einstellungen, die man durch den Aufruf von MarketInfo erhält.

Um die Gesamtzahl der Positionen zu erhalten, reicht es die Anzahl der offenen Marktorders zurückzugeben.

int PositionsTotal()
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() <= OP_SELL)
      {
        count++;
      }
    }
  }
  return count;
}

Um das Symbol einer Position nach ihrer Nummer zu erhalten, muss man alle Orders in der Schleife durchsuchen und dabei nur Marktorders zählen.

string PositionGetSymbol(int index)
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() <= OP_SELL)
      {
        if(index == count)
        {
          return OrderSymbol();
        }
        count++;
      }
    }
  }
  return "";
}

Die Funktion für das Erhalten des Tickets einer Position nach ihrer Nummer wird gleich erstellt.

ulong PositionGetTicket(int index)
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() <= OP_SELL)
      {
        if(index == count)
        {
          return OrderTicket();
        }
        count++;
      }
    }
  }
  return 0;
}

Für die Auswahl einer Position nach dem Namen des Symbols durchsuchen wir die Marktorders in der Schleife und bleiben wir bei der ersten, die nach dem Symbol übereinstimmt.

bool PositionSelect(string symbol)
{
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderSymbol() == symbol && (OrderType() <= OP_SELL))
      {
        return true;
      }
    }
  }
  return false;
}

Für die Implementierung der Auswahl einer Position nach dem Ticket wird keine Schleife benötigt.

bool PositionSelectByTicket(ulong ticket)
{
  if(OrderSelect((int)ticket, SELECT_BY_TICKET))
  {
    if(OrderType() <= OP_SELL)
    {
      return true;
    }
  }
  return false;
}

Die Eigenschaften der ausgewählten Position müssen drei Funktionen zurückgeben, die in MetaTrader 5 geläufig sind — _GetDouble, _GetInteger und _GetString. Hier führen wir ihre Implementierung für Positionen an, für Orders sehen sie sehr ähnlich aus, aus diesem Grund werden sie in diesem Artikel nicht erläutert. Wenn Sie möchten, können Sie sich ihren Code in der beigefügten Datei anschauen.

// Position = Order, nur OP_BUY oder OP_SELL
ENUM_POSITION_TYPE Order2Position(int type)
{
  return type == OP_BUY ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
}

bool PositionGetInteger(ENUM_POSITION_PROPERTY_INTEGER property_id, long &long_var)
{
  switch(property_id)
  {
    case POSITION_TICKET:
    case POSITION_IDENTIFIER:
      long_var = OrderTicket();
      return true;
    case POSITION_TIME:
    case POSITION_TIME_UPDATE:
      long_var = OrderOpenTime();
      return true;
    case POSITION_TIME_MSC:
    case POSITION_TIME_UPDATE_MSC:
      long_var = OrderOpenTime() * 1000;
      return true;
    case POSITION_TYPE:
      long_var = Order2Position(OrderType());
      return true;
    case POSITION_MAGIC:
      long_var = OrderMagicNumber();
      return true;
  }
  return false;
}

bool PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE property_id, double &double_var)
{
  switch(property_id)
  {
    case POSITION_VOLUME:
      double_var = OrderLots();
      return true;
    case POSITION_PRICE_OPEN:
      double_var = OrderOpenPrice();
      return true;
    case POSITION_SL:
      double_var = OrderStopLoss();
      return true;
    case POSITION_TP:
      double_var = OrderTakeProfit();
      return true;
    case POSITION_PRICE_CURRENT:
      double_var = MarketInfo(OrderSymbol(), OrderType() == OP_BUY ? MODE_BID : MODE_ASK);
      return true;
    case POSITION_COMMISSION:
      double_var = OrderCommission();
      return true;
    case POSITION_SWAP:
      double_var = OrderSwap();
      return true;
    case POSITION_PROFIT:
      double_var = OrderProfit();
      return true;
  }
  return false;
}

bool PositionGetString(ENUM_POSITION_PROPERTY_STRING property_id, string &string_var)
{
  switch(property_id)
  {
    case POSITION_SYMBOL:
      string_var = OrderSymbol();
      return true;
    case POSITION_COMMENT:
      string_var = OrderComment();
      return true;
  }
  return false;
}

Analog den Positionen, die Marktorders sind, sollte man auch den Set von Funktionen für die Bearbeitung von Pending Orders umsetzen. Hier gibt es aber eine Schwierigkeit. Wir können die Funktion OrdersTotal und andere OrderGet_ nicht umsetzen, denn diese sind bereits im Kern definiert, und man darf eingebaute Funktionen nicht neu definieren. Der Compiler gibt die folgende Meldung aus:

'OrderGetString' - override system function MT5Bridge.mqh

Aus diesem Grund müssen wir alle Funktionen umbenennen, deren Namen mit dem Präfix Order_ beginnen. Es scheint logisch, die Namen mit PendingOrder_ zu beginnen, denn diese Funktionen bearbeiten ausschließlich Pending Orders. Zum Beispiel:

int PendingOrdersTotal()
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() > OP_SELL)
      {
        count++;
      }
    }
  }
  return count;
}

Dann müssen alle Aufrufe im Code der Standardbibliothek durch unsere neuen Funktionen aus MT5Bridge.mqh ersetzt werden.

Die Funktion OrderGetTicket, die das Ticket einer Order nach ihrer Nummer zurückgibt, gibt es nicht in MetaTrader 4, deswegen lassen wir sie wie sie ist d.h. entsprechend der API MetaTrader 5.‌

ulong OrderGetTicket(int index)
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() > OP_SELL)
      {
        if(index == count)
        {
          return OrderTicket();
        }
        count++;
      }
    }
  }
  return 0;
}

Die OrderSelect Funktion existiert in MetaTrader 4 mit einer erweiterten Liste der Parameter im Vergleich zu MetaTrader 5. Aus diesem Grund behalten wir ihre Aufrufe und ergänzen sie durch den notwendigen Parameter SELECT_BY_TICKET.

Die vollständige Implementierung der Lesefunktionen für die Eigenschaften von Pending Orders ist in der beigefügt Header-Datei zu finden.‌

Gehen wir nun auf die Funktionen für das Arbeiten mit der Historie von Orders und Abschlüssen ein. Ihre Implementierung erfordert Phantasie. Die Variante unten, eine der vielen möglichen, wurde wegen ihrer Einfachheit ausgewählt.

Jede Marktorder des MetaTrader 4 wird in der Historie anhand zwei Orders à la MetaTrader 5 angezeigt: Einstieg und Ausstieg. Darüber hinaus muss auch in der Historie ein entsprechendes Paar von Abschlüssen vorhanden sein. Die Pending Orders werden wie sie sind angezeigt. Die Historie wird in zwei Arrays mit Tickets gespeichert.

int historyDeals[], historyOrders[];

Sie werden mithilfe der Funktion HistorySelect aus MQL5 API ausgefüllt.

bool HistorySelect(datetime from_date, datetime to_date)
{
  int deals = 0, orders = 0;
  ArrayResize(historyDeals, 0);
  ArrayResize(historyOrders, 0);
  for(int i = 0; i < OrdersHistoryTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
    {
      if(OrderOpenTime() >= from_date || OrderCloseTime() <= to_date)
      {
        if(OrderType() <= OP_SELL) // deal
        {
          ArrayResize(historyDeals, deals + 1);
          historyDeals[deals] = OrderTicket();
          deals++;
        }
        ArrayResize(historyOrders, orders + 1);
        historyOrders[orders] = OrderTicket();
        orders++;
      }
    }
  }
  return true;
}

Nach dem die Arrays ausgefüllt wurden, kann man die Größe der Historie erhalten.

int HistoryDealsTotal()
{
  return ArraySize(historyDeals) * 2;
}

int HistoryOrdersTotal()
{
  return ArraySize(historyOrders) * 2;
}

Die Array-Größen werden mit 2 multipliziert, denn jede MetaTrader 4 Order stellt entweder zwei Orders oder zwei Abschlüsse des MetaTrader 5 dar. Das gilt zwar nicht für Pending Orders, aber um den Ansatz einheitlich zu halten, reservieren wir 2 Tickets, dabei wird ein Ticket nicht verwendet (s. unten die Funktion HistoryOrderGetTicket). Der Abschluss, mit welchem man in den Markt einsteigt, bekommt das gleiche Ticket, wie seine Order in MetaTrader 4, infolge deren der Abschluss entsteht. Und für den Ausstieg werden wir dieses Ticket durch das höchstwertigen 1-Bit ergänzen.

ulong HistoryDealGetTicket(int index)
{
  if(OrderSelect(historyDeals[index / 2], SELECT_BY_TICKET, MODE_HISTORY))
  {
    // odd - enter - positive, even - exit - negative
    return (index % 2 == 0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000);
  }
  return 0;
}

Gerade Nummern in der Historie beinhalten immer Tickets zum Einstieg (reale), ungerade — zum Ausstieg (virtuelle).

Für Orders ist alles etwas komplizierter, denn es kann unter ihnen Pending Orders geben, die eins zu eins angezeigt werden. In diesem Fall gibt eine gerade Nummer das richtige Ticket einer Pending Order zurück, und die nächste ungerade — 0.‌

ulong HistoryOrderGetTicket(int index)
{
  if(OrderSelect(historyOrders[index / 2], SELECT_BY_TICKET, MODE_HISTORY))
  {
    if(OrderType() <= OP_SELL)
    {
      return (index % 2 == 0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000);
    }
    else if(index % 2 == 0) // pending order is returned once
    {
      return OrderTicket();
    }
    else
    {
      Print("History order ", OrderType(), " ticket[", index, "]=", OrderTicket(), " -> 0");
    }
  }
  return 0;
}

Die Auswahl eines Abschlusses nach dem Ticket wird unter Berücksichtigung dieser Besonderheit mit dem Setzen des höchstwertigen Bits implementiert — hier muss es weggelassen werden.

bool HistoryDealSelect(ulong ticket)
{
  ticket &= ~0x8000000000000000;
  return OrderSelect((int)ticket, SELECT_BY_TICKET, MODE_HISTORY);
}

Für Orders gilt das Gleiche.

#define HistoryOrderSelect HistoryDealSelect

Wenn wir einen Abschluss haben, der mit HistoryDealSelect oder HistoryDealGetTicket ausgewählt wurde, kann man die Implementierung der Funktionen schreiben, die den Zugang zu den Eigenschaften des Abschlusses gewährleisten.

#define REVERSE(type) ((type + 1) % 2)

ENUM_DEAL_TYPE OrderType2DealType(const int type)
{
  static ENUM_DEAL_TYPE types[] = {DEAL_TYPE_BUY, DEAL_TYPE_SELL, -1, -1, -1, -1, DEAL_TYPE_BALANCE};
  return types[type];
}

bool HistoryDealGetInteger(ulong ticket_number, ENUM_DEAL_PROPERTY_INTEGER property_id, long &long_var)
{
  bool exit = ((ticket_number & 0x8000000000000000) != 0);
  ticket_number &= ~0x8000000000000000;
  if(OrderSelect((int)ticket_number, SELECT_BY_TICKET, MODE_HISTORY))
  {
    switch(property_id)
    {
      case DEAL_TICKET:
      case DEAL_ORDER:
      case DEAL_POSITION_ID:
        long_var = OrderTicket();
        return true;
      case DEAL_TIME:
        long_var = exit ? OrderCloseTime() : OrderOpenTime();
        return true;
      case DEAL_TIME_MSC:
        long_var = (exit ? OrderCloseTime() : OrderOpenTime()) * 1000;
        return true;
      case DEAL_TYPE:
        long_var = OrderType2DealType(exit ? REVERSE(OrderType()) : OrderType());
        return true;
      case DEAL_ENTRY:
        long_var = exit ? DEAL_ENTRY_OUT : DEAL_ENTRY_IN;
        return true;
      case DEAL_MAGIC:
        long_var = OrderMagicNumber();
        return true;
    }
  }
  return false;
}
  
bool HistoryDealGetDouble(ulong ticket_number, ENUM_DEAL_PROPERTY_DOUBLE property_id, double &double_var)
{
  bool exit = ((ticket_number & 0x8000000000000000) != 0);
  ticket_number &= ~0x8000000000000000;
  switch(property_id)
  {
    case DEAL_VOLUME:
      double_var = OrderLots();
      return true;
    case DEAL_PRICE:
      double_var = exit ? OrderClosePrice() : OrderOpenPrice();
      return true;
    case DEAL_COMMISSION:
      double_var = exit? 0 : OrderCommission();
      return true;
    case DEAL_SWAP:
      double_var = exit ? OrderSwap() : 0;
      return true;
    case DEAL_PROFIT:
      double_var = exit ? OrderProfit() : 0;
      return true;
  }
  return false;
}

bool HistoryDealGetString(ulong ticket_number, ENUM_DEAL_PROPERTY_STRING property_id, string &string_var)
{
  switch(property_id)
  {
    case DEAL_SYMBOL:
      string_var = OrderSymbol();
      return true;
    case DEAL_COMMENT:
      string_var = OrderComment();
      return true;
  }
  return false;
}

Hoffentlich ist die Idee klar. Auf die gleiche Weise wird die Gruppe der Funktionen für das Arbeiten mit Orders in der Historie implementiert.

Änderungen in Dateien der Standardbibliothek

Manche notwendige Korrekturen der Bibliothek wurden bereits bei der Implementierung von Funktionen erörtert. Sie können die Dateien im Paket des MetaTrader 5 und die Dateien, die im Rahmen dieses Projekts erstellt wurden, selbst vergleichen, um eine vollständige Liste der Änderungen zu bekommen. Weiter werden nur die wichtigsten Momente betrachtet, Kommentare zu kleinen Korrekturen wurden weggelassen. Vielen Dateien wurde die neue Anweisung #include hinzugefügt, um MT5Bridge.mqh miteinzubeziehen.

Tabelle der grundlegenden Änderungen in Dateien der Standardbibliothek


Datei/Methode Änderungen
Trade.mqh SetAsyncMode String gelöscht, der den asynchronen Modus verwendet, denn der Modus wird nicht unterstützt
SetMarginMode Modus ACCOUNT_MARGIN_MODE_RETAIL_HEDGING explizit geschrieben
OrderOpen Kombination von Parametern, die den Expirationsmodus setzen, als SYMBOL_EXPIRATION_GTC | SYMBOL_EXPIRATION_SPECIFIED explizit geschrieben
OrderTypeCheck Fälle der Bearbeitung der nicht existierenden Typen ORDER_TYPE_BUY_STOP_LIMIT, ORDER_TYPE_SELL_STOP_LIMIT ausgeschlossen
OrderSend Aufruf der nicht vorhandenen asynchronen Funktion OrderSendAsync gelöscht
 
OrderInfo.mqh alle Aufrufe der Funktionen OrderGetInteger, OrderGetDouble und OrderGetString wurden durch gleichnamige Funktionen mit dem Präfix PendingOrder ersetzt
alle Aufrufe von OrderSelect(m_ticket) wurden durch OrderSelect((int)m_ticket, SELECT_BY_TICKET) ersetzt
 
PositionInfo.mqh FormatPosition
SelectByIndex
Margin-Modus ACCOUNT_MARGIN_MODE_RETAIL_HEDGING aktiviert
 
SymbolInfo.mqh Refresh viele Überprüfungen gelöscht, die in MetaTrader 4 nicht unterstützt werden
 
AccountInfo.mqh MarginMode gibt die Konstante ACCOUNT_MARGIN_MODE_RETAIL_HEDGING zurück
 
Expert.mqh TimeframeAdd
TimeframesFlags
Timeframes gelöscht, die in MetaTrader 4 nicht unterstützt werden
 
ExpertBase.mqh #include <Indicators\IndicatorsExt.mqh> hinzugefügt
SetMarginMode auf ACCOUNT_MARGIN_MODE_RETAIL_HEDGING unbedingt gesetzt


Die Datei IndicatorsExt.mqh wird für die Behebung kleiner Fehler in der Standarddatei Indicators.mqh benötigt. Darüber hinaus bezieht sie eine weitere für Indikatoren notwendige Header-Datei TimeSeriesExt.mqh mitein.‌

Die Datei TimeSeriesExt.mqh beinhaltet die Definition von Klassen, die für den Handel à la MetaTrader 5 benötigt werden, aber in der Standarddatei TimeSeries.mqh fehlen, die zusammen mit MetaTrader 4 bereitgestellt wird.

Unter anderem gehören dazu die Klassen: CTickVolumeBuffer, CSpreadBuffer, CiSpread, CiTickVolume, CRealVolumeBuffer, CiRealVolume. Viele davon sind Stubs, die keine Funktion haben (denn keine entsprechenden Funktionen sind in MetaTrader 4 verfügbar).

Testen

Wir haben die angepassten Handelsklassen der Standardbibliothek ins Verzeichnis Include MetaTrader 4 (mit dem Beibehalten der Hierarchie der Unterverzeichnisse) und MT5Bridge.mqh in den Ordner Include/Trade kopiert, nun können wir Experten, die vom MetaTrader 5 Wizard erzeugt wurden, in MetaTrader 4 kompilieren und starten.

MetaTrader 5 wird mit mehreren Beispielen für generierte Expert Advisors (im Ordner Experts/Advisors) bereitgestellt. Nehmen wir einen von ihnen — ExpertMACD.mq5. Kopieren wir ihn in den Ordner MQL4/Experts und benennen in ExpertMACD.mq4 um. Die Kompilation im Editor liefert ungefähr das folgende Ergebnis:

Der im MetaTrader 5 Wizard erzeugte Expert Advisor wird in MetaTrader 4 kompiliert

Der im MetaTrader 5 Wizard erzeugte Expert Advisor wird in MetaTrader 4 kompiliert

Man sieht dass die Dateien aus der Bibliothek miteinbezogen sind und dass sie ohne Fehler und Warnungen bearbeitet werden. Wenn es keine Kompilierungsfehler gibt, heißt das nicht automatisch, dass es keine Probleme in der Logik des Programms gibt, das wird aber weiter in der Praxis überprüft.

Starten wir den kompilierten Expert Advisor mit Standardeinstellungen im MetaTrader 4 Strategietester.

MetaTrader 4 Testbericht für den Expert Advisor, der in MetaTrader 5 generiert wurde

MetaTrader 4 Testbericht für den Expert Advisor, der in MetaTrader 5 generiert wurde

Wenn man will, kann man sich vergewissern, dass es keine Verarbeitungsfehler der Orders im Journal gibt.‌

Auf dem Chart EURUSD M15 sieht der Handel des Expert Advisors normal aus, inklusive das Setzen der Stop Loss und Take Profit Levels.

Ein Fenster mit dem Chart, dass die Arbeit eines Expert Advisors, der im MetaTrader 5 Wizard generiert wurde, in MetaTrader 4 veranschaulicht

Fenster mit dem Chart, der die Arbeit des Expert Advisors von dem MetaTrader 5 Wizard in MetaTrader 4

Vergleichen wir das mit den Ergebnissen des MetaTrader 5 Testers.

MetaTrader 5 Testbericht für den erzeugten Expert Advisor

MetaTrader 5 Testbericht für den erzeugten Expert Advisor

Offensichtlich gibt es Unterschiede. Man kann sie mit den Abweichungen in den Kursen selbst (z.B. wenn MetaTrader 5 einen variablen Spread verwendet) sowie in den Algorithmen des Testers erklären. Im Großen und Ganzen sind die Tests ähnlich: die Anzahl der Abschlüsse und der allgemeine Charakter der Kontostand-Kurve stimmen ungefähr überein.

Natürlich kann der Nutzer einen eigenen Expert Advisor im Wizard mit einem beliebigen Set von Modulen generieren, und es muss genauso einfach sein, ihn auf den MetaTrader 4 zu übertragen. Während des Testens des Projekts wurden unter anderem Expert Advisors mit Trailing und einer variablen Lotgröße.

Fazit

Wir haben eine der möglichen Methode der Übertragung der vom MetaTrader 5 Wizard generierten Expert Advisors auf die MetaTrader 4 Plattform betrachtet. Ihr größter Vorteil ist eine relativ einfache Implementierung, basierend auf der möglichst vollständigen Anwendung des vorhandenen Codes der Handelsklassen aus der Standardbibliothek des MetaTrader 5. Der größte Nachteil — die Notwendigkeit beide Terminals auf dem Rechner zu haben: eins für die Generierung von Expert Advisors und das zweite, um sie laufen zu lassen.

Im Anhang finden Sie zwei Dateien:

  • eine Zip-Datei mit den modifizierten Dateien der Standardbibliothek, die entpackt werden muss, mit dem Beibehalten der Hierarchie von Unterverzeichnissen, im Verzeichnis Include MetaTrader 4. In der Zip-Datei befinden sich nur die Dateien, die nicht mit dem Terminal bereitgestellt werden, deswegen besteht keine Gefahr die existierenden Dateien zu überschreiben;
  • die Datei MT5Bridge.mqh, die in den Ordner Include/Trade kopiert werden sollte.

Diese Version der Bibliothek wurde dem MetaTrader 5 Build 1545 entnommen. Zukünftige Versionen können Änderungen in der Standardbibliothek enthalten, die nützlich sein können (was eine erneute Synchronisierung mit den Korrekturen des Emulators erfordert). Im Idealfall wäre es toll, irgendwann eine Version der Standardbibliothek von MetaQuotes zu sehen, in welcher vom Anfang an zwei Varianten der Umsetzung von Handelsklassen mithilfe der Anweisungen der virtuellen Kompilation vereint wären — für MetaTrader 5 und MetaTrader 4.

Es ist anzumerken, dass die Umsetzung einer vollständigen Emulation der MetaTrader 5 Handelsumgebung in MetaTrader 4 nicht möglich ist. Das neue Terminal bietet neue Möglichkeiten, die es im alten auf Kernebene nicht gibt. Deswegen sind Situationen möglich, wenn ein oder das andere Modul, die in den generierten Expert Advisors verwendet werden, nicht funktionieren werden.

Man darf auch nicht vergessen, dass die vorgestellte Implementierung des Emulators als Beta-Version verbreitet wird und kann versteckte Fehler beinhalten. Nur ein langes und umfassendes Testen ermöglicht die Entwicklung einer Software für echtes Trading. Das Vorhandensein der Quellcodes ermöglicht, das gemeinsam zu tun.