
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 17): Weitere Vorbereitung auf den realen Handel
Einführung
In einem der vorangegangenen Artikel haben wir uns bereits mit den EA-Verbesserungen beschäftigt, die für die Arbeit auf echten Konten notwendig sind. Bis jetzt haben wir uns hauptsächlich darauf konzentriert, akzeptable EA-Ergebnisse im Strategietester zu erzielen. Der echte Handel erfordert viel mehr Vorbereitungen.
Neben der Wiederherstellung des EA-Betriebs nach dem Neustart des Terminals, der Möglichkeit, leicht abweichende Namen von Handelsinstrumenten zu verwenden, und der automatischen Beendigung des Handels, wenn die festgelegten Indikatoren erreicht sind, stehen wir auch vor folgendem Problem: Um den Initialisierungs-String zu bilden, verwenden wir Informationen, die wir direkt aus der Datenbank erhalten, in der alle Ergebnisse der Optimierungen von Handelsstrategie-Instanzen und ihren Gruppen gespeichert sind.
Um den EA auszuführen, müssen wir eine Datei mit der Datenbank im gemeinsamen Terminalordner haben. Der Umfang der Datenbank beträgt bereits mehrere Gigabyte und wird in Zukunft noch wachsen. Es ist also nicht sinnvoll, die Datenbank zu einem integralen Bestandteil des EA zu machen - nur ein sehr kleiner Teil der dort gespeicherten Informationen wird für den Start benötigt. Daher ist es notwendig, einen Mechanismus zur Extraktion und Verwendung dieser Informationen in der EA zu implementieren.
Der Weg ist vorgezeichnet
Erinnern wir uns, dass wir die Automatisierung von zwei Testphasen in Betracht gezogen und umgesetzt haben. In der ersten Phase werden die Parameter einer einzelnen Instanz der Handelsstrategie optimiert (Teil 11). Die untersuchte Modellhandelsstrategie verwendet nur ein Handelsinstrument (Symbol) und einen Zeitrahmen. Deshalb haben wir sie immer wieder durch den Optimierer laufen lassen und dabei Symbole und Zeitrahmen geändert. Für jede Kombination von Symbol und Zeitrahmen wurde die Optimierung nacheinander nach verschiedenen Optimierungskriterien durchgeführt. Alle Ergebnisse der Optimierungsläufe wurden in der Tabelle der Durchgänge „passes“ unserer Datenbank gespeichert.
In der zweiten Phase haben wir die Auswahl einer Gruppe von Parametersätzen optimiert, die in der ersten Phase ermittelt wurden und die bei gemeinsamer Anwendung die besten Ergebnisse lieferten (Teil 6 und Teil 13). Wie in der ersten Phase haben wir Parametersätze, die dasselbe Symbol-Zeitrahmen-Paar verwenden, zu einer Gruppe zusammengefasst. Informationen über die Ergebnisse aller während der Optimierung überprüften Gruppen wurden ebenfalls in unserer Datenbank gespeichert.
In der dritten Stufe haben wir den Standard-Strategietester-Optimierer nicht mehr verwendet, sodass wir noch nicht über seine Automatisierung sprechen. In der dritten Stufe wurde für jede verfügbare Kombination von Symbol und Zeitrahmen eine der besten Gruppen aus der zweiten Stufe ausgewählt. Wir haben die Optimierung auf drei Symbole (EURGBP, EURUSD, GBPUSD) und drei Zeitrahmen (H1, M30, M15) angewendet. Das Ergebnis der dritten Stufe werden also neun ausgewählte Gruppen sein. Aber um die Berechnungen im Tester zu vereinfachen und zu beschleunigen, haben wir uns in den letzten Artikeln nur auf die drei besten Gruppen (mit drei verschiedenen Symbolen und dem H1-Zeitrahmen) beschränkt.
Das Ergebnis der dritten Stufe war eine Reihe von Zeilenkennungen aus der Tabelle „passes“, die wir als Eingabeparameter an unseren endgültigen EA SimpleVolumesExpert.mq5 weitergegeben haben:
input string passes_ = "734469," "736121," "776928"; // - Comma-separated pass IDs
Wir können diesen Parameter ändern, bevor wir den EA-Test starten. So war es möglich, den endgültigen EA mit einer beliebigen Teilmenge von Gruppen aus der in der Datenbank in der Tabelle „passes“ verfügbaren Menge von Gruppen durchzuführen, genauer gesagt mit einer Teilmenge, deren Länge 247 Zeichen nicht überschreitet. Dies ist eine Einschränkung, die durch die MQL5-Sprache für die Werte von Eingabe-String-Parametern auferlegt wird. Laut Dokumentation kann die maximale Länge eines String-Parameterwerts zwischen 191 und 253 Zeichen betragen, abhängig von der Länge des Parameternamens.
Wenn wir also mehr als, grob gesagt, 40 Gruppen in die Arbeit einbeziehen wollen, dann ist das auf diese Weise nicht möglich. Wir müssen zum Beispiel die Variable passes_ zu einer einfachen Stirng-Variablen machen und nicht zu einem Parameter für eine Eingabe-String, indem wir das Wort input aus dem Code entfernen. In diesem Fall können wir die erforderlichen Gruppen nur im Quellcode angeben. Wir brauchen jedoch noch keine so großen Mengen zu verwenden. Außerdem ist es nach den in Teil 5 durchgeführten Experimenten für uns profitabler, eine Gruppe nicht aus einer großen Anzahl von einzelnen Kopien von Handelsstrategien oder Gruppen von Handelsstrategien zu bilden. Es ist rentabler, die anfängliche Anzahl von Einzelexemplaren von Handelsstrategien in mehrere Untergruppen aufzuteilen, aus denen dann eine kleinere Anzahl neuer Gruppen zusammengestellt werden kann. Diese neuen Gruppen können entweder zu einer endgültigen Gruppe zusammengefasst werden, oder der Gruppierungsprozess kann durch Aufteilung in neue Untergruppen wiederholt werden. Daher müssen wir auf jeder Vereinheitlichungsebene eine relativ kleine Anzahl von Strategien oder Gruppen als eine einzige Gruppe betrachten.
Wenn der EA Zugriff auf die Datenbank mit den Ergebnissen aller Optimierungsläufe hat, genügt es, eine Liste von IDs der gewünschten Optimierungsläufe über die Eingabe zu übergeben. Der EA erhält die Initialisierungs-Strings derjenigen Gruppen von Handelsstrategien, die an den aufgeführten Durchläufen teilgenommen haben, selbständig aus der Datenbank. Auf der Grundlage der aus der Datenbank erhaltenen Initialisierungs-Strings wird ein Initialisierungs-String für ein EA-Objekt erstellt, das alle Handelsstrategien aus den aufgelisteten Gruppen enthält. Dieser EA handelt mit allen in ihm enthaltenen Handelsstrategie-Instanzen.
Wenn kein Zugriff auf die Datenbank besteht, muss der EA immer noch irgendwie einen Initialisierungs-String für das EA-Objekt generieren, der die erforderliche Zusammensetzung von einzelnen Instanzen von Handelsstrategien oder Gruppen von Handelsstrategien enthält. Wir können sie zum Beispiel in einer Datei speichern und den Namen der Datei an den EA weitergeben, aus dem sie den Initialisierungs-String laden wird. Oder wir können den Inhalt des Initialisierungs-Strings über eine zusätzliche mqh-Bibliotheksdatei in den Quellcode des EA einfügen. Wir können sogar beide Methoden kombinieren, indem wir die Initialisierungs-Strings in einer Datei speichern und sie dann mit den Dateiimportfunktionen in MetaEditor importieren (Bearbeiten → Einfügen → Datei).
Wenn wir jedoch die Möglichkeit bieten wollen, mit verschiedenen ausgewählten Gruppen in einem EA zu arbeiten und die gewünschte Gruppe in den Eingaben auszuwählen, dann wird dieser Ansatz schnell seine schwache Skalierbarkeit zeigen. Wir werden eine Menge manueller, sich wiederholender Arbeiten erledigen müssen. Versuchen wir daher, das Problem ein wenig anders zu formulieren: Wir wollen eine Bibliothek guter Initialisierungs-Strings bilden, aus der wir einen für den aktuellen EA-Start auswählen können. Die Bibliothek sollte ein integraler Bestandteil des EA sein, sodass wir keine weitere separate Datei dazu verwenden müssen.
Unter Berücksichtigung der obigen Ausführungen können die anstehenden Arbeiten in die folgenden Phasen unterteilt werden:
- Auswählen und Speichern. In diesem Stadium sollten wir über ein Werkzeug verfügen, mit dem wir Gruppen auswählen und ihre Initialisierungs-Strings zur späteren Verwendung speichern können. Es wäre wahrscheinlich eine gute Idee, die Möglichkeit zu bieten, einige zusätzliche Informationen über die ausgewählten Gruppen zu speichern (Name, kurze Beschreibung, ungefähre Zusammensetzung, Erstellungsdatum usw.).
- Aufbau der Bibliothek. Aus den in der vorherigen Phase ausgewählten Gruppen wird eine endgültige Auswahl derjenigen getroffen, die in der Bibliothek für eine bestimmte Version des EA verwendet werden sollen, und es wird eine Include-Datei mit allen erforderlichen Informationen erstellt.
- Erstellung des endgültigen EA. Indem wir den EA aus dem vorigen Teil ändern, werden wir ihn in einen neuen endgültigen EA umwandeln, der die erstellte Gruppenbibliothek verwendet. Dieser EA benötigt keinen Zugriff mehr auf unsere Optimierungsdatenbank, da alle notwendigen Informationen über die verwendeten Handelsstrategiegruppen in ihr enthalten sind.
Lassen Sie uns mit der Umsetzung unserer Pläne beginnen.
Rückblick auf frühere Errungenschaften
Die genannten Schritte sind ein Prototyp für die in Teil 9 beschriebene Umsetzung von Stufe 8. Erinnern wir uns daran, dass wir in diesem Artikel eine Reihe von Schritten aufgelistet haben, deren Abschluss es uns ermöglichen kann, einen fertigen EA mit guter Handelsleistung zu erhalten. In Phase 8 werden die besten Gruppen, die für verschiedene Handelsstrategien, Symbole, Zeitrahmen und andere Parameter gefunden wurden, in einem endgültigen EA zusammengefasst. Wir haben uns jedoch noch nicht eingehend mit der Frage beschäftigt, wie genau die besten Gruppen ausgewählt werden sollen.
Einerseits kann die Antwort auf diese Frage recht einfach ausfallen. Wir könnten zum Beispiel einfach die besten Ergebnisse aus allen Gruppen nach einem bestimmten Parameter (Gesamtgewinn, Sharpe-Ratio, normalisierter durchschnittlicher Jahresgewinn) auswählen. Andererseits könnte sich die Antwort als viel komplizierter erweisen. Was ist, wenn zum Beispiel bessere Testergebnisse erzielt werden, wenn ein komplexes Kriterium zur Auswahl der besten Gruppen verwendet wird? Oder was ist, wenn einige der besten Gruppen überhaupt nicht in die endgültige EA aufgenommen werden sollten, da ihre Aufnahme die ohne sie erzielten Ergebnisse verschlechtern würde? Dieses Thema wird höchstwahrscheinlich eine eigene detaillierte Studie erfordern.
Eine weitere Frage, die ebenfalls gesondert untersucht werden muss, ist die optimale Aufteilung der Gruppen in Untergruppen mit Normalisierung der Untergruppen. Ich habe dieses Thema bereits in Teil 5 angesprochen, noch bevor wir mit der Automatisierung von Testphasen begonnen haben. Anschließend wählten wir manuell neun einzelne Instanzen von Handelsstrategien aus, drei Instanzen für jedes der drei verwendeten Handelsinstrumente (Symbole).
Es stellte sich heraus, dass die Ergebnisse in den Tests etwas besser ausfallen, wenn man zunächst drei normalisierte Gruppen von drei Strategien für jedes Symbol erstellt und diese dann zu einer endgültigen normalisierten Gruppe zusammenfasst, als wenn man neun einzelne Kopien von Handelsstrategien zu einer endgültigen normalisierten Gruppe zusammenfasst. Aber wir können nicht mit Sicherheit sagen, ob diese Methode der Gruppierung optimal ist. Und wäre es für andere Handelsstrategien besser, als sie einfach in einer Gruppe zusammenzufassen? Generell besteht auch hier noch Raum für weitere Forschung.
Zum Glück können wir diese beiden Fragen auf einen späteren Zeitpunkt verschieben. Um sie zu erforschen, bräuchten wir Hilfsmittel, die noch nicht implementiert sind. Ohne sie wird die Arbeit viel weniger effizient sein und viel mehr Zeit in Anspruch nehmen.
Auswählen und Speichern von Gruppen
Es mag den Anschein haben, dass wir bereits alles haben, was wir brauchen. Nehmen Sie einfach die SimpleVolumesExpert.mq5 EA aus dem vorherigen Teil, setzen Sie kommagetrennte IDs von Durchgängen in die Eingabevariable passes_, starten Sie einen einzelnen Testdurchlauf und erhalten Sie die erforderliche Initialisierungs-String, die in der Datenbank gespeichert wird. Das einzige, was noch fehlt, sind einige zusätzliche Daten. Es stellte sich jedoch heraus, dass die Informationen über den Durchlauf nicht in die Datenbank aufgenommen wurden.
Der Punkt ist, dass wir nur die Ergebnisse der Optimierungsläufe in die Datenbank hochgeladen haben. Die Ergebnisse eines einzelnen Durchgangs werden nicht hochgeladen. Wie Sie sich vielleicht erinnern, wird das Hochladen innerhalb der Methode CTesterHandler::ProcessFrames() durchgeführt, die von der Ereignisbehandlung durch OnTesterPass() in der oberen Ebene aufgerufen wird:
//+------------------------------------------------------------------+ //| Handling incoming frames | //+------------------------------------------------------------------+ void CTesterHandler::ProcessFrames(void) { // Open the database DB::Connect(); // Variables for reading data from frames ... // Go through frames and read data from them while(FrameNext(pass, name, id, value, data)) { // Convert the array of characters read from the frame into a string values = CharArrayToString(data); // Form a string with names and values of the pass parameters inputs = GetFrameInputs(pass); // Form an SQL query from the received data query = StringFormat("INSERT INTO passes " "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s');", s_idTask, pass, values, inputs, TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS)); // Add it to the SQL query array APPEND(queries, query); } // Execute all requests DB::ExecuteTransaction(queries); // Close the database DB::Close(); }
Wenn ein Einzeldurchlauf gestartet wird, wird die Ereignisbehandlung nicht aufgerufen, da dies im Ereignismodell für Einzeldurchläufe nicht vorgesehen ist. Diese Ereignisbehandlung wird nur in einem Expert Advisor aufgerufen, der im Datenrahmen-Sammelmodus läuft. Der Start einer EA-Instanz in diesem Modus erfolgt automatisch beim Start der Optimierung, nicht aber beim Start eines einzelnen Durchlaufs. Daher stellt sich heraus, dass die bestehende Implementierung keine Informationen über einzelne Durchläufe in der Datenbank speichert.
Wir können natürlich alles so lassen, wie es ist, und einen EA entwickeln, der zur Optimierung nach einigen unnötigen Parametern gestartet werden muss. Das Ziel einer solchen Optimierung ist es, die Ergebnisse des ersten Durchgangs zu erhalten, wonach die Optimierung beendet wird. Auf diese Weise werden die Ergebnisse des Durchgangs in die Datenbank eingegeben. Aber das scheint zu hässlich zu sein, also werden wir einen anderen Weg gehen.
Bei der Ausführung eines einzelnen Durchlaufs im EA wird die Ereignisbehandlung durch OnTester() nach Abschluss aufgerufen. Daher müssen wir den Code zum Speichern der Ergebnisse eines einzelnen Durchlaufs entweder direkt in der Ereignisbehandlung oder in eine der von der Ereignisbehandlung aufgerufenen Methoden einfügen. Der geeignetste Ort zum Einfügen der Methode ist wahrscheinlich CTesterHandler::Tester(). Es ist jedoch zu bedenken, dass diese Methode auch aufgerufen wird, wenn der EA den Optimierungsdurchlauf abschließt. Diese Methode enthält nun Code, der die Ergebnisse des Optimierungsdurchlaufs erzeugt und über den Datenrahmenmechanismus sendet.
Wenn ein einzelner Durchlauf gestartet wird, werden die Daten für den Rahmen immer noch generiert, aber der Datenrahmen selbst, selbst wenn er erstellt wurde, kann nicht verwendet werden. Wenn wir versuchen, die Funktion FrameNext() zum Abrufen eines Frames zu verwenden, nachdem wir den Frame mit der Funktion FrameAdd() in dem im Single-Pass-Modus gestarteten EA erstellt haben, wird FrameNext() den erstellten Frame nicht lesen. Es verhält sich dann so, als ob keine „Frames“ erstellt worden wären.
Gehen wir also wie folgt vor. In CTesterHandler::Tester() wird geprüft, ob es sich um einen einzelnen Durchlauf handelt oder ob er im Rahmen einer Optimierung erfolgt. Je nach Ergebnis werden die Ergebnisse des Durchlaufs entweder sofort in der Datenbank gespeichert (für einen einzelnen Durchlauf) oder es wird ein „Frame“ für die Daten erstellt, der an den Haupt-EA gesendet wird (für die Optimierung). Fügen wir eine neue Methode hinzu, die zum Speichern eines einzelnen Durchlaufs aufgerufen wird, sowie eine weitere Hilfsmethode, die eine SQL-Abfrage zum Einfügen der erforderlichen Daten in die Tabelle der Durchläufepasses generiert. Letzteres brauchen wir, weil eine solche Aktion jetzt an zwei Stellen im Code ausgeführt wird und nicht an einer. Daher werden wir sie in eine separate Methode verschieben.
//+------------------------------------------------------------------+ //| Optimization event handling class | //+------------------------------------------------------------------+ class CTesterHandler { ... static void ProcessFrame(string values); // Handle single pass data // Generate SQL query to insert pass results static string GetInsertQuery(string values, string inputs, ulong pass = 0); public: ... };
Wir haben ja bereits die Implementierungvon GetInsertQuery(). Alles, was wir tun müssen, ist, den Codeblock aus der Methode ProcessFrames() zu verschieben und ihn an der richtigen Stelle in der Methode ProcessFrames() Methode aufzurufen:
//+------------------------------------------------------------------+ //| Generate SQL query to insert pass results | //+------------------------------------------------------------------+ string CTesterHandler::GetInsertQuery(string values, string inputs, ulong pass) { return StringFormat("INSERT INTO passes " "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s');", s_idTask, pass, values, inputs, TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS)); } //+------------------------------------------------------------------+ //| Handling incoming frames | //+------------------------------------------------------------------+ void CTesterHandler::ProcessFrames(void) { ... // Go through frames and read data from them while(FrameNext(pass, name, id, value, data)) { // Convert the array of characters read from the frame into a string values = CharArrayToString(data); // Form a string with names and values of the pass parameters inputs = GetFrameInputs(pass); // Form an SQL query from the received data query = GetInsertQuery(values, inputs, pass); // Add it to the SQL query array APPEND(queries, query); } ... }
Um die Daten eines einzelnen Durchlaufs zu speichern, rufen wir eine neue Methode ProcessFrame() auf , die einen String als Parameter akzeptiert, der Teil einer SQL-Abfrage ist und Daten über den Durchlauf zum Einfügen in die Tabelle passes enthält. In der Methode selbst stellen wir einfach eine Verbindung zur Datenbank her, erstellen die endgültige SQL-Abfrage und führen sie aus:
//+------------------------------------------------------------------+ //| Handle single pass data | //+------------------------------------------------------------------+ void CTesterHandler::ProcessFrame(string values) { // Open the database DB::Connect(); // Form an SQL query from the received data string query = GetInsertQuery(values, "", 0); // Execute the request DB::Execute(query); // Close the database DB::Close(); }
Unter Berücksichtigung der hinzugefügten Methoden kann der Handler für das Ereignis „Pass Completion“ wie folgt geändert werden:
//+------------------------------------------------------------------+ //| Handling completion of tester pass for agent | //+------------------------------------------------------------------+ void CTesterHandler::Tester(double custom, // Custom criteria string params // Description of EA parameters in the current pass ) { ... // Generate a string with pass data data = StringFormat("%s,'%s'", data, params); // If this is a pass within the optimization, if(MQLInfoInteger(MQL_OPTIMIZATION)) { // Open a file to write a frame data int f = FileOpen(s_fileName, FILE_WRITE | FILE_TXT | FILE_ANSI); // Write a description of the EA parameters FileWriteString(f, data); // Close the file FileClose(f); // Create a frame with data from the recorded file and send it to the main terminal if(!FrameAdd("", 0, 0, s_fileName)) { PrintFormat(__FUNCTION__" | ERROR: Frame add error: %d", GetLastError()); } } else { // Otherwise, it is a single pass, call the method to add its results to the database CTesterHandler::ProcessFrame(data); } }
Speichern wir die an der Datei TesterHandler.mqh vorgenommenen Änderungen im aktuellen Ordner.
Nun werden nach jedem einzelnen Durchgang Informationen über die Ergebnisse in unsere Datenbank eingegeben. Wir sind nicht allzu sehr an verschiedenen statistischen Parametern des Passes in Bezug auf die aktuelle Aufgabe interessiert. Das Wichtigste für uns ist der gespeicherte Initialisierungs-String der im Durchlauf verwendeten normalisierten Strategiegruppe. Die gespeicherte Zeichenfolge ist das, was wir hier am meisten brauchen.
Das Vorhandensein der erforderlichen Initialisierungs-Strings in einer der Spalten der Übergabetabelle reicht jedoch nicht aus, um sie bequem weiter zu verwenden. Wir wollten auch einige Informationen an den Initialisierungs-String anhängen. Es lohnt sich jedoch nicht, die Spalten der Tabelle passes zu erweitern, da die meisten Zeilen in dieser Tabelle Informationen über die Ergebnisse der Optimierungsläufe enthalten, für die keine zusätzlichen Informationen benötigt werden.
Lassen Sie uns daher eine neue Tabelle erstellen, in der die ausgewählten Ergebnisse gespeichert werden. Dies ist bereits in der Phase der Bibliotheksbildung zu beobachten.
Aufbau der Bibliothek
Wir sollten die neue Tabelle nicht mit überflüssigen Feldern überfrachten, die Informationen enthalten, die aus anderen Datenbanktabellen entnommen werden können. Wenn beispielsweise ein Eintrag in der neuen Tabelle über einen externen Schlüssel mit einem Eintrag in der Tabelle der Durchläufe (passes) verknüpft ist, dann gibt es bereits ein Erstellungsdatum. Anhand der Durchlaufs-ID können wir außerdem eine Kette von Verbindungen aufbauen und feststellen, zu welchem Projekt dieser Pass gehört, und somit die Gruppe der im Pass verwendeten Strategien.
Legen wir also die Tabelle strategy_groups mit den folgenden Feldern an:
- id_pass. Die Durchlaufs-ID aus der Tabelle passes (externer Schlüssel)
- name. Der Name der Strategiegruppe, die verwendet wird, um Enumeration für die Eingabe der Strategiegruppenauswahl zu erstellen.
Der SQL-Code zur Erstellung der gewünschten Tabelle könnte wie folgt aussehen:
-- Table: strategy_groups DROP TABLE IF EXISTS strategy_groups; CREATE TABLE strategy_groups ( id_pass INTEGER REFERENCES passes (id_pass) ON DELETE CASCADE ON UPDATE CASCADE PRIMARY KEY, name TEXT );
Lassen Sie uns die Hilfsklasse CGroupsLibrary erstellen, um die meisten weiteren Aktionen durchzuführen. Zu seinen Aufgaben gehören das Einfügen und Abrufen von Informationen über Strategiegruppen aus der Datenbank und das Erstellen einer mqh-Datei mit der tatsächlichen Bibliothek der guten Gruppen, die vom endgültigen EA verwendet werden. Wir werden später darauf zurückkommen. Lassen Sie uns zunächst einen EA erstellen, den wir zur Bildung der Bibliothek verwenden werden.
Der vorhandene EA SimpleVolumesExpert.mq5 erfüllt fast alle Anforderungen, muss aber noch verbessert werden. Wir hatten vor, sie als endgültige Version des endgültigen EA zu verwenden. Speichern wir sie also unter einem neuen Namen SimpleVolumesStage3.mq5. Nun sollten wir die notwendigen Ergänzungen in der neuen Datei vornehmen. Wir vermissen zwei Dinge: die Möglichkeit, den Namen der für die aktuell ausgewählten Durchgänge gebildeten Gruppe anzugeben (im Parameter passes_) und die Speicherung des Initialisierungs-Strings dieser Gruppe in der neuen Tabelle strategy_groups.
Ersteres ist recht einfach zu realisieren. Fügen wir einen neuen EA-Eingang hinzu, der später als Gruppenname verwendet werden soll. Ist der Parameter leer, erfolgt keine Speicherung in der Bibliothek.
input group "::: Saving to library" input string groupName_ = ""; // - Group name (if empty - no saving)
Aber im ersten Fall müssen wir uns ein wenig mehr anstrengen. Um Daten in die Tabelle strategy_groups einfügen zu können, müssen wir die ID kennen, die dem aktuellen Datensatz beim Einfügen in die Tabelle passes zugewiesen wurde. Da sein Wert automatisch von der Datenbank selbst zugewiesen wird (in der Abfrage übergeben wir einfach NULL anstelle seines Wertes), existiert er im Code nicht als Wert einer Variablen. Daher können wir es derzeit nicht an anderer Stelle einsetzen, wo es gebraucht wird. Wir müssen diesen Wert irgendwie definieren.
Dies kann auf unterschiedliche Weise geschehen. Wenn Sie zum Beispiel wissen, dass die Bezeichner, die neuen Zeilen zugewiesen werden, eine aufsteigende Reihenfolge bilden, können Sie einfach den Wert des aktuell größten Bezeichners nach dem Einfügen auswählen. Dies ist möglich, wenn wir sicher wissen, dass derzeit keine neuen Zeichenfolgen an die Tabelle passes übergeben werden. Wenn jedoch parallel dazu eine andere Optimierung der ersten oder zweiten Stufe läuft, können deren Ergebnisse in derselben Datenbank landen. In diesem Fall können wir nicht mehr sicher sein, dass die letzte ID diejenige ist, die dem Pass entspricht, den wir zur Bildung der Bibliothek gestartet haben. Im Allgemeinen ist dies nur möglich, wenn wir bereit sind, einige Einschränkungen in Kauf zu nehmen und uns diese zu merken.
Eine wesentlich zuverlässigere Methode, die frei von den oben beschriebenen möglichen Fehlern ist, ist die folgende. Wir können die SQL-Abfrage zum Einfügen von Daten leicht abändern, indem wir sie in eine Abfrage umwandeln, die als Ergebnis die generierte ID der neuen Tabellenzeile zurückgibt. Fügen Sie dazu einfach den Operator „RETURNING rowid“ an das Ende der SQL-Abfrage an. Dies geschieht in der Methode GetInsertQuery(), die eine SQL-Abfrage zum Einfügen einer neuen Zeile in die Tabelle passes generiert. Obwohl die ID-Spalte in der Tabelle passes id_pass heißt, können wir sie rowid nennen, da sie den entsprechenden Typ hat (INTEGER PRIMARY KEY AUTOINCREMENT) und die versteckte Spalte rowid ersetzt, die in SQLite-Tabellen automatisch vorhanden ist.
//+------------------------------------------------------------------+ //| Generate SQL query to insert pass results | //+------------------------------------------------------------------+ string CTesterHandler::GetInsertQuery(string values, string inputs, ulong pass) { return StringFormat("INSERT INTO passes " "VALUES (NULL, %d, %d, %s,\n'%s',\n'%s') RETURNING rowid;", s_idTask, pass, values, inputs, TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS)); }
Wir müssen auch den MQL5-Code ändern, der diese Anfrage sendet. Derzeit verwenden wir dafür die Methode DB::Execute(query). Es impliziert, dass die übergebene Abfrage keine Abfrage ist, die irgendwelche Daten zurückgibt.
Daher erhält die Klasse CDatabase die neue Methode Insert(), die die übergebene Insert-Abfrage ausführt und einen einzelnen gelesenen Ergebniswert zurückgibt. Darin wird anstelle der Funktion DatabaseExecute() die Funktion DatabasePrepare() verwendet, mit der wir dann auf die Abfrageergebnisse zugreifen können:
//+------------------------------------------------------------------+ //| Class for handling the database | //+------------------------------------------------------------------+ class CDatabase { ... public: ... // Execute a query to the database for insertion with return of the new entry ID static ulong Insert(string query); }; ... //+------------------------------------------------------------------+ //| Execute a query to the database for insertion returning the | //| new entry ID | //+------------------------------------------------------------------+ ulong CDatabase::Insert(string query) { ulong res = 0; // Execute the request int request = DatabasePrepare(s_db, query); // If there is no error if(request != INVALID_HANDLE) { // Data structure for reading a single string of a query result struct Row { int rowid; } row; // Read data from the first result string if(DatabaseReadBind(request, row)) { res = row.rowid; } else { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: Reading row for request \n%s\nfailed with code %d", query, GetLastError()); } } else { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: Request \n%s\nfailed with code %d", query, GetLastError()); } return res; } //+------------------------------------------------------------------+
Ich habe beschlossen, diese Methode nicht durch zusätzliche Prüfungen zu verkomplizieren, dass die übermittelte Abfrage tatsächlich eine INSERT-Abfrage ist, dass sie einen Befehl zur Rückgabe einer ID enthält und dass der zurückgegebene Wert nicht zusammengesetzt ist. Abweichungen von diesen Bedingungen führen zu Fehlern bei der Ausführung dieses Codes, aber da diese Methode nur an einer Stelle im Projekt verwendet wird, werden wir versuchen, ihr eine korrekte Anfrage zu übergeben.
Wir speichern die Änderungen in der Datei Database.mqh des aktuellen Ordners.
Das nächste Problem, das sich bei der Implementierung stellte, war die Übergabe des ID-Wertes an die höhere Code-Ebene, da die Verarbeitung am Empfangsort dazu führte, dass bestehende Methoden mit externen Funktionen und zusätzlichen übergebenen Parametern ausgestattet werden mussten. Daher haben wir uns für die folgende Vorgehensweise entschieden: Die Klasse CTesterHandler erhielt die statische Eigenschaft s_idPass. Darin wurde die ID des aktuellen Durchlaufs eingetragen. Von hier aus können wir den Wert an jedem beliebigen Punkt des Programms abrufen:
//+------------------------------------------------------------------+ //| Optimization event handling class | //+------------------------------------------------------------------+ class CTesterHandler { ... public: ... static ulong s_idPass; }; ... ulong CTesterHandler::s_idPass = 0; ... //+------------------------------------------------------------------+ //| Handle single pass data | //+------------------------------------------------------------------+ void CTesterHandler::ProcessFrame(string values) { // Open the database DB::Connect(); // Form an SQL query from the received data string query = GetInsertQuery(values, "", 0); // Execute the request s_idPass = DB::Insert(query); // Close the database DB::Close(); }
Wir speichern die an der Datei TesterHandler.mqh vorgenommenen Änderungen im aktuellen Ordner.
Nun ist es an der Zeit, zu der deklarierten Hilfsklasse CGroupsLibrary zurückzukehren. Am Ende mussten wir zwei öffentliche Methoden darin deklarieren - eine private Methode und ein statisches Array:
//+------------------------------------------------------------------+ //| Class for working with a library of selected strategy groups | //+------------------------------------------------------------------+ class CGroupsLibrary { private: // Exporting group names and initialization strings extracted from the database as MQL5 code static void ExportParams(string &p_names[], string &p_params[]); public: // Add the pass name and ID to the database static void Add(ulong p_idPass, string p_name); // Export passes to mqh file static void Export(string p_idPasses); // Array to fill with initialization strings from mqh file static string s_params[]; };
In der bibliotheksbildenden EA wird nur die MethodeAdd() verwendet. Sie erhält die Durchlaufs-ID und den Gruppennamen, die in der Bibliothek gespeichert werden sollen. Der Methodencode selbst ist sehr einfach: Wir erstellen aus den Eingabedaten eine SQL-Abfrage zum Einfügen eines neuen Eintrags in die Tabelle strategy_groups und führen sie aus.
//+------------------------------------------------------------------+ //| Add the pass name and ID to the database | //+------------------------------------------------------------------+ void CGroupsLibrary::Add(ulong p_idPass, string p_name) { string query = StringFormat("INSERT INTO strategy_groups VALUES(%d, '%s')", p_idPass, p_name); // Open the database if(DB::Connect()) { // Execute the request DB::Execute(query); // Close the database DB::Close(); } }
Um die Entwicklung des Werkzeugs zur Bibliotheksbildung abzuschließen, müssen wir jetzt nur noch den Aufruf der Methode Add() in den EA SimpleVolumesStage3.mq5 hinzufügen, nachdem der Testdurchlauf abgeschlossen ist:
//+------------------------------------------------------------------+ //| Test results | //+------------------------------------------------------------------+ double OnTester(void) { // Handle the completion of the pass in the EA object double res = expert.Tester(); // If the group name is not empty, save the pass to the library if(groupName_ != "") { CGroupsLibrary::Add(CTesterHandler::s_idPass, groupName_); } return res; }
Wir speichern die Änderungen an den Dateien SimpleVolumesStage3.mq5 und GroupsLibrary.mqh im aktuellen Ordner. Wenn wir Teile für den Rest der Methoden der CGroupsLibrary-Klasse hinzufügen, können wir bereits den kompilierten EA SimpleVolumesStage3.mq5 verwenden.
Füllen der Bibliothek
Versuchen wir nun, aus den neun zuvor ausgewählten guten Durchlaufs-IDs eine Bibliothek zu bilden. Dazu starten wir den EA SimpleVolumesStage3.ex5 im Tester und geben verschiedene Kombinationen aus neun IDs in der Eingabe passes_ an. In der Eingabe groupName_ wird ein eindeutiger Name festgelegt, der die Zusammensetzung der aktuellen Gruppe von Einzelinstanzen von Handelsstrategien widerspiegelt, die zu einer Gruppe zusammengefasst werden.
Nach mehreren Durchläufen sehen wir uns die Ergebnisse an, die in der Tabelle strategy_groups erscheinen, indem wir zu Informationszwecken einige Parameter für die Durchläufe mit verschiedenen Gruppen hinzufügen. Die folgende SQL-Abfrage hilft uns zum Beispiel dabei:
SELECT sg.id_pass, sg.name, p.custom_ontester, p.sharpe_ratio, p.profit, p.profit_factor, p.equity_dd_relative FROM strategy_groups sg JOIN passes p ON sg.id_pass = p.id_pass;
Das Ergebnis der Abfrage ist die folgende Tabelle:
Abb. 1. Zusammensetzung der Gruppenbibliothek
In der Namensspalte werden die Namen der Gruppen angezeigt, die die Handelsinstrumente (Symbole), die Zeitrahmen und die Anzahl der Instanzen der in dieser Gruppe verwendeten Handelsstrategien widerspiegeln. Zum Beispiel bedeutet das Vorhandensein von „EUR-GBP-USD“, dass diese Gruppe Instanzen von Handelsstrategien enthält, die auf drei Symbolen arbeiten: EURGBP, EURUSD und GBPUSD. Wenn der Gruppenname mit „Only EURGBP“ beginnt, enthält er Instanzen von Strategien nur für das EURGBP-Symbol. Die verwendeten Zeitrahmen werden in ähnlicher Weise bezeichnet. Die Anzahl der Instanzen von Handelsstrategien wird am Ende des Namens angegeben. Die Angabe „3x16 Items“ bedeutet zum Beispiel, dass diese Gruppe drei standardisierte Gruppen von jeweils 16 Strategien umfasst.
Die Spalte custom_ontester zeigt den normalisierten durchschnittlichen Jahresgewinn für jede Gruppe an. Es sei darauf hingewiesen, dass die Bandbreite der Werte für diesen Parameter den erwarteten Wert übersteigt, sodass es in Zukunft notwendig wäre, die Gründe für dieses Phänomen zu verstehen. So waren beispielsweise die Ergebnisse der Gruppen, in denen nur GBPUSD verwendet wurde, deutlich höher als die der Gruppen mit mehreren Symbolen. Das beste Ergebnis wurde zuletzt in Zeile 20 gespeichert. In diese Gruppe haben wir Untergruppen aufgenommen, die für jedes Symbol und einen oder mehrere Zeitrahmen die besten Ergebnisse liefern.
Exportieren der Bibliothek
Der nächste Schritt besteht darin, die Gruppenbibliothek aus der Datenbank in eine mqh-Datei zu übertragen, die mit dem endgültigen EA verbunden werden kann. Dazu implementieren wir die Methoden in der Klasse CGroupsLibrary, die für den Export zuständig sind, und eine weitere Hilfs-EA, die zur Ausführung dieser Methoden verwendet wird.
In der Methode Export() werden die Namen der Bibliotheksgruppen und ihre Initialisierungs-Strings aus der Datenbank abgerufen und den entsprechenden Arrays hinzugefügt. Die erzeugten Arrays werden an die nächste Methode ExportParams() übergeben:
//+------------------------------------------------------------------+ //| Exporting passes to mqh file | //+------------------------------------------------------------------+ void CGroupsLibrary::Export(string p_idPasses) { // Array of group names string names[]; // Array of group initialization strings string params[]; // If the connection to the main database is established, if(DB::Connect()) { // Form a request to receive passes with the specified IDs string query = "SELECT sg.id_pass," " sg.name," " p.params" " FROM strategy_groups sg" " JOIN" " passes p ON sg.id_pass = p.id_pass"; query = StringFormat("%s " "WHERE p.id_pass IN (%s);", query, p_idPasses); // Prepare and execute the request int request = DatabasePrepare(DB::Id(), query); // If the request is successful if(request != INVALID_HANDLE) { // Structure for reading results struct Row { ulong idPass; string name; string params; } row; // For all query results, add the name and initialization string to the arrays while(DatabaseReadBind(request, row)) { APPEND(names, row.name); APPEND(params, row.params); } } DB::Close(); // Export to mqh file ExportParams(names, params); } }
In der Methode ExportParams() bilden wir einen String mit MQL5-Code, der eine Enumeration (enum) mit einem bestimmten Namen ENUM_GROUPS_LIBRARY erstellt und mit Elementen füllt. Jedes Element wird mit einem Kommentar versehen, der den Gruppennamen enthält. Als Nächstes deklariert der Code ein statisches String-Array CGroupsLibrary::s_params[], das mit Initialisierungs-Strings für Gruppen aus der Bibliothek gefüllt wird. Jeder Initialisierungs-String wird vorverarbeitet: Alle Zeilenvorschübe werden durch Leerzeichen ersetzt, und vor doppelten Anführungszeichen wird ein Backslash eingefügt. Dies ist notwendig, um die Initialisierungs-Strings im generierten Code in Anführungszeichen zu setzen.
Sobald der Code in der Datenvariablen vollständig ausgebildet ist, erstellen wir eine Datei mit dem Namen ExportedGroupsLibrary.mqh und speichern den erhaltenen Code darin.
//+------------------------------------------------------------------+ //| Export group names extracted from the database and | //| initialization strings in the form of MQL5 code | //+------------------------------------------------------------------+ void CGroupsLibrary::ExportParams(string &p_names[], string &p_params[]) { // ENUM_GROUPS_LIBRARY enumeration header string data = "enum ENUM_GROUPS_LIBRARY {\n"; // Fill the enumeration with group names FOREACH(p_names, { data += StringFormat(" GL_PARAMS_%d, // %s\n", i, p_names[i]); }); // Close the enumeration data += "};\n\n"; // Group initialization string array header and its opening bracket data += "string CGroupsLibrary::s_params[] = {"; // Fill the array by replacing invalid characters in the initialization strings string param; FOREACH(p_names, { param = p_params[i]; StringReplace(param, "\r", ""); StringReplace(param, "\n", " "); StringReplace(param, "\"", "\\\""); data += StringFormat("\"%s\",\n", param); }); // Close the array data += "};\n"; // Open the file to write data int f = FileOpen("ExportedGroupsLibrary.mqh", FILE_WRITE | FILE_TXT | FILE_ANSI); // Write the generated code FileWriteString(f, data); // Close the file FileClose(f); }
Nun kommt der sehr wichtige Teil:
// Connecting the exported mqh file. // It will initialize the CGroupsLibrary::s_params[] static variable // and ENUM_GROUPS_LIBRARY enumeration #include "ExportedGroupsLibrary.mqh"
Wir fügen die Datei, die nach dem Export empfangen wird, direkt in die Datei GroupsLibrary.mqh ein. In diesem Fall muss der endgültige EA nur diese Datei enthalten, um die exportierte Bibliothek verwenden zu können. Dieser Ansatz bringt eine kleine Unannehmlichkeit mit sich: Um den EA kompilieren zu können, der den Bibliotheksexport verarbeitet, sollte die Datei ExportedGroupsLibrary.mqh, die erst nach dem Export erscheint, bereits existieren. Wichtig ist jedoch nur das Vorhandensein dieser Datei, nicht ihr Inhalt. Daher sollten wir einfach eine leere Datei mit diesem Namen im aktuellen Ordner erstellen, und die Kompilierung wird ohne Fehler ablaufen.
Um die EA-Methode auszuführen, benötigen wir ein Skript oder einen EA, in dem dies geschehen soll. Das könnte folgendermaßen aussehen:
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "::: Exporting from library" input string passes_ = "802150,802151,802152,802153,802154," "802155,802156,802157,802158,802159," "802160,802161,802162,802164,802165," "802166,802167,802168,802169,802173"; // - Comma-separated IDs of the saved passes //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Call the group library export method CGroupsLibrary::Export(passes_); // Successful initialization return(INIT_SUCCEEDED); } void OnTick() { ExpertRemove(); }
Durch Änderung des Parameters passes_ können wir die Zusammensetzung und die Reihenfolge wählen, in der die Gruppen aus der Bibliothek in die Datenbank exportiert werden. Nachdem Sie den EA einmal auf dem Chart ausgeführt haben, erscheint die Datei ExportedGroupsLibrary.mqh im Datenordner des Terminals. Er sollte in den aktuellen Ordner übertragen werden, der den Projektcode enthält.
Erstellung des endgültigen EA
Wir haben endlich die letzte Phase erreicht. Es müssen nur noch einige kleinere Änderungen an dem EA SimpleVolumesExpert.mq5 vorgenommen werden. Zunächst müssen wir die Datei GroupsLibrary.mqh einbinden:
#include "GroupsLibrary.mqh"
Als Nächstes ersetzen wir passes_ durch eine neue Eingabe, die es uns ermöglicht, eine Gruppe aus der Bibliothek auszuwählen:
input group "::: Selection for the group" input ENUM_GROUPS_LIBRARY groupId_ = -1; // - Group from the library
In OnInit() werden die Initialisierungs-Strings nicht mehr (wie bisher) aus der Datenbank über die Durchlaufs-IDs geholt, sondern einfach aus dem Array CGroupsLibrary::s_params[] mit einem Index, der dem ausgewählten Wert der Eingabe groupId_ entspricht:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ... // Initialization string with strategy parameter sets string strategiesParams = NULL; // If the selected strategy group index from the library is valid, then if(groupId_ >= 0 && groupId_ < ArraySize(CGroupsLibrary::s_params)) { // Take the initialization string from the library for the selected group strategiesParams = CGroupsLibrary::s_params[groupId_]; } // If the strategy group from the library is not specified, then we interrupt the operation if(strategiesParams == NULL) { return INIT_FAILED; } ... // Successful initialization return(INIT_SUCCEEDED); }
Wir speichern die an der Datei SimpleVolumesExpert.mq5 vorgenommenen Änderungen im aktuellen Ordner.
Da wir den Enumerationselementen ENUM_GROUPS_LIBRARY Kommentare mit Namen hinzugefügt haben, können wir im Dialog für die Auswahl der EA-Parameter verständliche Namen und nicht nur eine Folge von Zahlen sehen:
Abb. 2. Auswahl einer Gruppe aus der Bibliothek über den Namen in den EA-Parametern
Führen wir den EA mit der letzten Gruppe aus der Liste aus und sehen wir uns das Ergebnis an:
Abb. 3. Ergebnisse des Tests des endgültigen EA mit der attraktivsten Gruppe aus der Bibliothek
Es ist klar, dass die Ergebnisse für den durchschnittlichen jährlichen normalisierten Gewinnindikator nahe an den in der Datenbank gespeicherten Ergebnissen lagen. Geringe Unterschiede sind in erster Linie darauf zurückzuführen, dass der endgültige EA eine standardisierte Gruppe verwendet hat (dies lässt sich anhand des Wertes des maximalen relativen Drawdowns überprüfen, der etwa 10 % der verwendeten Einlage beträgt). Bei der Generierung des Initialisierungs-Strings für diese Gruppe im EA SimpleVolumesStage3.ex5 war die Gruppe während des Durchlaufs noch nicht standardisiert, sodass der Drawdown dort etwa 5,4 % betrug.
Schlussfolgerung
Wir haben den endgültigen EA erhalten, der unabhängig von der im Optimierungsprozess gefüllten Datenbank arbeiten kann. Vielleicht werden wir auf dieses Thema noch einmal zurückkommen, denn die Praxis kann ihre eigenen Anpassungen vornehmen, und die in diesem Artikel vorgeschlagene Methode kann sich als weniger geeignet erweisen als eine andere. Aber in jedem Fall ist das Erreichen des gesetzten Ziels ein Schritt nach vorn.
Bei der Arbeit an dem Code für diesen Artikel wurden neue Umstände entdeckt, die eine weitere Untersuchung erfordern. Es hat sich zum Beispiel herausgestellt, dass die Ergebnisse beim Testen dieses EA nicht nur vom Kurs-Server abhängen, sondern auch von dem Symbol, das in den Einstellungen des Strategietesters als Hauptsymbol ausgewählt wurde. Möglicherweise müssen wir in der ersten und zweiten Phase einige Anpassungen an der Optimierungsautomatisierung vornehmen. Aber dazu beim nächsten Mal mehr.
Abschließend möchte ich eine Warnung aussprechen, die schon vorher implizit vorhanden war. Ich habe in den vorangegangenen Abschnitten nie behauptet, dass Sie, wenn Sie die vorgeschlagene Richtung einschlagen, einen garantierten Gewinn erzielen können. Im Gegenteil, wir haben in einigen Punkten enttäuschende Testergebnisse erhalten. Außerdem können wir trotz der Anstrengungen, die wir unternommen haben, um den EA für den realen Handel vorzubereiten, wahrscheinlich nicht sagen, dass wir alles Mögliche und Unmögliche getan haben, um den korrekten Betrieb des EA auf realen Konten zu gewährleisten. Dies ist ein perfektes Ergebnis, das angestrebt werden kann und sollte, aber es zu erreichen scheint immer eine Frage der nebligen Zukunft zu sein. Das hindert uns jedoch nicht daran, uns ihr zu nähern.
Alle in diesem Artikel und in allen vorangegangenen Artikeln dieser Reihe vorgestellten Ergebnisse beruhen lediglich auf historischen Testdaten und sind keine Garantie für zukünftige Gewinne. Die Arbeiten im Rahmen dieses Projekts haben Forschungscharakter. Alle veröffentlichten Ergebnisse können von jedermann auf eigenes Risiko verwendet werden.
Vielen Dank für Ihre Aufmerksamkeit! Bis bald!
Inhalt des Archivs
# | Name | Version | Beschreibung | Jüngste Änderungen |
---|---|---|---|---|
MQL5/Experten/Artikel.15360 | ||||
1 | Advisor.mqh | 1.04 | EA-Basisklasse | Teil 10 |
2 | Database.mqh | 1.04 | Klasse für den Umgang mit der Datenbank | Teil 17 |
3 | ExpertHistory.mqh | 1.00. | Klasse für den Export der Handelshistorie in eine Datei | Teil 16 |
4 | ExportedGroupsLibrary.mqh | — | Generierte Datei mit den Namen der Strategiegruppen und dem Array ihrer Initialisierungs-Strings | Teil 17 |
5 | Factorable.mqh | 1.01 | Basisklasse von Objekten, die aus einer Zeichenkette erstellt werden | Teil 10 |
6 | GroupsLibrary.mqh | 1.00. | Klasse für die Arbeit mit einer Bibliothek ausgewählter Strategiegruppen | Teil 17 |
7 | HistoryReceiverExpert.mq5 | 1.00. | EA für die Wiedergabe der Historie von Geschäften mit dem Risikomanager | Teil 16 |
8 | HistoryStrategy.mqh | 1.00. | Klasse der Handelsstrategie für die Wiederholung der Handelshistorie | Teil 16 |
9 | Interface.mqh | 1.00. | Basisklasse zur Visualisierung verschiedener Objekte | Teil 4 |
10 | LibraryExport.mq5 | 1.00. | EA, der Initialisierungs-Strings ausgewählter Pässe aus der Bibliothek in der Datei ExportedGroupsLibrary.mqh speichert | Teil 17 |
11 | Macros.mqh | 1.02 | Nützliche Makros für Array-Operationen | Teil 16 |
12 | Money.mqh | 1.01 | Grundkurs Geldmanagement | Teil 12 |
13 | NewBarEvent.mqh | 1.00. | Klasse zur Definition eines neuen Balkens für ein bestimmtes Symbol | Teil 8 |
14 | Receiver.mqh | 1.04 | Basisklasse für die Umwandlung von offenen Volumina in Marktpositionen | Teil 12 |
15 | SimpleHistoryReceiverExpert.mq5 | 1.00. | Vereinfachter EA für die Wiedergabe des Geschäftsverlaufs | Teil 16 |
16 | SimpleVolumesExpert.mq5 | 1.20 | EA für den parallelen Betrieb von mehreren Gruppen von Modellstrategien. Die Parameter werden aus der eingebauten Gruppenbibliothek übernommen. | Teil 17 |
17 | SimpleVolumesStage3.mq5 | 1.00. | Der EA, der eine generierte standardisierte Gruppe von Strategien in einer Bibliothek von Gruppen mit einem bestimmten Namen speichert. | Teil 17 |
18 | SimpleVolumesStrategy.mqh | 1.09 | Klasse der Handelsstrategie mit Tick-Volumen | Teil 15 |
19 | Strategy.mqh | 1.04 | Handelsstrategie-Basisklasse | Teil 10 |
20 | TesterHandler.mqh | 1.03 | Klasse zur Behandlung von Optimierungsereignissen | Teil 17 |
21 | VirtualAdvisor.mqh | 1.06 | Klasse des EA, der virtuelle Positionen (Aufträge) bearbeitet | Teil 15 |
22 | VirtualChartOrder.mqh | 1.00. | Grafische virtuelle Positionsklasse | Teil 4 |
23 | VirtualFactory.mqh | 1.04 | Objekt-Fabrik-Klasse | Teil 16 |
24 | VirtualHistoryAdvisor.mqh | 1.00. | Die Klasse des EA zur Wiederholung des Handelsverlaufs | Teil 16 |
25 | VirtualInterface.mqh | 1.00. | EA GUI-Klasse | Teil 4 |
26 | VirtualOrder.mqh | 1.04 | Klasse der virtuellen Aufträge und Positionen | Teil 8 |
27 | VirtualReceiver.mqh | 1.03 | Klasse für die Umwandlung von offenen Volumina in Marktpositionen (Empfänger) | Teil 12 |
28 | VirtualRiskManager.mqh | 1.02 | Klasse Risikomanagement (Risikomanager) | Teil 15 |
29 | VirtualStrategy.mqh | 1.05 | Klasse einer Handelsstrategie mit virtuellen Positionen | Teil 15 |
30 | VirtualStrategyGroup.mqh | 1.00. | Klasse der Handelsstrategien Gruppe(n) | Teil 11 |
31 | VirtualSymbolReceiver.mqh | 1.00. | Symbol-Empfängerklasse | Teil 3 |
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/15360





- 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.