Entwicklung eines Expertenberaters für mehrere Währungen (Teil 24): Hinzufügen einer neuen Strategie (II)
Einführung
Wir setzen unsere Arbeit fort, die wir im vorigen Artikel begonnen haben. Wir möchten Sie daran erinnern, dass wir nach der Aufteilung des gesamten Projektcodes in den Bibliotheks- und den Projektteil beschlossen haben, zu prüfen, wie wir von der Modellhandelsstrategie SimpleVolumes zu einer anderen übergehen können. Was müssen wir dafür tun? Wie einfach wird es sein? Es versteht sich von selbst, dass es notwendig war, eine Klasse für eine neue Handelsstrategie zu schreiben. Doch dann traten einige unvorhersehbare Komplikationen auf.
Sie waren genau mit dem Wunsch verbunden, dass der Bibliotheksteil unabhängig vom Projektteil sein sollte. Hätten wir beschlossen, diese neu eingeführte Regel zu brechen, hätte es keine Schwierigkeiten gegeben. Schließlich wurde jedoch ein Weg gefunden, die Trennung des Codes beizubehalten und gleichzeitig die Integration der neuen Handelsstrategie zu ermöglichen. Dies erforderte Änderungen an den Bibliotheksdateien des Projekts, die zwar vom Umfang her nicht sehr groß, aber von der Bedeutung her bedeutend waren.
Infolgedessen konnten wir die Optimierung des EA der ersten Stufe (stage) mit einer neuen Strategie namens SimpleCandles kompilieren und ausführen. Die nächsten Schritte bestanden darin, das System mit dem automatischen Optimierungsförderer zum Laufen zu bringen. Für die vorherige Strategie haben wir den EA CreateProject.mq5 entwickelt, mit dem eine Datenbank zur Aufgabenoptimierung für die Ausführung auf dem Förderband erstellt werden konnte. In den EA-Parametern konnten wir angeben, welche Handelsinstrumente (Symbole) und Zeitrahmen wir optimieren wollten, die Namen der EA-Stufen und andere notwendige Informationen. Wenn die Optimierungsdatenbank vorher nicht existierte, wurde sie automatisch erstellt.
Schauen wir uns an, wie das mit der neuen Handelsstrategie funktionieren kann.
Der Weg ist vorgezeichnet
Wir beginnen die Hauptarbeit mit der Analyse des EA-Codes CreateProject.mq5. Unser Ziel ist es, Code zu identifizieren, der in verschiedenen Projekten gleich oder fast gleich ist. Dieser Code kann in einen Bibliotheksabschnitt aufgeteilt werden, der bei Bedarf in mehrere separate Dateien aufgeteilt wird. Wir werden den Teil des Codes, der für verschiedene Projekte unterschiedlich sein wird, im Projektabschnitt belassen und beschreiben, welche Änderungen daran vorgenommen werden müssen.
Doch zunächst wollen wir einen Fehler beheben, der beim Speichern von Testergebnisdaten in der Optimierungsdatenbank auftritt, die Makros für die Organisation von Zyklen verfeinern und uns ansehen, wie man einer bereits entwickelten Handelsstrategie neue Parameter hinzufügt.
Korrekturen in CDatabase
In den letzten Artikeln haben wir damit begonnen, relativ kurze Testintervalle für Optimierungsprojekte zu verwenden. Anstelle von Intervallen von 5 Jahren oder mehr haben wir begonnen, Intervalle von mehreren Monaten zu nehmen. Das lag daran, dass unsere Hauptaufgabe darin bestand, die Funktionsweise des Fördermechanismus für die automatische Optimierung zu testen, und durch die Verringerung des Intervalls konnten wir die Zeit für einen einzelnen Testdurchlauf und damit die Gesamtoptimierungszeit erheblich reduzieren.
Um Informationen über Durchläufe in der Optimierungsdatenbank zu speichern, sendet jeder Testagent (lokal, remote oder in der Cloud) sie als Teil eines Datenrahmens an das Terminal, auf dem der Optimierungsprozess läuft. In diesem Terminal wird nach Beginn der Optimierung eine zusätzliche Instanz des optimierten EA in einem speziellen Modus gestartet – dem Datenrahmen-Sammelmodus. Diese Instanz wird nicht im Tester gestartet, sondern auf einer separaten Terminal-Chart. Er empfängt und speichert alle Informationen, die von Testagenten kommen.
Obwohl der Code für den Event-Handler für die Ankunft neuer Datenrahmen von Testagenten keine asynchronen Operationen enthält, begannen während der Optimierung Meldungen über Fehler beim Einfügen in die Datenbank zu erscheinen, die darauf zurückzuführen waren, dass die Datenbank durch eine andere Operation gesperrt war. Dieser Fehler war relativ selten. Bei mehreren Dutzend von mehreren Tausend Durchläufen konnten die Ergebnisse jedoch nicht in die Optimierungsdatenbank aufgenommen werden.
Die Ursache für diese Fehler scheint die zunehmende Zahl von Situationen zu sein, in denen mehrere Testagenten gleichzeitig einen Lauf durchführen und einen Datenrahmen an den EA im Hauptterminal senden. Und dieser EA versucht, einen neuen Eintrag in die Datenbank schneller einzufügen, als der vorherige Einfügevorgang auf der Datenbankseite abgeschlossen werden kann.
Um dies zu beheben, werden wir einen separaten Handler für diese Kategorie von Fehlern hinzufügen. Wenn die Ursache des Fehlers darin liegt, dass die Datenbank oder die Tabelle durch eine andere Operation gesperrt ist, müssen wir die fehlgeschlagene Operation einfach nach einiger Zeit wiederholen. Wenn nach einer bestimmten Anzahl von Versuchen, die Daten wieder einzufügen, derselbe Fehler erneut auftritt, sollten die Versuche abgebrochen werden.
Zum Einfügen verwenden wir die Methode CDatabase::ExecuteTransaction(), die wir wie folgt ändern. Hinzufügen des Zählers für die Ausführungsversuche der Anfrage zu den Methodenargumenten. Tritt ein solcher Fehler auf, pausieren wir für eine zufällige Anzahl von Millisekunden (0 – 50) und rufen die gleiche Funktion mit einem erhöhten Wert des Zählers auf.
//+------------------------------------------------------------------+ //| Execute multiple DB queries in one transaction | //+------------------------------------------------------------------+ bool CDatabase::ExecuteTransaction(string &queries[], int attempt = 0) { // Open a transaction DatabaseTransactionBegin(s_db); s_res = true; // Send all execution requests FOREACH(queries, { s_res &= DatabaseExecute(s_db, queries[i]); if(!s_res) break; }); // If an error occurred in any request, then if(!s_res) { // Cancel transaction DatabaseTransactionRollback(s_db); if((_LastError == ERR_DATABASE_LOCKED || _LastError == ERR_DATABASE_BUSY) && attempt < 20) { PrintFormat(__FUNCTION__" | ERROR: ERR_DATABASE_LOCKED. Repeat Transaction in DB [%s]", s_fileName); Sleep(rand() % 50); ExecuteTransaction(queries, attempt + 1); } else { // Report it PrintFormat(__FUNCTION__" | ERROR: Transaction failed in DB [%s], error code=%d", s_fileName, _LastError); } } else { // Otherwise, confirm transaction DatabaseTransactionCommit(s_db); //PrintFormat(__FUNCTION__" | Transaction done successfully"); } return s_res; }
Vorsichtshalber nehmen wir die gleichen Änderungen an der Methode CDatabase::Execute() vor, um eine SQL-Abfrage ohne Transaktion auszuführen.
Eine weitere kleine Änderung, die für uns in Zukunft nützlich sein wird, war das Hinzufügen einer statischen booleschen Variable zur CDatabase-Klasse. Es wird sich daran erinnern, dass bei der Ausführung von Anfragen ein Fehler aufgetreten ist:
//+------------------------------------------------------------------+ //| Class for handling the database | //+------------------------------------------------------------------+ class CDatabase { // ... static bool s_res; // Query execution result public: static int Id(); // Database connection handle static bool Res(); // Query execution result // ... }; bool CDatabase::s_res = true;
Speichern Sie die an der Datei Database/Database.mqh vorgenommenen Änderungen im Bibliotheksordner.
Korrekturen in Macros.h
Lassen Sie uns eine Änderung erwähnen, die schon lange überfällig war. Wie Sie sich vielleicht erinnern, haben wir das Makro FOREACH(A, D) geschaffen, um das Schreiben der Kopfzeilen von Schleifen zu vereinfachen, die über alle Werte in einem bestimmten Array iterieren sollen:
#define FOREACH(A, D) { for(int i=0, im=ArraySize(A);i<im;i++) {D;} }
Hier ist A ein Array-Name, während D ein Schleifenkörper ist. Diese Implementierung hatte den Nachteil, dass es unmöglich war, die schrittweise Ausführung des Codes innerhalb des Schleifenkörpers beim Debuggen richtig zu verfolgen. Obwohl dies nur selten erforderlich war, war es sehr lästig. Als ich eines Tages in der Dokumentation stöberte, sah ich eine andere Möglichkeit, ein ähnliches Makro zu implementieren. Das Makro gab nur den Schleifenkopf an, und der Körper wurde außerhalb des Makros verschoben. Es gab jedoch einen weiteren Parameter, der den Namen der Schleifenvariablen angab.
In unserer früheren Implementierung war der Name der Schleifenvariablen (der Index des Array-Elements) fest vorgegeben (i), was nirgendwo Probleme verursachte. Selbst an der Stelle, an der eine Doppelschleife erforderlich war, konnte man aufgrund der unterschiedlichen Geltungsbereiche dieser Indizes mit denselben Namen auskommen. Daher erhielt die neue Implementierung auch einen festen Indexnamen. Der einzige Parameter, der übergeben wird, ist der Name des Arrays, über das in der Schleife iteriert werden soll:
#define FOREACH(A) for(int i=0, im=ArraySize(A);i<im;i++)
Um auf die neue Version umzusteigen, mussten Änderungen an allen Stellen vorgenommen werden, an denen dieses Makro verwendet wurde. Zum Beispiel:
//+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CAdvisor::Tick(void) { // Call OnTick handling for all strategies //FOREACH(m_strategies, m_strategies[i].Tick();) FOREACH(m_strategies) m_strategies[i].Tick(); }
Zusammen mit diesem Makro haben wir ein weiteres Makro hinzugefügt, das die Erstellung eines Schleifenkopfes ermöglicht. In dem Makro wird jedes Element des Arrays A nacheinander in das Array E (das im Voraus angekündigt werden sollte) eingefügt. Vor dem Schleifenkopf wird das erste Element des Arrays, falls vorhanden, in diese Variable gesetzt. Als Schleifenvariable verwenden wir eine Variable mit einem Namen, der aus dem Buchstaben i und dem Variablennamen E besteht. Im dritten Teil des Schleifenkopfes wird die Schleifenvariable inkrementiert, während die Variable E den Wert des Arrayelements A mit dem erhöhten Index erhält. Indem wir einen Index modulo der Anzahl der Arrayelemente nehmen, können wir vermeiden, dass wir bei der letzten Iteration der Schleife über die Array-Grenzen hinausgehen:
#define FOREACH_AS(A, E) if(ArraySize(A)) E=A[0]; \ for(int i##E=0, im=ArraySize(A);i##E<im;E=A[++i##E%im])
Wir speichern die Änderungen in der Datei Utils/Macros.h im Bibliotheksordner.
Hinzufügen eines Parameters zu einer Handelsstrategie
Wie fast der gesamte Code ist auch die Umsetzung einer Handelsstrategie Änderungen unterworfen. Wenn diese Änderungen die Änderung der Zusammensetzung der Eingabeparameter einer einzelnen Instanz einer Handelsstrategie betreffen, dann müssen nicht nur Änderungen an der Handelsstrategieklasse, sondern auch an einigen anderen Stellen vorgenommen werden. Schauen wir uns ein Beispiel an, um zu sehen, was dafür getan werden muss.
Nehmen wir an, dass wir beschließen, der Handelsstrategie einen Parameter für den maximalen Spread hinzuzufügen. Seine Verwendung besteht darin, dass, wenn zum Zeitpunkt des Empfangs eines Signals zur Eröffnung einer Position der aktuelle Spread den in diesem Parameter eingestellten Wert überschreitet, die Position nicht eröffnet wird.
Zunächst fügen wir der ersten Stage-EA einen Eingang hinzu, über den wir diesen Wert bei der Ausführung des Testers einstellen können. Dann fügen wir in der Funktion zur Bildung des Initialisierungsstrings die Ersetzung des neuen Parameterwerts in den Initialisierungsstring ein:
//+------------------------------------------------------------------+ //| 4. Strategy inputs | //+------------------------------------------------------------------+ sinput string symbol_ = ""; // Symbol sinput ENUM_TIMEFRAMES period_ = PERIOD_CURRENT; // Timeframe for candles input group "=== Opening signal parameters" input int signalSeqLen_ = 6; // Number of unidirectional candles input int periodATR_ = 0; // ATR period (if 0, then TP/SL in points) input group "=== Pending order parameters" input double stopLevel_ = 25000; // Stop Loss (in ATR fraction or points) input double takeLevel_ = 3630; // Take Profit (in ATR fraction or points) input group "=== Money management parameters" input int maxCountOfOrders_ = 9; // Max number of simultaneously open orders input int maxSpread_ = 10; // Max acceptable spread (in points) //+------------------------------------------------------------------+ //| 5. Strategy initialization string generation function | //| from the inputs | //+------------------------------------------------------------------+ string GetStrategyParams() { return StringFormat( "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d,%d)", (symbol_ == "" ? Symbol() : symbol_), period_, signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_, maxSpread_ ); }
Der Initialisierungsstring enthält nun einen Parameter mehr als zuvor. Die nächste Änderung besteht also darin, die neue Eigenschaft der Klasse hinzuzufügen und die Werte aus der Initialisierungszeichenkette im Konstruktor in sie einzulesen:
//+------------------------------------------------------------------+ //| Trading strategy using unidirectional candlesticks | //+------------------------------------------------------------------+ class CSimpleCandlesStrategy : public CVirtualStrategy { protected: // ... //--- Money management parameters int m_maxCountOfOrders; // Max number of simultaneously open positions int m_maxSpread; // Max acceptable spread (in points) // ... }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CSimpleCandlesStrategy::CSimpleCandlesStrategy(string p_params) { // Read the parameters from the initialization string m_params = p_params; m_symbol = ReadString(p_params); m_timeframe = (ENUM_TIMEFRAMES) ReadLong(p_params); m_signalSeqLen = (int) ReadLong(p_params); m_periodATR = (int) ReadLong(p_params); m_stopLevel = ReadDouble(p_params); m_takeLevel = ReadDouble(p_params); m_maxCountOfOrders = (int) ReadLong(p_params); m_maxSpread = (int) ReadLong(p_params); // ... }
Nun kann der neue Parameter nach Belieben in den Methoden der Handelsstrategieklasse verwendet werden. Je nach Verwendungszweck kann der folgende Code der Methode zum Empfang des Positionsöffnungssignals hinzugefügt werden.
//+------------------------------------------------------------------+ //| Signal for opening pending orders | //+------------------------------------------------------------------+ int CSimpleCandlesStrategy::SignalForOpen() { // By default, there is no signal int signal = 0; MqlRates rates[]; // Copy the quote values (candles) to the destination array. // To check the signal we need m_signalSeqLen of closed candles and the current candle, // so in total m_signalSeqLen + 1 int res = CopyRates(m_symbol, m_timeframe, 0, m_signalSeqLen + 1, rates); // If the required number of candles has been copied if(res == m_signalSeqLen + 1) { signal = 1; // buy signal // Go through all closed candles for(int i = 1; i <= m_signalSeqLen; i++) { // If at least one upward candle occurs, cancel the signal if(rates[i].open < rates[i].close ) { signal = 0; break; } } if(signal == 0) { signal = -1; // otherwise, sell signal // Go through all closed candles for(int i = 1; i <= m_signalSeqLen; i++) { // If at least one downward candle occurs, cancel the signal if(rates[i].open > rates[i].close ) { signal = 0; break; } } } } // If there is a signal, then if(signal != 0) { // If the current spread is greater than the maximum allowed, then if(rates[0].spread > m_maxSpread) { PrintFormat(__FUNCTION__" | IGNORE %s Signal, spread is too big (%d > %d)", (signal > 0 ? "BUY" : "SELL"), rates[0].spread, m_maxSpread); signal = 0; // Cancel the signal } } return signal; }
In ähnlicher Weise können wir weitere neue Parameter zu Handelsstrategien hinzufügen oder überflüssig gewordene Parameter loswerden.
Analyse von CreateProject.mq5
Beginnen wir mit der Analyse des EA-Codes für die Projekterstellung CreateProject.mq5. In seiner Initialisierungsfunktion haben wir den Code bereits in einzelne Funktionen aufgeteilt. Der Zweck der einzelnen Programme ist aus dem Namen ersichtlich:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Connect to the database DB::Connect(fileName_); // Create a project CreateProject(projectName_, projectVersion_, StringFormat("%s - %s", TimeToString(fromDate_, TIME_DATE), TimeToString(toDate_, TIME_DATE) ) ); // Create project stages CreateStages(); // Creating jobs and tasks CreateJobs(); // Queueing the project for execution QueueProject(); // Close the database DB::Close(); // Successful initialization return(INIT_SUCCEEDED); }
Diese Aufteilung ist nicht sehr praktisch, denn die ausgewählten Funktionen erwiesen sich als recht umständlich und lösen ganz unterschiedliche Probleme. In der Funktion CreateJobs() werden zum Beispiel Eingabedaten vorverarbeitet, Parametervorlagen für Aufträge erstellt, Informationen in die Datenbank eingefügt und dann ähnliche Aktionen durchgeführt, um Optimierungsaufgaben in der Datenbank zu erstellen. Es wäre besser, wenn es andersherum wäre: Die Funktionen wären einfacher und würden ein kleines Problem lösen.
Um die neue Strategie in der aktuellen Implementierung zu verwenden, müssten wir die Vorlage der Parameter der ersten Stufe und möglicherweise auch die Anzahl der Aufgaben mit Optimierungskriterien dafür ändern. Die Parametervorlage für die erste Stufe der vorherigen Handelsstrategie wurde in der globalen Variable paramsTemplate1 angegeben:
// Template of optimization parameters at the first stage string paramsTemplate1 = "; === Open signal parameters\n" "signalPeriod_=212||12||40||240||Y\n" "signalDeviation_=0.1||0.1||0.1||2.0||Y\n" "signaAddlDeviation_=0.8||0.1||0.1||2.0||Y\n" "; === Pending order parameters\n" "openDistance_=10||0||10||250||Y\n" "stopLevel_=16000||200.0||200.0||20000.0||Y\n" "takeLevel_=240||100||10||2000.0||Y\n" "ordersExpiration_=22000||1000||1000||60000||Y\n" "; === Capital management parameters\n" "maxCountOfOrders_=3||3||1||30||N\n";
Glücklicherweise war dies bei allen Optimierungsaufträgen der ersten Stufe der Fall. Dies ist jedoch nicht immer der Fall. In der neuen Strategie haben wir zum Beispiel die Symbolwerte und den Zeitrahmen, mit dem die Strategie arbeiten soll, in die Parameter aufgenommen. Das bedeutet, dass in verschiedenen Optimierungsaufträgen der ersten Stufe, die für unterschiedliche Symbole und Zeiträume erstellt wurden, die Parametervorlage variable Teile haben wird. Um die Werte festzulegen, müssen Sie jedoch in die Tiefen des Codes der Aufgabenerstellungsfunktion eindringen und ihn ändern. Dann ist es nicht mehr möglich, sie in den Bibliotheksbereich zu bringen.
Darüber hinaus erstellt unser EA zur Erstellung von Optimierungsprojekten jetzt ein Projekt mit drei festen Stufen. Wir sind während der Entwicklung zu diesem einfachen Satz von Stufen gekommen, obwohl wir versucht haben, weitere Stufen hinzuzufügen (siehe zum Beispiel Teil 18 und Teil 19). Zusätzliche Schritte führten nicht zu einer signifikanten Verbesserung des Endergebnisses, obwohl dies bei anderen Handelsstrategien möglicherweise nicht der Fall ist. Wenn wir also den aktuellen Code in den Bibliotheksteil verschieben, können wir die Zusammensetzung der Stufen in Zukunft nicht mehr ändern, wenn wir dies wünschen.
So sehr wir uns auch wünschen, mit wenig Aufwand auszukommen, so ist es doch besser, jetzt eine ernsthafte Überarbeitung des Codes vorzunehmen, als sie auf später zu verschieben. Wir wollen versuchen, den EA-Code für die Projekterstellung in mehrere Klassen aufzuteilen. Die Klassen werden in den Bibliotheksbereich verschoben, und im Projektbereich werden wir sie verwenden, um Projekte mit der gewünschten Zusammensetzung von Stufen und deren Inhalt zu erstellen. Gleichzeitig wird dies auch als Vorlage für die künftige Anzeige von Informationen über den Fortschritt des Förderers dienen.
Zunächst versuchten wir zu schreiben, wie der endgültige Code aussehen könnte. Diese vorläufige Fassung blieb bis zur Veröffentlichung der endgültigen Fassung praktisch unverändert. Den Methodenaufrufen wurden nur bestimmte Parameterzusammensetzungen hinzugefügt. Sehen wir uns also an, wie die neue Version der Initialisierungsfunktion für die Erstellung des Optimierungsprojekts EA aussieht. Um nicht durch kleine Details abzulenken, werden die Argumente der Methoden nicht gezeigt:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Create an optimization project object for the given database COptimizationProject p; // Create a new project in the database p.Create(...); // Add the first stage p.AddStage(...); // Adding the first stage jobs p.AddJobs(...); // Add tasks for the first stage jobs p.AddTasks(...); // Add the second stage p.AddStage(...); // Add the second stage jobs p.AddJobs(...); // Add tasks for the second stage jobs p.AddTasks(...); // Add the third stage p.AddStage(...); // Add the third stage job p.AddJobs(...); // Add a task for the third stage job p.AddTasks(...); // Put the project in the execution queue p.Queue(); // Delete the EA ExpertRemove(); // Successful initialization return(INIT_SUCCEEDED); }
Mit dieser Codestruktur können wir leicht neue Stufen hinzufügen und ihre Parameter flexibel ändern. Aber im Moment sehen wir nur eine neue Klasse, die wir auf jeden Fall brauchen werden – die Optimierungsprojektklasse COptimizationProject. Schauen wir uns den Code an.
Klasse COptimizationProject
Bei der Entwicklung dieser Klasse wurde schnell klar, dass wir für alle Arten von Entitäten, die wir in der Optimierungsdatenbank speichern, separate Klassen benötigen würden. Es folgen die Klassen COptimizationStage für die Projektphasen, COptimizationJob für die Projektphasenaufträge und COptimizationTask für die Aufgaben der einzelnen Projektphasenaufträge.
Da die Objekte dieser Klassen im Wesentlichen eine Darstellung von Einträgen aus verschiedenen Tabellen der Optimierungsdatenbank sind, wird die Zusammensetzung der Klassenfelder die Zusammensetzung der Felder der entsprechenden Tabellen wiederholen. Zusätzlich zu diesen Feldern werden wir weitere Felder und Methoden zu diesen Klassen hinzufügen, die für die Ausführung der ihnen zugewiesenen Aufgaben erforderlich sind.
Der Einfachheit halber werden wir alle Eigenschaften und Methoden der erstellten Klassen öffentlich machen. Jede Klasse hat ihre eigene Methode, um einen neuen Eintrag in der Optimierungsdatenbank zu erstellen. In Zukunft werden wir Methoden zum Ändern eines bestehenden Eintrags und zum Lesen eines Eintrags aus der Datenbank hinzufügen, da wir sie beim Erstellen des Projekts nicht benötigen.
Anstelle der bisher verwendeten Vorlagen der Tester-Parameter werden wir separate Funktionen erstellen, die bereits gefüllte Parameter entsprechend der Vorlage zurückgeben. Auf diese Weise werden die Parametervorlagen innerhalb dieser Funktionen verschoben. Diese Funktionen nehmen einen Projektzeiger als Parameter entgegen und können damit auf die erforderlichen Projektinformationen zugreifen, die in der Vorlage ersetzt werden sollen. Wir werden die Deklaration dieser Funktionen in den Projektabschnitt verschieben, und im Bibliotheksabschnitt werden wir nur einen neuen Typ deklarieren – einen Zeiger auf die Funktion des folgenden Typs:
// Create a new type - a pointer to a string generation function // for optimization job parameters (job) accepting the pointer // to the optimization project object as an argument typedef string (*TJobsTemplateFunc)(COptimizationProject*);
Dadurch können wir die Funktionen zur Erzeugung der Stufenparameter in der Klasse COptimizationProject nutzen. Es gibt sie noch nicht, aber in Zukunft werden wir sie im Designteil auf jeden Fall hinzufügen müssen.
So sieht die Beschreibung dieser Klasse aus:
//+------------------------------------------------------------------+ //| Optimization project class | //+------------------------------------------------------------------+ class COptimizationProject { public: string m_fileName; // Database name // Properties stored directly in the database ulong id_project; // Project ID string name; // Name string version; // Version string description; // Description string status; // Status // Arrays of all stages, jobs and tasks COptimizationStage* m_stages[]; // Project stages COptimizationJob* m_jobs[]; // Jobs of all project stages COptimizationTask* m_tasks[]; // Tasks of all jobs of project stages // Properties for the current state of the project creation string m_symbol; // Current symbol string m_timeframe; // Current timeframe COptimizationStage* m_stage; // Last created stage (current stage) COptimizationJob* m_job; // Last created job (current job) COptimizationTask* m_task; // Last created task (current task) // Methods COptimizationProject(string p_fileName); // Constructor ~COptimizationProject(); // Destructor // Create a new project in the database COptimizationProject* COptimizationProject::Create(string p_name, string p_version = "", string p_description = "", string p_status = "Done"); void Insert(); // Insert an entry into the database void Update(); // Update an entry in the database // Add a new stage to the database COptimizationProject* AddStage(COptimizationStage* parentStage, string stageName, string stageExpertName, string stageSymbol, string stageTimeframe, int stageOptimization, int stageModel, datetime stageFromDate, datetime stageToDate, int stageForwardMode, datetime stageForwardDate, int stageDeposit = 10000, string stageCurrency = "USD", int stageProfitInPips = 0, int stageLeverage = 200, int stageExecutionMode = 0, int stageOptimizationCriterion = 7, string stageStatus = "Done"); // Add new jobs to the database for the specified symbols and timeframes COptimizationProject* AddJobs(string p_symbols, string p_timeframes, TJobsTemplateFunc p_templateFunc); COptimizationProject* AddJobs(string &p_symbols[], string &p_timeframes[], TJobsTemplateFunc p_templateFunc); // Add new tasks to the database for the specified optimization criteria COptimizationProject* AddTasks(string p_criterions); COptimizationProject* AddTasks(string &p_criterions[]); void Queue(); // Put the project in the execution queue // Convert a string name to a timeframe static ENUM_TIMEFRAMES StringToTimeframe(string s); };
Am Anfang stehen die Eigenschaften, die direkt in der Optimierungsdatenbank in der Tabelle Projekte gespeichert sind. Als Nächstes folgen Arrays aller Projektphasen, Aufträge und Aufgaben, und dann Eigenschaften für den aktuellen Stand der Projekterstellung.
Da diese Klasse derzeit nur eine Aufgabe hat (Anlegen eines Projekts in der Optimierungsdatenbank), stellen wir im Konstruktor sofort eine Verbindung zur gewünschten Datenbank her und öffnen eine Transaktion. Der Abschluss dieser Transaktion erfolgt im Destruktor. An dieser Stelle kommt das statische Klassenfeld CDatabase::s_res zum Einsatz. Sein Wert kann verwendet werden, um festzustellen, ob beim Einfügen von Einträgen in die Optimierungsdatenbank beim Erstellen eines Projekts ein Fehler aufgetreten ist. Wenn keine Fehler aufgetreten sind, wird die Transaktion bestätigt, andernfalls wird sie storniert. Außerdem wird der Speicher für erstellte dynamische Objekte im Destruktor freigegeben.
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ COptimizationProject::COptimizationProject(string p_fileName) : m_fileName(p_fileName), id_project(0) { // Connect to the database if (DB::Connect(m_fileName)) { // Start a transaction DatabaseTransactionBegin(DB::Id()); } } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ COptimizationProject::~COptimizationProject() { // If no errors occurred, then if(DB::Res()) { // Confirm the transaction DatabaseTransactionCommit(DB::Id()); } else { // Otherwise, cancel the transaction DatabaseTransactionRollback(DB::Id()); } // Close connection to the database DB::Close(); // Delete created task, job, and stage objects FOREACH(m_tasks) { delete m_tasks[i]; } FOREACH(m_jobs) { delete m_jobs[i]; } FOREACH(m_stages) { delete m_stages[i]; } }
Die Methoden zum Hinzufügen von Aufträgen und Aufgaben werden in zwei Varianten angegeben. Im ersten Fall werden die Listen mit den Symbolen, Zeitrahmen und Kriterien in String-Parametern durch Kommas getrennt an sie übergeben. Innerhalb der Methode werden diese Zeichenfolgen in Arrays von Werten umgewandelt und beim Aufruf der zweiten Version der Methode, die Arrays akzeptiert, als Argumente ersetzt.
Hier sind die Methoden zum Hinzufügen von Aufgaben:
//+------------------------------------------------------------------+ //| Add new jobs to the database for the specified | //| symbols and timeframes in strings | //+------------------------------------------------------------------+ COptimizationProject* COptimizationProject::AddJobs(string p_symbols, string p_timeframes, TJobsTemplateFunc p_templateFunc) { // Array of symbols for strategies string symbols[]; StringReplace(p_symbols, ";", ","); StringSplit(p_symbols, ',', symbols); // Array of timeframes for strategies string timeframes[]; StringReplace(p_timeframes, ";", ","); StringSplit(p_timeframes, ',', timeframes); return AddJobs(symbols, timeframes, p_templateFunc); } //+------------------------------------------------------------------+ //| Add new jobs to the database for the specified | //| symbols and timeframes in arrays | //+------------------------------------------------------------------+ COptimizationProject* COptimizationProject::AddJobs(string &p_symbols[], string &p_timeframes[], TJobsTemplateFunc p_templateFunc) { // For each symbol FOREACH_AS(p_symbols, m_symbol) { // For each timeframe FOREACH_AS(p_timeframes, m_timeframe) { // Get the parameters for work for a given symbol and timeframe string params = p_templateFunc(&this); // Create a new job object m_job = new COptimizationJob(0, m_stage, m_symbol, m_timeframe, params); // Insert it into the optimization database m_job.Insert(); // Add it to the array of all jobs APPEND(m_jobs, m_job); // Add it to the array of current stage jobs APPEND(m_stage.jobs, m_job); } } return &this; }
Das dritte Argument ist der Zeiger auf die Funktion zur Erstellung von Optimierungsparametern für die Stage-EAs.
Die Klasse COptimizationStage
Diese Klassenbeschreibung hat viele Eigenschaften im Vergleich zu anderen Klassen, aber das liegt nur daran, dass es mehrere Felder in der Stufentabelle der Optimierungsdatenbank gibt. Für jede von ihnen gibt es eine entsprechende Eigenschaft in dieser Klasse. Beachten Sie auch, dass der Zeiger auf das Projektobjekt (das diese Stufe enthält) und der Zeiger auf das vorherige Stufenobjekt an den Stufenkonstruktor übergeben werden. Für die erste Stufe gibt es keine vorherige Stufe, daher wird in diesem Parameter NULL übergeben.
//+------------------------------------------------------------------+ //| Optimization stage class | //+------------------------------------------------------------------+ class COptimizationStage { public: ulong id_stage; ulong id_project; ulong id_parent_stage; string name; string expert; string symbol; string period; int optimization; int model; datetime from_date; datetime to_date; int forward_mode; datetime forward_date; int deposit; string currency; int profit_in_pips; int leverage; int execution_mode; int optimization_criterion; string status; COptimizationProject* project; COptimizationStage* parent_stage; COptimizationJob* jobs[]; COptimizationStage(ulong p_idStage, COptimizationProject* p_project, COptimizationStage* parentStage, string p_name, string p_expertName, string p_symbol = "GBPUSD", string p_timeframe = "H1", int p_optimization = 0, int p_model = 0, datetime p_fromDate = 0, datetime p_toDate = 0, int p_forwardMode = 0, datetime p_forwardDate = 0, int p_deposit = 10000, string p_currency = "USD", int p_profitInPips = 0, int p_leverage = 200, int p_executionMode = 0, int p_optimizationCriterion = 7, string p_status = "Done") : id_stage(p_idStage), project(p_project), id_project(!!p_project ? p_project.id_project : 0), parent_stage(parentStage), id_parent_stage(!!parentStage ? parentStage.id_stage : 0), name(p_name), expert(p_expertName), symbol(p_symbol), period(p_timeframe), optimization(p_optimization), model(p_model), from_date(p_fromDate), to_date(p_toDate), forward_mode(p_forwardMode), forward_date(p_forwardDate), deposit(p_deposit), currency(p_currency), profit_in_pips(p_profitInPips), leverage(p_leverage), execution_mode(p_executionMode), optimization_criterion(p_optimizationCriterion), status(p_status) {} // Create a stage in the database void Insert(); }; //+------------------------------------------------------------------+ //| Create a stage in the database | //+------------------------------------------------------------------+ void COptimizationStage::Insert() { string query = StringFormat("INSERT INTO stages VALUES(" "%s," // id_stage "%I64u," // id_project "%s," // id_parent_stage "'%s'," // name "'%s'," // expert "'%s'," // symbol "'%s'," // period "%d," // optimization "%d," // model "'%s'," // from_date "'%s'," // to_date "%d," // forward_mode "%s," // forward_date "%d," // deposit "'%s'," // currency "%d," // profit_in_pips "%d," // leverage "%d," // execution_mode "%d," // optimization_criterion "'%s'" // status ");", (id_stage == 0 ? "NULL" : (string) id_stage), // id_stage id_project, // id_project (id_parent_stage == 0 ? "NULL" : (string) id_parent_stage), // id_parent_stage name, // name expert, // expert symbol, // symbol period, // period optimization, // optimization model, // model TimeToString(from_date, TIME_DATE), // from_date TimeToString(to_date, TIME_DATE), // to_date forward_mode, // forward_mode (forward_mode == 4 ? "'" + TimeToString(forward_date, TIME_DATE) + "'" : "NULL"), // forward_date deposit, // deposit currency, // currency profit_in_pips, // profit_in_pips leverage, // leverage execution_mode, // execution_mode optimization_criterion, // optimization_criterion status // status ); PrintFormat(__FUNCTION__" | %s", query); id_stage = DB::Insert(query); }
Die Aktionen, die im Konstruktor und in der Methode zum Einfügen eines neuen Eintrags in die Stufentabelle durchgeführt werden, sind sehr einfach: Sie merken sich die übergebenen Argumentwerte in den Objekteigenschaften und verwenden sie, um eine SQL-Abfrage zum Einfügen eines Eintrags in die gewünschte Tabelle der Optimierungsdatenbank zu bilden.
Die Klasse COptimizationJob
Diese Klasse ist von der Struktur her identisch mit der Klasse COptimizationStage. Der Konstruktor merkt sich die Parameter, während die Methode Insert() eine neue Zeile in die Jobtabelle der Optimierungsdatenbank einfügt. Außerdem wird der Zeiger auf das Stage-Objekt (das das aktuelle Auftragsobjekt enthalten soll) bei der Erstellung an jedes Auftragsobjekt übergeben.
//+------------------------------------------------------------------+ //| Optimization job class | //+------------------------------------------------------------------+ class COptimizationJob { public: ulong id_job; // job ID ulong id_stage; // stage ID string symbol; // Symbol string timeframe; // Timeframe string params; // Optimizer operation parameters string status; // Status COptimizationStage* stage; // Stage a job belongs to COptimizationTask* tasks[]; // Array of tasks related to the job // Constructor COptimizationJob(ulong p_jobId, COptimizationStage* p_stage, string p_symbol, string p_timeframe, string p_params, string p_status = "Done"); // Create a job in the database void Insert(); }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ COptimizationJob::COptimizationJob(ulong p_jobId, COptimizationStage* p_stage, string p_symbol, string p_timeframe, string p_params, string p_status = "Done") : id_job(p_jobId), stage(p_stage), id_stage(!!p_stage ? p_stage.id_stage : 0), symbol(p_symbol), timeframe(p_timeframe), params(p_params), status(p_status) {} //+------------------------------------------------------------------+ //| Create a job in the database | //+------------------------------------------------------------------+ void COptimizationJob::Insert() { // Request to create a second stage job for a given symbol and timeframe string query = StringFormat("INSERT INTO jobs " " VALUES (NULL,%I64u,'%s','%s','%s','%s');", id_stage, symbol, timeframe, params, status); id_job = DB::Insert(query); PrintFormat(__FUNCTION__" | %s -> %I64u", query, id_job); }
Die letzte verbleibende Klasse COptimizationTask ist auf die gleiche Weise konstruiert, sodass ich ihren Code hier nicht bereitstellen werde.
Umschreiben von CreateProject.mq5
Kehren wir zur Datei CreateProject.mq5 zurück und sehen wir uns ihre wichtigsten Parameter an. Diese Datei befindet sich im Projektbereich, sodass wir für jedes einzelne Projekt die gewünschten Standardparameterwerte darin angeben können, um sie beim Start nicht zu ändern.
Zunächst geben wir den Namen der Optimierungsdatenbank an:
input string fileName_ = "article.17328.db.sqlite"; // - Optimization database file
In der nächsten Gruppe von Parametern geben wir die durch Komma getrennten Symbole und Zeitrahmen an, für die die erste und zweite Stufe der EA-Optimierung durchgeführt werden soll:
input string symbols_ = "GBPUSD,EURUSD,EURGBP"; // - Symbols input string timeframes_ = "H1,M30"; // - Timeframes
Bei dieser Auswahl werden für jede der möglichen Kombinationen aus drei Symbolen und zwei Zeitrahmen sechs Aufträge erstellt.
Als Nächstes folgt die Auswahl des Intervalls, in dem die Optimierung stattfinden soll:
input group "::: Project parameters - Optimization interval" input datetime fromDate_ = D'2022-09-01'; // - Start date input datetime toDate_ = D'2023-01-01'; // - End date
In der Gruppe der Kontoparameter wählen wir das Hauptsymbol aus, das in der dritten Phase verwendet wird, wenn der EA mit mehreren Symbolen im Tester arbeiten wird. Die Auswahl wird wichtig, wenn sich unter den Symbolen solche befinden, die auch am Wochenende gehandelt werden (z. B. Kryptowährungen). In diesem Fall müssen wir diesen als den wichtigsten auswählen, da er sonst während des Testlaufs nicht an allen Wochenenden Ticks erzeugt.
input group "::: Project parameters - Account" input string mainSymbol_ = "GBPUSD"; // - Main symbol input int deposit_ = 10000; // - Initial deposit
In der Parametergruppe für die erste Stufe wird der Name der ersten Stage-EA angegeben, der jedoch gleich bleiben kann. Als Nächstes legen wir die Optimierungskriterien fest, die in der ersten Phase für jeden Auftrag verwendet werden sollen. Dies sind nur Zahlen, die durch Kommata getrennt sind. Der Wert 6 entspricht dem Kriterium der Nutzeroptimierung.
input group "::: Stage 1. Search" input string stage1ExpertName_ = "Stage1.ex5"; // - Stage EA input string stage1Criterions_ = "6,6,6"; // - Optimization criteria for tasks
In diesem Fall haben wir das Nutzerkriterium dreimal angegeben, sodass jeder Auftrag drei Optimierungsprobleme mit dem angegebenen Kriterium enthält.
In der Gruppe der Parameter für die zweite Stufe haben wir die Möglichkeit hinzugefügt, alle Werte der EA-Parameter für die zweite Stufe anzugeben, und nicht nur den Namen und die Anzahl der Strategien in der Gruppe. Diese Parameter beeinflussen die Auswahl der Durchgänge der ersten Stufe, deren Parameter für die Auswahl der Gruppen in der zweiten Stufe verwendet werden.
input group "::: Stage 2. Grouping" input string stage2ExpertName_ = "Stage2.ex5"; // - Stage EA input string stage2Criterion_ = "6"; // - Optimization criterion for tasks //input bool stage2UseClusters_= false; // - Use clustering? input double stage2MinCustomOntester_ = 500; // - Min value of norm. profit input uint stage2MinTrades_ = 20; // - Min number of trades input double stage2MinSharpeRatio_ = 0.7; // - Min Sharpe ratio input uint stage2Count_ = 8; // - Number of strategies in the group
Wenn beispielsweise stage2MinTrades_ = 20 ist, können nur die einzelnen Handelsstrategie-Instanzen, die in der ersten Phase mindestens 20 Handelsgeschäfte getätigt haben, der Gruppe beitreten. Der Parameter stage2UseClusters_ wurde vorerst auskommentiert, da wir derzeit keine Clusterung der Ergebnisse der zweiten Stufe verwenden. Sie sollte daher durch false ersetzt werden.
Wir haben auch die Parametergruppe der dritten Stufe um einige Dinge erweitert. Zusätzlich zum Namen des EA der dritten Stufe (der bei einem Projektwechsel ebenfalls nicht geändert werden muss) wurden zwei Parameter hinzugefügt, die die Bildung des Namens der Datenbank des endgültigen EA steuern. Im endgültigen EA selbst wird dieser Name in der Funktion CVirtualAdvisor::FileName() nach der folgenden Vorlage gebildet:
<Project name>-<Magic>.test.db.sqlite // To run in the tester <Project name>-<Magic>.db.sqlite // To run on a trading account
Daher verwendet die dritte Stage-EA die gleiche Vorlage: <Projektname> wird durch Projektname_ ersetzt, <Magie> durch stage3Magic_. Der Parameter stage3Tester_ ist für das Hinzufügen des Suffixes „.test“ verantwortlich.
input group "::: Stage 3. Result" input string stage3ExpertName_ = "Stage3.ex5"; // - Stage EA input ulong stage3Magic_ = 27183; // - Magic input bool stage3Tester_ = true; // - For the tester?
Im Prinzip wäre es möglich, einen Parameter zu erstellen, der einfach den vollständigen Namen der endgültigen EA-Datenbank angibt. Nach Abschluss der dritten Stufe kann die resultierende Datei dieser Datenbank vor der weiteren Verwendung sicher nach Wunsch umbenannt werden.
Jetzt müssen wir nur noch Funktionen zur Erzeugung von Parametern für Stage-EAs unter Verwendung der vorgegebenen Vorlagen erstellen. Da wir drei Stufen verwenden, benötigen wir drei Funktionen.
In der ersten Phase sieht die Funktion wie folgt aus:
// Template of optimization parameters at the first stage string paramsTemplate1(COptimizationProject *p) { string params = StringFormat( "symbol_=%s\n" "period_=%d\n" "; === Open signal parameters\n" "signalSeqLen_=4||2||1||8||Y\n" "periodATR_=21||7||2||48||Y\n" "; === Pending order parameters\n" "stopLevel_=2.34||0.01||0.01||5.0||Y\n" "takeLevel_=4.55||0.01||0.01||5.0||Y\n" "; === Capital management parameters\n" "maxCountOfOrders_=15||1||1||30||Y\n", p.m_symbol, p.StringToTimeframe(p.m_timeframe)); return params; }
Sie basiert auf den Optimierungsparametern der ersten Stage-EA, die vom Strategietester kopiert wurden, wobei die gewünschten Bereiche für die Iteration über die einzelnen Eingabeparameter festgelegt wurden. Diese Zeichenkette wird mit den Werten des Symbols und des Zeitrahmens gefüllt, für die zum Zeitpunkt des Aufrufs dieser Funktion ein Auftragsobjekt im Projekt erstellt wird. Wenn es zum Beispiel für einen bestimmten Zeitraum notwendig ist, andere Bereiche von Eingaben zu verwenden, über die iteriert werden soll, dann kann diese Logik in dieser Funktion implementiert werden.
Beim Wechsel zu einem anderen Projekt mit einer anderen Handelsstrategie sollte diese Funktion durch eine andere ersetzt werden, die für die neue Handelsstrategie und ihre Eingaben geschrieben wurde.
Für die zweite und dritte Stufe haben wir diese Funktionen ebenfalls in der Datei CreateProject.mq5 implementiert. Wenn Sie jedoch zu einem anderen Projekt wechseln, müssen sie höchstwahrscheinlich nicht geändert werden. Aber wir sollten sie nicht gleich in die Bibliotheksabteilung bringen. Sie sollen erst einmal hier bleiben:
// Template of optimization parameters for the second stage string paramsTemplate2(COptimizationProject *p) { // Find the parent job ID for the current job // by matching the symbol and timeframe at the current and parent stages int i; SEARCH(p.m_stage.parent_stage.jobs, (p.m_stage.parent_stage.jobs[i].symbol == p.m_symbol && p.m_stage.parent_stage.jobs[i].timeframe == p.m_timeframe), i); ulong parentJobId = p.m_stage.parent_stage.jobs[i].id_job; string params = StringFormat( "idParentJob_=%I64u\n" "useClusters_=%s\n" "minCustomOntester_=%f\n" "minTrades_=%u\n" "minSharpeRatio_=%.2f\n" "count_=%u\n", parentJobId, (string) false, //(string) stage2UseClusters_, stage2MinCustomOntester_, stage2MinTrades_, stage2MinSharpeRatio_, stage2Count_ ); return params; } // Template of optimization parameters at the third stage string paramsTemplate3(COptimizationProject *p) { string params = StringFormat( "groupName_=%s\n" "advFileName_=%s\n" "passes_=\n", StringFormat("%s_v.%s_%s", p.name, p.version, TimeToString(toDate_, TIME_DATE)), StringFormat("%s-%I64u%s.db.sqlite", p.name, stage3Magic_, (stage3Tester_ ? ".test" : ""))); return params; }
Als Nächstes folgt der Code für die Initialisierungsfunktion, die die gesamte Arbeit erledigt und den EA vor der Beendigung aus dem Chart entfernt. Zeigen wir es nun mit den Parametern der aufgerufenen Funktionen:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Create an optimization project object for the given database COptimizationProject p(fileName_); // Create a new project in the database p.Create(projectName_, projectVersion_, StringFormat("%s - %s", TimeToString(fromDate_, TIME_DATE), TimeToString(toDate_, TIME_DATE))); // Add the first stage p.AddStage(NULL, "First", stage1ExpertName_, mainSymbol_, "H1", 2, 2, fromDate_, toDate_, 0, 0, deposit_); // Adding the first stage jobs p.AddJobs(symbols_, timeframes_, paramsTemplate1); // Add tasks for the first stage jobs p.AddTasks(stage1Criterions_); // Add the second stage p.AddStage(p.m_stages[0], "Second", stage2ExpertName_, mainSymbol_, "H1", 2, 2, fromDate_, toDate_, 0, 0, deposit_); // Add the second stage jobs p.AddJobs(symbols_, timeframes_, paramsTemplate2); // Add tasks for the second stage jobs p.AddTasks(stage2Criterion_); // Add the third stage p.AddStage(p.m_stages[1], "Save to library", stage3ExpertName_, mainSymbol_, "H1", 0, 2, fromDate_, toDate_, 0, 0, deposit_); // Add the third stage job p.AddJobs(mainSymbol_, "H1", paramsTemplate3); // Add a task for the third stage job p.AddTasks("0"); // Put the project in the execution queue p.Queue(); // Delete the EA ExpertRemove(); // Successful initialization return(INIT_SUCCEEDED); }
Dieser Teil des Codes muss auch beim Wechsel zu einem anderen Projekt nicht geändert werden, es sei denn, wir wollen die Zusammensetzung der Förderstufen für die automatische Optimierung ändern. Mit der Zeit werden wir sie auch verbessern. Zum Beispiel enthält der Code derzeit numerische Konstanten, die zur besseren Lesbarkeit durch benannte Konstanten ersetzt werden sollten. Wenn sich herausstellt, dass dieser Code wirklich nicht geändert werden muss, werden wir ihn in den Bibliotheksbereich verschieben.
Damit ist der EA für die Erstellung von Optimierungsprojekten in der Datenbank fertig. Lassen Sie uns nun die Stage-EAs erstellen.
Stage-EAs
Wir haben Stage1.mq5 bereits im vorigen Artikel implementiert, sodass wir jetzt nur noch Änderungen vorgenommen haben, die sich auf das Hinzufügen des neuen Parameters maxSpread_ in die Handelsstrategie beziehen. Diese Änderungen wurden bereits oben erörtert.
// 1. Define a constant with the EA name #define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) // 2. Connect the required strategy #include "Strategies/SimpleCandlesStrategy.mqh"; // 3. Connect the general part of the first stage EA from the Advisor library #include <antekov/Advisor/Experts/Stage1.mqh> //+------------------------------------------------------------------+ //| 4. Strategy inputs | //+------------------------------------------------------------------+ sinput string symbol_ = ""; // Symbol sinput ENUM_TIMEFRAMES period_ = PERIOD_CURRENT; // Timeframe for candles input group "=== Opening signal parameters" input int signalSeqLen_ = 6; // Number of unidirectional candles input int periodATR_ = 0; // ATR period (if 0, then TP/SL in points) input group "=== Pending order parameters" input double stopLevel_ = 25000; // Stop Loss (in ATR fraction or points) input double takeLevel_ = 3630; // Take Profit (in ATR fraction or points) input group "=== Money management parameters" input int maxCountOfOrders_ = 9; // Max number of simultaneously open orders input int maxSpread_ = 10; // Max acceptable spread (in points) //+------------------------------------------------------------------+ //| 5. Strategy initialization string generation function | //| from the inputs | //+------------------------------------------------------------------+ string GetStrategyParams() { return StringFormat( "class CSimpleCandlesStrategy(\"%s\",%d,%d,%d,%.3f,%.3f,%d,%d)", (symbol_ == "" ? Symbol() : symbol_), period_, signalSeqLen_, periodATR_, stopLevel_, takeLevel_, maxCountOfOrders_, maxSpread_ ); }
In den EAs der zweiten und dritten Stufe müssen wir nur die Konstante __NAME__ mit dem eindeutigen EA-Namen definieren und die Datei oder Dateien der verwendeten Handelsstrategien verbinden. Der Rest des Codes wird aus der Bibliotheksdatei der entsprechenden Stufe übernommen. So könnte der Code für den EA der zweiten Stufe aussehen Stage2.mq5:
// 1. Define a constant with the EA name #define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) // 2. Connect the required strategy #include "Strategies/SimpleCandlesStrategy.mqh"; #include <antekov/Advisor/Experts/Stage2.mqh>
und die dritte Stufe Stage3.mq5:
// 1. Define a constant with the EA name #define __NAME__ "SimpleCandles" + MQLInfoString(MQL_PROGRAM_NAME) // 2. Connect the required strategy #include "Strategies/SimpleCandlesStrategy.mqh"; #include <antekov/Advisor/Experts/Stage3.mqh>
Endgültiger EA
Im endgültigen EA müssen wir nur noch die Verbindung zur verwendeten Strategie hinzufügen. Die Konstante __NAME__ brauchen wir hier nicht zu deklarieren, da in diesem Fall sowohl die Konstante als auch die Funktion zur Erzeugung der Initialisierungszeichenfolge in der eingebundenen Datei aus dem Bibliotheksteil deklariert werden. Im folgenden Code haben wir in den Kommentaren gezeigt, wie der EA-Name und die Funktion zur Erzeugung des Initialisierungsstrings in diesem Fall aussehen:
// 1. Define a constant with the EA name //#define __NAME__ MQLInfoString(MQL_PROGRAM_NAME) // 2. Connect the required strategy #include "Strategies/SimpleCandlesStrategy.mqh"; #include <antekov/Advisor/Experts/Expert.mqh> //+------------------------------------------------------------------+ //| Function for generating the strategy initialization string | //| from the default inputs (if no name was specified). | //| Import the initialization string from the EA database | //| by the strategy group ID | //+------------------------------------------------------------------+ //string GetStrategyParams() { //// Take the initialization string from the new library for the selected group //// (from the EA database) // string strategiesParams = CVirtualAdvisor::Import( // CVirtualAdvisor::FileName(__NAME__, magic_), // groupId_ // ); // //// If the strategy group from the library is not specified, then we interrupt the operation // if(strategiesParams == NULL && useAutoUpdate_) { // strategiesParams = ""; // } // // return strategiesParams; //}
Wenn wir plötzlich etwas daran ändern wollen, dann genügt es, die Kommentare aus diesem Code zu entfernen und die notwendigen Änderungen vorzunehmen.
Im Projektteil befinden sich also die folgenden Dateien:

Kompilieren wir alle Dateien des Projektteils so, dass für jede Datei mit der Endung mq5 eine Datei mit der Endung ex5 erstellt wird.
Alles zusammenfügen
Schritt 1: Ein Projekt erstellen
Ziehen wir den EA CreateProject.ex5 auf einen beliebigen Chart im Terminal (dieser EA muss nicht im Tester ausgeführt werden!). Im EA-Quellcode haben wir bereits versucht, die aktuellen Werte für alle Eingaben anzugeben, sodass wir im Dialog einfach auf OK klicken können.

Abb. 1. Starten der Projekterstellung EA in der Optimierungsdatenbank
Als Ergebnis erhalten wir die Datei article.17328.db.sqlite mit der Optimierungsdatenbank.
Schritt 2: Beginn der Optimierung
Ziehen wir den EA Optimization.ex5 (auch dieser EA muss nicht im Tester ausgeführt werden!) auf einen beliebigen Chart. In dem sich öffnenden Dialogfeld aktivieren wir die Verwendung der DLL und vergewissern uns, dass wir den richtigen Namen der Optimierungsdatenbank angegeben haben:


Abb. 2. Starten des automatischen Optimierungs-EAs
Wenn alles in Ordnung ist, sollten wir etwa Folgendes sehen: Im Testgerät beginnt die Optimierung des EA der ersten Stufe mit dem ersten Symbol-Zeitrahmen-Paar, während wir auf dem Chart mit dem EA Optimization.ex5 Folgendes sehen werden: „Gesamte Aufgaben in der Warteschlange: ..., Aktuelle Aufgaben-ID: ...“.

Abb. 3. Automatische Optimierung des EA-Betriebs.
Anschließend müssen wir einige Zeit warten, bis alle Optimierungsaufgaben abgeschlossen sind. Diese Zeitspanne kann bei einem langen Testintervall und einer großen Anzahl von Symbolen und Zeitfenstern beträchtlich sein. Mit den derzeitigen Standardeinstellungen von 33 Agenten dauerte der gesamte Vorgang etwa vier Stunden.
Auf der letzten Stufe wird die Optimierung nicht mehr durchgeführt, sondern ein einzelner Durchlauf der dritten Stage-EAs gestartet. Als Ergebnis wird eine Datei mit der Datenbank des endgültigen EA erstellt. Da wir bei der Projekterstellung den Projektnamen „SimpleCandles“ gewählt haben, die magische Zahl 27183 ist und stage3Tester_=true, wird eine Datei mit dem Namen SimpleCandles-27183.test.db.sqlite im gemeinsamen Terminal erstellt.
Schritt 3: Starten des endgültigen EAs im Tester
Versuchen wir, den endgültigen EA im Testprogramm auszuführen. Da der Code nun vollständig aus dem Bibliotheksteil stammt, werden die Standardparameterwerte auch dort definiert. Wenn wir also den EA SimpleCandles.ex5 im Tester starten, ohne die Werte der Eingaben zu ändern, verwendet er die zuletzt hinzugefügte Strategiegruppe (groupId_= 0) mit aktivierten automatischen Aktualisierungen (useAutoUpdate_= true) aus der Datenbank mit dem Namen SimpleCandles-27183.test.db.sqlite (SimpleCandles EA-Dateiname plus die standardmäßige magische Zahl magic_= 27183 und plus „.test“-Suffix aufgrund der Ausführung im Tester).
Leider haben wir noch keine speziellen Tools entwickelt, die es uns ermöglichen, die vorhandenen Strategiegruppen-IDs in der Datenbank des endgültigen EA einzusehen. Wir können nur die Datenbank selbst in einem beliebigen SQLite-Editor öffnen und sie in der Tabelle strategy_groups anzeigen.
Wenn jedoch nur ein Optimierungsprojekt erstellt und einmal ausgeführt wurde, erscheint in der endgültigen EA-Datenbank nur eine Strategiegruppe mit der ID 1. Daher macht es aus Sicht der Gruppenauswahl keinen Unterschied, ob wir eine bestimmte groupId_= 1 angeben oder groupId_= 0 belassen. In jedem Fall wird die einzige vorhandene Gruppe geladen. Wenn wir dasselbe Projekt noch einmal durchführen (dies kann durch Änderung des Projektstatus direkt in der Datenbank geschehen) oder ein anderes, ähnliches Projekt erstellen und durchführen, werden neue Strategiegruppen in der Datenbank des endgültigen EA erscheinen. In diesem Fall werden verschiedene Gruppen für verschiedene Parameterwerte groupId_ verwendet.
Der Parameter zur Aktivierung der automatischen Aktualisierung (useAutoUpdate_= true) erfordert ebenfalls unsere Aufmerksamkeit. Auch wenn es nur eine Gruppe gibt, wirkt sich dieser Parameter auf die Funktionsweise des endgültigen EA aus. Dies äußert sich darin, dass bei aktivierter automatischer Aktualisierung nur die Strategiegruppen zur Arbeit geladen werden können, deren Erscheinungsdatum kleiner als das aktuelle simulierte Datum ist.
Das heißt, wenn wir den endgültigen Berater in demselben Intervall ausführen, das wir für die Optimierung verwendet haben (2022.09.01 – 2023.01.01), wird unsere einzige Strategiegruppe nicht geladen, da sie das Gründungsdatum 2023.01.01 hat. Daher müssen wir entweder die automatischen Aktualisierungen deaktivieren (useAutoUpdate_= false) und die spezifische ID der verwendeten Handelsstrategiegruppe (groupId_= 1) in den Eingaben angeben, wenn wir den endgültigen EA starten, oder ein anderes Intervall wählen, das nach dem Enddatum des Optimierungsintervalls liegt.
Solange wir noch nicht entschieden haben, welche Strategien im endgültigen EA verwendet werden sollen, und noch nicht das Ziel haben, sie auf die Durchführbarkeit einer regelmäßigen Neuoptimierung zu testen, kann dieser Parameter auf false gesetzt werden und die spezifische ID der verwendeten Handelsstrategiegruppe angeben.
Der letzte Satz wichtiger Parameter ist dafür verantwortlich, welchen Datenbanknamen der endgültige EA verwenden wird. In der Standardeinstellung ist die magische Zahl dieselbe, die wir bei der Erstellung des Projekts in den Einstellungen angegeben haben. Außerdem haben wir den Namen der endgültigen EA-Datei an den Namen des Projekts angepasst. Beim Erstellen des Projekts war der Wert des Parameters stage3Tester_ gleich true, sodass der Dateiname der erstellten Datenbank des endgültigen EA SimpleCandles-27183.test.db.sqlite lautet. Sie stimmt vollständig mit derjenigen überein, die der endgültige SimpleCandles.ex5 EA verwenden wird.
Schauen wir uns die Ergebnisse der Ausführung des endgültigen EA im Optimierungsintervall an:


Abb. 4. Der automatische Optimierungs-EA-Betrieb im Zeitraum 2022.09.01 – 2023.01.01
Wenn wir es in einem anderen Zeitintervall durchführen, werden die Ergebnisse wahrscheinlich nicht so schön sein:


Abb. 5. Die automatische Optimierung EA-Betrieb auf dem Intervall 2023.01.01 – 2023.02.01
Wir haben das Intervall von einem Monat unmittelbar nach dem Optimierungsintervall als Beispiel genommen. In der Tat überstieg der Drawdown leicht den erwarteten Wert von 10 %, und der normalisierte Gewinn verringerte sich um etwa das Fünffache. Ist es möglich, die Optimierung für die letzten drei Monate erneut durchzuführen, um ein ähnliches Bild vom Verhalten des EA im nächsten Monat zu erhalten? Diese Frage bleibt vorerst offen.
Schritt 4: Starten des fertigen EA auf einem Handelskonto
Um den fertigen EA auf einem Handelskonto auszuführen, müssen wir den Namen der resultierenden Datenbankdatei anpassen. Wir sollten die Endung „.test“ entfernen. Mit anderen Worten, wir benennen SimpleCandles-27183.test.db.sqlite einfach um und kopieren es in SimpleCandles-27183.db.sqlite. Sein Speicherort bleibt derselbe – im gemeinsamen Terminalordner.
Ziehen Sie den fertigen EA SimpleCandles.ex5 auf ein beliebiges Terminal-Chart und legen Sie ihn dort ab. In den Eingaben könnten wir alles mit den Standardwerten belassen, da wir mit dem Laden der letzten Gruppe von Strategien zufrieden sind und das aktuelle Datum natürlich größer ist als das Erstellungsdatum dieser Gruppe.

Abb. 6. Standardeingaben für den endgültigen EA
Während der Vorbereitung des Artikels wurde der fertige EA etwa eine Woche lang auf einem Demokonto getestet und zeigte die folgenden Ergebnisse:

Abb. 7. Ergebnisse der letzten EA-Operation auf dem Handelskonto
Es war eine ziemlich gute Woche für die EA. Bei einem Drawdown von 1,27 % lag der Gewinn bei etwa 2 %. Der EA wurde aufgrund eines Neustarts des Computers einige Male neu gestartet, stellte aber erfolgreich Informationen über offene virtuelle Positionen wieder her und arbeitete weiter.
Schlussfolgerung
Schauen wir mal, was wir haben. Wir haben endlich die Ergebnisse eines ziemlich langen Entwicklungsprozesses zu etwas zusammengefasst, das einem kohärenten System ähnelt. Das daraus resultierende Tool zur Organisation der Auto-Optimierung und des Testens von Handelsstrategien ermöglicht eine signifikante Verbesserung der Testergebnisse selbst einfacher Handelsstrategien durch Diversifikation über verschiedene Handelsinstrumente.
Außerdem kann die Zahl der Vorgänge, die zum Erreichen der gleichen Ziele einen manuellen Eingriff erfordern, erheblich reduziert werden. Jetzt müssen wir nicht mehr den Abschluss einer weiteren Optimierung verfolgen, bevor wir die nächste starten, und wir müssen nicht mehr darüber nachdenken, wie wir die Zwischenergebnisse der Optimierung speichern und in einen Handels-EA integrieren können. Stattdessen können wir uns direkt auf die Entwicklung der Logik hinter unseren Handelsstrategien konzentrieren.
Natürlich gibt es noch viel zu tun, um dieses Instrument zu verbessern und bequemer zu machen. Die Idee einer vollwertigen Weboberfläche, die nicht nur die Erstellung, den Start und die Überwachung laufender Optimierungsprojekte verwaltet, sondern auch den Betrieb von EAs, die auf verschiedenen Terminals laufen, und die Anzeige ihrer Statistiken, liegt noch in weiter Ferne. Dies ist eine sehr große Aufgabe, aber rückblickend kann man dasselbe über die Aufgabe sagen, die heute bereits mehr oder weniger vollständig gelöst ist.
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.17328 | Arbeitsordner des Projekts | |||
| 1 | CreateProject.mq5 | 1.02 | EA-Skript zur Erstellung eines Projekts mit Phasen, Aufträgen und Optimierungsaufgaben. | Teil 25 |
| 2 | Optimization.mq5 | 1.00 | EA für Projekte Auto-Optimierung | Teil 23 |
| 3 | SimpleCandles.mq5 | 1.01 | Endgültiger EA für den Parallelbetrieb mehrerer Gruppen von Modellstrategien. Die Parameter werden aus der integrierten Gruppenbibliothek übernommen. | Teil 25 |
| 4 | Stage1.mq5 | 1.02 | Handelsstrategie Einzelinstanzoptimierung EA (Phase 1) | Teil 25 |
| 5 | Stage2.mq5 | 1.01 | Handelsstrategien Instanzen Gruppe Optimierung EA (Phase 2) | Teil 25 |
| 6 | Stage3.mq5 | 1.01 | Der EA, der eine generierte standardisierte Gruppe von Strategien in einer EA-Datenbank mit einem bestimmten Namen speichert. | Teil 25 |
| MQL5/Experts/Article.17328/Strategies | Ordner Projektstrategien | |||
| 7 | SimpleCandlesStrategy.mqh | 1.01 | SimpleCandles Handelsstrategie Klasse | Teil 25 |
| MQL5/Include/antekov/Advisor/Base | Basisklassen, von denen andere Projektklassen erben | |||
| 8 | Advisor.mqh | 1.04 | EA-Basisklasse | Teil 10 |
| 9 | Factorable.mqh | 1.05 | Basisklasse von Objekten, die aus einer Zeichenkette erstellt werden | Teil 24 |
| 10 | FactorableCreator.mqh | 1.00 | Teil 24 | |
| 11 | Interface.mqh | 1.01 | Basisklasse zur Visualisierung verschiedener Objekte | Teil 4 |
| 12 | Receiver.mqh | 1.04 | Basisklasse für die Umwandlung von offenen Volumina in Marktpositionen | Teil 12 |
| 13 | Strategy.mqh | 1.04 | Handelsstrategie-Basisklasse | Teil 10 |
| MQL5/Include/antekov/Advisor/Database | Dateien für den Umgang mit allen Arten von Datenbanken, die von Projekt-EAs verwendet werden | |||
| 14 | Database.mqh | 1.12 | Klasse für den Umgang mit der Datenbank | Teil 25 |
| 15 | db.adv.schema.sql | 1.00 | Endgültige Datenbankstruktur von EA | Teil 22 |
| 16 | db.cut.schema.sql | 1.00 | Struktur der verkürzten Optimierungsdatenbank | Teil 22 |
| 17 | db.opt.schema.sql | 1.05 | Optimierung der Datenbankstruktur | Teil 22 |
| 18 | Storage.mqh | 1.01 | Klasse zur Handhabung der Schlüssel-Wert-Speicherung für den endgültigen EA in der EA-Datenbank | Teil 23 |
| MQL5/Include/antekov/Advisor/Experts | Dateien mit gemeinsamen Teilen der verwendeten EAs verschiedener Typen | |||
| 19 | Expert.mqh | 1.22 | Die Bibliotheksdatei für den endgültigen EA. Gruppenparameter können aus der EA-Datenbank übernommen werden | Teil 23 |
| 20 | Optimization.mqh | 1.04 | Bibliotheksdatei für den EA, der den Start von Optimierungsaufgaben verwaltet | Teil 23 |
| 21 | Stage1.mqh | 1.19 | Bibliotheksdatei für die Einzelinstanz der Handelsstrategieoptimierung EA (Stage 1) | Teil 23 |
| 22 | Stage2.mqh | 1.04 | Bibliotheksdatei für den EA, der eine Gruppe von Handelsstrategieinstanzen optimiert (Stage 2) | Teil 23 |
| 23 | Stage3.mqh | 1.04 | Bibliotheksdatei für den EA, die eine generierte standardisierte Gruppe von Strategien in einer EA-Datenbank mit einem bestimmten Namen speichert. | Teil 23 |
| MQL5/Include/antekov/Advisor/Optimization | Für die automatische Optimierung zuständige Klassen | |||
| 24 | OptimizationJob.mqh | 1.00 | Optimierung der Projektphase Jobklasse | Teil 25 |
| 25 | OptimizationProject.mqh | 1.00 | Optimierungsprojekt Klasse | Teil 25 |
| 26 | OptimizationStage.mqh | 1.00 | Optimierungsprojekt Stage Klasse | Teil 25 |
| 27 | OptimizationTask.mqh | 1.00 | Optimierungs-Aufgabenklasse (Erstellung) | Teil 25 |
| 28 | Optimizer.mqh | 1.03 | Klasse für den Projektautooptimierungsmanager | Teil 22 |
| 29 | OptimizerTask.mqh | 1.03 | Optimierungs-Aufgabenklasse (Förderer) | Teil 22 |
| MQL5/Include/antekov/Advisor/Strategies | Beispiele für Handelsstrategien, die die Funktionsweise des Projekts veranschaulichen | |||
| 30 | HistoryStrategy.mqh | 1.00 | Klasse der Handelsstrategie für die Wiederholung der Handelshistorie | Teil 16 |
| 31 | SimpleVolumesStrategy.mqh | 1.11 | Klasse der Handelsstrategie mit Tick-Volumen | Teil 22 |
| MQL5/Include/antekov/Advisor/Utils | Hilfsprogramme, Makros zur Code-Reduzierung | |||
| 32 | ExpertHistory.mqh | 1.00 | Klasse für den Export der Handelshistorie in eine Datei | Teil 16 |
| 33 | Macros.mqh | 1.06 | Nützliche Makros für Array-Operationen | Teil 25 |
| 34 | NewBarEvent.mqh | 1.00 | Klasse zur Definition eines neuen Balkens für ein bestimmtes Symbol | Teil 8 |
| 35 | SymbolsMonitor.mqh | 1.00 | Klasse zur Beschaffung von Informationen über Handelsinstrumente (Symbole) | Teil 21 |
| MQL5/Include/antekov/Advisor/Virtual | Klassen zur Erstellung verschiedener Objekte, die durch ein System virtueller Handelsaufträge und -positionen verbunden sind | |||
| 36 | Money.mqh | 1.01 | Basisklasse Geldmanagement | Teil 12 |
| 37 | TesterHandler.mqh | 1.07 | Klasse zur Behandlung von Optimierungsereignissen | Teil 23 |
| 38 | VirtualAdvisor.mqh | 1.10 | Klasse des EA, der virtuelle Positionen (Aufträge) bearbeitet | Teil 24 |
| 39 | VirtualChartOrder.mqh | 1.01 | Grafische virtuelle Positionsklasse | Teil 18 |
| 40 | VirtualHistoryAdvisor.mqh | 1.00 | Die Klasse des EA zur Wiederholung des Handelsverlaufs | Teil 16 |
| 41 | VirtualInterface.mqh | 1.00 | EA GUI-Klasse | Teil 4 |
| 42 | VirtualOrder.mqh | 1.09 | Klasse der virtuellen Aufträge und Positionen | Teil 22 |
| 43 | VirtualReceiver.mqh | 1.04 | Klasse für die Umwandlung von offenen Volumina in Marktpositionen (Empfänger) | Teil 23 |
| 44 | VirtualRiskManager.mqh | 1.05 | Klasse Risikomanagement (Risikomanager) | Teil 24 |
| 45 | VirtualStrategy.mqh | 1.09 | Klasse einer Handelsstrategie mit virtuellen Positionen | Teil 23 |
| 46 | VirtualStrategyGroup.mqh | 1.03 | Klasse der Handelsstrategien Gruppe(n) | Teil 24 |
| 47 | VirtualSymbolReceiver.mqh | 1.00 | Symbol-Empfängerklasse | Teil 3 |
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/17328
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 View- und Controller-Komponenten für Tabellen im MQL5 MVC-Paradigma: Einfache Steuerung
Die View Komponente für Tabellen im MQL5 MVC Paradigma: Grafisches Basiselement
Eine alternative Log-datei mit der Verwendung der HTML und CSS
Vom Neuling zum Experten: Automatisierung der Handelsdisziplin mit einem MQL5 Risk Enforcement EA
- 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.
Zunächst einmal möchte ich wissen, in welcher Sprache dies ist.
Es ist Koreanisch. Ihr Browser zeigt es aus irgendeinem Grund nicht an.
Es ist koreanisch. Ihr Browser zeigt es aus irgendeinem Grund nicht an.
Ganz genau. Ich habe an diesem Tag, dem 2025.07.08, von Anfang an nichts in diesem Thread gepostet. Wenn du dem Link zum Thema folgst, wird ein Beitrag mit einem anderen Datum angezeigt. Wahrscheinlich ist es auch die Schuld meines Browsers, dass Ihre übrigen Programmierer nicht mithalten können.
Ganz genau. Ich habe an diesem Tag, dem 2025.07.08, von Anfang an nichts in diesem Thread gepostet. Wenn Sie diesem Link zum Thema folgen, sehen Sie einen Beitrag mit einem anderen Datum. Wahrscheinlich ist es auch die Schuld meines Browsers, dass Ihre übrigen Programmierer nicht mithalten können.
Danke für Ihre Hartnäckigkeit, ich habe es behoben.
Danke für Ihre Beharrlichkeit, korrigiert.
Sorry für die Hartnäckigkeit, ich sehe keine Lösung. Der Link führt immer noch zu einer seltsamen Meldung, die ich nicht geschrieben habe. Nun, selbst wenn wir davon ausgehen, dass ich sie geschrieben habe, warum steht dann keine Meldung auf Russisch daneben? Oder glaubst du, wenn ich kein Englisch kann, habe ich Koreanisch gelernt und habe Spaß....
Das ist der Unterschied bei einer Diskussion in verschiedenen Sprachen.
Das ist der Link.
Das ist die russische Übersetzung.
Und das ist der Inhalt der russischen Version des Artikels.
In welcher Sprache habe ich also versucht zu schreiben????
Es ist alles ein einziges Thema. Und wenn Sie sich die anderen Themen ansehen, werden Sie Nachrichten seltsamen Ursprungs in Sprachen finden, die ich mir nie hätte träumen lassen.
Vielleicht habe ich überreagiert. Ich habe nur eine weitere ähnliche Meldung gefunden, auf Englisch und wahrscheinlich eine echte Übersetzung.
Bitte löschen Sie die obige Meldung in allen Sprachversionen, und sie wird wahrscheinlich korrigiert werden. Vielleicht nicht ganz so wie beim letzten Mal.......
Forum über Handel, automatisierte Handelssysteme und das Testen von Handelsstrategien
Diskussion des Artikels "Entwicklung eines Multicurrency Expert Advisors (Teil 25): Einfügen einer neuen Strategie (II)"
Rashid Umarov, 2025.07.06 14:04
Danke, wir werden es herausfinden.
Wir haben dieses Problem bereits gelöst, aber es scheint nicht vollständig gelöst zu sein.