English Русский Português
preview
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 10): Erstellen von Objekten aus einer Zeichenkette

Entwicklung eines Expertenberaters für mehrere Währungen (Teil 10): Erstellen von Objekten aus einer Zeichenkette

MetaTrader 5Handel | 10 Oktober 2024, 10:08
22 0
Yuriy Bykov
Yuriy Bykov

Einführung

In dem vorangegangenen Artikel habe ich einen allgemeinen Plan für die Entwicklung des EA skizziert, der mehrere Phasen umfasst. Jede Phase erzeugt eine bestimmte Menge an Informationen, die in den folgenden Phasen verwendet werden können. Ich beschloss, diese Informationen in einer Datenbank zu speichern und erstellte darin eine Tabelle, in der wir die Ergebnisse einzelner Durchläufe des Strategietesters für verschiedene EAs unterbringen können.

Um diese Informationen in den nächsten Schritten nutzen zu können, müssen wir eine Möglichkeit haben, die notwendigen Objekte (Handelsstrategien, ihre Gruppen und EAs) aus den in der Datenbank gespeicherten Informationen zu erstellen. Es gibt keine Möglichkeit, Objekte direkt in der Datenbank zu speichern. Am besten ist es, alle Eigenschaften von Objekten in eine Zeichenkette umzuwandeln, diese in der Datenbank zu speichern, dann diese Zeichenkette aus der Datenbank zu lesen und daraus das gewünschte Objekt zu erstellen.

Die Erstellung eines Objekts aus einer Zeichenkette kann auf unterschiedliche Weise erfolgen. Wir können zum Beispiel ein Objekt der gewünschten Klasse mit Standardparametern erstellen und dann eine spezielle Methode oder Funktion verwenden, um die aus der Datenbank gelesene Zeichenfolge zu analysieren und den Objekteigenschaften die entsprechenden Werte zuzuweisen. Alternativ können wir auch einen zusätzlichen Objektkonstruktor erstellen, der nur eine Zeichenkette als Eingabe akzeptiert. Diese Zeichenkette wird im Konstruktor in ihre Bestandteile zerlegt und die entsprechenden Werte werden dort den Objekteigenschaften zugewiesen. Um zu verstehen, welche Option besser ist, sollten wir uns zunächst ansehen, wie wir Informationen über Objekte in der Datenbank speichern.


Speichern von Informationen über Objekte

Öffnen wir die Tabelle in der Datenbank, die wir im vorigen Artikel gefüllt haben, und sehen wir uns die letzten Spalten an. Die Spalten params und inputs speichern das Ergebnis der Konvertierung des Handelsstrategieobjekts der Klasse CSimpleVolumesStrategy in eine Zeichenkette und die Inputs eines einzelnen Optimierungsdurchgangs.

Abb. 1. Das Fragment der Tabelle der Durchläufe mit Informationen über die angewandte Strategie und die Prüfparameter


Obwohl sie miteinander verwandt sind, gibt es Unterschiede zwischen ihnen: Die Eingabespalte enthält die Namen der Eingaben (auch wenn sie nicht genau mit den Namen der Eigenschaften des Strategieobjekts übereinstimmen), aber einige Parameter, wie das Symbol und der Zeitraum, fehlen. Daher wird es für uns bequemer sein, das Eingabeformular aus der Spalte params zu verwenden, um das Objekt neu zu erstellen.

Erinnern wir uns, woher wir die Implementierung der Umwandlung eines Strategieobjekts in eine Zeichenkette haben. Im vierten Teil der Artikelserie habe ich die Speicherung des EA-Status in einer Datei implementiert, sodass er nach einem Neustart wiederhergestellt werden kann. Um zu verhindern, dass der EA versehentlich eine Datei mit Daten aus einem anderen ähnlichen EA verwendet, habe ich die Speicherung von Daten über die Parameter aller Instanzen der in diesem EA verwendeten Strategien in die Datei implementiert.

Mit anderen Worten: Die ursprüngliche Aufgabe bestand darin, sicherzustellen, dass Instanzen von Handelsstrategien mit unterschiedlichen Parametern unterschiedliche Zeichenketten erzeugen. Daher war ich nicht besonders besorgt über die Möglichkeit, ein neues Handelsstrategieobjekt auf der Grundlage solcher Strings zu erstellen. Im neunten Teil habe ich den bestehenden Mechanismus zur Umwandlung von Zeichenketten ohne zusätzliche Änderungen übernommen, da mein Ziel darin bestand, den Prozess des Hinzufügens solcher Informationen zur Datenbank zu debuggen.


Weiter zur Umsetzung

Jetzt ist es an der Zeit, darüber nachzudenken, wie wir Objekte mit solchen Zeichenketten neu erstellen können. Wir haben also eine Zeichenfolge, die in etwa so aussieht:

class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,17,0.70,0.90,50,10000.00,750.00,10000,3)

Wenn wir es an den Objektkonstruktor der Klasse CSimpleVolumesStrategy übergeben, sollte es folgendes tun:

  • den Teil, der vor der ersten öffnenden Klammer steht, entfernen;
  • den verbleibenden Teil bis zur schließenden Klammer durch Kommazeichen trennen;
  • jeden erhaltenen Teil den entsprechenden Objekteigenschaften zuordnen und sie gegebenenfalls in Zahlen umwandeln.

Ein Blick auf die Liste dieser Aktionen zeigt, dass die erste Aktion auf einer höheren Ebene durchgeführt werden kann. Wenn wir nämlich zuerst den Klassennamen aus dieser Zeile entnehmen, können wir die Klasse des erstellten Objekts definieren. Dann ist es für den Konstruktor bequemer, nur den Teil der Zeichenkette zu übergeben, der sich innerhalb der Klammern befindet.

Außerdem sind die Anforderungen an die Erstellung von Objekten aus einer Zeichenkette nicht auf diese eine Klasse beschränkt. Erstens können wir nicht nur eine Handelsstrategie haben. Zweitens müssen wir Objekte der Klasse CVirtualStrategyGroup erstellen, d.h. Gruppen von mehreren Instanzen von Handelsstrategien mit unterschiedlichen Parametern. Dies ist nützlich, wenn mehrere zuvor ausgewählte Gruppen zu einer Gruppe zusammengefasst werden sollen. Drittens: Was hindert uns daran, die Möglichkeit zu schaffen, ein EA-Objekt (die Klasse CVirtualAdvisor) aus einem String zu erstellen? Dies wird es uns ermöglichen, einen universellen EA zu schreiben, der aus einer Datei eine Textbeschreibung aller Gruppen von Strategien laden kann, die verwendet werden sollen. Durch Änderung der Beschreibung in der Datei kann die Zusammensetzung der darin enthaltenen Strategien vollständig aktualisiert werden, ohne den EA neu zu kompilieren.

Wenn wir versuchen, uns vorzustellen, wie der Initialisierungsstring von Objekten der Klasse CVirtualStrategyGroup aussehen könnte, dann erhalten wir etwas wie dieses:

class CVirtualStrategyGroup([
  class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,17,0.70,0.90,50,10000.00,750.00,10000,3),
  class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,27,0.70,0.90,60,10000.00,550.00,10000,3),
  class CSimpleVolumesStrategy(EURGBP,PERIOD_H1,37,0.70,0.90,80,10000.00,150.00,10000,3)
], 0.33)

Der erste Parameter des Objektkonstruktors der Klasse CVirtualStrategyGroup ist ein Array von Handelsstrategieobjekten oder ein Array von Handelsstrategiegruppenobjekten. Daher ist es notwendig zu lernen, wie man einen Teil einer Zeichenkette analysiert, die ein Array ähnlicher Objektbeschreibungen sein wird. Wie Sie sehen können, habe ich die in JSON oder Python verwendete Standardnotation verwendet, um eine Liste (Array) von Elementen darzustellen: Die Elementeinträge sind durch Kommas getrennt und befinden sich innerhalb eines Paars eckiger Klammern.

Wir werden auch lernen müssen, wie man aus einer Zeichenkette nicht nur Teile zwischen Kommas extrahiert, sondern auch solche, die eine Beschreibung eines anderen verschachtelten Klassenobjekts darstellen. Um das Handelsstrategie-Objekt in eine Zeichenkette umzuwandeln, haben wir zufällig die Funktion typename() verwendet, die den Klassennamen eines Objekts als Zeichenkette mit vorangestellten Klassenwörtern zurückgibt. Wir können dieses Wort nun beim Parsen einer Zeichenkette als Zeichen dafür verwenden, dass das, was folgt, eine Zeichenkette ist, die ein Objekt einer bestimmten Klasse beschreibt, und nicht ein einfacher Wert wie eine Zahl oder eine Zeichenkette.

Auf diese Weise verstehen wir die Notwendigkeit, das Factory-Entwurfsmuster zu implementieren, wenn ein spezielles Objekt auf Anfrage mit der Erstellung von Objekten verschiedener Klassen beauftragt wird. Die Objekte, die eine Fabrik (factory) produzieren kann, sollten in der Regel einen gemeinsamen Vorfahren in der Klassenhierarchie haben. Beginnen wir also mit der Erstellung einer neuen gemeinsamen Klasse, von der alle Klassen (deren Objekte über die Initialisierungszeichenfolge erstellt werden können) letztendlich abgeleitet werden.


Neue Basisklasse

Bisher waren unsere Basisklassen, die an der Vererbungshierarchie beteiligt sind, folgende:

  • СAdvisor. Die Klasse zur Erstellung von EAs, von der die Klasse CVirtualAdvisor abgeleitet ist.
  • CStrategy. Die Klasse zur Erstellung von Handelsstrategien. CSimpleVolumesStrategy ist von ihr abgeleitet
  • CVirtualStrategyGroup. Die Klasse für Gruppen von Handelsstrategien. Sie hat keine Nachkommen und es werden auch keine erwartet.
  • Ist das alles?

Ja, ich sehe keine Basisklassen mehr, die Nachkommen haben, die mit einer Zeichenkette initialisiert werden können müssen. Das bedeutet, dass diese drei Klassen einen gemeinsamen Vorfahren haben müssen, in dem alle notwendigen Hilfsmethoden gesammelt werden, um die Initialisierung mit einer Zeichenkette zu gewährleisten.

Der Name, den ich für den neuen Vorfahren gewählt habe, ist noch nicht sehr aussagekräftig. Ich möchte irgendwie betonen, dass die Nachkommen der Klasse in der Factory produziert werden können, also „factoryable“ sind. Im weiteren Verlauf der Entwicklung des Codes verschwand der Buchstabe „y“ irgendwo, und es blieb nur der Name CFaсtorable.

Ursprünglich sah die Klasse etwa so aus:

//+------------------------------------------------------------------+
//| Base class of objects created from a string                      |
//+------------------------------------------------------------------+
class CFactorable {
protected:
   virtual void      Init(string p_params) = 0;
public:
   virtual string    operator~() = 0;

   static string     Read(string &p_params);
};

Daher mussten die Nachkommen dieser Klasse über die Methode Init() verfügen, die die gesamte erforderliche Arbeit zur Umwandlung der eingegebenen Initialisierungszeichenfolge in Objekteigenschaftswerte erledigt, sowie über den Tilde-Operator, der die umgekehrte Umwandlung von Eigenschaften in eine Initialisierungszeichenfolge vornimmt. Die Existenz der statischen Methode Read() wird ebenfalls angegeben. Es sollte in der Lage sein, einige der Daten aus dem Initialisierungsstring zu lesen. Unter einem Datenteil verstehen wir eine Teilzeichenkette, die entweder eine gültige Initialisierungszeichenkette eines anderen Objekts oder ein Array anderer Datenteile oder eine Zahl oder eine Zeichenkettenkonstante enthält.

Obwohl diese Implementierung in einen funktionsfähigen Zustand gebracht wurde, beschloss ich, erhebliche Änderungen daran vorzunehmen.

Zunächst erschien die Methode Init(), weil ich sowohl die alten Objektkonstruktoren als auch den neuen Konstruktor (der die Initialisierungszeichenfolge akzeptiert) beibehalten wollte. Um doppelten Code zu vermeiden, habe ich ihn einmal in Init() implementiert und ihn von mehreren möglichen Konstruktoren aus aufgerufen. Letztendlich stellte sich jedoch heraus, dass es keinen Bedarf für verschiedene Konstrukteure gab. Wir können mit einem einzigen neuen Konstruktor auskommen. Daher wurde der Code der Methode Init() in den neuen Konstruktor verschoben, während die Methode selbst entfernt wurde.

Zweitens enthielt die ursprüngliche Implementierung keinerlei Mittel zur Überprüfung der Gültigkeit von Initialisierungsstrings und Fehlerberichten. Wir gehen davon aus, dass die Initialisierungsstrings automatisch generiert werden, was das Auftreten solcher Fehler fast vollständig ausschließt, aber wenn wir plötzlich etwas mit den generierten Initialisierungsstrings durcheinander bringen, wäre es schön, wenn wir rechtzeitig davon erfahren und den Fehler finden könnten. Für diese Zwecke habe ich eine neue logische Eigenschaft m_isValid hinzugefügt, die angibt, ob der gesamte Code des Objektkonstruktors erfolgreich ausgeführt wurde oder ob einige Teile der Initialisierungszeichenfolge Fehler enthielten. Die Eigenschaft wird privat gemacht, während die entsprechenden Methoden IsValid() und SetInvalid() hinzugefügt werden, um ihren Wert zu erhalten und zu setzen. Außerdem ist die Eigenschaft anfangs immer true, während die Methode SetInvalid() ihren Wert nur auf false setzen kann.

Drittens wurde die Methode Read() aufgrund der implementierten Prüfungen und Fehlerbehandlung zu schwerfällig. Daher wurde sie in mehrere separate Methoden aufgeteilt, die sich auf das Lesen verschiedener Datentypen aus dem Initialisierungsstring spezialisieren. Außerdem wurden mehrere private Hilfsmethoden für Datenlesemethoden hinzugefügt. Es ist erwähnenswert, dass die Datenlesemethoden den Initialisierungsstring, der ihnen übergeben wird, ändern. Wenn der nächste Teil der Daten erfolgreich gelesen wurde, wird er als Ergebnis der Methode zurückgegeben, und der übergebene Initialisierungsstring verliert den gelesenen Teil.

Viertens kann die Methode der Rückkonvertierung eines Objekts in einen Initialisierungsstring für Objekte verschiedener Klassen nahezu identisch sein, wenn der ursprüngliche Initialisierungsstring mit den Parametern des erstellten Objekts gespeichert wird. Daher wurde der Basisklasse die Eigenschaft m_params hinzugefügt, um die Initialisierungszeichenfolge im Objektkonstruktor zu speichern.

Unter Berücksichtigung der vorgenommenen Ergänzungen sieht die Deklaration der Klasse CFactorable wie folgt aus:

//+------------------------------------------------------------------+
//| Base class of objects created from a string                      |
//+------------------------------------------------------------------+
class CFactorable {
private:
   bool              m_isValid;  // Is the object valid?

   // Clear empty characters from left and right in the initialization string 
   static void       Trim(string &p_params);

   // Find a matching closing bracket in the initialization string
   static int        FindCloseBracket(string &p_params, char closeBraket = ')');

   // Clear the initialization string with a check for the current object validity 
   bool              CheckTrimParams(string &p_params);

protected:
   string            m_params;   // Current object initialization string

   // Set the current object to the invalid state 
   void              SetInvalid(string function = NULL, string message = NULL);

public:
                     CFactorable() : m_isValid(true) {}  // Constructor
   bool              IsValid();                          // Is the object valid?

   // Convert object to string
   virtual string    operator~() = 0;

   // Does the initialization string start with the object definition?
   static bool       IsObject(string &p_params, const string className = "");

   // Does the initialization string start with defining an object of the desired class?
   static bool       IsObjectOf(string &p_params, const string className);

   // Read the object class name from the initialization string 
   static string     ReadClassName(string &p_params, bool p_removeClassName = true);

   // Read an object from the initialization string 
   string            ReadObject(string &p_params);
   
   // Read an array from the initialization string as a string 
   string            ReadArrayString(string &p_params);
   
   // Read a string from the initialization string
   string            ReadString(string &p_params);
   
   // Read a number from the initialization string as a string
   string            ReadNumber(string &p_params);
   
   // Read a real number from the initialization string
   double            ReadDouble(string &p_params);
   
   // Read an integer from the initialization string
   long              ReadLong(string &p_params);
};


Ich werde mich hier nicht mit der Implementierung der Klassenmethoden befassen. Ich möchte jedoch anmerken, dass die Arbeit aller Lesemethoden in etwa die gleichen Handlungen beinhaltet. Zunächst wird geprüft, ob die Initialisierungszeichenfolge nicht leer ist und das Objekt gültig ist. Das Objekt könnte in einen ungültigen Zustand geraten sein, z. B. als Ergebnis eines früheren erfolglosen Vorgangs zum Lesen eines Teils der Daten aus der Implementierungszeichenfolge. Daher hilft eine solche Prüfung, unnötige Aktionen an einem offensichtlich fehlerhaften Objekt zu vermeiden.

Dann werden bestimmte Bedingungen überprüft, um sicherzustellen, dass der Initialisierungsstring Daten des richtigen Typs (Objekt, Array, String oder Zahl) enthält. Wenn dies der Fall ist, finden wir die Stelle, an der diese Daten in der Initialisierungszeichenfolge enden. Alles, was sich links von dieser Stelle befindet, wird verwendet, um den Rückgabewert zu erhalten, und alles, was sich rechts davon befindet, ersetzt die Initialisierungszeichenfolge.

Wenn wir zu irgendeinem Zeitpunkt der Prüfungen ein negatives Ergebnis erhalten, rufen wir die Methode auf, mit der das aktuelle Objekt in den ungültigen Zustand versetzt wird, wobei wir Informationen über den Ort und die Art des Fehlers an das Objekt weitergeben.

Speichern Sie den Code der Klasse in der Datei Factorable.mqh im aktuellen Ordner.


Objektfabrik

Da die Objektinitialisierungszeichenfolgen immer den Klassennamen enthalten, können wir eine öffentliche Funktion oder statische Methode erstellen, die als Objekt-„Fabrik“ fungiert. Wir übergeben ihm eine Initialisierungszeichenfolge, die einen Zeiger auf das erstellte Objekt der angegebenen Klasse erhält.

Für Objekte der Klassen, deren Name an einer bestimmten Stelle im Programm einen einzigen Wert annehmen kann, ist das Vorhandensein einer solchen Fabrik natürlich nicht notwendig. Wir können ein Objekt auf die übliche Weise mit dem Operator new erstellen, indem wir den Initialisierungsstring mit den Parametern des erstellten Objekts an den Konstruktor übergeben. Wenn wir jedoch Objekte erstellen müssen, deren Klassenname unterschiedlich sein kann (z. B. verschiedene Handelsstrategien), dann kann uns der Operator new nicht helfen, da wir zunächst die Klasse des zu erstellenden Objekts definieren müssen. Überlassen wir diese Arbeit der Fabrik, oder besser gesagt, ihrer einzigen statischen Methode - Create().

//+------------------------------------------------------------------+
//| Object factory class                                             |
//+------------------------------------------------------------------+
class CVirtualFactory {
public:
   // Create an object from the initialization string
   static CFactorable* Create(string p_params) {
      // Read the object class name
      string className = CFactorable::ReadClassName(p_params);
      
      // Pointer to the object being created
      CFactorable* object = NULL;

      // Call the corresponding constructor  depending on the class name
      if(className == "CVirtualAdvisor") {
         object = new CVirtualAdvisor(p_params);
      } else if(className == "CVirtualStrategyGroup") {
         object = new CVirtualStrategyGroup(p_params);
      } else if(className == "CSimpleVolumesStrategy") {
         object = new CSimpleVolumesStrategy(p_params);
      }

      // If the object is not created or is created in the invalid state, report an error
      if(!object) {
         PrintFormat(__FUNCTION__" | ERROR: Constructor not found for:\nclass %s(%s)",
                     className, p_params);
      } else if(!object.IsValid()) {
         PrintFormat(__FUNCTION__
                     " | ERROR: Created object is invalid for:\nclass %s(%s)",
                     className, p_params);
         delete object; // Remove the invalid object
         object = NULL;
      }

      return object;
   }
};

Speichern Sie diesen Code in der Datei VirtualFactory.mqh des aktuellen Ordners.

Wir erstellen zwei nützliche Makros, damit wir die Fabrik in Zukunft leichter nutzen können. Im ersten Fall wird ein Objekt aus dem Initialisierungsstring erstellt, das durch den Aufruf der Methode CVirtualFactory::Create() ersetzt wird:

// Create an object in the factory from a string
#define NEW(Params) CVirtualFactory::Create(Params)

Das zweite Makro wird nur vom Konstruktor eines anderen Objekts ausgeführt, das ein Nachkomme der Klasse CFactorable sein sollte. Mit anderen Worten, dies geschieht nur, wenn wir das Hauptobjekt erstellen, während wir andere (verschachtelte) Objekte anhand der Initialisierungszeichenfolge in seinem Konstruktor implementieren. Das Makro soll drei Parameter erhalten: den Klassennamen des erstellten Objekts (Class), den Namen der Variablen, die den Zeiger auf das erstellte Objekt erhält (Object) und die Initialisierungszeichenfolge (Params).

Zu Beginn deklariert das Makro eine Zeigervariable mit dem angegebenen Namen und der Klasse und initialisiert sie mit dem Wert NULL. Dann prüfen wir, ob das Hauptobjekt gültig ist. Wenn ja, dann rufen wir die Objekterstellungsmethode in der Fabrik über das Makro NEW() auf. Dann versuchen wir, den erstellten Zeiger auf die gewünschte Klasse zu übertragen. Die Verwendung des dynamic_cast<>()-Operators zu diesem Zweck vermeidet einen Laufzeitfehler, wenn die Fabrik ein Objekt einer anderen Klasse als der aktuell benötigten erzeugt. In diesem Fall bleibt der Objektzeiger einfach gleich NULL, und das Programm läuft weiter. Dann prüfen wir die Gültigkeit des Zeigers. Wenn er leer oder ungültig ist, wird das Hauptobjekt auf den ungültigen Zustand gesetzt, ein Fehler gemeldet und die Ausführung des Hauptobjektkonstruktors abgebrochen.

So sieht das Makro aus:

// Creating a child object in the factory from a string with verification.
// Called only from the current object constructor.
// If the object is not created, the current object becomes invalid
// and exit from the constructor is performed
#define CREATE(Class, Object, Params)                                                                       \
    Class *Object = NULL;                                                                                   \
    if (IsValid()) {                                                                                        \
       Object = dynamic_cast<C*> (NEW(Params));                                                             \
       if(!Object) {                                                                                        \
          SetInvalid(__FUNCTION__, StringFormat("Expected Object of class %s() at line %d in Params:\n%s",  \
                                                #Class, __LINE__, Params));                                 \
          return;                                                                                           \
       }                                                                                                    \
    }                                                                                                       \

Wir fügen diese Makros an den Anfang der Datei Factorable.mqh ein.


Modifikation der bisherigen Basisklassen

Wir fügen die Klasse CFactorable als Basisklasse zu allen vorherigen Basisklassen hinzu: СAdvisor, СStrategy und СVirtualStrategyGroup. Bei den ersten beiden sind keine weiteren Änderungen erforderlich:

//+------------------------------------------------------------------+
//| EA base class                                                    |
//+------------------------------------------------------------------+
class CAdvisor : public CFactorable {
protected:
   CStrategy         *m_strategies[];  // Array of trading strategies
   virtual void      Add(CStrategy *strategy);  // Method for adding a strategy
public:
                    ~CAdvisor();                // Destructor
   virtual void      Tick();                    // OnTick event handler
   virtual double    Tester() {
      return 0;
   }
};
//+------------------------------------------------------------------+
//| Base class of the trading strategy                               |
//+------------------------------------------------------------------+
class CStrategy : public CFactorable {
public:                     
   virtual void      Tick() = 0; // Handle OnTick events
};

СVirtualStrategyGroup hat weitere gravierende Änderungen erfahren. Da es sich nicht mehr um eine abstrakte Basisklasse handelt, mussten wir eine Implementierung des Konstruktors schreiben, der ein Objekt aus der Initialisierungszeichenfolge erzeugt. Auf diese Weise sind wir zwei separate Konstruktoren losgeworden, die entweder ein Array von Strategien oder ein Array von Gruppen benötigten. Auch die Methode der Konvertierung in eine Zeichenkette hat sich geändert. In der Methode fügen wir nun einfach den Klassennamen zum gespeicherten Initialisierungsstring mit Parametern hinzu. Die Skalierungsmethode Scale() ist unverändert geblieben.

//+------------------------------------------------------------------+
//| Class of trading strategies group(s)                             |
//+------------------------------------------------------------------+
class CVirtualStrategyGroup : public CFactorable {
protected:
   double            m_scale;                // Scaling factor
   void              Scale(double p_scale);  // Scaling the normalized balance
public:
                     CVirtualStrategyGroup(string p_params); // Constructor

   virtual string    operator~() override;      // Convert object to string

   CVirtualStrategy      *m_strategies[];       // Array of strategies
   CVirtualStrategyGroup *m_groups[];           // Array of strategy groups
};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualStrategyGroup::CVirtualStrategyGroup(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the initialization string of the array of strategies or groups
   string items = ReadArrayString(p_params);

// Until the string is empty
   while(items != NULL) {
      // Read the initialization string of one strategy or group object
      string itemParams = ReadObject(items);

      // If this is a group of strategies,
      if(IsObjectOf(itemParams, "CVirtualStrategyGroup")) {
         // Create a strategy group and add it to the groups array
         CREATE(CVirtualStrategyGroup, group, itemParams);
         APPEND(m_groups, group);
      } else {
         // Otherwise, create a strategy and add it to the array of strategies
         CREATE(CVirtualStrategy, strategy, itemParams);
         APPEND(m_strategies, strategy);
      }
   }

// Read the scaling factor
   m_scale = ReadDouble(p_params);

// Correct it if necessary
   if(m_scale <= 0.0) {
      m_scale = 1.0;
   }

   if(ArraySize(m_groups) > 0 && ArraySize(m_strategies) == 0) {
      // If we filled the array of groups, and the array of strategies is empty, then
      // Scale all groups
      Scale(m_scale / ArraySize(m_groups));
   } else if(ArraySize(m_strategies) > 0 && ArraySize(m_groups) == 0) {
      // If we filled the array of strategies, and the array of groups is empty, then
      // Scale all strategies
      Scale(m_scale / ArraySize(m_strategies));
   } else {
      // Otherwise, report an error in the initialization string
      SetInvalid(__FUNCTION__, StringFormat("Groups or strategies not found in Params:\n%s", p_params));
   }
}

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


    ... 

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

Modifikation der EA-Klasse

Im vorangegangenen Artikel erhielt die EA-Klasse CVirtualAdvisor die Methode Init(), mit der doppelter Code für verschiedene EA-Konstruktoren vermieden werden sollte. Wir hatten einen Konstruktor, der eine einzelne Strategie als erstes Argument nahm, und einen Konstruktor, der ein Strategiegruppenobjekt als erstes Argument nahm. Es wird uns wahrscheinlich nicht schwer fallen, uns darauf zu einigen, dass es nur einen Konstruktor geben wird - denjenigen, der eine Gruppe von Strategien annimmt. Wenn wir eine Instanz einer Handelsstrategie verwenden müssen, erstellen wir zunächst einfach eine Gruppe mit dieser einen Strategie und übergeben die erstellte Gruppe an den EA-Konstruktor. Dann sind die Methode Init() und zusätzliche Konstruktoren überflüssig. Daher werde ich einen Konstruktor belassen, der ein EA-Objekt aus dem Initialisierungsstring erzeugt:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
   ...

public:
                     CVirtualAdvisor(string p_param);    // Constructor
                    ~CVirtualAdvisor();         // Destructor

   virtual string    operator~() override;      // Convert object to string

   ...
};

...

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the initialization string of the strategy group object
   string groupParams = ReadObject(p_params);

// Read the magic number
   ulong p_magic = ReadLong(p_params);

// Read the EA name
   string p_name = ReadString(p_params);

// Read the work flag only at the bar opening
   m_useOnlyNewBar = (bool) ReadLong(p_params);

// If there are no read errors,
   if(IsValid()) {
      // Create a strategy group
      CREATE(CVirtualStrategyGroup, p_group, groupParams);

      // Initialize the receiver with the static receiver
      m_receiver = CVirtualReceiver::Instance(p_magic);

      // Initialize the interface with the static interface
      m_interface = CVirtualInterface::Instance(p_magic);

      m_name = StringFormat("%s-%d%s.csv",
                            (p_name != "" ? p_name : "Expert"),
                            p_magic,
                            (MQLInfoInteger(MQL_TESTER) ? ".test" : "")
                           );

      // Save the work (test) start time
      m_fromDate = TimeCurrent();

      // Reset the last save time
      m_lastSaveTime = 0;

      // Add the contents of the group to the EA
      Add(p_group);

      // Remove the group object
      delete p_group;
   }
}

Im Konstruktor lesen wir zunächst alle Daten aus dem Initialisierungsstring. Wird in dieser Phase eine Unstimmigkeit festgestellt, wird das aktuell erstellte EA-Objekt ungültig. Wenn alles in Ordnung ist, erstellt der Konstruktor eine Strategiegruppe, fügt ihre Strategien zu ihrem Strategie-Array hinzu und setzt die übrigen Eigenschaften auf der Grundlage der aus der Initialisierungszeichenfolge gelesenen Daten.

Durch die Gültigkeitsprüfung vor dem Anlegen der Empfänger- und Interface-Objekte im Konstruktor können diese Objekte nun aber nicht angelegt werden. Daher müssen wir im Destruktor eine Überprüfung der Korrektheit von Zeigern auf diese Objekte hinzufügen, bevor wir sie löschen:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   if(!!m_receiver)  delete m_receiver;         // Remove the recipient
   if(!!m_interface) delete m_interface;        // Remove the interface
   DestroyNewBar();           // Remove the new bar tracking objects 
}

Wir speichern die Änderungen in der Datei VirtualAdvisor.mqh des aktuellen Ordners.

Modifikation der Handelsstrategieklasse

In der Strategieklasse CSimpleVolumesStrategy entfernen wir den Konstruktor mit separaten Parametern und schreiben den Code des Konstruktors, der die Initialisierungszeichenfolge akzeptiert, unter Verwendung der Methoden der Klasse CFactorable um.

Im Konstruktor lesen wir die Parameter aus dem Initialisierungsstring, nachdem wir zuvor den Anfangszustand in der Eigenschaft m_params gespeichert haben. Wenn beim Lesen keine Fehler aufgetreten sind, die dazu führen würden, dass das Strategieobjekt ungültig wird, führen wir die grundlegenden Aktionen zur Initialisierung des Objekts durch: Wir füllen das Array der virtuellen Positionen, initialisieren den Indikator und registrieren den Event-Handler für einen neuen Balken auf dem Minuten-Zeitrahmen.

Die Methode zur Umwandlung eines Objekts in eine Zeichenkette hat sich ebenfalls geändert. Anstatt sie aus den Parametern zu bilden, werden wir einfach den Klassennamen und den gespeicherten Initialisierungsstring aneinanderhängen, wie wir es in den beiden zuvor betrachteten Klassen getan haben. 

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

public:
   //--- Public methods
                     CSimpleVolumesStrategy(string p_params); // Constructor

   virtual string    operator~() override;         // Convert object to string

   virtual void      Tick() override;              // OnTick event handler
};


//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CSimpleVolumesStrategy::CSimpleVolumesStrategy(string p_params) {
// Save the initialization string
   m_params = p_params;
   
// Read the parameters from the initialization string
   m_symbol = ReadString(p_params);
   m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params);
   m_signalPeriod = (int) ReadLong(p_params);
   m_signalDeviation = ReadDouble(p_params);
   m_signaAddlDeviation = ReadDouble(p_params);
   m_openDistance = (int) ReadLong(p_params);
   m_stopLevel = ReadDouble(p_params);
   m_takeLevel = ReadDouble(p_params);
   m_ordersExpiration = (int) ReadLong(p_params);
   m_maxCountOfOrders = (int) ReadLong(p_params);
   m_fittedBalance = ReadDouble(p_params);

// If there are no read errors,
   if(IsValid()) {
      // Request the required number of virtual positions
      CVirtualReceiver::Get(GetPointer(this), m_orders, m_maxCountOfOrders);

      // Load the indicator to get tick volumes
      m_iVolumesHandle = iVolumes(m_symbol, m_timeframe, VOLUME_TICK);

      // If the indicator is loaded successfully
      if(m_iVolumesHandle != INVALID_HANDLE) {

         // Set the size of the tick volume receiving array and the required addressing
         ArrayResize(m_volumes, m_signalPeriod);
         ArraySetAsSeries(m_volumes, true);

         // Register the event handler for a new bar on the minimum timeframe
         IsNewBar(m_symbol, PERIOD_M1);
      } else {
         // Otherwise, set the object state to invalid
         SetInvalid(__FUNCTION__, "Can't load iVolumes()");
      }
   }
}

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

Wir haben auch die Methoden Save() und Load() aus der Klasse entfernt, da sich ihre Implementierung in der übergeordneten Klasse CVirtualStrategy als völlig ausreichend erwiesen hat, um die ihr zugewiesenen Aufgaben zu erfüllen.

Wir speichern die Änderungen in der Datei CSimpleVolumesStrategy.mqh des aktuellen Ordners.


Der EA für eine einzelne Instanz der Handelsstrategie

Um die Parameter einer einzelnen Handelsstrategie-Instanz zu optimieren, müssen wir nur die Initialisierungsfunktion OnInit() ändern. In dieser Funktion sollten wir einen String für die Initialisierung des Handelsstrategieobjekts aus den EA-Eingaben bilden und ihn dann verwenden, um das EA-Objekt in den Initialisierungsstring zu ersetzen.

Dank unserer Implementierung von Methoden zum Lesen von Daten aus dem Initialisierungsstring können wir darin zusätzliche Leerzeichen und Zeilenvorschübe verwenden. Bei der Ausgabe in das Protokoll oder bei einem Eintrag in die Datenbank wird die Initialisierungszeichenfolge etwa so formatiert:

Core 1  2023.01.01 00:00:00   OnInit | Expert Params:
Core 1  2023.01.01 00:00:00   class CVirtualAdvisor(
Core 1  2023.01.01 00:00:00       class CVirtualStrategyGroup(
Core 1  2023.01.01 00:00:00          [
Core 1  2023.01.01 00:00:00           class CSimpleVolumesStrategy("EURGBP",16385,17,0.70,0.90,150,10000.00,85.00,10000,3,0.00)
Core 1  2023.01.01 00:00:00          ],1
Core 1  2023.01.01 00:00:00       ),
Core 1  2023.01.01 00:00:00       ,27181,SimpleVolumesSingle,1
Core 1  2023.01.01 00:00:00   )

In der Funktion OnDeinit() müssen wir sicherstellen, dass der Zeiger auf das EA-Objekt korrekt ist, bevor wir es entfernen. Jetzt können wir nicht mehr garantieren, dass das EA-Objekt immer erstellt wird, da wir theoretisch einen falschen Initialisierungsstring haben könnten, der zum vorzeitigen Löschen des EA-Objekts durch die Fabrik führen würde.

...

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   CMoney::FixedBalance(fixedBalance_);

// Prepare the initialization string for a single strategy instance
   string strategyParams = StringFormat(
                              "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d,%.2f)",
                              symbol_, timeframe_,
                              signalPeriod_, signalDeviation_, signaAddlDeviation_,
                              openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                              maxCountOfOrders_, 0
                           );

// Prepare the initialization string for an EA with a group of a single strategy
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    class CVirtualStrategyGroup(\n"
                            "       [\n"
                            "        %s\n"
                            "       ],1\n"
                            "    ),\n"
                            "    ,%d,%s,%d\n"
                            ")",
                            strategyParams, magic_, "SimpleVolumesSingle", true
                         );

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

// Create an EA handling virtual positions
   expert = NEW(expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}

...

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   if(!!expert) delete expert;
}

Wir speichern den erhaltenen Code in der Datei SimpleVolumesExpertSingle.mq5 des aktuellen Ordners.


EA für mehrere Instanzen

Um die EA-Erstellung mit mehreren Handelsstrategie-Instanzen zu testen, nehmen wir den EA aus dem achten Teil, den wir für den Lasttest verwendet haben. In der Funktion OnInit() ersetzen wir den EA-Erstellungsmechanismus durch den in diesem Artikel entwickelten. Zu diesem Zweck werden wir nach dem Laden der Strategieparameter aus der CSV-Datei den Initialisierungsstring des Strategie-Arrays auf der Grundlage dieser Parameter ergänzen. Dann werden wir sie verwenden, um den Initialisierungsstring für die Strategiegruppe und den EA selbst zu bilden:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
// Load strategy parameter sets
   int totalParams = LoadParams(fileName_, strategyParams);

// If nothing is loaded, report an error
   if(totalParams == 0) {
      PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n"
                  "Check that it exists in data folder or in common data folder.",
                  fileName_);
      return(INIT_PARAMETERS_INCORRECT);
   }

// Report an error if
   if(count_ < 1) { // number of instances is less than 1
      return INIT_PARAMETERS_INCORRECT;
   }

   ArrayResize(strategyParams, count_);

// Set parameters in the money management class
   CMoney::DepoPart(expectedDrawdown_ / 10.0);
   CMoney::FixedBalance(fixedBalance_);

// Prepare the initialization string for the array of strategy instances
   string strategiesParams;
   FOREACH(strategyParams, strategiesParams += StringFormat(" class CSimpleVolumesStrategy(%s),\n      ",
                                                            strategyParams[i % totalParams]));

// Prepare the initialization string for an EA with the strategy group
   string expertParams = StringFormat("class CVirtualAdvisor(\n"
                                      "   class CVirtualStrategyGroup(\n"
                                      "      [\n"
                                      "      %s],\n"
                                      "      %.2f\n"
                                      "   ),\n"
                                      "   %d,%s,%d\n"
                                      ")",
                                      strategiesParams, scale_,
                                      magic_, "SimpleVolumes_BenchmarkInstances", useOnlyNewBars_);
   
// Create an EA handling virtual positions
   expert = NEW(expertParams);

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}

Ähnlich wie beim vorherigen EA erhält die Funktion OnDeinit() die Möglichkeit, die Gültigkeit des Zeigers auf das EA-Objekt zu prüfen, bevor es gelöscht wird.

Wir speichern den erhaltenen Code in der Datei BenchmarkInstancesExpert.mq5 im aktuellen Ordner.


Überprüfung der Funktionalität

Wir verwenden den EA BenchmarkInstancesExpert.mq5 aus dem achten Teil und den gleichen EA aus dem aktuellen Artikel. Wir starten ihn mit denselben Parametern: 256 Instanzen von Handelsstrategien aus der Datei Params_SV_EURGBP_H1.csv, das Jahr 2022 dienen als Testzeitraum.


Abb. 2. Die Testergebnisse der beiden EA-Versionen sind identisch


Die Ergebnisse sind identisch ausgefallen. Daher werden sie im Bild als eine Instanz dargestellt. Das ist sehr gut, da wir nun die neueste Version für die weitere Entwicklung nutzen können.


Schlussfolgerung

Wir haben also die Möglichkeit geschaffen, alle erforderlichen Objekte mit Hilfe von Initialisierungsstrings zu erstellen. Bisher haben wir diese Zeilen fast manuell erstellt, aber in Zukunft werden wir in der Lage sein, sie aus der Datenbank zu lesen. Das ist im Allgemeinen der Grund, warum wir eine solche Überarbeitung des bereits funktionierenden Codes begonnen haben.

Identische Ergebnisse beim Testen von EAs, die sich nur in der Methode der Objekterzeugung unterscheiden, d.h. mit denselben Sets von Handelsstrategie-Instanzen arbeiten, rechtfertigen die vorgenommenen Änderungen. 

Nun können wir zur Automatisierung der ersten geplanten Phase übergehen - dem sequentiellen Start mehrerer Prozesse der EA-Optimierung, um die Parameter einer einzelnen Instanz der Handelsstrategie auszuwählen. Wir werden dies in den kommenden Artikeln tun.

Vielen Dank für Ihre Aufmerksamkeit! Bis bald!



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

Schildkrötenpanzer-Evolutionsalgorithmus (TSEA) Schildkrötenpanzer-Evolutionsalgorithmus (TSEA)
Dies ist ein einzigartiger Optimierungsalgorithmus, der von der Evolution des Schildkrötenpanzers inspiriert wurde. Der TSEA-Algorithmus emuliert die allmähliche Bildung keratinisierter Hautbereiche, die optimale Lösungen für ein Problem darstellen. Die besten Lösungen werden „härter“ und befinden sich näher an der Außenfläche, während die weniger erfolgreichen Lösungen „weicher“ bleiben und sich im Inneren befinden. Der Algorithmus verwendet eine Gruppierung der Lösungen nach Qualität und Entfernung, wodurch weniger erfolgreiche Optionen erhalten bleiben und Flexibilität und Anpassungsfähigkeit gewährleistet werden.
Risikomanager für den algorithmischen Handel Risikomanager für den algorithmischen Handel
Ziel dieses Artikels ist es, die Notwendigkeit des Einsatzes eines Risikomanagers zu beweisen und die Prinzipien der Risikokontrolle im algorithmischen Handel in einer eigenen Klasse zu implementieren, damit jeder die Wirksamkeit des Ansatzes der Risikostandardisierung im Intraday-Handel und bei Investitionen auf den Finanzmärkten überprüfen kann. In diesem Artikel werden wir eine Risikomanager-Klasse für den algorithmischen Handel erstellen. Dies ist eine logische Fortsetzung des vorangegangenen Artikels, in dem wir die Erstellung eines Risikomanagers für den manuellen Handel besprochen haben.
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.
Entwicklung eines Replay Systems (Teil 46): Chart Trade Projekt (V) Entwicklung eines Replay Systems (Teil 46): Chart Trade Projekt (V)
Sind Sie es leid, Zeit mit der Suche nach genau der Datei zu verschwenden, die Ihre Anwendung zum Funktionieren braucht? Wie wäre es, alles in die ausführbare Datei aufzunehmen? Auf diese Weise müssen Sie nicht nach den Dingen suchen. Ich weiß, dass viele Menschen diese Form der Verteilung und Speicherung nutzen, aber es gibt einen viel geeigneteren Weg. Zumindest was die Verteilung von ausführbaren Dateien und deren Speicherung betrifft. Die hier vorgestellte Methode kann sehr nützlich sein, da Sie den MetaTrader 5 selbst als hervorragenden Assistenten verwenden können, ebenso wie MQL5. Außerdem ist es nicht so schwer zu verstehen.