Entwicklung eines Expertenberaters für mehrere Währungen (Teil 10): Erstellen von Objekten aus einer Zeichenkette
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 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
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.