Entwicklung eines Expert Advisors für mehrere Währungen (Teil 22): Beginn des Übergangs zum Hot-Swapping von Einstellungen
Einführung
In den beiden vorangegangenen Teilen unserer Artikelserie haben wir uns ernsthaft auf weitere Experimente mit der automatischen Optimierung von Handels-EAs vorbereitet. Das Hauptaugenmerk lag dabei auf der Erstellung eines Optimierungsförderers, der derzeit aus drei Stufen besteht:
- Optimierung einzelner Strategieinstanzen für bestimmte Kombinationen von Symbolen und Zeitrahmen.
- Bildung von Gruppen aus den besten Einzelproben, die in der ersten Stufe gewonnen wurden.
- Generierung der Initialisierungszeichenfolge des endgültigen EA, Kombination der gebildeten Gruppen und Speicherung in der Bibliothek.
Um die Möglichkeit zu gewährleisten, die Erstellung des Förderbandes selbst zu automatisieren, wurde ein spezielles EA-Skript entwickelt. Es ermöglicht die Befüllung der Datenbank mit Optimierungsprojekten und die Erstellung von Phasen, Aufträgen und Aufgaben für diese Projekte gemäß den festgelegten Parametern und Vorlagen. Dieser Ansatz bietet die Möglichkeit, weitere Optimierungsaufgaben in einer bestimmten Reihenfolge auszuführen, indem man von Stufe zu Stufe geht.
Außerdem haben wir nach Möglichkeiten gesucht, die Leistung durch Profiling und Codeoptimierung zu verbessern. Der Schwerpunkt lag dabei auf der Arbeit mit Objekten, die den Empfang von Informationen über Handelsinstrumente (Symbole) vermitteln. Dadurch konnte die Anzahl der Methodenaufrufe, die zum Abrufen von Preis- und Symbolspezifikationsdaten erforderlich sind, erheblich reduziert werden.
Das Ergebnis dieser Arbeit war die automatische Generierung von Ergebnissen, die für weitere Experimente und Analysen verwendet werden können. Dies eröffnet die Möglichkeit, Hypothesen darüber zu testen, wie sich die Häufigkeit und Reihenfolge der Re-Optimierung auf die Handelsleistung auswirken kann.
In diesem neuen Artikel werden wir uns mit der Implementierung eines neuen Mechanismus zum Laden von Parametern endgültiger EAs beschäftigen, der es ermöglichen soll, die Zusammensetzung und die Parameter einzelner Instanzen von Handelsstrategien teilweise oder vollständig zu ersetzen, sowohl während eines einzelnen Laufs im Strategietester als auch wenn der endgültige EA auf einem Handelskonto läuft.
Der Weg ist vorgezeichnet
Lassen Sie uns versuchen, genauer zu beschreiben, was wir erreichen wollen. Im Idealfall sollte das System in etwa so funktionieren:
- Es wird ein Projekt mit dem aktuellen Datum als Enddatum des Optimierungszeitraums erstellt.
- Das Projekt wird auf dem Förderband gestartet. Ihre Umsetzung nimmt einige Zeit in Anspruch – von einigen Tagen bis zu mehreren Wochen.
- Die Ergebnisse werden in den endgültigen EA geladen. Wenn der endgültige EA noch nicht gehandelt wurde, wird er auf einem echten Konto gestartet. Wenn er bereits an dem Konto gearbeitet hat, werden seine Parameter durch neue ersetzt, die er erhalten hat, nachdem das letzte Projekt die Förderanlage durchlaufen hat.
- Lassen Sie uns zu Punkt 1 übergehen.
Gehen wir auf jeden dieser Punkte ein. Zur Umsetzung des ersten Punktes haben wir bereits ein Projektgenerierungsskript EA aus dem vorherigen Teil, in dem wir Parameter zur Auswahl des Enddatums der Optimierung verwenden können. Im Moment kann es jedoch nur manuell gestartet werden. Dies kann behoben werden, indem eine zusätzliche Phase in den Projektausführungsprozess aufgenommen wird, die ein neues Projekt erzeugt, sobald alle anderen Phasen des aktuellen Projekts abgeschlossen sind. Dann können wir es beim ersten Mal nur manuell ausführen.
Für den zweiten Punkt brauchen wir nur ein Terminal mit dem installierten EA Optimization.ex5, in deren Parametern die gewünschte Datenbank angegeben ist. Sobald neue offene Projektaufgaben in der Warteschlange erscheinen, werden sie in der Reihenfolge der Warteschlange zur Ausführung gebracht. Die letzte Phase, die vor der Phase der Erstellung eines neuen Projekts liegt, sollte in irgendeiner Form die Ergebnisse der Projektoptimierung in den endgültigen EA übertragen.
Der dritte Punkt ist der schwierigste. Wir haben bereits eine einzige Option für die Übergabe von Parametern an den endgültigen EA implementiert, aber sie erfordert immer noch manuelle Eingriffe: Wir müssen einen separaten EA ausführen, der die Parameterbibliothek in eine Datei exportiert, dann diese Datei in den Projektordner kopieren und anschließend den endgültigen EA neu kompilieren. Obwohl wir nun die Ausführung dieser Operationen an den Programmcode delegieren können, wird die Struktur selbst unnötig schwerfällig. Ich würde gerne etwas Einfacheres und Zuverlässigeres machen.
Ein weiterer Nachteil der implementierten Methode zur Übergabe von Parametern an den endgültigen EA ist die Unmöglichkeit, Parameter teilweise zu ersetzen. Nur eine vollständige Ersetzung, die zur Schließung aller offenen Positionen, falls vorhanden, und zum Beginn des Handels von Grund auf führt. Und dieser Nachteil lässt sich nicht grundlegend beseitigen, wenn wir im Rahmen der bestehenden Methode bleiben.
Erinnern wir uns daran, dass wir mit Parametern jetzt die Parameter einer großen Anzahl von Instanzen einzelner Handelsstrategien meinen, die parallel in einem endgültigen EA arbeiten. Wenn alte Parameter sofort durch neue ersetzt werden, auch wenn sie weitgehend mit den alten identisch sind, kann die aktuelle Implementierung höchstwahrscheinlich keine Informationen über zuvor geöffnete virtuelle Positionen korrekt laden. Dies ist nur möglich, wenn die Anzahl und Reihenfolge, in der die Parameter der einzelnen Instanzen im Initialisierungsstring des endgültigen EA stehen, völlig identisch sind.
Um eine teilweise Ersetzung von Parametern zu ermöglichen, muss das gleichzeitige Vorhandensein von alten und neuen Parametern irgendwie gehandhabt werden. In diesem Fall kann ein fließender Übergangsalgorithmus entwickelt werden, der einige einzelne Instanzen unverändert lässt. Deren virtuelle Positionen sollten weiterhin in Betrieb bleiben. Die Positionen derjenigen Instanzen, die nicht zu den neuen Parametern gehören, sollten korrekt geschlossen werden. Neu hinzugefügte Instanzen sollten von Anfang an funktionieren.
Es sieht so aus, als ob sich größere Veränderungen anbahnen, als uns lieb ist. Aber was können wir tun, wenn wir keine andere Möglichkeit sehen, das gewünschte Ergebnis zu erreichen? Es ist besser, die Notwendigkeit von Veränderungen früher zu erkennen. Wenn wir uns weiter in eine Richtung bewegen, die nicht ganz richtig ist, dann wird es umso schwieriger, einen neuen Weg einzuschlagen, je weiter wir gehen.
Es ist also an der Zeit, sich der dunklen Seite der Speicherung aller Informationen über die Arbeit des EA in der Datenbank zuzuwenden. Außerdem in einer separaten Datenbank, da die für die Optimierung verwendeten Datenbanken sehr groß sind (mehrere Gigabyte pro Projekt). Es macht keinen Sinn, sie dem endgültigen EA zur Verfügung zu stellen, da nur ein winziger Teil der darin enthaltenen Informationen für die eigentliche Arbeit benötigt wird.
Wir würden auch gerne die Reihenfolge der Phasen der automatischen Optimierung ändern. Wir haben das in Teil 20 erwähnt, indem wir es als Gruppierung nach Symbol und Zeitrahmen bezeichnet haben. Aber wir haben uns damals nicht dafür entschieden, da ohne die Möglichkeit der teilweisen Ersetzung von Parametern kein Bedarf für einen solchen Auftrag bestand. Wenn jetzt alles klappt, wird es sich als vorteilhafter herausstellen. Aber lassen Sie uns zunächst versuchen, den Übergang zur Verwendung einer separaten Datenbank für den endgültigen EA zu vollziehen, um ein heißes Austauschen der Parameter einzelner Instanzen von Handelsstrategien zu gewährleisten.
Umwandlung der Initialisierungszeichenfolge
Die anstehende Aufgabe ist recht umfangreich, daher werden wir in kleinen Schritten vorgehen. Beginnen wir mit der Tatsache, dass wir Informationen über einzelne Instanzen von Handelsstrategien in der EA-Datenbank speichern müssen. Diese Information wird nun im EA-Initialisierungsstring bereitgestellt. Der EA kann sie entweder aus der Optimierungsdatenbank oder aus den in den EA-Code eingebauten Daten (String-Konstanten) beziehen, die in der Kompilierungsphase aus der Parameterbibliothek entnommen werden. Die erste Methode wird in den Optimierungs-EAs (SimpleVolumesStage2.mq5 und SimpleVolumesStage3.mq5) verwendet, die zweite Methode im endgültigen EA (SimpleVolumesExpert.mq5).
Wir möchten eine dritte Möglichkeit hinzufügen: Der Initialisierungsstring sollte in Teile unterteilt werden, die sich auf verschiedene einzelne Instanzen von Handelsstrategien beziehen; diese Teile werden in der EA-Datenbank gespeichert. Dann kann der EA sie aus seiner Datenbank lesen und aus den Teilen einen vollständigen Initialisierungsstring bilden. Es wird verwendet, um ein EA-Objekt zu erstellen, das alle weiteren Arbeiten ausführt.
Um zu verstehen, wie wir die Initialisierungszeichenfolge aufteilen können, schauen wir uns ein typisches Beispiel aus dem vorherigen Artikel an. Sie ist recht umfangreich (~200 Strings), daher zeigen wir nur den minimal notwendigen Teil, um eine Vorstellung von seiner Struktur zu vermitteln.
class CVirtualStrategyGroup([ class CVirtualStrategyGroup([ class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,1.00,1.30,80,3200.00,930.00,12000,3) ],8.428150), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,172,1.40,1.20,140,2200.00,1220.00,19000,3) ],12.357884), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,1.20,0.10,0,1800.00,780.00,8000,3) ],4.756016), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,172,0.30,0.10,150,4400.00,1000.00,1000,3) ],4.459508), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,0.50,1.10,200,2800.00,1030.00,32000,3) ],5.021593), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,172,1.40,1.70,100,200.00,1640.00,32000,3) ],18.155410), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.40,160,8400.00,1080.00,44000,3) ],4.313320), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,52,0.50,1.00,110,3600.00,1030.00,53000,3) ],4.490144), ],4.615527), class CVirtualStrategyGroup([ class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.80,240,4800.00,1620.00,57000,3) ],6.805962), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,52,0.50,1.80,40,400.00,930.00,53000,3) ],11.825922), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,212,1.30,1.50,160,600.00,1000.00,28000,3) ],16.866251), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,0.30,1.50,30,3000.00,1280.00,28000,3) ],5.824790), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,1.30,0.10,10,2000.00,780.00,1000,3) ],3.476085), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.10,0,16000.00,700.00,11000,3) ],4.522636), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,52,0.40,1.80,80,2200.00,360.00,25000,3) ],8.206812), class CVirtualStrategyGroup([ class CSimpleVolumesStrategy("GBPUSD",16385,12,0.10,0.10,0,19200.00,700.00,44000,3) ],2.698618), ],5.362505), class CVirtualStrategyGroup([ ... ],5.149065), ... class CVirtualStrategyGroup([ ... ],2.718278), ],2.072066)
Dieser Initialisierungsstring besteht aus verschachtelten Gruppen von Handelsstrategien der ersten, zweiten und dritten Ebene. Die einzelnen Instanzen von Handelsstrategien sind nur in Gruppen der dritten Ebene verschachtelt. Für jede Instanz sind Parameter festgelegt. Jede Gruppe hat einen Skalierungsfaktor, der auf der ersten, zweiten und dritten Ebene vorhanden ist. Die Verwendung von Skalierungsfaktoren wurde in Teil 5 erörtert. Sie werden benötigt, um die während des Testzeitraums erreichte maximale Absenkung auf den Wert von 10 % zu normalisieren. Außerdem wird der Wert des Skalierungsfaktors für eine Gruppe, die mehrere verschachtelte Gruppen oder mehrere verschachtelte Instanzen von Strategien enthält, zunächst durch die Anzahl der Elemente in dieser Gruppe geteilt, und dann wird dieser neue Faktor auf alle verschachtelten Elemente angewendet. So sieht es im Code der Datei VirtualStrategyGroup.mqh aus:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualStrategyGroup::CVirtualStrategyGroup(string p_params) { // Save the initialization string m_params = p_params; ... // 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 PrintFormat(__FUNCTION__" | Scale = %.2f, total groups = %d", m_scale, ArraySize(m_groups)); // 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 PrintFormat(__FUNCTION__" | Scale = %.2f, total strategies = %d", m_scale, ArraySize(m_strategies)); // 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)); } }
Der Initialisierungsstring hat also eine hierarchische Struktur, bei der die oberen Ebenen von Gruppen von Strategien besetzt sind und die Strategien selbst sich ganz unten befinden. Obwohl eine Strategiegruppe mehrere Strategien enthalten kann, kamen wir während der Entwicklung des Projekts zu dem Schluss, dass es für uns bequemer ist, nicht mehrere Strategien in einer Gruppe zu verwenden, sondern jede Instanz einer Strategie in ihre eigene persönliche Gruppe auf der unteren Ebene zu packen. Dies ist der Ausgangspunkt für die dritte Ebene. Die ersten beiden Ebenen ergeben sich aus der Gruppierung der Ergebnisse der ersten Optimierungsstufe und der anschließenden Gruppierung der Ergebnisse der zweiten Optimierungsstufe auf dem Förderband.
Wir können natürlich eine Tabellenstruktur in der Datenbank anlegen, um die bestehende Hierarchie zwischen Strategien und Gruppen beizubehalten, aber ist das wirklich notwendig? Nicht wirklich. In der Optimierungsförderung ist eine hierarchische Struktur erforderlich. Wenn es um die endgültige EA-Performance auf einem Handelskonto geht, ist alles, was zählt, eine Liste von Einzelinstanzen von Handelsstrategien mit korrekt berechneten Skalierungsfaktoren. Eine solche Liste erfordert eine einfache Tabelle zur Speicherung in einer Datenbank. Fügen wir also eine Methode hinzu, die eine solche Liste aus dem Initialisierungsstring füllt, und eine Methode, die die umgekehrte Aufgabe der Bildung eines Initialisierungsstrings für den endgültigen EA übernimmt, indem sie eine Liste einzelner Instanzen von Handelsstrategien mit den entsprechenden Multiplikatoren verwendet.
Exportieren einer Liste von Strategien
Beginnen wir mit der Methode, um eine Liste von EA-Strategien zu erhalten. Diese Methode sollte eine Methode der Klasse EA sein, da wir in ihr alle Informationen haben, die wir in die gewünschte Form für die Speicherung bringen wollen. Was wollen wir für jede einzelne Instanz einer Handelsstrategie speichern? Zunächst einmal die Initialisierungsparameter und der Skalierungsfaktor.
Als der vorige Absatz geschrieben wurde, gab es noch nicht einmal ansatzweise einen Code, der diese Aufgabe erfüllen würde. Es schien, dass die Ungewissheit in Form von Wahlfreiheit bei der Umsetzung es mir einfach nicht erlaubte, mich für eine bestimmte Variante zu entscheiden. Es kamen viele Fragen auf, wie man sie im Hinblick auf die künftige Nutzung verbessern kann. Da wir aber keine klare Vorstellung davon hatten, was wir in Zukunft brauchen würden und was nicht, konnten wir nicht einmal die trivialsten Entscheidungen treffen. Ist es zum Beispiel notwendig, die Versionsnummer in den Dateinamen der Datenbank aufzunehmen, die der EA verwenden wird? Und was ist mit der magischen Zahl? Sollte dieser Name in den Parametern des endgültigen EA angegeben werden, oder sollte er nach einem bestimmten Algorithmus aus dem Namen der Strategie und der magischen Zahl generiert werden? Oder etwas anderes?
Im Allgemeinen gibt es in solchen Fällen nur eine Möglichkeit, aus diesem Teufelskreis endloser Fragen auszubrechen. Wir müssen zumindest eine Entscheidung treffen, auch wenn es nicht die beste ist. Darauf aufbauend werden wir die nächste Version erstellen und so weiter. Andernfalls werden wir nicht vom Fleck kommen. Jetzt, da der Code geschrieben ist, können wir in aller Ruhe zurückblicken und die Schritte durchgehen, die Sie während der Entwicklung durchlaufen mussten. Nicht jede Lösung hat es in den endgültigen Code geschafft, und nicht jede Lösung wurde nicht angepasst, aber sie alle haben dazu beigetragen, den aktuellen Stand zu erreichen, den wir versuchen werden, weiter zu beschreiben.
Kommen wir also zum Export der Liste der Strategien. Als erstes müssen wir entscheiden, von wo aus sie aufgerufen werden soll. Dies sei der EA der dritten Stufe, der zuvor eine Gruppe von Strategien für den endgültigen EA exportiert hat. Wie bereits erwähnt, mussten jedoch zusätzlich andere Manipulationen vorgenommen werden, um diese Informationen in der endgültigen EA zu verwenden. Am Ausgang der dritten Stufe erhielten wir nur die IDs der Durchgänge mit den zugewiesenen Namen in der Tabelle strategy_groups in der Optimierungsdatenbank. So sieht der Inhalt nach der Optimierung aus, die während der Arbeit an Teil 21 durchgeführt wurde:

Jeder dieser vier Durchläufe enthält einen gespeicherten Initialisierungsstring für eine Gruppe von einzelnen Handelsstrategieinstanzen, die während der Optimierung in einem Testintervall mit demselben Startdatum (2018.01.01) und einem leicht abweichenden Enddatum, das im Gruppennamen angegeben ist, ausgewählt wurden.
Wir ersetzen in der Datei SimpleVolumesStage3.mq5 den Aufruf der Funktion, die den Export in dieser Form durchgeführt hat, durch den Aufruf einer anderen (noch fehlenden) Funktion:
//+------------------------------------------------------------------+ //| 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_, fileName_); expert.Export(groupName_, advFileName_); } return res; }
Zur Klasse CVirtualAdvisor fügen wir die neue Methode Export() hinzu. Als Parameter werden der Name der neuen Gruppe und der Name der EA-Datenbankdatei, in die exportiert werden soll, übergeben. Bitte beachten Sie, dass es sich hierbei um eine neue Datenbank handelt und nicht um die bisher verwendete Optimierungsdatenbank. Um diesem Argument einen Wert zuzuweisen, fügen wir der dritten Stufe EA einen Eingang hinzu:
input group "::: Saving to library" input string groupName_ = "SimpleVolumes_v.1.20_2023.01.01"; // - Version name (if empty - not saving) input string advFileName_ = "SimpleVolumes-27183.test.db.sqlite"; // - EA database name
Wir haben noch nie direkt mit der Datenbank auf der Ebene der EA-Klasse gearbeitet. Alle Methoden, die direkt SQL-Abfragen erzeugen, wurden in die separate statische Klasse CTesterHandler verschoben. Wir wollen diese Struktur also nicht zerstören und leiten die empfangenen Argumente an die neue Methode CTesterHandler::Export() weiter, die ihnen das Array der EA-Strategien hinzufügt:
//+------------------------------------------------------------------+ //| Export the current strategy group to the specified EA database | //+------------------------------------------------------------------+ void CVirtualAdvisor::Export(string p_groupName, string p_advFileName) { CTesterHandler::Export(m_strategies, p_groupName, p_advFileName); }
Um diese Methode zu implementieren, müssen wir die Struktur der Tabellen in der EA-Datenbank bestimmen, und das Vorhandensein einer neuen Datenbank wird die Notwendigkeit mit sich bringen, die Fähigkeit zur Verbindung mit verschiedenen Datenbanken sicherzustellen.
Zugang zu verschiedenen Datenbanken
Nach langem Überlegen habe ich mich für die folgende Option entschieden. Wir ändern wir die bestehende Klasse CDatabase so, dass wir nicht nur den Namen der Datenbankdatei, sondern auch ihren Typ angeben können. In Anbetracht des neuen Datenbanktyps werden wir drei verschiedene Typen verwenden müssen:
- Optimierungsdatenbank. Dient zum Anordnen von Autooptimierungsprojekten und zum Speichern von Informationen über Strategieprüfungsdurchläufe, die innerhalb des Autooptimierungsförderers durchgeführt wurden.
- Datenbank für die Gruppenauswahl (verkürzte Optimierungsdatenbank). Wird verwendet, um den erforderlichen Teil der Optimierungsdatenbank an entfernte Testagenten in der zweiten Phase des Autooptimierungsförderers zu senden.
- Experten-Datenbank (finaler EA). Eine Datenbank, die vom endgültigen EA, der auf dem Handelskonto arbeitet, verwendet wird, um alle notwendigen Informationen über seine Arbeit zu speichern, einschließlich der Zusammensetzung der Gruppe der einzelnen Instanzen der verwendeten Handelsstrategien.
Wir erstellen drei Dateien, um den SQL-Code für die Erstellung der einzelnen Datenbanktypen zu speichern, verbinden sie als Ressourcen mit der Datei Database.mqh und erstellen eine Enumeration für die drei Datenbanktypen:
// Import SQL files for creating database structures of different types #resource "db.opt.schema.sql" as string dbOptSchema #resource "db.cut.schema.sql" as string dbCutSchema #resource "db.adv.schema.sql" as string dbAdvSchema // Database type enum ENUM_DB_TYPE { DB_TYPE_OPT, // Optimization database DB_TYPE_CUT, // Database for group selection (stripped down optimization database) DB_TYPE_ADV, // EA (final EA) database };
Da wir nun Zugang zu Skripten haben, mit denen wir jede dieser drei Arten von Datenbanken erstellen können (natürlich nur, wenn wir sie mit dem entsprechenden Inhalt füllen), können wir die Logik der Datenbankverbindungsmethode Connect() ändern. Wenn sich herausstellt, dass die Datenbank mit dem übergebenen Namen nicht existiert, wird anstelle einer Fehlermeldung eine Verbindung mit der neu erstellten Datenbank hergestellt.
Um zu verstehen, welche Art von Datenbank wir benötigen, fügen wir der Verbindungsmethode eine Eingabe hinzu, über die wir den gewünschten Typ übergeben können. Um die Notwendigkeit zu verringern, bestehenden Code zu bearbeiten, setzen wir den Standardwert für diesen Parameter auf den Datenbanktyp Optimierung, da wir uns bisher überall mit diesem verbunden haben:
//+------------------------------------------------------------------+ //| Create an empty DB | //+------------------------------------------------------------------+ void CDatabase::Create(string p_schema) { bool res = Execute(p_schema); if(res) { PrintFormat(__FUNCTION__" | Database successfully created from %s", "db.*.schema.sql"); } } //+------------------------------------------------------------------+ //| Check connection to the database with the given name | //+------------------------------------------------------------------+ bool CDatabase::Connect(string p_fileName, ENUM_DB_TYPE p_dbType = DB_TYPE_OPT) { // If the database is open, close it Close(); // If a file name is specified, save it s_fileName = p_fileName; // Set the shared folder flag for the optimization and EA databases s_common = (p_dbType != DB_TYPE_CUT ? DATABASE_OPEN_COMMON : 0); // Open the database // Try to open an existing DB file s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | s_common); // If the DB file is not found, try to create it when opening if(!IsOpen()) { s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | s_common); // Report an error in case of failure if(!IsOpen()) { PrintFormat(__FUNCTION__" | ERROR: %s Connect failed with code %d", s_fileName, GetLastError()); return false; } if(p_dbType == DB_TYPE_OPT) { Create(dbOptSchema); } else if(p_dbType == DB_TYPE_CUT) { Create(dbCutSchema); } else { Create(dbAdvSchema); } } return true; }
Bitte beachten Sie, dass ich mich entschieden habe, die Optimierungs- und EA-Datenbanken im gemeinsamen Ordner des Terminals und die Gruppenauswahldatenbank im Arbeitsordner des Terminals zu speichern. Andernfalls ist es nicht möglich, den automatischen Versand an die Testpersonen zu veranlassen.
EA-Datenbank
Um Informationen über die generierten Strategiegruppen in der EA-Datenbank zu speichern, habe ich mich für zwei Tabellen entschieden: strategy_groups und strategies mit der folgenden Struktur:

CREATE TABLE strategies ( id_strategy INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, id_group INTEGER REFERENCES strategy_groups (id_group) ON DELETE CASCADE ON UPDATE CASCADE, hash TEXT NOT NULL, params TEXT NOT NULL ); CREATE TABLE strategy_groups ( id_group INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, from_date TEXT, to_date TEXT, create_date TEXT );
Wie wir sehen können, verweist jeder Eintrag in der Strategietabelle auf einen Eintrag in der Strategiegruppentabelle. Daher können wir viele verschiedene Gruppen von Strategien gleichzeitig in dieser Datenbank speichern.
Im Hash-Feld der Strategietabelle wird der Hash-Wert der Parameter einer einzelnen Instanz einer Handelsstrategie gespeichert. Damit kann man später nachvollziehen, ob eine einzelne Instanz aus einer bestimmten Gruppe mit einer Instanz aus einer anderen Gruppe identisch ist.
Das Feld params in der Tabelle strategies speichert den Initialisierungsstring einer einzelnen Instanz einer Handelsstrategie. Aus dieser Instanz kann ein gemeinsamer Initialisierungsstring für die gesamte Gruppe von Strategien gebildet werden, um ein EA-Objekt (CVirtualAdvisor-Klasse) im endgültigen EA zu erstellen.
Die Felder from_date und to_date in der Tabelle strategy_groups speichern weiterhin das Start- und Enddatum des Optimierungsintervalls, das zur Bildung dieser Gruppe verwendet wurde. Bis auf weiteres bleiben sie einfach leer.
Erneut Strategien exportieren
Jetzt sind wir bereit, die Methode zum Exportieren einer Gruppe von Strategien in die EA-Datenbank in TesterHandler.mqh zu implementieren. Dazu müssen wir eine Verbindung zur gewünschten Datenbank herstellen, einen Datensatz für die neue Strategiegruppe in der Tabelle strategy_groups erstellen, einen Initialisierungsstring für jede Strategie aus der Gruppe mit ihrem aktuellen Normierungsfaktor erzeugen (verpackt in „class CVirtualStrategyGroup([strategy], scale)“) und sie in der Tabelle strategies speichern.
//+------------------------------------------------------------------+ //| Export an array of strategies to the specified EA database | //| as a new group of strategies | //+------------------------------------------------------------------+ void CTesterHandler::Export(CStrategy* &p_strategies[], string p_groupName, string p_advFileName) { // Connect to the required EA database if(DB::Connect(p_advFileName, DB_TYPE_ADV)) { string fromDate = ""; // Start date of the optimization interval string toDate = ""; // End date of the optimization interval // Create an entry for a new strategy group string query = StringFormat("INSERT INTO strategy_groups VALUES(NULL, '%s', '%s', '%s', NULL) RETURNING rowid;", p_groupName, fromDate, toDate); ulong groupId = DB::Insert(query); PrintFormat(__FUNCTION__" | Export %d strategies into new group [%s] with ID=%I64u", ArraySize(p_strategies), p_groupName, groupId); // For each strategy FOREACH(p_strategies, { CVirtualStrategy *strategy = p_strategies[i]; // Form an initialization string as a group of one strategy with a normalizing factor string params = StringFormat("class CVirtualStrategyGroup([%s],%0.5f)", ~strategy, strategy.Scale()); // Save it in the EA database with the new group ID specified string query = StringFormat("INSERT INTO strategies " "VALUES (NULL, %I64u, '%s', '%s')", groupId, strategy.Hash(~strategy), params); DB::Execute(query); }); // Close the database DB::Close(); } }
Um den Hash-Wert aus den Strategieparametern zu berechnen, haben wir die bestehende Methode aus der EA-Klasse in die übergeordnete Klasse CFactorable verschoben. Daher ist sie nun für alle Nachkommen dieser Klasse verfügbar, einschließlich der Handelsstrategieklassen.
Wenn wir nun die dritte Phase des Optimierungsprojekts erneut durchführen, werden wir feststellen, dass die Strategietabelle Einträge mit einzelnen Instanzen von Handelsstrategien enthält:

Die Tabelle strategy_group enthält nun Einträge zu den endgültigen Gruppen für jedes Projekt:

Nachdem wir den Export geklärt haben, wenden wir uns nun dem umgekehrten Vorgang zu – dem Importieren dieser Gruppen in den endgültigen EA.
Strategien importieren
Ich werde die zuvor implementierte Methode zum Exportieren von Gruppen vorerst nicht vollständig aufgeben. Wir sollten die Möglichkeit schaffen, die neue und die alte Methode parallel zu verwenden. Wenn sich die neue Methode als erfolgreich erweist, können wir darüber nachdenken, die alte Methode aufzugeben.
Nehmen wir unseren endgültigen EA SimpleVolumesExpert.mq5 und fügen eine neue Eingabe newGroupId_ hinzu, über die wir den Wert der Strategiegruppen-ID aus der neuen Bibliothek setzen können:
input group "::: Use a strategy group" input ENUM_GROUPS_LIBRARY groupId_ = -1; // - Group from the old library OR: input int newGroupId_ = 0; // - ID of the group from the new library (0 - last)
Fügen wir eine Konstante für den Namen des endgültigen EA hinzu:
#define __NAME__ "SimpleVolumes"
In der abschließenden EA-Initialisierungsfunktion wird zunächst geprüft, ob im Parameter groupId_ eine Gruppe aus der alten Bibliothek ausgewählt ist. Wenn nicht, wird der Initialisierungsstring aus der neuen Bibliothek geholt. Zu diesem Zweck erhält die EA-Klasse CVirtualAdvisor zwei neue statische Methoden: Dateiname() und Import(). Sie können aufgerufen werden, bevor das EA-Objekt erstellt wird.
//+------------------------------------------------------------------+ //| 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_]; } else { // Take the initialization string from the new library for the selected group // (from the EA database) strategiesParams = CVirtualAdvisor::Import( CVirtualAdvisor::FileName(__NAME__, magic_), newGroupId_ ); } // 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 werden weitere Änderungen in der Datei VirtualAdvisor.mqh vornehmen. Fügen wir die beiden oben genannten Methoden hinzu:
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: // ... public: // ... // Name of the file with the EA database static string FileName(string p_name, ulong p_magic = 1); // Get the strategy group initialization string // from the EA database with the given ID static string Import(string p_fileName, int p_groupId = 0); };
In der Methode FileName() legen wir die Regel für die Bildung des Namens der EA-Datenbankdatei fest. Sie enthält den Namen des endgültigen EA und seine magische Zahl, sodass EAs mit unterschiedlichen magischen Zahlen immer unterschiedliche Datenbanken verwenden. Die Endung „.test“ wird auch automatisch hinzugefügt, wenn der EA im Strategietester gestartet wird. Damit soll verhindert werden, dass ein im Testprogramm laufender EA versehentlich Informationen in der Datenbank eines bereits auf einem Handelskonto laufenden EA überschreibt.
//+------------------------------------------------------------------+ //| Name of the file with the EA database | //+------------------------------------------------------------------+ string CVirtualAdvisor::FileName(string p_name, ulong p_magic = 1) { return StringFormat("%s-%d%s.db.sqlite", (p_name != "" ? p_name : "Expert"), p_magic, (MQLInfoInteger(MQL_TESTER) ? ".test" : "") ); }
In der Methode Import() holen wir uns die Liste der Initialisierungsstrings einzelner Instanzen von Handelsstrategien, die zu einer bestimmten Gruppe gehören, aus der EA-Datenbank. Ist die ID der gewünschten Gruppe gleich Null, so wird die Liste der Strategien der zuletzt erstellten Gruppe geladen.
Aus der sich ergebenden Liste bilden wir eine Strategiegruppen-Initialisierungszeichenfolge, indem wir die durch Kommas getrennten Strategie-Initialisierungszeichenfolgen zusammenfügen und die resultierende Zeichenfolge an der gewünschten Stelle in die zu bildende Gruppen-Initialisierungszeichenfolge einfügen. Der Skalierungsfaktor für die Gruppe im Initialisierungsstring wird gleich der Anzahl der Strategien gesetzt. Dies ist notwendig, damit bei der Erstellung eines EA mit einem solchen Gruppeninitialisierungsstring die Skalierungsfaktoren aller Strategien den in der Expertendatenbank gespeicherten gleich sind. Denn bei der Erstellung werden die Multiplikatoren aller Strategien in der Gruppe automatisch durch die Anzahl der Strategien in der Gruppe geteilt. In diesem Fall war es genau das, was uns gestört hat, und um dieses Hindernis zu umgehen, haben wir den Gruppenmultiplikator gezielt um die gleiche Anzahl von Malen erhöht, um die er dann sinken sollte.
//+------------------------------------------------------------------+ //| Get the strategy group initialization string | //| from the EA database with the given ID | //+------------------------------------------------------------------+ string CVirtualAdvisor::Import(string p_fileName, int p_groupId = 0) { string params[]; // Array for strategy initialization strings // Request to get strategies of a given group or the last group string query = StringFormat("SELECT id_group, params " " FROM strategies" " WHERE id_group = %s;", (p_groupId > 0 ? (string) p_groupId : "(SELECT MAX(id_group) FROM strategy_groups)")); // Open EA database if(DB::Connect(p_fileName, DB_TYPE_ADV)) { // Execute the request int request = DatabasePrepare(DB::Id(), query); // If there is no error if(request != INVALID_HANDLE) { // Data structure for reading a single string of a query result struct Row { int groupId; string params; } row; // Read data from the first result string while(DatabaseReadBind(request, row)) { // Remember the strategy group ID // in the static property of the EA class s_groupId = row.groupId; // Add another strategy initialization string to the array APPEND(params, row.params); } } else { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: request \n%s\nfailed with code %d", query, GetLastError()); } // Close the EA database DB::Close(); } // Strategy group initialization string string groupParams = NULL; // Total number of strategies in the group int totalStrategies = ArraySize(params); // If there are strategies, then if(totalStrategies > 0) { // Concatenate their initialization strings with commas JOIN(params, groupParams, ","); // Create a strategy group initialization string groupParams = StringFormat("class CVirtualStrategyGroup([%s], %.5f)", groupParams, totalStrategies); } // Return the strategy group initialization string return groupParams; }
Diese Methode ist nicht ganz rein, denn sie gibt nicht nur den Initialisierungsstring der Gruppe zurück, sondern setzt auch den Wert einer statischen Eigenschaft der Klasse CVirtualAdvisor::s_groupId gleich der ID der geladenen Strategiegruppe. Diese Methode, sich zu merken, welche Gruppe aus der Bibliothek geladen wurde, schien recht einfach und zuverlässig, wenn auch nicht sehr hübsch.
Übermittlung der endgültigen EA-Daten
Da wir bereits eine separate Datenbank für die Speicherung der Parameter für die Erstellung einzelner Instanzen von Handelsstrategien eingerichtet haben, die vom endgültigen EA verwendet werden, bleiben wir nicht auf halbem Weg stehen und übertragen die Speicherung der restlichen Informationen über den Betrieb des endgültigen EA auf dem Handelskonto in dieselbe Datenbank. Bisher wurden solche Informationen mit der Methode CVitrualAdvisor::Save() in einer separaten Datei gespeichert und konnten bei Bedarf mit der Methode CVitrualAdvisor::Load() daraus geladen werden.
In der Datei werden unter anderem folgende Informationen gespeichert:
- Allgemeine EA-Parameter: letzte Speicherzeit, und... das ist alles für jetzt. Diese Liste kann jedoch in Zukunft erweitert werden.
- Die Daten jeder Strategie: eine Liste der virtuellen Positionen und aller Daten, die die Strategie möglicherweise speichern muss. Derzeit erfordern die verwendeten Strategien keine Speicherung zusätzlicher Daten, aber für andere Arten von Strategien kann dieser Bedarf entstehen.
- Daten des Risikomanagers: aktueller Status, aktueller Kontostand und Eigenkapitalausstattung, Multiplikatoren für die Positionsgröße usw.
Der Nachteil der bisher gewählten Implementierungsmethode ist, dass die Datendatei nur in ihrer Gesamtheit gelesen und interpretiert werden kann. Wenn wir z. B. die Anzahl der Strategien im Initialisierungsstring erhöhen und den endgültigen EA neu starten wollen, kann er die Datei mit den gespeicherten Daten nicht fehlerfrei lesen. Beim Lesen erwartet der endgültige EA, dass die Informationen für die hinzugefügten Strategien ebenfalls in der Datei vorhanden sind. Aber sie ist nicht da. Daher wird die Lademethode versuchen, die nächsten Daten aus der Datei, die sich in der Tat bereits auf die Risikomanagerdaten beziehen, als Daten für zusätzliche Handelsstrategien zu interpretieren. Es ist klar, dass dies nicht gut ausgehen wird.
Um dieses Problem zu lösen, müssen wir von der streng sequentiellen Speicherung aller Informationen über die Arbeit des endgültigen EA wegkommen, und die Verwendung einer Datenbank wird hier sehr nützlich sein. Lassen Sie uns darin eine einfache Speicherung beliebiger Daten in der Schlüssel-Wert-Form (Key-Value) vornehmen.
Key-Value-Speicherung
Obwohl wir oben die Speicherung beliebiger Daten erwähnt haben, muss die Aufgabe nicht so weit gefasst werden. Nachdem wir uns angesehen haben, was derzeit in der endgültigen EA-Datendatei gespeichert wird, können wir uns darauf beschränken, die Erhaltung einzelner Zahlen (ganzzahlig und reell) und virtueller Positionsobjekte sicherzustellen. Wir erinnern uns auch daran, dass jede Strategie ein Array virtueller Positionen mit einer festen Größe hat. Diese Größe wird in den Initialisierungsparametern der Strategie angegeben. Virtuelle Positionsobjekte existieren also immer als Teil eines Arrays. Und für die Zukunft werden wir ab sofort die Möglichkeit bieten, nicht nur einzelne Nummern, sondern auch eine Reihe von Nummern verschiedener Typen zu speichern.
Unter Berücksichtigung der obigen Ausführungen erstellen wir eine neue statische Klasse, die die folgenden Methoden enthalten wird:
- Verbindungen zur gewünschten Datenbank: Connect()/Close()
- Einstellung von Werten unterschiedlicher Art: Set(...)
- Lesen von Werten verschiedener Typen: Get(...)
//+------------------------------------------------------------------+ //| Class for working with the EA database in the form of | //| Key-Value storage for properties and virtual positions | //+------------------------------------------------------------------+ class CStorage { protected: static bool s_res; // Result of all database read/write operations public: // Connect to the EA database static bool Connect(string p_fileName); // Close connection to the database static void Close(); // Save a virtual order/position static void Set(int i, CVirtualOrder* order); // Store a single value of an arbitrary simple type template<typename T> static void Set(string key, const T &value); // Store an array of values of an arbitrary simple type template<typename T> static void Set(string key, const T &values[]); // Get the value as a string for the given key static string Get(string key); // Get an array of virtual orders/positions for a given strategy hash static bool Get(string key, CVirtualOrder* &orders[]); // Get the value for a given key into a variable of an arbitrary simple type template<typename T> static bool Get(string key, T &value); // Get an array of values of a simple type by a given key into a variable template<typename T> static bool CStorage::Get(string key, T &values[]); // Result of operations static bool Res() { return s_res; } };
Wir haben der Klasse die statische Eigenschaft s_res und die Methode zum Lesen ihres Wertes hinzugefügt. Sie speichert einen Hinweis auf alle Fehler, die bei Lese- und Schreibvorgängen in der Datenbank aufgetreten sind.
Da diese Klasse nur zum Speichern und Laden des Zustands des endgültigen EA verwendet werden soll, wird auch die Verbindung zur Datenbank nur zu diesen Zeitpunkten hergestellt. Solange die Verbindung nicht geschlossen ist, werden keine weiteren sinnvollen Operationen mit der Datenbank durchgeführt. Daher wird bei der Datenbankverbindungsmethode sofort eine Transaktion eröffnet, innerhalb derer alle Operationen mit der Datenbank stattfinden, und bei der Verbindungsschließungsmethode wird diese Transaktion entweder bestätigt oder abgebrochen:
//+------------------------------------------------------------------+ //| Connect to the EA database | //+------------------------------------------------------------------+ bool CStorage::Connect(string p_fileName) { // Connect to the EA database if(DB::Connect(p_fileName, DB_TYPE_ADV)) { // No errors yet s_res = true; // Start a transaction DatabaseTransactionBegin(DB::Id()); return true; } return false; } //+------------------------------------------------------------------+ //| Close the database connection | //+------------------------------------------------------------------+ void CStorage::Close() { // If there are no errors, if(s_res) { // Confirm the transaction DatabaseTransactionCommit(DB::Id()); } else { // Otherwise, cancel the transaction DatabaseTransactionRollback(DB::Id()); } // Close connection to the database DB::Close(); }
Fügen wir der endgültigen EA-Datenbankstruktur zwei weitere Tabellen mit dem folgenden Satz von Spalten hinzu:

Die erste Tabelle (strorage) wird zum Speichern einzelner numerischer Werte und von Arrays numerischer Werte verwendet. Aber auch Zeichenketten können dort gespeichert werden. Die zweite Tabelle (storage_orders) wird verwendet, um Informationen über die Elemente der virtuellen Positionsfelder für verschiedene Instanzen von Handelsstrategien zu speichern. Deshalb befinden sich die Spalten strategy_hash und strategy_index am Anfang der Tabelle und speichern den Hash-Wert der Strategieparameter (einzigartig für jede Strategie) und den Index der virtuellen Position im Array der virtuellen Positionen der Strategie.
Alle einzelnen numerischen Werte werden durch den Aufruf der Template-Methode Set() gespeichert, die als Parameter eine Zeichenkette mit dem Schlüsselnamen und eine Variable eines beliebig einfachen Typs T erhält. Dies kann z.B. int, ulong oder double sein. Bei der Erstellung einer SQL-Abfrage zum Speichern wird der Wert dieser Variablen in einen String umgewandelt und in der Datenbank als String gespeichert:
//+------------------------------------------------------------------+ //| Store a single value of an arbitrary simple type | //+------------------------------------------------------------------+ template<typename T> void CStorage::Set(string key, const T &value) { // Escape single quotes (can't avoid using them yet) // StringReplace(key, "'", "\\'"); // StringReplace(value, "'", "\\'"); // Request to save the value string query = StringFormat("REPLACE INTO storage(key, value) VALUES('%s', '%s');", key, (string) value); // Execute the request s_res &= DatabaseExecute(DB::Id(), query); if(!s_res) { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: Execution failed in DB [adv], query:\n" "%s\n" "error code = %d", query, GetLastError()); } }
In dem Fall, in dem wir ein Array von Werten des einfachen Typs für einen Schlüssel speichern wollen, erstellen wir zunächst einen String mit einem Trennzeichen aus allen Werten des übergebenen Arrays. Das Komma wird als Trennzeichen verwendet. Dies geschieht in einer anderen Template-Methode mit dem gleichen Namen wie Set(), nur dass ihr zweiter Parameter kein Verweis auf eine Variable eines einfachen Typs ist, sondern ein Verweis auf ein Array von Werten eines einfachen Typs:
//+------------------------------------------------------------------+ //| Store an array of values of an arbitrary simple type | //+------------------------------------------------------------------+ template<typename T> void CStorage::Set(string key, const T &values[]) { string value = ""; // Concatenate all values from the array into one string separated by commas JOIN(values, value, ","); // Save a string with a specified key Set(key, value); }
Um die umgekehrten Operationen – das Lesen aus der Datenbank – durchzuführen, fügen wir die Methode Get() hinzu, die bei Angabe eines Schlüsselwerts die in der Datenbank unter diesem Schlüssel gespeicherte Zeile zurückgibt. Um einen Wert des gewünschten einfachen Typs zu erhalten, erstellen wir eine Template-Methode mit demselben Namen, die jedoch zusätzlich einen Verweis auf eine Variable eines beliebigen einfachen Typs als zweites Argument akzeptiert. In dieser Methode erhalten wir zunächst einen Wert aus der Datenbank als String, und wenn wir ihn erhalten konnten, wandeln wir ihn von einem String in den gewünschten Typ um und schreiben ihn in die übergebene Variable.
//+------------------------------------------------------------------+ //| Get the value as a string for the given key | //+------------------------------------------------------------------+ string CStorage::Get(string key) { string value = NULL; // Return value // Request to get the value string query = StringFormat("SELECT value FROM storage WHERE key='%s'", key); // Execute the request int request = DatabasePrepare(DB::Id(), query); // If there is no error if(request != INVALID_HANDLE) { // Read data from the first result string DatabaseRead(request); if(!DatabaseColumnText(request, 0, value)) { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: Reading row in DB [adv] for request \n%s\n" "failed with code %d", query, GetLastError()); } } else { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: Request in DB [adv] \n%s\nfailed with code %d", query, GetLastError()); } return value; } //+------------------------------------------------------------------+ //| Get the value for a given key into a variable | //| of an arbitrary simple type | //+------------------------------------------------------------------+ template<typename T> bool CStorage::Get(string key, T &value) { // Get the value as a string string res = Get(key); // If the value is received if(res != NULL) { // Cast it to type T and assign it to the target variable value = (T) res; return true; } return false; }
Verwenden wir die hinzugefügten Methoden, um den Zustand des endgültigen EA zu speichern und zu laden.
Speichern und Herunterladen eines EA
In der EA-Zustandssicherungsmethode CVirtualAdvisor::Save() müssen wir nur eine Verbindung zur EA-Datenbank herstellen und alles, was wir brauchen, speichern, indem wir entweder direkt die Methoden der Klasse CStorage oder indirekt die Methoden Save()/Load() für die Objekte aufrufen, die gespeichert werden müssen.
Derzeit werden nur zwei Werte direkt gespeichert: der Zeitpunkt der letzten Änderungen in der Zusammensetzung der virtuellen Positionen und die ID der Strategiegruppe. Rufen Sie anschließend die Methode Save() für alle Strategien in der Schleife auf. Und schließlich wird die Speichermethode des Risikomanagers aufgerufen. Wir müssen auch Änderungen an den genannten Methoden vornehmen, damit sie auch in der EA-Datenbank gespeichert werden.
//+------------------------------------------------------------------+ //| Save status | //+------------------------------------------------------------------+ bool CVirtualAdvisor::Save() { // Save status if: if(true // later changes appeared && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime // currently, there is no optimization && !MQLInfoInteger(MQL_OPTIMIZATION) // and there is no testing at the moment or there is a visual test at the moment && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)) ) { // If the connection to the EA database is established if(CStorage::Connect(m_fileName)) { // Save the last modification time CStorage::Set("CVirtualReceiver::s_lastChangeTime", CVirtualReceiver::s_lastChangeTime); CStorage::Set("CVirtualAdvisor::s_groupId", CVirtualAdvisor::s_groupId); // Save all strategies FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save()); // Save the risk manager m_riskManager.Save(); // Update the last save time m_lastSaveTime = CVirtualReceiver::s_lastChangeTime; PrintFormat(__FUNCTION__" | OK at %s to %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS), m_fileName); // Close the connection CStorage::Close(); // Return the result return CStorage::Res(); } else { PrintFormat(__FUNCTION__" | ERROR: Can't open database [%s], LastError=%d", m_fileName, GetLastError()); return false; } } return true; }
In der Download-Methode CVirtualAdvisor::Load() werden die umgekehrten Operationen durchgeführt: Lesen des Wertes der letzten Änderungszeit und der ID der Strategiegruppe aus der Datenbank, wonach jede Strategie und jeder Risikomanager seine Informationen lädt. Wenn sich herausstellt, dass der Zeitpunkt der letzten Änderung in der Zukunft liegt, laden wir nichts weiter. Diese Situation kann auftreten, wenn wir den Strategietester erneut visuell ausführen. Beim vorherigen Durchlauf wurden die Informationen am Ende des Tests gespeichert, und beim Start des zweiten Durchlaufs verwendet der EA die gleiche Datenbank wie beim ersten Durchlauf. Deshalb müssen wir die bisherigen Informationen einfach ignorieren und bei Null anfangen.
Zum Zeitpunkt des Aufrufs der Lademethode ist das EA-Objekt bereits mit einer Strategiegruppe erstellt worden, deren ID den EA-Eingaben entnommen wird. Diese ID wird innerhalb der Methode CVirtualAdvisor::Import() in der statischen Eigenschaft CVirtualAdvisor::s_groupId gespeichert. Daher haben wir beim Laden einer Strategiegruppen-ID aus der EA-Datenbank die Möglichkeit, sie mit einem vorhandenen Wert zu vergleichen. Weichen sie voneinander ab, bedeutet dies, dass der endgültige EA mit einer neuen Gruppe von Strategien neu gestartet wurde und möglicherweise einige zusätzliche Maßnahmen erforderlich sind. Es ist jedoch noch nicht ganz klar, welche Maßnahmen wir in diesem Fall unbedingt ergreifen müssen. Lassen wir also für die Zukunft einfach einen entsprechenden Kommentar im Code stehen.
//+------------------------------------------------------------------+ //| Load status | //+------------------------------------------------------------------+ bool CVirtualAdvisor::Load() { bool res = true; ulong groupId = 0; // Load status if: if(true // file exists && FileIsExist(m_fileName, FILE_COMMON) // currently, there is no optimization && !MQLInfoInteger(MQL_OPTIMIZATION) // and there is no testing at the moment or there is a visual test at the moment && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)) ) { // If the connection to the EA database is established if(CStorage::Connect(m_fileName)) { // Download the last modification time res &= CStorage::Get("CVirtualReceiver::s_lastChangeTime", m_lastSaveTime); // Download the saved strategy group ID res &= CStorage::Get("CVirtualAdvisor::s_groupId", groupId); // If the last modification time is in the future, then ignore the download if(m_lastSaveTime > TimeCurrent()) { PrintFormat(__FUNCTION__" | IGNORE LAST SAVE at %s in the future", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS)); m_lastSaveTime = 0; return true; } PrintFormat(__FUNCTION__" | LAST SAVE at %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS)); if(groupId != CVirtualAdvisor::s_groupId) { // Actions when launching an EA with a new group of strategies. // Nothing is happening here yet } // Load all strategies FOREACH(m_strategies, { res &= ((CVirtualStrategy*) m_strategies[i]).Load(); if(!res) break; }); if(!res) { PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_fileName); } // Download the risk manager res &= m_riskManager.Load(); if(!res) { PrintFormat(__FUNCTION__" | ERROR loading risk manager from file %s", m_fileName); } // Close the connection CStorage::Close(); return res; } } return true; }
Gehen wir nun eine Ebene tiefer und betrachten wir die Implementierung von Methoden zum Speichern und Laden von Strategien.
Speichern und Herunterladen einer Strategie
In der Klasse CVirtualStrategy implementieren wir in diesen Methoden nur die Dinge, die allen Strategien, die virtuelle Positionen verwenden, gemeinsam sind. Sie enthalten jeweils eine Reihe von virtuellen Positionsobjekten, die gespeichert und geladen werden müssen. Wir werden die detaillierte Implementierung auf eine noch niedrigere Ebene setzen und hier nur speziell erstellte Methoden der Klasse CStorage aufrufen:
//+------------------------------------------------------------------+ //| Save status | //+------------------------------------------------------------------+ void CVirtualStrategy::Save() { // Save virtual positions (orders) of the strategy FOREACH(m_orders, CStorage::Set(i, m_orders[i])); } //+------------------------------------------------------------------+ //| Load status | //+------------------------------------------------------------------+ bool CVirtualStrategy::Load() { bool res = true; // Download virtual positions (orders) of the strategy res = CStorage::Get(this.Hash(), m_orders); return res; }
Im Falle der Nachkommen der Klasse CVirtualStrategy (einschließlich CSimpleVolumnesStrategy) müssen wir möglicherweise auch einige zusätzliche Daten in Bezug auf die Anordnung der virtuellen Positionen speichern. Unsere Modellstrategie ist zu einfach und erfordert nur die Speicherung einer Liste von virtuellen Positionen. Aber stellen wir uns vor, dass wir aus irgendeinem Grund ein Array von Tick-Volumen und den Wert des durchschnittlichen Tick-Volumens speichern wollen. Da die Speicher- und Lademethoden als virtuell deklariert sind, können wir sie in den abgeleiteten Klassen außer Kraft setzen, indem wir die Arbeit mit den erforderlichen Daten hinzufügen und die Methoden der Basisklasse aufrufen, um virtuelle Positionen zu speichern und zu laden:
//+------------------------------------------------------------------+ //| Save status | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::Save() { double avrVolume = ArrayAverage(m_volumes); // Let's form the common part of the key with the type and hash of the strategy string key = "CSimpleVolumesStrategy[" + this.Hash() + "]"; // Save the average tick volume CStorage::Set(key + ".avrVolume", avrVolume); // Save the array of tick volumes CStorage::Set(key + ".m_volumes", m_volumes); // Call the base class method (to save virtual positions) CVirtualStrategy::Save(); } //+------------------------------------------------------------------+ //| Load status | //+------------------------------------------------------------------+ bool CSimpleVolumesStrategy::Load() { bool res = true; double avrVolume = 0; // Let's form the common part of the key with the type and hash of the strategy string key = "CSimpleVolumesStrategy[" + this.Hash() + "]"; // Load the tick volume array res &= CStorage::Get(key + ".avrVolume", avrVolume); // Load the tick volume array res &= CStorage::Get(key + ".m_volumes", m_volumes); // Call the base class method (to load virtual positions) res &= CVirtualStrategy::Load(); return res; }
Es muss nur noch das Speichern und Laden von virtuellen Positionen implementiert werden.
Speichern/Laden von virtuellen Positionen
Bisher haben die Methoden Save() und Load() die erforderlichen Informationen über das aktuelle virtuelle Positionsobjekt direkt in einer Datendatei in der Klasse der virtuellen Positionen gespeichert. Jetzt werden wir die Struktur ein wenig ändern. Fügen wir die einfache Struktur CVirtualOrderStruct hinzu, die Felder für alle erforderlichen Daten für die virtuelle Position enthält:
// Structure for reading/writing // basic properties of a virtual order/position from the database struct VirtualOrderStruct { string strategyHash; int strategyIndex; ulong ticket; string symbol; double lot; ENUM_ORDER_TYPE type; datetime openTime; double openPrice; double stopLoss; double takeProfit; datetime closeTime; double closePrice; datetime expiration; string comment; double point; };
Im Gegensatz zu virtuellen Positionsobjekten, bei denen alle angelegten Instanzen streng aufgezeichnet und automatisch im Handelsvolumen-Empfängermodul verarbeitet werden, können solche Strukturen jederzeit und so oft wie gewünscht angelegt werden. Wir werden sie verwenden, um Informationen zwischen virtuellen Positionsobjekten und Methoden zum Speichern/Laden in der EA-Datenbank zu übertragen, die in der Klasse CStorage implementiert sind. Dann werden die Speicher- und Lademethoden in der Klasse der virtuellen Positionen selbst nur die übergebene Struktur füllen oder die Werte der Felder der übergebenen Struktur nehmen, um sie in ihre Eigenschaften zu schreiben:
//+------------------------------------------------------------------+ //| Load status | //+------------------------------------------------------------------+ void CVirtualOrder::Load(const VirtualOrderStruct &o) { m_ticket = o.ticket; m_symbol = o.symbol; m_lot = o.lot; m_type = o.type; m_openPrice = o.openPrice; m_stopLoss = o.stopLoss; m_takeProfit = o.takeProfit; m_openTime = o.openTime; m_closePrice = o.closePrice; m_closeTime = o.closeTime; m_expiration = o.expiration; m_comment = o.comment; m_point = o.point; PrintFormat(__FUNCTION__" | %s", ~this); s_ticket = MathMax(s_ticket, m_ticket); m_symbolInfo = m_symbols[m_symbol]; // Notify the recipient and the strategy that the position (order) is open if(IsOpen()) { m_receiver.OnOpen(&this); m_strategy.OnOpen(&this); } else { m_receiver.OnClose(&this); m_strategy.OnClose(&this); } } //+------------------------------------------------------------------+ //| Save status | //+------------------------------------------------------------------+ void CVirtualOrder::Save(VirtualOrderStruct &o) { o.ticket = m_ticket; o.symbol = m_symbol; o.lot = m_lot; o.type = m_type; o.openPrice = m_openPrice; o.stopLoss = m_stopLoss; o.takeProfit = m_takeProfit; o.openTime = m_openTime; o.closePrice = m_closePrice; o.closeTime = m_closeTime; o.expiration = m_expiration; o.comment = m_comment; o.point = m_point; }
Schließlich verwenden wir die erstellte Tabelle storage_orders in der EA-Datenbank, um die Eigenschaften der einzelnen virtuellen Positionen zu speichern. Die dafür zuständige Methode ist CStorage::Set(). Diese Methode sollte den virtuellen Positionsindex und das virtuelle Positionsobjekt selbst erhalten:
//+------------------------------------------------------------------+ //| Save a virtual order/position | //+------------------------------------------------------------------+ void CStorage::Set(int i, CVirtualOrder* order) { VirtualOrderStruct o; // Structure for virtual position data order.Save(o); // Fill it // Escape quotes in the comment StringReplace(o.comment, "'", "\\'"); // Request to save string query = StringFormat("REPLACE INTO storage_orders VALUES(" "'%s',%d,%I64u," "'%s',%.2f,%d,%I64d,%f,%f,%f,%I64d,%f,%I64d,'%s',%f);", order.Strategy().Hash(), i, o.ticket, o.symbol, o.lot, o.type, o.openTime, o.openPrice, o.stopLoss, o.takeProfit, o.closeTime, o.closePrice, o.expiration, o.comment, o.point); // Execute the request s_res &= DatabaseExecute(DB::Id(), query); if(!s_res) { // Report an error if necessary PrintFormat(__FUNCTION__" | ERROR: Execution failed in DB [adv], query:\n" "%s\n" "error code = %d", query, GetLastError()); } }
Die Methode CStorage::Get(), die als zweites Argument ein Array von virtuellen Positionsobjekten erhält, lädt Informationen über die virtuellen Positionen der Strategie mit dem im ersten Argument angegebenen Hash-Wert aus der Tabelle storage_orders herunter:
//+------------------------------------------------------------------+ //| Get an array of virtual orders/positions | //| by the given strategy hash | //+------------------------------------------------------------------+ bool CStorage::Get(string key, CVirtualOrder* &orders[]) { // Request to obtain data on virtual positions string query = StringFormat("SELECT * FROM storage_orders " " WHERE strategy_hash = '%s' " " ORDER BY strategy_index ASC;", key); // Execute the request int request = DatabasePrepare(DB::Id(), query); // If there is no error if(request != INVALID_HANDLE) { // Structure for virtual position information VirtualOrderStruct row; // Read the data from the query result string by string while(DatabaseReadBind(request, row)) { orders[row.strategyIndex].Load(row); } } else { // Save the error and report it if necessary s_res = false; PrintFormat(__FUNCTION__" | ERROR: Execution failed in DB [adv], query:\n" "%s\n" "error code = %d", query, GetLastError()); } return s_res; }
Damit ist der größte Teil der Änderungen im Zusammenhang mit dem Übergang zur Speicherung von Informationen über den endgültigen EA-Vorgang in einer separaten Datenbank abgeschlossen.
Kleiner Test
Trotz der vielen Änderungen, die wir vorgenommen haben, sind wir noch nicht so weit, dass wir ein echtes Hot-Swapping der Einstellungen des endgültigen EA während des Betriebs testen können. Aber wir können bereits sicherstellen, dass wir den Initialisierungsmechanismus des endgültigen EA nicht durcheinander gebracht haben.
Dazu haben wir das Initialisierungsstring-Array aus der Optimierungsdatenbank exportiert und dabei sowohl die alte als auch die neue Methode verwendet. Jetzt sind Informationen über vier Gruppen von Strategien sowohl in der Datei ExportedGroupsLibrary.mqh als auch in der EA-Datenbank mit dem Namen SimpleVolumes-27183.test.db.sqlite vorhanden. Kompilieren wir nun die Datei mit dem endgültigen SimpleVolumesExpert.mq5 EA-Code.
Wenn wir die Werte der Eingänge wie folgt einstellen,

dann wird der ausgewählte Initialisierungsstring aus dem internen Array des endgültigen EA geladen. Dieses Array wurde während der Kompilierung aus den Daten in der Datei ExportedGroupsLibrary.mqh gefüllt (alte Methode).
Wenn die Parameterwerte auf diese Weise angegeben werden,

dann wird der Initialisierungsstring auf der Grundlage der aus der EA-Datenbank erhaltenen Informationen generiert (neue Methode).
Lassen wir den endgültigen EA mit der alten Initialisierungsmethode über ein kurzes Intervall laufen, zum Beispiel über den letzten Monat. Wir erhalten die folgenden Ergebnisse:


Ergebnisse der letzten EA-Operation mit der alten Methode des Herunterladens von Strategien
Führen wir nun den endgültigen EA mit der neuen Initialisierungsmethode im gleichen Zeitintervall aus. Die Ergebnisse sind wie folgt:


Ergebnisse der letzten EA-Operation mit der neuen Methode des Herunterladens von Strategien
Wie Sie sehen können, sind die Ergebnisse der alten und der neuen Methode völlig identisch.
Schlussfolgerung
Die Aufgabe, die wir uns gestellt hatten, erwies sich als etwas schwieriger als ursprünglich gedacht. Obwohl wir noch nicht alle erwarteten Ergebnisse erzielt haben, haben wir eine voll funktionsfähige Lösung erhalten, die für weitere Tests und Entwicklungen geeignet ist. Wir können jetzt Optimierungsprojekte durchführen, indem wir neue Gruppen von Handelsstrategien direkt in die Datenbank exportieren, die von einem endgültigen Expert Advisor verwendet wird, der auf einem Handelskonto läuft. Die Korrektheit dieses Mechanismus muss jedoch noch geprüft werden.
Wir beginnen mit dem Testen, indem wir wie üblich das gewünschte Verhalten in einem EA simulieren, der im Strategietester läuft. Wenn die Ergebnisse dort zufriedenstellend sind, werden wir dazu übergehen, sie in den endgültigen EAs zu verwenden, die im Tester nicht mehr funktionieren werden. Aber dazu beim nächsten Mal mehr.
Vielen Dank für Ihre Aufmerksamkeit! Bis bald!
Wichtige Warnung:
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.
Inhalt des Archivs
| # | Name | Version | Beschreibung | Jüngste Änderungen |
|---|---|---|---|---|
| MQL5/Experts/Article.16452 | ||||
| 1 | Advisor.mqh | 1.04 | EA-Basisklasse | Teil 10 |
| 2 | ClusteringStage1.py | 1.01 | Programm zum Clustern der Ergebnisse der ersten Optimierungsstufe | Teil 20 |
| 3 | CreateProject.mq5 | 1.00 | EA-Skript zur Erstellung eines Projekts mit Phasen, Aufträgen und Optimierungsaufgaben. | Teil 21 |
| 4 | Database.mqh | 1.10 | Klasse für den Umgang mit der Datenbank | Teil 22 |
| 5 | db.adv.schema.sql | 1.00 | Endgültige Datenbankstruktur des EAs | Teil 22 |
| 6 | db.cut.schema.sql | 1.00 | Struktur der verkürzten Optimierungsdatenbank | Teil 22 |
| 7 | db.opt.schema.sql | 1.05 | Optimierung der Datenbankstruktur | Teil 22 |
| 8 | ExpertHistory.mqh | 1.00 | Klasse für den Export der Handelshistorie in eine Datei | Teil 16 |
| 9 | ExportedGroupsLibrary.mqh | — | Generierte Datei mit den Namen der Strategiegruppen und dem Array ihrer Initialisierungszeichenfolgen | Teil 22 |
| 10 | Factorable.mqh | 1.03 | Basisklasse von Objekten, die aus einer Zeichenkette erstellt werden | Teil 22 |
| 11 | GroupsLibrary.mqh | 1.01 | Klasse für die Arbeit mit einer Bibliothek ausgewählter Strategiegruppen | Teil 18 |
| 12 | HistoryReceiverExpert.mq5 | 1.00 | EA für die Wiedergabe der Historie von Geschäften mit dem Risikomanager | Teil 16 |
| 13 | HistoryStrategy.mqh | 1.00 | Klasse der Handelsstrategie für die Wiederholung der Handelshistorie | Teil 16 |
| 14 | Interface.mqh | 1.00 | Basisklasse zur Visualisierung verschiedener Objekte | Teil 4 |
| 15 | LibraryExport.mq5 | 1.01 | EA, der Initialisierungszeichenfolgen ausgewählter Durchläufe aus der Bibliothek in der Datei ExportedGroupsLibrary.mqh speichert | Teil 18 |
| 16 | Macros.mqh | 1.05 | Nützliche Makros für Array-Operationen | Teil 22 |
| 17 | Money.mqh | 1.01 | Grundkurs Geldmanagement | Teil 12 |
| 18 | NewBarEvent.mqh | 1.00 | Klasse zur Definition eines neuen Balkens für ein bestimmtes Symbol | Teil 8 |
| 19 | Optimization.mq5 | 1.04 | EA verwaltet die Einleitung von Optimierungsaufgaben | Teil 22 |
| 20 | Optimizer.mqh | 1.03 | Klasse für den Projektautooptimierungsmanager | Teil 22 |
| 21 | OptimizerTask.mqh | 1.03 | Klasse der Optimierungsaufgaben | Teil 22 |
| 22 | Receiver.mqh | 1.04 | Basisklasse für die Umwandlung von offenen Volumina in Marktpositionen | Teil 12 |
| 23 | SimpleHistoryReceiverExpert.mq5 | 1.00 | Vereinfachter EA für die Wiedergabe des Geschäftsverlaufs | Teil 16 |
| 24 | SimpleVolumesExpert.mq5 | 1.21 | Endgültiger EA für den Parallelbetrieb mehrerer Gruppen von Modellstrategien. Die Parameter werden aus der integrierten Gruppenbibliothek übernommen. | Teil 22 |
| 25 | SimpleVolumesStage1.mq5 | 1.18 | Handelsstrategie Einzelinstanzoptimierung EA (Phase 1) | Teil 19 |
| 26 | SimpleVolumesStage2.mq5 | 1.02 | Handelsstrategien Instanzen Gruppe Optimierung EA (Phase 2) | Teil 19 |
| 27 | SimpleVolumesStage3.mq5 | 1.03 | Der EA, der eine generierte standardisierte Gruppe von Strategien in einer Bibliothek von Gruppen mit einem bestimmten Namen speichert. | Teil 22 |
| 28 | SimpleVolumesStrategy.mqh | 1.11 | Klasse der Handelsstrategie mit Tick-Volumen | Teil 22 |
| 29 | Storage.mqh | 1.00 | Klasse für die Handhabung des Key-Value-Speichers für den endgültigen EA | Teil 22 |
| 30 | Strategy.mqh | 1.04 | Handelsstrategie-Basisklasse | Teil 10 |
| 31 | SymbolsMonitor.mqh | 1.00 | Klasse zur Beschaffung von Informationen über Handelsinstrumente (Symbole) | Teil 21 |
| 32 | TesterHandler.mqh | 1.06 | Klasse zur Behandlung von Optimierungsereignissen | Teil 22 |
| 33 | VirtualAdvisor.mqh | 1.09 | Klasse des EA, der virtuelle Positionen (Aufträge) bearbeitet | Teil 22 |
| 34 | VirtualChartOrder.mqh | 1.01 | Grafische virtuelle Positionsklasse | Teil 18 |
| 35 | VirtualFactory.mqh | 1.04 | Objekt-Fabrik-Klasse | Teil 16 |
| 36 | VirtualHistoryAdvisor.mqh | 1.00 | Die Klasse des EA zur Wiederholung des Handelsverlaufs | Teil 16 |
| 37 | VirtualInterface.mqh | 1.00 | EA GUI-Klasse | Teil 4 |
| 38 | VirtualOrder.mqh | 1.09 | Klasse der virtuellen Aufträge und Positionen | Teil 22 |
| 39 | VirtualReceiver.mqh | 1.03 | Klasse für die Umwandlung von offenen Volumina in Marktpositionen (Empfänger) | Teil 12 |
| 40 | VirtualRiskManager.mqh | 1.02 | Klasse Risikomanagement (Risikomanager) | Teil 15 |
| 41 | VirtualStrategy.mqh | 1.08 | Klasse einer Handelsstrategie mit virtuellen Positionen | Teil 22 |
| 42 | VirtualStrategyGroup.mqh | 1.00 | Klasse der Handelsstrategien Gruppe(n) | Teil 11 |
| 43 | VirtualSymbolReceiver.mqh | 1.00 | Symbol-Empfängerklasse | Teil 3 |
| MQL5/Common/Files | Gemeinsamer Terminal-Ordner | |||
| 44 | SimpleVolumes-27183.test.db.sqlite | — | EA-Datenbank mit vier zusätzlichen Strategiegruppen | |
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/16452
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Die Übertragung der Trading-Signale in einem universalen Expert Advisor.
Marktsimulation (Teil 08): Sockets (II)
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Von der Grundstufe bis zur Mittelstufe: Structs (II)
- 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.