English Русский
preview
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 4): Schwebende, virtuelle Aufträge und Speicherstatus

Entwicklung eines Expertenberaters für mehrere Währungen (Teil 4): Schwebende, virtuelle Aufträge und Speicherstatus

MetaTrader 5Handelssysteme | 22 Juli 2024, 10:08
12 0
Yuriy Bykov
Yuriy Bykov

Einführung

Im vorigen Artikel haben wir die Code-Architektur erheblich überarbeitet, um einen Multiwährungs-EA mit mehreren parallel arbeitenden Strategien zu erstellen. In dem Bemühen um Einfachheit und Klarheit haben wir bisher nur ein gewisses Mindestmaß an Funktionalität berücksichtigt. Selbst wenn man die Grenzen unserer Aufgabe berücksichtigt, haben wir den Code gegenüber den früheren Artikeln erheblich verändert.

Jetzt haben wir hoffentlich die Grundlagen geschaffen, die ausreichen, um die Funktionalität ohne radikale Änderungen am bereits geschriebenen Code zu erhöhen. Wir werden uns bemühen, nur dort ein Minimum an Bearbeitungen vorzunehmen, wo es wirklich notwendig ist.

Als weitere Entwicklung werden wir versuchen, Folgendes zu tun:

  • Hinzufügen der Möglichkeit, virtuelle, schwebende Aufträge (pending orders: Buy Stop, Sell Stop, Buy Limit, Sell Limit) und nicht nur virtuelle Positionen (Buy, Sell) zu eröffnen;
  • Hinzufügen einer einfachen Möglichkeit zur Visualisierung von platzierten virtuellen Aufträgen und Positionen, sodass wir beim Testen der korrekten Umsetzung der Regeln für die Eröffnung von Positionen/Aufträgen in den verwendeten Handelsstrategien visuell kontrollieren können;
  • die Speicherung aktueller Statusdaten durch den EA zu implementieren, sodass er bei einem Neustart des Terminals oder einem Wechsel des EA zu einem anderen Terminal seine Arbeit in dem Zustand fortsetzen kann, in dem er sich zum Zeitpunkt der Unterbrechung befand.

Beginnen wir mit der einfachsten Sache - der Bearbeitung virtueller, schwebender Aufträge.


Virtuelle schwebende Aufträge

Wir haben die Klasse CVirtualOrder geschaffen, um virtuelle Positionen zu behandeln. Wir können eine separate ähnliche Klasse erstellen, um virtuelle schwebende Aufträge zu behandeln. Wir wollen sehen, ob sich die Bearbeitung von Positionen wirklich so sehr von der Bearbeitung von Aufträgen unterscheidet.

Ihre Eigenschaften stimmen weitgehend überein. Bei schwebenden Aufträgen wird jedoch eine Eigenschaft hinzugefügt, die die Verfallszeit speichert. Aus diesem Grund fügen sie einen neuen Grund für die Schließung hinzu - bei Erreichen der Verfallszeit. Dementsprechend müssen wir bei jedem Tick überprüfen, ob der offene virtuelle, schwebende Auftrag aus diesem Grund geschlossen wurde, und wenn die Stop-Loss- und Take-Profit-Niveaus erreicht sind, sollte er nicht geschlossen werden.

Wenn der Kurs das Niveau der Auslösung eines virtuellen schwebenden Auftrags erreicht, sollte diese in eine offene, virtuelle Position umgewandelt werden. Hier wird bereits deutlich, dass wir bei der Implementierung in Form verschiedener Klassen zusätzliche Arbeit leisten müssen, um eine offene virtuelle Position zu erstellen und einen ausgelösten, virtuellen, schwebenden Auftrag zu löschen. Wenn wir sie als eine Klasse implementieren, müssen wir nur eine Eigenschaft des Klassenobjekts ändern - seinen Typ. Deshalb werden wir genau diese Option umsetzen.

Wie unterscheidet sich ein Auftrag sonst noch von einer Position? Wir müssen auch den Eröffnungskurs eines virtuellen, schwebenden Auftrags angeben. Bei einer virtuellen Position war die Angabe des Eröffnungskurses nicht erforderlich, da dieser automatisch aus den aktuellen Marktdaten ermittelt wurde. Jetzt machen wir ihn zu einem erforderlichen Parameter für die Öffnungsfunktion.

Fangen wir an, die Klasse zu erweitern. Wir werden hinzufügen:

  • die Eigenschaft m_expiration zum Speichern der Verfallszeit;
  • die logische Eigenschaft m_isExpired zur Angabe des Ablaufs einer schwebender Aufträge;
  • die Methode CheckTrigger(), um zu prüfen, ob ein schwebender Auftrag ausgelöst wurde, mehrere Methoden, um zu prüfen, ob ein bestimmtes Objekt zu einem bestimmten Typ gehört (schwebender Auftrag, Limit-Auftrag usw.);
  • die Bedingung, die besagt, dass „true“ auch in den Fällen zurückgegeben wird, in denen es sich um einen schwebenden Auftrag in der gewünschten Richtung handelt, unabhängig davon, ob es sich um eine Limit-Auftrag oder eine Stopp-Auftrag handelt - zu den Methoden zur Überprüfung, ob ein bestimmtes Objekt in der Richtung BUY oder SELL liegt; 
  • die Liste der Parameter der Methode Open() enthält den Eröffnungskurs und die Verfallszeit.

//+------------------------------------------------------------------+
//| Class of virtual orders and positions                            |
//+------------------------------------------------------------------+
class CVirtualOrder {
private:
   ...
//--- Order (position) properties
   ...
   datetime          m_expiration;     // Expiration time
   ...
   bool              m_isExpired;      // Expiration flag
   ...
//--- Private methods
   ...
   bool              CheckTrigger();   // Check for pending order trigger

public:
    ...

//--- Methods for checking the position (order) status
   ...
   bool              IsPendingOrder() {// Is it a pending order?
      return IsOpen() && (m_type == ORDER_TYPE_BUY_LIMIT
                          || m_type == ORDER_TYPE_BUY_STOP
                          || m_type == ORDER_TYPE_SELL_LIMIT
                          || m_type == ORDER_TYPE_SELL_STOP);
   }
   bool              IsBuyOrder() {    // Is it an open BUY position?
      return IsOpen() && (m_type == ORDER_TYPE_BUY
                          || m_type == ORDER_TYPE_BUY_LIMIT
                          || m_type == ORDER_TYPE_BUY_STOP);
   }
   bool              IsSellOrder() {   // Is it an open SELL position?
      return IsOpen() && (m_type == ORDER_TYPE_SELL 
                          || m_type == ORDER_TYPE_SELL_LIMIT
                          || m_type == ORDER_TYPE_SELL_STOP);
   }
   bool              IsStopOrder() {   // Is it a pending STOP order?
      return IsOpen() && (m_type == ORDER_TYPE_BUY_STOP || m_type == ORDER_TYPE_SELL_STOP);
   }
   bool              IsLimitOrder() {  // is it a pending LIMIT order?
      return IsOpen() && (m_type == ORDER_TYPE_BUY_LIMIT || m_type == ORDER_TYPE_SELL_LIMIT);
   }
   ...

//--- Methods for handling positions (orders)
   bool              CVirtualOrder::Open(string symbol,
                                         ENUM_ORDER_TYPE type,
                                         double lot,
                                         double price,
                                         double sl = 0,
                                         double tp = 0,
                                         string comment = "",
                                         datetime expiration = 0,
                                         bool inPoints = false); // Opening a position (order)

   ...
};

Fügen wir in der Methode Open() zum Öffnen einer virtuellen Position, die nun auch virtuelle schwebende Aufträge öffnet, die Zuweisung des offenen Preises an die Eigenschaft m_openPrice hinzu. Wenn sich herausstellt, dass es sich nicht um einen schwebenden Auftrag, sondern um eine Position handelt, fügen wir hinzu, dass der Eigenschaft der aktuelle Markteröffnungskurs zugewiesen wird:

bool CVirtualOrder::Open(string symbol,         // Symbol
                         ENUM_ORDER_TYPE type,  // Type (BUY or SELL)
                         double lot,            // Volume
                         double price = 0,      // Open price
                         double sl = 0,         // StopLoss level (price or points)
                         double tp = 0,         // TakeProfit level (price or points)
                         string comment = "",   // Comment
                         datetime expiration = 0,  // Expiration time
                         bool inPoints = false  // Are the SL and TP levels set in points?
                        ) {
   ...

   if(s_symbolInfo.Name(symbol)) {  // Select the desired symbol
      s_symbolInfo.RefreshRates();  // Update information about current prices

      // Initialize position properties
      m_openPrice = price;
     ...
      m_expiration = expiration;

      // The position (order) being opened is not closed by SL, TP or expiration
      ...
      m_isExpired = false;

      ...
      // Depending on the direction, set the opening price, as well as the SL and TP levels.
      // If SL and TP are specified in points, then we first calculate their price levels
      // relative to the open price
      if(IsBuyOrder()) {
         if(type == ORDER_TYPE_BUY) {
            m_openPrice = s_symbolInfo.Ask();
         }
         ...
      } else if(IsSellOrder()) {
         if(type == ORDER_TYPE_SELL) {
            m_openPrice = s_symbolInfo.Bid();
         }
         ...
      }

      ...

      return true;
   }
   return false;
}

In der Methode CheckTrigger(), die prüft, ob ein virtueller, schwebender Auftrag ausgelöst wurde, wird der aktuelle Marktpreis Bid oder Ask je nach Richtung des Auftrags ermittelt und geprüft, ob er den Eröffnungskurs auf der gewünschten Seite erreicht hat. Wenn ja, ersetzen wir die Eigenschaft m_type des aktuellen Objekts durch den Wert, der der Position der gewünschten Richtung entspricht, und teilen den Empfänger- und Strategieobjekten mit, dass eine neue virtuelle Position eröffnet wurde.

//+------------------------------------------------------------------+
//| Check whether a pending order is triggered                       |
//+------------------------------------------------------------------+
bool CVirtualOrder::CheckTrigger() {
   if(IsPendingOrder()) {
      s_symbolInfo.Name(m_symbol);     // Select the desired symbol
      s_symbolInfo.RefreshRates();     // Update information about current prices
      double price = (IsBuyOrder()) ? s_symbolInfo.Ask() : s_symbolInfo.Bid();
      int spread = s_symbolInfo.Spread();

      // If the price has reached the opening levels, turn the order into a position
      if(false
            || (m_type == ORDER_TYPE_BUY_LIMIT && price <= m_openPrice)
            || (m_type == ORDER_TYPE_BUY_STOP  && price >= m_openPrice)
        ) {
         m_type = ORDER_TYPE_BUY;
      } else if(false
                || (m_type == ORDER_TYPE_SELL_LIMIT && price >= m_openPrice)
                || (m_type == ORDER_TYPE_SELL_STOP  && price <= m_openPrice)
               ) {
         m_type = ORDER_TYPE_SELL;
      }

      // If the order turned into a position 
      if(IsMarketOrder()) {
         m_openPrice = price; // Remember the open price

         // Notify the recipient and the strategy of the position opening 
         m_receiver.OnOpen(GetPointer(this));
         m_strategy.OnOpen();
         return true;
      }
   }
   return false;
}

Diese Methode wird bei der Verarbeitung eines neuen Ticks in der Methode Tick() aufgerufen, wenn es sich tatsächlich um einen virtuellen, schwebenden Auftrag handelt:

//+------------------------------------------------------------------+
//| Handle a tick of a single virtual order (position)               |
//+------------------------------------------------------------------+
void CVirtualOrder::Tick() {
   if(IsOpen()) {  // If this is an open virtual position or order
      if(CheckClose()) {  // Check if SL or TP levels have been reached
         Close();         // Close when reached
      } else if (IsPendingOrder()) {   // If this is a pending order
         CheckTrigger();  // Check if it is triggered
      }
   }
}

In der Methode Tick() rufen wir die Methode CheckClose() auf, in der wir auch den Code hinzufügen müssen, der die Schließung eines virtuellen, schwebenden Auftrags auf der Grundlage der Verfallszeit überprüft:

//+------------------------------------------------------------------+
//| Check the need to close by SL, TP or EX                          |
//+------------------------------------------------------------------+
bool CVirtualOrder::CheckClose() {
   if(IsMarketOrder()) {               // If this is a market virtual position,
      ...
      // Check that the price has reached SL or TP
      ...
   } else if(IsPendingOrder()) {    // If this is a pending order
      // Check if the expiration time has been reached, if one is specified 
      if(m_expiration > 0 && m_expiration < TimeCurrent()) {
         m_isExpired = true;
         return true;
      }
   }
   return false;
}

Die Änderungen speichern wir in der Datei VirtualOrder.mqh im Ordner currebt.

Kehren wir nun zu unserer Handelsstrategieklasse CSimpleVolumesStrategy zurück. In dieser Strategie haben wir den Spielraum für künftige Änderungen gelassen, um die Unterstützung für die Bearbeitung virtueller, schwebender Aufträge hinzuzufügen. Es gab solche Stellen in den Methoden OpenBuyOrder() und OpenSellOrder(). Fügen wir hier den Aufruf der Methode Open() mit Parametern hinzu, die zur Eröffnung virtueller, schwebender Aufträge führen. Zunächst wird der Eröffnungskurs ausgehend vom aktuellen Kurs berechnet und um die durch den Parameter m_openDistance angegebene Anzahl von Punkten in die gewünschte Richtung verschoben. Wir präsentieren den Code nur für die Methode OpenBuyOrder(). In einem anderen Code werden die Bearbeitungen ähnlich sein.

//+------------------------------------------------------------------+
//| Open BUY order                                                   |
//+------------------------------------------------------------------+
void CSimpleVolumesStrategy::OpenBuyOrder() {
// Update symbol current price data
   ...
// Retrieve the necessary symbol and price data
   ...

// Let's make sure that the opening distance is not less than the spread
   int distance = MathMax(m_openDistance, spread);

// Opening price
   double price = ask + distance * point;

// StopLoss and TakeProfit levels
   ...

// Expiration time
   datetime expiration = TimeCurrent() + m_ordersExpiration * 60;

   ...
   for(int i = 0; i < m_maxCountOfOrders; i++) {   // Iterate through all virtual positions
      if(!m_orders[i].IsOpen()) {                  // If we find one that is not open, then open it
         if(m_openDistance > 0) {
            // Set SELL STOP pending order
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY_STOP, m_fixedLot,
                                   NormalizeDouble(price, digits),
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits),
                                   "", expiration);

         } else if(m_openDistance < 0) {
            // Set SELL LIMIT pending order
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY_LIMIT, m_fixedLot,
                                   NormalizeDouble(price, digits),
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits),
                                   "", expiration);

         } else {
            // Open a virtual SELL position
            res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot,
                                   0,
                                   NormalizeDouble(sl, digits),
                                   NormalizeDouble(tp, digits));

         }
         break; // and exit
      }
   }
  ...
}

Speichern wir die Änderungen in der Datei SimpleVolumesStrategy.mqh des aktuellen Ordners.

Damit sind die Änderungen abgeschlossen, die notwendig sind, um den Betrieb von Strategien mit virtuellen schwebenden Aufträgen zu unterstützen. Wir haben nur an zwei Dateien Änderungen vorgenommen und können nun die SimpleVolumesExpertSingle.mq5 EA kompilieren. Wenn der Parameter openDistance_ ungleich Null gesetzt wird, sollte der EA virtuelle schwebende Aufträge anstelle von virtuellen Positionen öffnen. Allerdings wird die Eröffnung nicht auf dem Chart zu sehen sein. Wir können die entsprechenden Meldungen nur im Protokoll sehen. Sie werden erst dann auf dem Chart zu sehen sein, wenn sie in eine offene virtuelle Position umgewandelt worden sind, die von den Empfängern der virtuellen Handelsvolumina auf den Markt gebracht wird.

Es wäre gut, wenn man die platzierten virtuellen schwebenden Aufträge auf dem Chart sehen könnte. Wir werden später auf dieses Thema zurückkommen, aber jetzt wollen wir uns einem wichtigeren Thema zuwenden - der Sicherstellung, dass der Status des EA nach einem Neustart gespeichert und geladen wird.


Status der Speicherung

Wir haben zwei EAs auf der Grundlage der entwickelten Klassen erstellt. Der erste (SimpleVolumesExpertSingle.mq5) sollte die Parameter einer einzelnen Instanz einer Handelsstrategie optimieren, und der zweite (SimpleVolumesExpert.mq5) enthielt bereits mehrere Kopien der Handelsstrategie mit den besten Parametern, die mithilfe des ersten EA ausgewählt wurden. In Zukunft soll nur der zweite EA auf realen Konten eingesetzt werden, der erste ist nur für den Einsatz im Strategietester vorgesehen. Daher müssen wir nur den Status im zweiten EA oder anderen, die auch viele Instanzen von Handelsstrategien enthalten, laden und speichern.

Als Nächstes sollte klargestellt werden, dass es jetzt um das Speichern und Laden des EA-Zustands geht und nicht um die verschiedenen Strategiesätze, die im EA verwendet werden sollen. Mit anderen Worten: Die Menge der Strategieinstanzen mit bestimmten Parametern im EA ist fest und bleibt bei jedem Start gleich. Nach dem ersten Start eröffnet der EA virtuelle und reale Positionen und berechnet eventuell einige Indikatoren auf der Grundlage von Kursdaten. Diese Informationen bilden den EA-Status als Ganzes. Wenn wir nun das Terminal neu starten, sollte der EA die offenen Positionen als seine eigenen erkennen, sowie alle seine virtuellen Positionen und die notwendigen berechneten Werte wiederherstellen. Während Informationen über offene Positionen vom Terminal abgerufen werden können, sollte der EA Informationen über virtuelle Positionen und berechnete Werte unabhängig speichern.

Bei der von uns betrachteten einfachen Handelsstrategie besteht keine Notwendigkeit, berechnete Daten zu akkumulieren, sodass der EA-Status ausschließlich durch eine Reihe virtueller Positionen und schwebender Aufträge bestimmt wird. Es reicht jedoch nicht aus, nur das Array mit allen Objekten der Klasse CVirtualOrder in der Datei zu speichern.

Stellen wir uns vor, dass wir bereits mehrere EAs mit unterschiedlichen Handelsstrategien haben. Die Gesamtzahl der Objekte der Klasse CVirtualOrder, die von jedem EA erstellt wurden, war jedoch identisch. Zum Beispiel haben wir in jedem Fall 9 Instanzen von Handelsstrategien verwendet, die 3 virtuelle Positionsobjekte anfordern. Dann wird jeder EA Informationen über 27 Objekte der Klasse CVirtualOrder speichern. In diesem Fall müssen wir uns irgendwie gegen die Tatsache absichern, dass einer der EAs Informationen nicht über seine 27 virtuellen Positionen, sondern über andere hochlädt. 

Zu diesem Zweck können wir der gespeicherten Datei Informationen über die Parameter der Strategien, die als Teil dieses EAs arbeiten, und möglicherweise auch Informationen über die Parameter des EAs selbst hinzufügen.

Überlegen wir nun, zu welchen Zeitpunkten der Zustand gespeichert werden soll. Wenn die Strategieparameter fest sind, sind sie zu jedem Zeitpunkt identisch. Gleichzeitig können die Objekte virtueller Positionen die Werte ihrer Eigenschaften während der Öffnungs- und Schließvorgänge ändern. Es ist daher sinnvoll, den Status nach diesen Vorgängen zu speichern. Häufigeres Speichern (z. B. bei jedem Tick oder durch einen Timer) scheint im Moment überflüssig.

Lassen Sie uns mit der Umsetzung fortfahren. Da wir eine hierarchische Struktur von Objekten wie

  • EA
    • Strategien
      • virtuelle Positionen

unseren Teil des Sparens an jede Ebene delegieren, anstatt alles auf die oberste Ebene zu konzentrieren, obwohl dies möglich ist.

Fügen wir auf jeder Ebene zwei Methoden zu den Klassen Save() und Load() hinzu, die für das Speichern bzw. Laden zuständig sind. Auf der obersten Ebene öffnen diese Methoden eine Datei, und auf niedrigeren Ebenen erhalten sie einen Deskriptor einer bereits geöffneten Datei als Parameter. Die Frage der Wahl eines Dateinamens für die Speicherung wird also nur auf der EA-Ebene behandelt. Diese Frage stellt sich nicht auf der Ebene der Strategie und der virtuellen Positionen.


EA-Änderung

Die Klasse CVirtualAdvisor erhält das Feld m_name zum Speichern des EA-Namens. Da sie sich während des Betriebs nicht ändern wird, weisen wir sie im Konstruktor zu. Es ist auch sinnvoll, den Namen sofort um die MagicNummer und das optionale Suffix „.test“ zu erweitern, falls der EA im visuellen Testmodus gestartet wird.

Um das Speichern nur bei Änderungen in der Zusammensetzung der virtuellen Positionen zu implementieren, fügen wir das Feld m_lastSaveTime hinzu, in dem der Zeitpunkt der letzten Speicherung gespeichert wird. 

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...
   
   string            m_name;           // EA name
   datetime          m_lastSaveTime;   // Last save time

public:
   CVirtualAdvisor(ulong p_magic = 1, string p_name = ""); // Constructor
   ...

   virtual bool      Save();           // Save status
   virtual bool      Load();           // Load status
};

Bei der Erstellung eines Expert Advisors werden zwei neue Eigenschaften wie folgt mit Anfangswerten versehen:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1, string p_name = "") :
   ...
   m_lastSaveTime(0) {
   m_name = StringFormat("%s-%d%s.csv",
                         (p_name != "" ? p_name : "Expert"),
                         p_magic,
                         (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                        );
};

Platzieren wir die Logik zur Überprüfung, ob das Speichern notwendig ist, innerhalb der Methode Save(). Wir können den Aufruf dieser Methode einfach bei jedem Tick hinzufügen, nachdem wir die übrigen Aktionen ausgeführt haben:

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Receiver handles virtual positions
   m_receiver.Tick();

// Start handling in strategies
   CAdvisor::Tick();

// Adjusting market volumes
   m_receiver.Correct();

// Save status
   Save();
}

In der Speichermethode müssen wir zunächst prüfen, ob wir sie ausführen müssen. Dazu müssen wir im Voraus vereinbaren, dass wir dem Empfängerobjekt eine neue Eigenschaft hinzufügen, in der der Zeitpunkt der letzten Änderung der offenen virtuellen Positionen oder der Zeitpunkt der letzten Korrektur der realen offenen Volumina gespeichert wird. Wenn der Zeitpunkt der letzten Speicherung kürzer ist als der Zeitpunkt der letzten Korrektur, dann sind Änderungen eingetreten, die wir speichern müssen.

Außerdem werden Änderungen nicht gespeichert, wenn gerade eine Optimierung oder ein Einzeltest durchgeführt wird, ohne den visuellen Modus zu verwenden. Wenn die Prüfung gerade im visuellen Modus läuft, wird die Speicherung durchgeführt. Dadurch können wir auch die Speicherung im Strategietester überprüfen.

So könnte die Funktion Save() auf EA-Ebene aussehen: Wir prüfen, ob eine Speicherung erforderlich ist, und speichern dann die aktuelle Zeit und die Anzahl der Strategien. Danach rufen wir die Speichermethode aller Strategien in einer Schleife auf.

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Save() {
   bool res = true;

   // Save status if:
   if(true
         // later changes appeared
         && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime
         // currently, there is no optimization
         && !MQLInfoInteger(MQL_OPTIMIZATION)
         // and there is no testing at the moment or there is a visual test at the moment
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_WRITE, '\t');

      if(f != INVALID_HANDLE) {  // If file is open, save
         FileWrite(f, CVirtualReceiver::s_lastChangeTime);  // Time of last changes
         FileWrite(f, ArraySize(m_strategies));             // Number of strategies

         // All strategies
         FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save(f));

         FileClose(f);

         // Update the last save time
         m_lastSaveTime = CVirtualReceiver::s_lastChangeTime;
         PrintFormat(__FUNCTION__" | OK at %s to %s",
                     TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS), m_name);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d",
                     m_name, GetLastError());
         res = false;
      }
   }
   return res;
}

Nach dem Speichern der Strategien wird der Zeitpunkt des letzten Speicherns entsprechend dem Zeitpunkt der letzten Änderungen der virtuellen Positionen aktualisiert. Nun wird die Speichermethode bis zur nächsten Änderung nichts in der Datei speichern.

Die Statuslademethode Load() sollte eine ähnliche Aufgabe erfüllen, aber anstatt zu schreiben, werden die Daten aus der Datei gelesen. Mit anderen Worten, wir lesen zunächst die gespeicherte Zeit und die Anzahl der Strategien ab. Hier können wir überprüfen, ob die Anzahl der gelesenen Strategien mit der Anzahl der dem EA hinzugefügten Strategien übereinstimmt, nur für alle Fälle. Wenn nicht, ist es sinnlos, weiterzulesen, da es sich um eine falsche Datei handelt. Wenn ja, dann ist alles gut, wir können fortfahren zu lesen. Dann delegieren wir die weitere Arbeit wieder an Objekte der nächsten Hierarchiestufe: Wir gehen alle hinzugefügten Strategien durch und rufen ihre Lesemethode aus der geöffneten Datei auf.

Im Code könnte das etwa so aussehen:

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualAdvisor::Load() {
   bool res = true;

   // Load status if:
   if(true
         // file exists
         && FileIsExist(m_name)
         // currently, there is no optimization
         && !MQLInfoInteger(MQL_OPTIMIZATION)
         // and there is no testing at the moment or there is a visual test at the moment
         && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE))
     ) {
      int f = FileOpen(m_name, FILE_CSV | FILE_READ, '\t');

      if(f != INVALID_HANDLE) {  // If the file is open, then load
         m_lastSaveTime = FileReadDatetime(f);     // Last save time
         PrintFormat(__FUNCTION__" | LAST SAVE at %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS));

         // Number of strategies
         long f_strategiesCount = StringToInteger(FileReadString(f));

         // Does the loaded number of strategies match the current one?
         res = (ArraySize(m_strategies) == f_strategiesCount);

         if(res) {
            // Load all strategies
            FOREACH(m_strategies, res &= ((CVirtualStrategy*) m_strategies[i]).Load(f));

            if(!res) {
               PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_name);
            }
         } else {
            PrintFormat(__FUNCTION__" | ERROR: Wrong strategies count (%d expected but %d found in file %s)",
                        ArraySize(m_strategies), f_strategiesCount, m_name);
         }
         FileClose(f);
      } else {
         PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError());
         res = false;
      }
   }
   return res;
}

Wir speichern die an der Datei VirtualAdvisor.mqh vorgenommenen Änderungen im aktuellen Ordner.

Wir sollten die Status-Loading-Methode nur einmal beim Starten des EA aufrufen, aber wir können dies nicht im Konstruktor des EA-Objekts tun, da zu diesem Zeitpunkt noch keine Strategien zum EA hinzugefügt wurden. Also, erledigen wir das in der EA-Datei der in der Funktion OnInit() tun, nachdem wir alle Strategie-Instanzen zum EA-Objekt hinzugefügt haben:

CVirtualAdvisor     *expert;                  // EA object

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Create and fill the array of strategy instances
   CStrategy *strategies[9];
   strategies[0] = ...
   ...
   strategies[8] = ...

// Create an EA handling virtual positions
   expert = new CVirtualAdvisor(magic_, "SimpleVolumes");

// Add strategies to the EA
   FOREACH(strategies, expert.Add(strategies[i]));

// Load the previous state if available   
   expert.Load();

   return(INIT_SUCCEEDED);
}

Speichern wir die Änderungen in der Datei SimpleVolumesExpert.mq5 im aktuellen Ordner.


Grundlegende Strategieänderung

Wir fügen der Basisklasse einer Handelsstrategie die Methoden Save() und Load() sowie die Methode zur Umwandlung des aktuellen Strategieobjekts in einen String hinzu. Der Einfachheit halber implementieren wir diese Methode als einen überladenen unären Operator ~ (Tilde).

//+------------------------------------------------------------------+
//| Class of a trading strategy with virtual positions               |
//+------------------------------------------------------------------+
class CVirtualStrategy : public CStrategy {
   ...

public:
   ...

   virtual bool      Load(const int f);   // Load status
   virtual bool      Save(const int f);   // Save status

   string operator~();                    // Convert object to string
};

Die Methode zur Umwandlung eines Objekts in eine Zeichenkette gibt eine Zeichenkette mit dem Namen der aktuellen Klasse und der Anzahl der Elemente des Arrays der virtuellen Positionen zurück:

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CVirtualStrategy::operator~() {
   return StringFormat("%s(%d)", typename(this), ArraySize(m_orders));
}

Der Deskriptor der zum Schreiben geöffneten Datei wird an die Methode Save() übergeben. Die vom Objekt bei der Konvertierung in eine Zeichenkette erhaltene Zeichenkette wird zuerst in die Datei geschrieben. Dann wird die Speichermethode in einer Schleife für jede virtuelle Position aufgerufen.

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualStrategy::Save(const int f) {
   bool res = true;
   FileWrite(f, ~this); // Save parameters

   // Save virtual positions (orders) of the strategy
   FOREACH(m_orders, res &= m_orders[i].Save(f));

   return res;
}

Die Methode Load() liest die Daten einfach in der gleichen Reihenfolge, in der sie geschrieben wurden, und stellt dabei sicher, dass die Parameterzeichenfolge in der Datei und in der Strategie übereinstimmen:

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualStrategy::Load(const int f) {
   bool res = true;
   // Current parameters are equal to read parameters   
   res = (~this == FileReadString(f));
   
   // If yes, then load the virtual positions (orders) of the strategy
   if(res) {
      FOREACH(m_orders, res &= m_orders[i].Load(f));
   }

   return res;
}

Wir speichern die vorgenommenen Änderungen in der Datei VirtualStrategy.mqh im aktuellen Ordner.


Änderung der Handelsstrategie

In der Klasse CSimpleVolumesStrategy der spezifischen Handelsstrategie müssen wir die gleiche Reihe von Methoden wie in der Basisklasse hinzufügen:

//+------------------------------------------------------------------+
//| Trading strategy using tick volumes                               |
//+------------------------------------------------------------------+
class CSimpleVolumesStrategy : public CVirtualStrategy {
   ...

public:
   ...

   virtual bool      Load(const int f) override;   // Load status
   virtual bool      Save(const int f) override;   // Save status

   string operator~();                    // Convert object to string
};

Bei der Methode zur Umwandlung einer Handelsstrategie in eine Zeichenkette wird das Ergebnis aus dem Namen der Klasse und der Auflistung aller wichtigen Parameter generiert. Der Einfachheit halber fügen wir hier die Werte aller Parameter ein, aber in Zukunft kann es für uns bequemer sein, diese Liste zu kürzen. Dies ermöglicht es uns, den EA mit leicht veränderten Parametern neu zu starten, ohne alle zuvor offenen Marktpositionen vollständig zu schließen. Wenn wir zum Beispiel den TakeProfit-Parameter erhöhen, können wir offene Positionen mit einem niedrigeren TakeProfit-Niveau ohne Probleme lassen wie sie sind.

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CSimpleVolumesStrategy::operator~() {
   return StringFormat("%s(%s,%s,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)",
                       // Strategy instance parameters
                       typename(this), m_symbol, EnumToString(m_timeframe), m_fixedLot,
                       m_signalPeriod, m_signalDeviation, m_signaAddlDeviation,
                       m_openDistance, m_stopLevel, m_takeLevel, m_ordersExpiration,
                       m_maxCountOfOrders
                      );
}

Es stellte sich heraus, dass dies ein weiterer Punkt ist, an dem wir alle Strategieparameter erneut in den Code schreiben müssen. Wir werden uns daran erinnern, bis wir mit den Eingaben arbeiten können.

Die Methode Save() erweist sich als sehr übersichtlich, da die Basisklasse die Hauptarbeit übernimmt:

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::Save(const int f) {
   bool res = true;
   FileWrite(f, ~this);                // Save parameters
   res &= CVirtualStrategy::Save(f);   // Save strategy
   return res;
}

Die Methode Load() wird etwas größer sein, was aber hauptsächlich auf die bessere Lesbarkeit des Codes zurückzuführen ist:

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CSimpleVolumesStrategy::Load(const int f) {
   bool res = true;
   string currentParams = ~this;             // Current parameters
   string loadedParams = FileReadString(f);  // Read parameters

   PrintFormat(__FUNCTION__" | %s", loadedParams);

   res = (currentParams == loadedParams);

   // Load if read parameters match the current ones
   if(res) {
      res &= CVirtualStrategy::Load(f);
   }

   return res;
}

Speichern wir die Änderungen in der Datei SimpleVolumesExpert.mqh des aktuellen Ordners.


Änderung der virtuellen Positionen

Wir müssen drei Methoden für die Klasse CVirtualOrder der virtuellen Positionen hinzufügen:

class CVirtualOrder {
   ...

   virtual bool      Load(const int f);   // Load status
   virtual bool      Save(const int f);   // Save status

   string            operator~();         // Convert object to string
};

Wir werden die Methode der Konvertierung in eine Zeichenkette beim Speichern in eine Datei nicht verwenden, aber sie wird beim Protokollieren der geladenen Objektdaten nützlich sein:

//+------------------------------------------------------------------+
//| Convert an object to a string                                    |
//+------------------------------------------------------------------+
string CVirtualOrder::operator~() {
   if(IsOpen()) {
      return StringFormat("#%d %s %s %.2f in %s at %.5f (%.5f, %.5f). %s, %f",
                          m_id, TypeName(), m_symbol, m_lot,
                          TimeToString(m_openTime), m_openPrice,
                          m_stopLoss, m_takeProfit,
                          TimeToString(m_closeTime), m_closePrice);
   } else {
      return StringFormat("#%d --- ", m_id);
   }

}

Wir könnten dies jedoch später ändern, indem wir die Methode zum Lesen von Objekteigenschaften aus einer Zeichenkette hinzufügen.

In der Methode Save() werden schließlich weitere wichtige Informationen in die Datei geschrieben:

//+------------------------------------------------------------------+
//| Save status                                                      |
//+------------------------------------------------------------------+
bool CVirtualOrder::Save(const int f) {
   FileWrite(f, m_id, m_symbol, m_lot, m_type, m_openPrice,
             m_stopLoss, m_takeProfit,
             m_openTime, m_closePrice, m_closeTime,
             m_expiration, m_comment, m_point);
   return true;
}

Load() liest dann nicht nur, was geschrieben wurde, und füllt die notwendigen Eigenschaften mit den gelesenen Informationen, sondern informiert auch die zugehörigen Strategieobjekte und den Empfänger darüber, ob diese virtuelle Position (schw. Auftrag) offen oder geschlossen ist:

//+------------------------------------------------------------------+
//| Load status                                                      |
//+------------------------------------------------------------------+
bool CVirtualOrder::Load(const int f) {
   m_id = (ulong) FileReadNumber(f);
   m_symbol = FileReadString(f);
   m_lot = FileReadNumber(f);
   m_type = (ENUM_ORDER_TYPE) FileReadNumber(f);
   m_openPrice = FileReadNumber(f);
   m_stopLoss = FileReadNumber(f);
   m_takeProfit = FileReadNumber(f);
   m_openTime = FileReadDatetime(f);
   m_closePrice = FileReadNumber(f);
   m_closeTime = FileReadDatetime(f);
   m_expiration = FileReadDatetime(f);
   m_comment = FileReadString(f);
   m_point = FileReadNumber(f);

   PrintFormat(__FUNCTION__" | %s", ~this);

// Notify the recipient and the strategy that the position (order) is open
   if(IsOpen()) {
      m_receiver.OnOpen(GetPointer(this));
      m_strategy.OnOpen();
   } else {
      m_receiver.OnClose(GetPointer(this));
      m_strategy.OnClose();
   }

   return true;
}

Wir speichern den erhaltenen Code in der Datei VirtualOrder.mqh des aktuellen Ordners.


Testen des Speicherns

Nun wollen wir das Speichern und Laden von Statusdaten testen. Um nicht auf einen günstigen Zeitpunkt für die Eröffnung von Positionen zu warten, nehmen wir vorübergehende Änderungen an unserer Handelsstrategie vor, die uns zwingen, beim Start eine Position oder einen schwebenden Auftrag zu eröffnen, wenn noch keine offenen Positionen/Aufträge vorhanden sind.

Da wir die Anzeige von Meldungen über den Ladefortschritt hinzugefügt haben, werden wir, wenn wir den EA neu starten (am einfachsten geht das, indem wir ihn einfach neu kompilieren), in den Protokollen etwas wie das Folgende sehen:

CVirtualAdvisor::Load | LAST SAVE at 2027.02.23 08:05:33

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.06,13,0.30,1.00,0,10500.00,465.00,1000,3)
CVirtualOrder::Load | Order#1 EURGBP 0.06 BUY in 2027.02.23 08:02 at 0.85494 (0.75007, 0.85985). 1970.01.01 00:00, 0.000000
CVirtualReceiver::OnOpen#EURGBP | OPEN VirtualOrder #1
CVirtualOrder::Load | Order#2  ---
CVirtualOrder::Load | Order#3  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.11,17,1.70,0.50,210,16500.00,220.00,1000,3)
CVirtualOrder::Load | Order#4 EURGBP 0.11 BUY STOP in 2027.02.23 08:02 at 0.85704 (0.69204, 0.85937). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#5  ---
CVirtualOrder::Load | Order#6  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,0.06,51,0.50,1.10,500,19500.00,370.00,22000,3)
CVirtualOrder::Load | Order#7 EURGBP 0.06 BUY STOP in 2027.02.23 08:02 at 0.85994 (0.66494, 0.86377). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#8  ---
CVirtualOrder::Load | Order#9  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.04,80,1.10,0.20,0,6000.00,1190.00,1000,3)
CVirtualOrder::Load | Order#10 GBPUSD 0.04 BUY in 2027.02.23 08:02 at 1.26632 (1.20638, 1.27834). 1970.01.01 00:00, 0.000000
CVirtualReceiver::OnOpen#GBPUSD | OPEN VirtualOrder #10
CVirtualOrder::Load | Order#11  ---
CVirtualOrder::Load | Order#12  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.11,128,2.00,0.90,220,2000.00,1170.00,1000,3)
CVirtualOrder::Load | Order#13 GBPUSD 0.11 BUY STOP in 2027.02.23 08:02 at 1.26852 (1.24852, 1.28028). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#14  ---
CVirtualOrder::Load | Order#15  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(GBPUSD,PERIOD_H1,0.07,13,1.50,0.80,550,2500.00,1375.00,1000,3)
CVirtualOrder::Load | Order#16 GBPUSD 0.07 BUY STOP in 2027.02.23 08:02 at 1.27182 (1.24682, 1.28563). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#17  ---
CVirtualOrder::Load | Order#18  ---

CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.04,24,0.10,0.30,330,7500.00,2400.00,24000,3)
CVirtualOrder::Load | Order#19 EURUSD 0.04 BUY STOP in 2027.02.23 08:02 at 1.08586 (1.01086, 1.10990). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#20  ---
CVirtualOrder::Load | Order#21  ---
CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.05,18,0.20,0.40,220,19500.00,1480.00,6000,3)
CVirtualOrder::Load | Order#22 EURUSD 0.05 BUY STOP in 2027.02.23 08:02 at 1.08476 (0.88976, 1.09960). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#23  ---
CVirtualOrder::Load | Order#24  ---
CSimpleVolumesStrategy::Load | class CSimpleVolumesStrategy(EURUSD,PERIOD_H1,0.05,128,0.70,0.30,550,3000.00,170.00,42000,3)
CVirtualOrder::Load | Order#25 EURUSD 0.05 BUY STOP in 2027.02.23 08:02 at 1.08806 (1.05806, 1.08980). 1970.01.01 00:00, 0.000000
CVirtualOrder::Load | Order#26  ---
CVirtualOrder::Load | Order#27  ---

CVirtualAdvisor::Save | OK at 2027.02.23 08:19:48 to SimpleVolumes-27182.csv

Wie wir sehen können, werden die Daten über offene virtuelle Positionen und schwebende Aufträge erfolgreich geladen. Bei virtuellen Positionen wird der Open Event Handler aufgerufen, der ggf. reale Marktpositionen öffnet, z.B. wenn diese manuell geschlossen wurden.

Im Allgemeinen ist das Verhalten des EA in Bezug auf offene Positionen während eines Neustarts eine ziemlich komplexe Angelegenheit. Wenn wir z.B. die MagicNummer ändern wollen, wird der EA die zuvor geöffneten Handelsgeschäfte nicht mehr als seine eigenen betrachten. Sollten sie zwangsweise geschlossen werden? Wenn wir die EA-Version ersetzen wollen, müssen wir möglicherweise das Vorhandensein gespeicherter virtueller Positionen vollständig ignorieren und alle offenen Positionen zwangsweise schließen. Wir sollten jedes Mal entscheiden, welches Szenario am besten zu uns passt. Diese Fragen sind noch nicht sehr dringlich, daher werden wir sie auf später verschieben.


Visualisierung

Nun ist es an der Zeit, virtuelle Positionen und schwebende Aufträge zu visualisieren. Auf den ersten Blick scheint es ganz natürlich, sie als Erweiterung der virtuellen Positionsklasse CVirtualOrder zu implementieren. Es verfügt bereits über alle notwendigen Informationen über das visualisierte Objekt. Sie weiß besser als jeder andere, wann sie sich selbst umgestalten muss. Der erste Umsetzungsentwurf wurde genau auf diese Weise erstellt. Doch dann tauchten sehr unangenehme Fragen auf.

Eine dieser Fragen lautet: „Auf welcher Chat wollen wir die Visualisierung durchführen?“ Die einfachste Antwort ist die aktuelle. Aber es ist nur für den Fall geeignet, dass der EA auf einem Symbol arbeitet und dieses mit dem Chart-Symbol übereinstimmt, auf dem der EA gestartet wird. Sobald es mehrere Symbole gibt, wird es sehr unpraktisch, alle virtuellen Positionen auf dem SymbolChart anzuzeigen. Das Chart wird zu einem Durcheinander.

Mit anderen Worten: Das Problem der Auswahl eines Charts für die Anzeige erforderte eine Lösung, die jedoch nicht mehr mit der Hauptfunktionalität der Objekte der Klasse CVirtualOrder zusammenhing. Sie haben auch ohne unsere Visualisierung gut funktioniert.

Lassen wir also diese Klasse in Ruhe und schauen wir ein wenig nach vorne. Wenn wir unsere Ziele ein wenig erweitern, wäre es schön, die Anzeige virtueller Positionen, gruppiert nach Strategie oder Typ, selektiv aktivieren/deaktivieren zu können, sowie detailliertere Daten zu implementieren, z. B. sichtbare offene Preise, StopLoss und TakeProfit, geplanter Verlust und Gewinn, wenn sie ausgelöst werden, und vielleicht sogar die Möglichkeit, diese Ebenen manuell zu ändern. Letzteres wäre allerdings sinnvoller, wenn wir ein Panel für den halbautomatischen Handel entwickeln und nicht einen EA, der auf streng algorithmischen Strategien basiert. Selbst wenn wir mit einer einfachen Implementierung beginnen, können wir uns die weitere Entwicklung des Codes schon ein wenig ausmalen. Dies kann uns dabei helfen, eine Richtung zu wählen, in der wir weniger wahrscheinlich zu einer Überarbeitung des bereits geschriebenen Codes zurückkehren werden.

Erstellen wir nun also eine neue Basisklasse für alle Objekte, die auf die eine oder andere Weise mit der Visualisierung von Charts verbunden sind:

//+------------------------------------------------------------------+
//| Basic class for visualizing various objects                      |
//+------------------------------------------------------------------+
class CInterface {
protected:
   static ulong      s_magic;       // EA magic number
   bool              m_isActive;    // Is the interface active?
   bool              m_isChanged;   // Does the object have any changes?
public:
   CInterface();                    // Constructor
   virtual void      Redraw() = 0;  // Draw changed objects on the chart
   virtual void      Changed() {    // Set the flag for changes
      m_isChanged = true;
   }
};

ulong CInterface::s_magic = 0;

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CInterface::CInterface() :
   m_isActive(!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)),
   m_isChanged(true) {}

Speichern wir diesen Code in der Datei Interface.mqh des aktuellen Ordners.

Wir erstellen zwei neue Klassen, die auf dieser Klasse basieren:

  • CVirtualChartOrder — ein Objekt, das eine virtuelle Position oder schwebenden Auftrag auf einem Chart im Terminal anzeigt (grafische virtuelle Position). Es kann eine virtuelle Position auf dem Chart einzeichnen, wenn sich dort Änderungen ergeben haben, und der Chart mit dem gewünschten Instrument wird automatisch geöffnet, wenn er nicht im Terminal geöffnet wurde.
  • CVirtualInterface — ein Aggregator für alle grafischen Objekte der EA-Schnittstelle. Im Moment enthält er nur das Array der grafischen virtuellen Positionen. Es wird jedes Mal, wenn ein virtuelles Positionsobjekt erstellt wird, die Erstellung grafischer virtueller Positionsobjekte übernehmen. Es empfängt auch Meldungen über Änderungen in der Zusammensetzung der virtuellen Positionen und veranlasst die Neuzeichnung der entsprechenden grafischen virtuellen Positionen. Ein solcher Aggregator besteht aus einer einzigen Instanz (die das Singleton-Designmuster implementiert) und ist in der Klasse CVirtualAdvisor verfügbar.

Die Klasse CVirtualChartOrder sieht wie folgt aus:

//+------------------------------------------------------------------+
//| Graphic virtual position class                                   |
//+------------------------------------------------------------------+
class CVirtualChartOrder : public CInterface {
   CVirtualOrder*    m_order;          // Associated virtual position (order)
   CChart            m_chart;          // Chart object to be displayed

   // Objects on the chart to display the virtual position
   CChartObjectHLine m_openLine;       // Open price line

   long              FindChart();      // Search/open the desired chart
public:
   CVirtualChartOrder(CVirtualOrder* p_order);     // Constructor
   ~CVirtualChartOrder();                          // Destructor

   bool              operator==(const ulong id) {  // Comparison operator by Id
      return m_order.Id() == id;
   }

   void              Show();    // Show a virtual position (order)
   void              Hide();    // Hide a virtual position (order) 

   virtual void      Redraw() override;   // Redraw a virtual position (order) 
};

Die Methode Redraw() prüft, ob sie ausgeführt werden muss, und ruft gegebenenfalls Methoden auf, um eine virtuelle Position im Chart ein- oder auszublenden:

//+------------------------------------------------------------------+
//| Redraw a virtual position (order)                                |
//+------------------------------------------------------------------+
void CVirtualChartOrder::Redraw() {
   if(m_isChanged) {
      if(m_order.IsOpen()) {
         Show();
      } else {
         Hide();
      }
      m_isChanged = false;
   }
}

In der Anzeigemethode Show() rufen wir zunächst die Methode FindChart() auf, um festzustellen, in welchem Chart die virtuelle Position angezeigt werden soll. Bei dieser Methode durchlaufen wir einfach alle offenen Charts, bis wir einen Chart mit einem passenden Symbol finden. Wenn wir keine finden, öffnen wir ein neues Chart. Das gefundene (oder geöffnete) Chart wird in der Eigenschaft m_chart gespeichert.

//+------------------------------------------------------------------+
//| Finding a chart to display                                       |
//+------------------------------------------------------------------+
long CVirtualChartOrder::FindChart() {
   if(m_chart.ChartId() == -1 || m_chart.Symbol() != m_order.Symbol()) {
      long currChart, prevChart = ChartFirst();
      int i = 0, limit = 1000;

      currChart = prevChart;

      while(i < limit) { // we probably have no more than 1000 open charts
         if(ChartSymbol(currChart) == m_order.Symbol()) {
            return currChart;
         }
         currChart = ChartNext(prevChart); // get new chart on the basis of the previous one
         if(currChart < 0)
            break;        // end of chart list is reached
         prevChart = currChart; // memorize identifier of the current chart for ChartNext()
         i++;
      }

      // If a suitable chart is not found, then open a new one
      if(currChart == -1) {
         m_chart.Open(m_order.Symbol(), PERIOD_CURRENT);
      }
   }
   return m_chart.ChartId();
}

Die Methode Show() zeichnet einfach eine horizontale Linie, die dem Eröffnungskurs entspricht. Seine Farbe und Art wird je nach Position (Reihenfolge), Richtung und Art bestimmt. Mit der Methode Hide() wird diese Zeile gelöscht. Mit diesen Methoden wird die Klasse weiter bereichert.

Wir speichern den erhaltenen Code in der Datei VirtualChartOrder.mqh im aktuellen Ordner.

Die Implementierung der Klasse CVirtualInterface kann wie folgt vorgenommen werden:

//+------------------------------------------------------------------+
//| EA GUI class                                                     |
//+------------------------------------------------------------------+
class CVirtualInterface : public CInterface {
protected:
// Static pointer to a single class instance
   static   CVirtualInterface *s_instance;

   CVirtualChartOrder *m_chartOrders[];   // Array of graphical virtual positions

//--- Private methods
   CVirtualInterface();   // Closed constructor

public:
   ~CVirtualInterface();  // Destructor

//--- Static methods
   static
   CVirtualInterface  *Instance(ulong p_magic = 0);   // Singleton - creating and getting a single instance

//--- Public methods
   void              Changed(CVirtualOrder *p_order); // Handle virtual position changes
   void              Add(CVirtualOrder *p_order);     // Add a virtual position

   virtual void      Redraw() override;   // Draw changed objects on the chart
};

Fügen wir in der statischen Methode zur Erstellung einer einzelnen Instance() die Initialisierung der statischen Variable s_magic hinzu, wenn eine MagicNummer ungleich Null übergeben wurde:

//+------------------------------------------------------------------+
//| Singleton - creating and getting a single instance               |
//+------------------------------------------------------------------+
CVirtualInterface* CVirtualInterface::Instance(ulong p_magic = 0) {
   if(!s_instance) {
      s_instance = new CVirtualInterface();
   }
   if(s_magic == 0 && p_magic != 0) {
      s_magic = p_magic;
   }
   return s_instance;
}

In der Methode zur Behandlung des Öffnungs-/Schließereignisses einer virtuellen Position suchen wir das entsprechende grafische virtuelle Positionsobjekt und markieren, dass an diesem Objekt Änderungen stattgefunden haben:

//+------------------------------------------------------------------+
//| Handle virtual position changes                                  |
//+------------------------------------------------------------------+
void CVirtualInterface::Changed(CVirtualOrder *p_order) {
   // Remember that this position has changes
   int i;
   FIND(m_chartOrders, p_order.Id(), i);
   if(i != -1) {
      m_chartOrders[i].Changed();
      m_isChanged = true;
   }
}

Schließlich rufen wir in der Rendering-Methode der Schnittstelle Methoden Redraw() zum Zeichnen aller grafischen virtuellen Positionen in einer Schleife auf:

//+------------------------------------------------------------------+
//| Draw changed objects on a chart                                  |
//+------------------------------------------------------------------+
void CVirtualInterface::Redraw() {
   if(m_isActive && m_isChanged) {  // If the interface is active and there are changes
      // Start redrawing graphical virtual positions 
      FOREACH(m_chartOrders, m_chartOrders[i].Redraw());
      m_isChanged = false;          // Reset the changes flag
   }
}

Speichern wir den erhaltenen Code in der Datei VirtualInterface.mqh des aktuellen Ordners.

Jetzt müssen nur noch die letzten Änderungen vorgenommen werden, damit das Subsystem für die Anzeige virtueller Positionen in Charten funktioniert. Fügen wir in der Klasse CVirtualAdvisor die neue Eigenschaft m_interface hinzu, die eine einzelne Instanz des Display-Interface-Objekts speichern wird. Wir müssen dafür sorgen, dass es im Konstruktor initialisiert und im Destruktor gelöscht wird:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...
   CVirtualInterface *m_interface;     // Interface object to show the status to the user
   
   ...
};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1, string p_name = "") :
   ...
// Initialize the interface with the static interface
   m_interface(CVirtualInterface::Instance(p_magic)),
   ... {
   ...
};

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   
... 
   delete m_interface;        // Remove the interface
}

In OnTick fügen wir nach allen Operationen den Aufruf der Methode zum Neuzeichnen der Schnittstelle hinzu, da dies der unwichtigste Teil der Tick-Verarbeitung ist: 

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Receiver handles virtual positions
   m_receiver.Tick();

// Start handling in strategies
   CAdvisor::Tick();

// Adjusting market volumes
   m_receiver.Correct();

// Save status
   Save();
   
// Render the interface
   m_interface.Redraw();
}

In der Klasse CVirtualReceiver fügen wir auch die neue Eigenschaft m_interface hinzu, in der eine einzelne Instanz des Display-Interface-Objekts gespeichert wird. Wir müssen uns um die Initialisierung im Konstruktor kümmern:

//+------------------------------------------------------------------+
//| Class for converting open volumes to market positions (receiver) |
//+------------------------------------------------------------------+
class CVirtualReceiver : public CReceiver {
protected:
   ...
   CVirtualInterface
   *m_interface;                          // Interface object to show the status to the user

   ...
};

//+------------------------------------------------------------------+
//| Closed constructor                                               |
//+------------------------------------------------------------------+
CVirtualReceiver::CVirtualReceiver() :
   m_interface(CVirtualInterface::Instance()),
   ... {}

Bei der Methode zur Zuweisung der erforderlichen Anzahl virtueller Positionen an Strategien werden wir diese gleichzeitig zur Schnittstelle hinzufügen:

//+------------------------------------------------------------------+
//| Allocate the necessary amount of virtual positions to strategy   |
//+------------------------------------------------------------------+
static void CVirtualReceiver::Get(CVirtualStrategy *strategy,   // Strategy
                                  CVirtualOrder *&orders[],     // Array of strategy positions
                                  int n                         // Required number
                                 ) {
   CVirtualReceiver *self = Instance();   // Receiver singleton
   CVirtualInterface *draw = CVirtualInterface::Instance();
   ArrayResize(orders, n);                // Expand the array of virtual positions
   FOREACH(orders,
           orders[i] = new CVirtualOrder(strategy); // Fill the array with new objects
           APPEND(self.m_orders, orders[i]);
           draw.Add(orders[i])) // Register the created virtual position
   PrintFormat(__FUNCTION__ + " | OK, Strategy orders: %d from %d total",
               ArraySize(orders),
               ArraySize(self.m_orders));
}

Als Letztes müssen wir eine Schnittstellenwarnung in die Methoden für das Öffnen/Schließen von virtuellen Positionen in dieser Klasse einfügen:

void CVirtualReceiver::OnOpen(CVirtualOrder *p_order) {
   m_interface.Changed(p_order);
   ...
}

//+------------------------------------------------------------------+
//| Handle closing a virtual position                                |
//+------------------------------------------------------------------+
void CVirtualReceiver::OnClose(CVirtualOrder *p_order) {
   m_interface.Changed(p_order);
   ...
   }
}

Die an den Dateien VirtualAdvisor.mqh und VirtualReceiver.mqh vorgenommenen Änderungen speichern wir im aktuellen Ordner.

Wenn wir nun unseren EA kompilieren und ausführen, können wir etwa so sehen, ob es offene virtuelle Positionen oder schwebende Aufträge gibt:

Abb. 1. Anzeige von virtuellen Aufträgen und Positionen im Chart

Abb. 1. Anzeige von virtuellen Aufträgen und Positionen auf dem Chart

Hier zeigen die gepunkteten Linien virtuelle schwebende Aufträge: orange - SELL STOP, blau - BUY STOP, während die durchgezogenen Linien virtuelle Positionen anzeigen: blau - BUY, rot - SELL. Im Moment sind nur die Öffnungsstufen sichtbar, aber in Zukunft wird es möglich sein, die Anzeige stärker zu sättigen.


Schöne Grafik

In den Forumsdiskussionen wurde die Idee geäußert, dass die Leser von Artikeln (zumindest einige von ihnen) in erster Linie auf das Ende des Artikels schauen, wo sie ein schönes Chart erwarten, das das Wachstum des Geldes beim Testen eines entwickelten EA zeigt. Wenn es tatsächlich eine solche Tabelle gibt, ist dies ein zusätzlicher Anreiz, zum Anfang des Artikels zurückzukehren und ihn zu lesen.

Während der Arbeit an diesem Artikel haben wir keine Änderungen an der verwendeten Demo-Strategie oder an der Menge der Strategie-Instanzen im Demo-EA vorgenommen. Alles bleibt in demselben Zustand wie zum Zeitpunkt der Veröffentlichung des vorherigen Artikels. Daher gibt es hier keine beeindruckenden Grafiken.

Parallel zum aktuellen Artikel habe ich jedoch weitere Strategien optimiert und trainiert, die später veröffentlicht werden. Ihre Testergebnisse erlauben uns zu sagen, dass die Kombination einer großen Anzahl von Strategieinstanzen in einem EA eine vielversprechende Methode ist.

Hier sind zwei Beispiele für Testläufe des EA, der etwa 170 Instanzen von Strategien verwendet, die auf verschiedenen Symbolen und Zeitrahmen arbeiten. Der Testzeitraum: von 2023-01-01 bis 2024-02-23. Die Daten des Zeitraums wurden nicht für die Optimierung und das Training verwendet. In den Einstellungen für das Kapitalmanagement wurden Parameter festgelegt, die davon ausgingen, dass der akzeptierte Drawdown in einem Fall bei etwa 10 % und in einem anderen Fall bei etwa 40 % liegt.


Abb. 2. Testergebnisse mit einem akzeptierten Drawdown von 10%


Abb. 3. Testergebnisse mit einem akzeptierten Drawdown von 40%

Das Vorhandensein solcher Ergebnisse ist keine Garantie dafür, dass sie sich in Zukunft wiederholen. Aber wir können daran arbeiten, dieses Ergebnis wahrscheinlicher zu machen. Ich werde versuchen, diese Ergebnisse nicht zu verschlechtern und nicht in den Selbstbetrug des Übertrainings zu verfallen.


Schlussfolgerung

In diesem Artikel haben wir eine Arbeit geleistet, die uns etwas von der Hauptrichtung abbringt. Ich würde gerne zu einer automatischen Optimierung übergehen, die zum Beispiel in einem kürzlich veröffentlichten Artikel Verwendung von Optimierungsalgorithmen zur Konfiguration von EA-Parametern im laufenden Betrieb diskutiert wurde.

Nichtsdestotrotz ist die Möglichkeit, den Status zu speichern und zu laden, ein wichtiger Bestandteil eines jeden EA, der Aussicht hat, jemals auf einem echten Konto gestartet zu werden. Nicht weniger wichtig ist die Fähigkeit, nicht nur mit Marktpositionen, sondern auch mit schwebenden Aufträgen zu arbeiten, die in vielen Handelsstrategien verwendet werden. Gleichzeitig kann die Visualisierung der EA-Arbeit helfen, eventuelle Implementierungsfehler in der Entwicklungsphase zu erkennen.

Wir danken Ihnen für Ihre Aufmerksamkeit und sehen uns im nächsten Teil!

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

Algorithmen zur Optimierung mit Populationen: Widerstand gegen das Steckenbleiben in lokalen Extremen (Teil II) Algorithmen zur Optimierung mit Populationen: Widerstand gegen das Steckenbleiben in lokalen Extremen (Teil II)
Wir setzen unser Experiment fort, das darauf abzielt, das Verhalten von Populationsoptimierungsalgorithmen im Zusammenhang mit ihrer Fähigkeit zu untersuchen, lokale Minima bei geringer Populationsvielfalt effizient zu umgehen und globale Maxima zu erreichen. Forschungsergebnisse werden vorgelegt.
Neuronale Netze leicht gemacht (Teil 77): Cross-Covariance Transformer (XCiT) Neuronale Netze leicht gemacht (Teil 77): Cross-Covariance Transformer (XCiT)
In unseren Modellen verwenden wir häufig verschiedene Aufmerksamkeitsalgorithmen. Und am häufigsten verwenden wir wahrscheinlich Transformers. Ihr größter Nachteil ist der Ressourcenbedarf. In diesem Artikel wird ein neuer Algorithmus vorgestellt, der dazu beitragen kann, die Rechenkosten ohne Qualitätseinbußen zu senken.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Neuronale Netze leicht gemacht (Teil 76): Erforschung verschiedener Interaktionsmuster mit Multi-Future Transformer Neuronale Netze leicht gemacht (Teil 76): Erforschung verschiedener Interaktionsmuster mit Multi-Future Transformer
Dieser Artikel setzt das Thema der Vorhersage der kommenden Kursentwicklung fort. Ich lade Sie ein, sich mit der Architektur eines Multi-Future Transformers vertraut zu machen. Die Hauptidee besteht darin, die multimodale Verteilung der Zukunft in mehrere unimodale Verteilungen zu zerlegen, was es ermöglicht, verschiedene Modelle der Interaktion zwischen Agenten auf der Szene effektiv zu simulieren.