Entwicklung eines Expertenberaters für mehrere Währungen (Teil 8): Belastungstest und Handhabung eines neuen Balkens
Einführung
Im ersten Artikel haben wir einen EA entwickelt, der zwei Instanzen von Handelsstrategien enthält. In der zweiten haben wir bereits neun Instanzen verwendet, während diese Zahl im letzten auf 32 angestiegen ist. Es gab keine Probleme mit der Testzeit. Je kürzer die Dauer eines einzelnen Testdurchlaufs ist, desto besser. Aber wenn die gesamte Optimierung ein paar Stunden dauert, ist das immer noch besser als mehrere Tage oder Wochen. Wenn wir mehrere Strategieinstanzen in einem EA kombiniert haben und die Ergebnisse sehen wollen, sollte ein einzelner Durchlauf in Sekunden oder Minuten abgeschlossen sein, nicht in Stunden oder Tagen.
Wenn wir die Optimierung für ausgewählte Gruppen von Strategieinstanzen durchführen, dann nehmen mehrere Instanzen bereits an allen Optimierungsdurchgängen teil. Dadurch erhöht sich der Zeitaufwand für einzelne Durchgänge und für die gesamte Optimierung im Allgemeinen. Daher beschränkten wir uns bei dieser Optimierung auf die Auswahl von Gruppen mit nicht mehr als acht Instanzen.
Versuchen wir herauszufinden, wie die Zeit eines einzelnen Durchlaufs im Tester von der Anzahl der Instanzen von Handelsstrategien für unterschiedlich lange Testzeiträume abhängt. Betrachten wir auch den verbrauchten Speicherplatz. Natürlich müssen wir sehen, wie sich EAs mit einer unterschiedlichen Anzahl von Handelsstrategie-Instanzen verhalten, wenn sie auf dem Terminal-Chart gestartet werden.
Unterschiedliche Anzahl von Instanzen im Tester
Um ein solches Experiment durchzuführen, müssen wir einen neuen EA schreiben, der auf einem der bestehenden EAs basiert. Nehmen wir den EA OptGroupExpert.mq5 als Grundlage und nehmen wir die folgenden Änderungen daran vor:
- Entfernen wir die Eingaben, die die Indizes der acht Parametersätze angeben, die aus dem vollständigen Array der aus der Datei geladenen Sätze entnommen wurden. Lassen wir den Parameter count_, der nun die Anzahl der zu ladenden Sets aus dem vollständigen Array der Sets angibt.
- Entfernen wir die Prüfung auf Eindeutigkeit von Indizes, die nicht mehr existieren. Wir fügen neue Strategien zum Array der Strategien hinzu, wobei die Parametersätze aus den ersten count_ Elementen des Arrays der Parametersätze params entnommen werden. Wenn nicht genügend Instanzen in diesem Array vorhanden sind, werden die neuen Instanzen in der Schleife vom Anfang des Arrays genommen.
- Entfernen wir die Funktionen OnTesterInit() und OntesterDeinit(), da wir diesen EA noch nicht für die Optimierung von irgendetwas verwenden werden.
Wir werden den folgenden Code erhalten:
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "::: Money management" sinput double expectedDrawdown_ = 10; // - Maximum risk (%) sinput double fixedBalance_ = 10000; // - Used deposit (0 - use all) in the account currency sinput double scale_ = 1.00; // - Group scaling multiplier input group "::: Selection for the group" sinput string fileName_ = "Params_SV_EURGBP_H1.csv"; // - File with strategy parameters (*.csv) input int count_ = 8; // - Number of strategies in the group (1 .. 8) input group "::: Other parameters" sinput ulong magic_ = 27183; // - Magic ... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Load strategy parameter sets int totalParams = LoadParams(fileName_, params); // If nothing is loaded, report an error if(totalParams == 0) { PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n" "Check that it exists in data folder or in common data folder.", fileName_); return(INIT_PARAMETERS_INCORRECT); } // Report an error if if(count_ < 1) { // number of instances is less than 1 return INIT_PARAMETERS_INCORRECT; } ArrayResize(params, count_); // Set parameters in the money management class CMoney::DepoPart(expectedDrawdown_ / 10.0); CMoney::FixedBalance(fixedBalance_); // Create an EA handling virtual positions expert = new CVirtualAdvisor(magic_, "SimpleVolumes_BenchmarkInstances"); // Create and fill the array of all strategy instances CVirtualStrategy *strategies[]; FOREACH(params, APPEND(strategies, new CSimpleVolumesStrategy(params[i % totalParams]))); // Form and add a group of strategies to the EA expert.Add(CVirtualStrategyGroup(strategies, scale_)); return(INIT_SUCCEEDED); }
Speichern wir den resultierenden Code in der Datei BenchmarkInstancesExpert.mq5 im aktuellen Ordner.
Versuchen wir nun, diesen EA mehrmals im Tester mit einer unterschiedlichen Anzahl von Handelsstrategie-Instanzen und verschiedenen Tick-Simulationsmodi laufen zu lassen.
Testergebnisse für verschiedene Modi
Beginnen wir mit dem bereits bekannten „1-Minuten-OHLC“-Tick-Simulationsmodus, den wir in allen vorherigen Artikeln verwendet haben. Wir werden die Anzahl der Instanzen beim nächsten Start verdoppeln. Beginnen wir mit 8 Instanzen. Sollte die Testzeit zu lang werden, werden wir die Testdauer verkürzen.
Abb. 1. Ergebnisse von Einzelläufen im Modus „1 Minute OHLC“.
Wie Sie sehen können, haben wir bei den Tests mit bis zu 512 Instanzen einen Testzeitraum von 6 Jahren verwendet, sind dann zu einem Zeitraum von 1 Jahr übergegangen und haben für die letzten beiden Durchgänge nur 3 Monate verwendet.
Um die Zeitkosten für verschiedene Testzeiträume vergleichen zu können, berechnen wir einen separaten Wert: die Simulationszeit einer Strategieinstanz im Laufe eines Tages. Dazu dividieren Sie die Gesamtzeit durch die Anzahl der Strategieinstanzen und durch die Dauer des Testzeitraums in Tagen. Um nicht mit kleinen Zahlen zu kämpfen, rechnen wir diese Zeit in Nanosekunden um, indem wir mit 10^9 multiplizieren.
In den Protokollen gibt der Tester Informationen über den während des Laufs verbrauchten Speicher an, wobei er das Gesamtvolumen und das für historische und Tick-Daten verbrauchte Volumen angibt. Zieht man diese vom Gesamtspeichervolumen ab, erhält man die Speicherbedarf, den der EA selbst benötigt.
Anhand der Ergebnisse können wir sagen, dass selbst die maximale Anzahl von Kopien (16.384) keinen katastrophalen Zeitaufwand für die Ausführung des Testers erfordert. In der Regel reicht eine solche Anzahl von Kopien aus, um beispielsweise ein gemeinsames Werk von fünfzehn Symbolen mit jeweils hundert Instanzen zu gestalten. Das ist also schon eine ganze Menge. Gleichzeitig steigt der Speicherbedarf mit zunehmender Anzahl der Instanzen nicht wesentlich an. Aus irgendeinem Grund gibt es eine Spitze im Speicherbedarf für den EA selbst bei 8192 Instanzen, aber danach wurde wieder weniger Speicher benötigt.
Um genauere Ergebnisse zu erhalten, können wir mehrere Durchläufe für jede Anzahl von Instanzen wiederholen und die durchschnittlichen Zeiten und durchschnittlichen Speichergrößen berechnen, da die Ergebnisse bei verschiedenen Durchläufen mit der gleichen Anzahl von Instanzen immer noch unterschiedlich sind. Aber diese Unterschiede waren nicht sehr groß, sodass es wenig Sinn machte, weitergehende Tests durchzuführen. Wir wollten nur sicherstellen, dass wir auch bei relativ geringen Auflagen nicht an Grenzen stoßen.
Schauen wir uns nun die Ergebnisse an, wenn wir den EA im Tester im Simulationsmodus „Jeder Tick“ ausführen.
Abb. 2. Ergebnisse von Einzelläufen im Modus „Jeder Tick“.
Die Zeit für einen Durchgang hat sich um etwa das Zehnfache erhöht, sodass wir den Testzeitraum bei gleicher Anzahl von Instanzen im Vergleich zum vorherigen Modus verkürzt haben. Die Größe des Tickspeichers hat sich natürlich vergrößert, was zu einem Anstieg des gesamten zugewiesenen Speichers geführt hat. Es stellte sich jedoch heraus, dass der dem EA zugewiesene Speicherplatz für alle verwendeten Instanzen fast gleich groß war. Es gibt zwar ein gewisses Wachstum, aber es ist recht langsam.
Anomal niedrige Laufzeiten wurden für 512 und 1024 Instanzen beobachtet - fast doppelt so schnell wie für andere Instanzgrößen. Der Grund dafür ist höchstwahrscheinlich die Reihenfolge der Parameter der Handelsstrategie-Instanz in der CSV-Datendatei.
Der letzte zu untersuchende Simulationsmodus ist „Jeder Tick anhand realer Ticks“. Wir haben dafür ein paar mehr Durchläufe gemacht als für den Modus „Jeder Tick“.
Abb. 3. Ergebnisse einzelner Läufe im Modus „Jeder Tick anhand realer Ticks“.
Im Vergleich zum vorherigen Modus hat sich der Zeitaufwand um etwa 30 % erhöht, und der verwendete Speicherplatz ist um etwa 20 % gestiegen.
Es ist erwähnenswert, dass eine Kopie des an den Chart angehängten EA während des Tests im Terminal lief. Es wurden 8192 Instanzen verwendet. In diesem Fall lag der Verbrauch an Terminalspeicher bei etwa 200 MB, während der Verbrauch an CPU-Ressourcen zwischen 0 % und 4 % lag.
Insgesamt hat das Experiment gezeigt, dass wir eine ziemlich große Reserve für die mögliche Anzahl von Instanzen von Handelsstrategien haben, die in einem EA zusammenarbeiten können. Dieser Betrag hängt natürlich weitgehend von den spezifischen Inhalten der Handelsstrategien ab. Je mehr Berechnungen eine Instanz durchführen muss, desto weniger können wir uns leisten, sie zu kombinieren.
Überlegen wir nun, welche einfachen Maßnahmen wir ergreifen können, um den Test zu beschleunigen.
Deaktivierung der Ausgabe
In der aktuellen Implementierung zeigen wir während des Betriebs des EA eine ganze Reihe von Informationen an. Bei der Optimierung einzelner Instanzen stellt dies kein Problem dar, da die Ausgabefunktionen einfach nicht ausgeführt werden. Wenn wir einen einzigen Durchlauf des EA im Tester durchführen, werden alle Meldungen an das Protokoll gesendet. In der verwendeten Bibliothek VirtualOrder.mqh wird eine Meldung über die Behandlung von Ereignissen für jede virtuelle Bestellung angezeigt. Wenn es nur wenige virtuelle Aufträge gibt, wirkt sich dies kaum auf die Testzeit aus, aber wenn die Zahl der Aufträge in die Zehntausende geht, kann dies spürbare Auswirkungen haben.
Versuchen wir, es zu messen. Wir können die Ausgabe aller unserer Meldungen in das Protokoll deaktivieren, indem wir die folgende Zeile am Anfang der EA-Datei einfügen:
#define PrintFormat StringFormat
Aufgrund der Verwandtschaft dieser Funktionen können alle Aufrufe von PrintFormat() durch StringFormat() ersetzt werden. Sie erzeugen eine Zeichenkette, geben sie aber nicht in das Protokoll aus.
Nach mehreren Starts zeigte sich bei einigen eine Zeitersparnis von 5-10 %, bei anderen konnte die Zeit sogar leicht erhöht werden. Vielleicht brauchen wir in Zukunft noch eine ähnliche Methode zur Ersetzung von PrintFormat().
Umstellung auf 1-Minuten-OHLC
Eine weitere Möglichkeit, den Prozess sowohl der einzelnen Testdurchläufe als auch der Optimierung zu beschleunigen, besteht darin, die Simulationsmodi „Jeder Tick“ und „Jeder Tick basierend auf realen Ticks“ zu vermeiden.
Es ist klar, dass sich das nicht alle Handelsstrategien leisten können. Wenn die Strategie sehr häufige Positionseröffnungen und -schließungen vorsieht (mehr als einmal pro Minute), ist es unmöglich, auf das Testen aller Ticks zu verzichten. Auch der Hochfrequenzhandel findet nicht die ganze Zeit statt, sondern nur in bestimmten Zeiträumen. Wenn die Strategie jedoch keine häufigen Eröffnungen/Schließungen erfordert und nicht so empfindlich auf den Verlust mehrerer Punkte aufgrund einer unzureichend genauen Auslösung von Stop-Loss- und Take-Profit-Niveaus reagiert, warum sollte man diese Möglichkeit nicht nutzen?
Die als Beispiel betrachtete Handelsstrategie ist eine derjenigen, die es uns ermöglicht, vom All-Tick-Modus wegzukommen. Hier stellt sich jedoch ein weiteres Problem. Wenn wir einfach die Parameter einzelner Instanzen im Modus „1-Minuten-OHLC“ optimieren und dann den zusammengestellten EA im Terminal arbeiten lassen, dann muss der EA in jedem Tick-Modus funktionieren. Sie wird nicht nur 4 Ticks pro Minute erhalten, sondern viel mehr. Daher wird die Funktion OnTick() häufiger aufgerufen, und die Menge der Preise, die der EA verarbeitet, wird etwas vielfältiger sein.
Dieser Unterschied kann das Bild der von der EA gezeigten Ergebnisse verändern. Um zu prüfen, wie realistisch dieses Szenario ist, vergleichen wir die Handelsergebnisse, die beim Testen des EA mit denselben Eingaben in den Modi „1 Minute OHLC“ und „Jeder Tick basierend auf echten Ticks“ erzielt wurden.
Abb. 4. Der Vergleich der Ergebnisse der einzelnen Läufe in den Modi
„Jeder Tick auf Basis echter Ticks“ (links) und „1 Minute OHLC“ (rechts)
Wir können sehen, dass die Eröffnungs- und Schlusszeit sowie der Preis für die verschiedenen Modi leicht unterschiedlich sind. Zunächst ist dies der einzige Unterschied, aber dann kommt der Moment, in dem sich links ein Deal öffnet und rechts gleichzeitig keins: Sehen Sie sich die Zeilen mit Deal #25 an. So enthalten die Ergebnisse für den Modus „1 Minute OHLC“ weniger Abschlüsse als für den Modus „Jeder Tick basierend auf echten Ticks“.
In jedem Tick-Modus fiel der Gewinn etwas höher aus. Betrachtet man die Saldenkurve, so gibt es keine signifikanten Unterschiede zwischen ihnen:
Abb. 5. Testergebnisse in „1 Minute OHLC“ (oben) und „Jeder Tick basierend auf echten Ticks“ (unten)
Wenn wir diesen EA also im Terminal ausführen, werden wir höchstwahrscheinlich nicht schlechtere Ergebnisse erzielen als beim Test im „1-Minuten-OHLC“-Modus. Dies bedeutet, dass ein schnellerer Tick-Simulationsmodus für die Optimierung verwendet werden kann. Wenn einige Berechnungen für eine Strategie nur zu Beginn eines neuen Balkens durchgeführt werden können, dann können wir die Arbeit des EA weiter beschleunigen, indem wir solche Berechnungen bei jedem Tick ablehnen. Dazu benötigen wir eine Möglichkeit, einen neuen Balken im EA zu bestimmen.
Wenn die Ergebnisse im Modus „Jeder Tick“ schlechter sind als im Modus „1 Minute OHLC“, können wir versuchen, dem EA zu verbieten, Transaktionen nicht zu Beginn des Balkens durchzuführen. In diesem Fall sollten wir in allen Tickmodellierungsmodi die bestmöglichen Ergebnisse erzielen. Um dies zu erreichen, benötigen wir wiederum eine Möglichkeit, einen neuen Balken im EA zu definieren.
Einen neuen Balken definieren
Lassen Sie uns zunächst unsere Wünsche formulieren. Wir möchten eine Funktion haben, die true zurückgibt, wenn ein neuer Balken für ein bestimmtes Symbol und einen bestimmten Zeitrahmen aufgetreten ist. Bei der Entwicklung eines EA, der eine einzelne Instanz einer Handelsstrategie implementiert, wird eine solche Funktion in der Regel für ein Symbol und einen Zeitrahmen (oder für ein Symbol und mehrere Zeitrahmen) implementiert, wobei Variablen verwendet werden, um den Zeitpunkt des letzten Balkens zu speichern. Oft ist zu erkennen, dass der Code, der diese Funktionalität implementiert, nicht als separate Funktion zugewiesen wird, sondern an der einzigen Stelle implementiert ist, an der er benötigt wird.
Dieser Ansatz ist recht unpraktisch, wenn für verschiedene Instanzen von Handelsstrategien mehrfach geprüft werden muss, ob ein neuer Balken aufgetreten ist. Es ist natürlich möglich, diesen Code direkt in die Implementierung einer Handelsstrategieinstanz einzubetten, aber wir werden es anders machen.
Machen wir die Funktion IsNewBar(symbol, timeframe) zu einer öffentlichen Funktion, die in der Lage sein sollte, das Auftreten eines neuen Balkens zum aktuellen Tick nach Symbol und Zeitrahmen zu melden. Es ist wünschenswert, dass neben dem Aufruf dieser Funktion keine weiteren Variablen oder Aktionen in den Handelslogik-Code der Strategien eingefügt werden müssen. Auch wenn ein neuer Balken zum aktuellen Tick eingetroffen ist und die Funktion mehrmals aufgerufen wird (z. B. von verschiedenen Instanzen von Handelsstrategien), sollte sie bei jedem Aufruf „true“ zurückgeben, nicht nur beim ersten Aufruf.
Dann müssen wir Informationen über die Zeiten des letzten Balkens für jedes Symbol und jeden Zeitrahmen speichern. Mit „alle“ meinen wir aber nicht alle, die im Terminal zur Verfügung stehen, sondern nur diejenigen, die für den Betrieb bestimmter Instanzen von Handelsstrategien tatsächlich erforderlich sind. Um den Bereich dieser notwendigen Symbole und Zeitrahmen zu definieren, werden wir die Liste der von der Funktion IsNewBar(Symbol, Zeitrahmen) durchgeführten Aktionen erweitern. Wir überprüfen zunächst, ob es für den aktuellen Balken auf dem angegebenen Symbol und Zeitrahmen eine Form von erinnerter Zeit gibt. Existiert er nicht, legt die Funktion einen solchen Zeitspeicherplatz an. Wenn er existiert, gibt die Funktion das Ergebnis der Prüfung auf einen neuen Balken zurück.
Damit unsere Funktion IsNewBar() bei einem einzigen Tick mehrfach aufgerufen werden kann, müssen wir sie in zwei separate Funktionen aufteilen. Die erste Funktion prüft, ob zu Beginn eines Ticks neue Balken für alle interessierenden Symbole und Zeitrahmen aufgetreten sind, und speichert diese Informationen für die zweite Funktion, die einfach das gewünschte Ergebnis des Ereignisses „Auftreten eines neuen Balkens“ ermittelt und zurückgibt. Nennen wir die erste Funktion UpdateNewBar(). Wir werden es so gestalten, dass es auch einen logischen Wert zurückgibt, der anzeigt, dass mindestens ein Symbol und ein Zeitrahmen einen neuen Balken haben.
Die Funktion UpdateNewBar() sollte einmal zu Beginn der Verarbeitung eines neuen Ticks aufgerufen werden. Sein Aufruf kann beispielsweise an den Anfang der Methode CVirtualAdvisor::Tick() gesetzt werden:
void CVirtualAdvisor::Tick(void) { // Define a new bar for all required symbols and timeframes UpdateNewBar(); ... // Start handling in strategies where IsNewBar(...) can already be used CAdvisor::Tick(); ... }
Um die Speicherung der Zeiten der letzten Takte zu organisieren, erstellen Sie zunächst die statische Klasse CNewBarEvent. Das bedeutet, dass wir keine Objekte dieser Klasse erstellen, sondern nur ihre statischen Eigenschaften und Methoden verwenden werden. Dies ist im Wesentlichen gleichbedeutend mit der Erstellung der erforderlichen globalen Variablen und Funktionen in einem eigenen Namensraum.
In dieser Klasse gibt es zwei Arrays: das Array mit den Symbolnamen (m_symbols) und das Array mit Zeigern auf die Objekte der neuen Klasse (m_symbolNewBarEvent). Der erste enthält die Symbole, mit denen wir die neuen Balkenereignisse verfolgen werden. Der zweite ist ein Zeiger auf die neue Klasse CSymbolNewBarEvent, die Bar-Zeiten für ein Symbol, aber für verschiedene Zeitrahmen speichert.
Diese beiden Klassen werden drei Methoden haben:
- Die Methode zur Registrierung eines neuen überwachten Symbols oder Symbolzeitrahmens Register(...)
- Die Methode zum Aktualisieren neuer Flags der Balken Update()
- Die Methode zur Ermittlung eines neuen Flags der Balken IsNewBar(...)
Wenn es notwendig ist, die Verfolgung eines neuen Balkenereignisses für ein neues Symbol zu registrieren, wird ein neues Klassenobjekt CSymbolNewBarEvent erstellt. Daher ist es notwendig, den von diesen Objekten belegten Speicher zu bereinigen, wenn der EA seine Arbeit beendet hat. Diese Aufgabe wird von der statischen Methode CNewBarEvent::Destroy() und der globalen Funktion DestroyNewBar() erfüllt. Wir werden den Funktionsaufruf in den EA-Destruktor einfügen:
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CVirtualAdvisor::~CVirtualAdvisor() { delete m_receiver; // Remove the recipient delete m_interface; // Remove the interface DestroyNewBar(); // Remove the new bar tracking objects }
Eine vollständige Implementierung dieser Klassen könnte etwa so aussehen:
//+------------------------------------------------------------------+ //| Class for defining a new bar for a specific symbol | //+------------------------------------------------------------------+ class CSymbolNewBarEvent { private: string m_symbol; // Tracked symbol long m_timeFrames[]; // Array of tracked symbol timeframes long m_timeLast[]; // Array of times of the last bars for timeframes bool m_res[]; // Array of flags of a new bar occurrence for timeframes // The method for registering a new tracked timeframe for a symbol int Register(ENUM_TIMEFRAMES p_timeframe) { APPEND(m_timeFrames, p_timeframe); // Add it to the timeframe array APPEND(m_timeLast, 0); // The last time bar for it is still unknown APPEND(m_res, false); // No new bar for it yet Update(); // Update new bar flags return ArraySize(m_timeFrames) - 1; } public: // Constructor CSymbolNewBarEvent(string p_symbol) : m_symbol(p_symbol) // Set a symbol {} // Method for updating new bar flags bool Update() { bool res = (ArraySize(m_res) == 0); FOREACH(m_timeFrames, { // Get the current bar time long time = iTime(m_symbol, (ENUM_TIMEFRAMES) m_timeFrames[i], 0); // If it does not match the saved one, it is a new bar m_res[i] = (time != m_timeLast[i]); res |= m_res[i]; // Save the new time m_timeLast[i] = time; }); return res; } // Method for getting the new bar flag bool IsNewBar(ENUM_TIMEFRAMES p_timeframe) { int index; // Search for the required timeframe index FIND(m_timeFrames, p_timeframe, index); // If not found, then register a new timeframe if(index == -1) { PrintFormat(__FUNCTION__" | Register new event handler for %s %s", m_symbol, EnumToString(p_timeframe)); index = Register(p_timeframe); } // Return the new bar flag for the necessary timeframe return m_res[index]; } }; //+------------------------------------------------------------------+ //| Static class for defining a new bar for all | //| symbols and timeframes | //+------------------------------------------------------------------+ class CNewBarEvent { private: // Array of objects to define a new bar for one symbol static CSymbolNewBarEvent *m_symbolNewBarEvent[]; // Array of required symbols static string m_symbols[]; // Method to register new symbol and timeframe to track a new bar static int Register(string p_symbol) { APPEND(m_symbols, p_symbol); APPEND(m_symbolNewBarEvent, new CSymbolNewBarEvent(p_symbol)); return ArraySize(m_symbols) - 1; } public: // There is no need to create objects of this class - delete the constructor CNewBarEvent() = delete; // Method for updating new bar flags static bool Update() { bool res = (ArraySize(m_symbolNewBarEvent) == 0); FOREACH(m_symbols, res |= m_symbolNewBarEvent[i].Update()); return res; } // Method to free memory for automatically created objects static void Destroy() { FOREACH(m_symbols, delete m_symbolNewBarEvent[i]); ArrayResize(m_symbols, 0); ArrayResize(m_symbolNewBarEvent, 0); } // Method for getting the new bar flag static bool IsNewBar(string p_symbol, ENUM_TIMEFRAMES p_timeframe) { int index; // Search for the required symbol index FIND(m_symbols, p_symbol, index); // If not found, then register a new symbol if(index == -1) index = Register(p_symbol); // Return the new bar flag for the necessary symbol and timeframe return m_symbolNewBarEvent[index].IsNewBar(p_timeframe); } }; // Initialize static members of the CSymbolNewBarEvent class members; CSymbolNewBarEvent* CNewBarEvent::m_symbolNewBarEvent[]; string CNewBarEvent::m_symbols[]; //+------------------------------------------------------------------+ //| Function for checking a new bar occurrence | //+------------------------------------------------------------------+ bool IsNewBar(string p_symbol, ENUM_TIMEFRAMES p_timeframe) { return CNewBarEvent::IsNewBar(p_symbol, p_timeframe); } //+------------------------------------------------------------------+ //| Function for updating information about new bars | //+------------------------------------------------------------------+ bool UpdateNewBar() { return CNewBarEvent::Update(); } //+------------------------------------------------------------------+ //| Function for removing new bar tracking objects | //+------------------------------------------------------------------+ void DestroyNewBar() { CNewBarEvent::Destroy(); } //+------------------------------------------------------------------+
Wir speichern diesen Code in der Datei NewBarEvent.mqh des aktuellen Ordners.
Schauen wir uns nun an, wie diese Bibliothek in einer Handelsstrategie und einem EA angewendet werden kann. Lassen Sie uns jedoch zunächst einige kleinere Anpassungen an der Handelsstrategie vornehmen, die nicht mit der Handhabung eines neuen Balkens zusammenhängen.
Verbesserungen der Handelsstrategie
Leider wurden beim Verfassen dieses Artikels zwei Fehler in der verwendeten Strategie entdeckt. Sie hatten keinen signifikanten Einfluss auf die vorherigen Ergebnisse, aber da sie entdeckt wurden, sollten wir sie korrigieren.
Der erste Fehler ergab sich aus der Tatsache, dass ein negativer Wert für openDistance_ in den Parametern auf eine kleine positive Zahl zurückgesetzt wurde, die der Spanne für das aktuelle Symbol entsprach. Mit anderen Worten: Anstatt BUY STOP und SELL_STOP zu eröffnen, wurden Marktpositionen eröffnet. Dies bedeutete, dass wir während der Optimierung nicht die Ergebnisse sahen, die durch den Handel mit solchen schwebenden Aufträgen hätten erzielt werden können. Das bedeutet, dass wir einige potenziell gewinnbringende Parametersätze verpasst haben.
Der Fehler trat in der folgenden Code-Zeile in den Funktionen zum Öffnen von Pending Orders der Datei SimpleVolumesStrategy.mqh auf:
// Let's make sure that the opening distance is not less than the spread int distance = MathMax(m_openDistance, spread);
Wenn m_openDistance negativ ist, dann ist der Abstandswert der Verschiebung des Eröffnungskurses vom aktuellen Kurs positiv geworden. Um das gleiche Vorzeichen in der Entfernung zu speichern wie in m_openDistance, müssen wir einfach den folgenden Ausdruck damit multiplizieren:
// Let's make sure that the opening distance is not less than the spread int distance = MathMax(MathAbs(m_openDistance), spread) * (m_openDistance < 0 ? -1 : 1);
Der zweite Fehler bestand darin, dass bei der Berechnung des durchschnittlichen Volumens der letzten Balken auch das Volumen des aktuellen Balkens in die Berechnung einbezogen wurde. Obwohl die Strategiebeschreibung besagt, dass wir sie nicht zur Berechnung des Durchschnitts verwenden sollten. Allerdings ist die Auswirkung dieses Fehlers wahrscheinlich auch recht gering. Je länger der Zeitraum der Volumenmittelung ist, desto geringer ist der Beitrag des letzten Balkens zum Durchschnitt.
Um diesen Fehler zu beheben, müssen wir nur die Funktion zur Berechnung des Durchschnitts geringfügig ändern und das allererste Element des übergebenen Arrays ausschließen:
//+------------------------------------------------------------------+ //| Average value of the array of numbers from the second element | //+------------------------------------------------------------------+ double CSimpleVolumesStrategy::ArrayAverage(const double &array[]) { double s = 0; int total = ArraySize(array) - 1; for(int i = 1; i <= total; i++) { s += array[i]; } return s / MathMax(1, total); }
Speichern wir die Änderungen in der Datei SimpleVolumesStrategy.mqh des aktuellen Ordners.
Feststellung eines neuen Balken in der Strategie
Damit einige Aktionen in der Handelsstrategie nur dann ausgeführt werden, wenn ein neuer Balken auftritt, müssen wir diesen Codeblock nur in einem bedingten Operator wie diesem platzieren:
// If a new bar arrived on H1 for the current strategy symbol, then if(IsNewBar(m_symbol, PERIOD_H1)) { // perform the necessary actions ... }
Das Vorhandensein eines solchen Codes in der Strategie führt automatisch zur Registrierung der Verfolgung des neuen Bar-Ereignisses auf H1 und im Strategiesymbol m_symbol.
Wir können die Prüfung auf das Auftreten neuer Balken in anderen zusätzlichen Zeitrahmen leicht hinzufügen. Wenn die Strategie zum Beispiel die Werte einer durchschnittlichen Preisspanne (ATR oder ADR) verwendet, kann ihre Neuberechnung auf einfache Weise nur einmal am Tag auf folgende Weise erfolgen:
// If a new bar arrived on D1 for the current strategy symbol, then if(IsNewBar(m_symbol, PERIOD_H1)) { CalcATR(); // call our ATR calculation function }
Bei der Handelsstrategie, die wir in dieser Artikelserie betrachten, können wir alle Aktionen außerhalb des Zeitpunkts der Ankunft des neuen Balkens vollständig ausschließen:
//+------------------------------------------------------------------+ //| "Tick" event handler function | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::Tick() override { // If there is no new bar on M1, if(!IsNewBar(m_symbol, PERIOD_M1)) return; // If their number is less than allowed if(m_ordersTotal < m_maxCountOfOrders) { // Get an open signal int signal = SignalForOpen(); if(signal == 1 /* || m_ordersTotal < 1 */) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } }
Wir können auch jegliche Verarbeitung im OnTick-Ereignishandler des EA zu den Zeiten der Ticks verbieten, die nicht mit dem Beginn eines neuen Balkens für eines der verwendeten Symbole oder Zeitrahmen übereinstimmen. Um dies zu erreichen, können wir die folgenden Änderungen an der Methode CVirtualAdvisor::Tick() vornehmen:
//+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CVirtualAdvisor::Tick(void) { // Define a new bar for all required symbols and timeframes bool isNewBar = UpdateNewBar(); // If there is no new bar anywhere, and we only work on new bars, then exit if(!isNewBar && m_useOnlyNewBar) { return; } // Receiver handles virtual positions m_receiver.Tick(); // Start handling in strategies CAdvisor::Tick(); // Adjusting market volumes m_receiver.Correct(); // Save status Save(); // Render the interface m_interface.Redraw(); }
In diesem Code haben wir eine neue EA-Eigenschaft m_useOnlyNewBar hinzugefügt, die bei der Erstellung eines EA-Objekts festgelegt werden kann:
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: ... bool m_useOnlyNewBar; // Handle only new bar ticks public: CVirtualAdvisor(ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false); // Constructor ... }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false) : // Initialize the receiver with a static receiver m_receiver(CVirtualReceiver::Instance(p_magic)), // Initialize the interface with the static interface m_interface(CVirtualInterface::Instance(p_magic)), m_lastSaveTime(0), m_useOnlyNewBar(p_useOnlyNewBar) { m_name = StringFormat("%s-%d%s.csv", (p_name != "" ? p_name : "Expert"), p_magic, (MQLInfoInteger(MQL_TESTER) ? ".test" : "") ); };
Wir hätten eine neue EA-Klasse erstellen können, indem wir sie von CVirtualAdvisor geerbt und ihr eine neue Eigenschaft sowie eine neue Überprüfung der Baranwesenheit hinzugefügt hätten. Aber wir können alles so lassen, wie es ist, da mit dem Standardwert für m_useOnlyNewBar = false alles so funktioniert, wie es sollte, ohne dass diese Funktionalität der EA-Klasse hinzugefügt wird.
Wenn wir die EA-Klasse auf diese Weise erweitert haben, können wir innerhalb der Handelsstrategieklasse auf die Überprüfung des neuen Minutenbalkenereignisses innerhalb der Tick()-Methode verzichten. Es reicht aus, die Funktion IsNewBar() einmal im Strategiekonstruktor mit dem aktuellen Symbol und M1-Zeitrahmen aufzurufen, um das Ereignis eines neuen Balkens mit einem solchen Symbol und Zeitrahmen zu verfolgen. In diesem Fall wird der EA mit m_useOnlyNewBar = true einfach keine Tick-Behandlung für Strategie-Instanzen auslösen, wenn es keinen neuen Takt auf M1 gibt:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CSimpleVolumesStrategy::CSimpleVolumesStrategy( ...) : // Initialization list ... { CVirtualReceiver::Get(GetPointer(this), m_orders, m_maxCountOfOrders); // Load the indicator to get tick volumes m_iVolumesHandle = iVolumes(m_symbol, m_timeframe, VOLUME_TICK); // Set the size of the tick volume receiving array and the required addressing ArrayResize(m_volumes, m_signalPeriod); ArraySetAsSeries(m_volumes, true); // Register the event handler for a new bar on the minimum timeframe IsNewBar(m_symbol, PERIOD_M1); } //+------------------------------------------------------------------+ //| "Tick" event handler function | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::Tick() override { // If their number is less than allowed if(m_ordersTotal < m_maxCountOfOrders) { // Get an open signal int signal = SignalForOpen(); if(signal == 1 /* || m_ordersTotal < 1 */) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } }
Speichern wir die Änderungen in der Datei SimpleVolumesStrategy.mqh des aktuellen Ordners.
Ergebnisse
Der EA BenchmarkInstancesExpert.mq5 erhält eine neue Eingabe useOnlyNewBars_, in der wir festlegen, ob er Ticks behandeln soll, die nicht mit dem Beginn eines neuen Balkens übereinstimmen. Bei der Initialisierung des EA übergeben wir den Parameterwert an den Konstruktor des EA:
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ ... input group "::: Other parameters" sinput ulong magic_ = 27183; // - Magic input bool useOnlyNewBars_ = true; // - Work only at bar opening ... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ... // Create an EA handling virtual positions expert = new CVirtualAdvisor(magic_, "SimpleVolumes_BenchmarkInstances", useOnlyNewBars_); ... }
Führen wir einen Test für einen kleinen Zeitraum mit 256 Instanzen von Handelsstrategien im Modus „Jeder Tick basiert auf echten Ticks“ durch - zunächst mit useOnlyNewBars_ = false, dann mit useOnlyNewBars_ = true.
Im ersten Fall, als die EAs bei jedem Tick arbeiteten, betrug der Gewinn 296 USD, der Lauf wurde in 04:15 abgeschlossen. Im zweiten Fall, als der EA alle Ticks außer denen zu Beginn eines neuen Balkens übersprungen hat, betrug der Gewinn 434 USD, der Durchlauf wurde um 00:25 Uhr abgeschlossen. Wir haben also nicht nur die Rechenkosten um das Zehnfache gesenkt, sondern im zweiten Fall auch einen etwas höheren Gewinn erzielt.
Aber wir sollten hier nicht zu optimistisch sein. Bei anderen Handelsstrategien ist die Wiederholung ähnlicher Ergebnisse keineswegs garantiert. Jede Handelsstrategie sollte separat geprüft werden.
Schlussfolgerung
Werfen wir noch einmal einen Blick auf die erzielten Ergebnisse. Wir haben die Leistung des EAs getestet, als eine ziemlich große Anzahl von Handelsstrategie-Instanzen gleichzeitig ausgeführt wurde. Dies eröffnet Perspektiven für eine gute Diversifizierung des Handels über verschiedene Symbole, Zeitrahmen und Handelsstrategien hinweg, da wir in der Lage sein werden, diese in einem EA zu kombinieren.
Außerdem haben wir unsere Klassenbibliothek um eine neue Funktionalität erweitert: die Möglichkeit, neue Bar-Ereignisse zu verfolgen. Obwohl wir diese Funktion in der hier betrachteten Strategie nicht wirklich benötigen, kann ihr Vorhandensein für die Umsetzung anderer Handelsstrategien sehr nützlich sein. Die Möglichkeit, den Betrieb des EA auf den Beginn eines neuen Balkens zu beschränken, kann außerdem dazu beitragen, die Rechenkosten zu senken und ähnlichere Ergebnisse für Tests in verschiedenen Tick-Simulationsmodi zu erzielen.
Aber auch hier sind wir ein wenig vom geplanten Projektverlauf abgewichen. Nun, auch das kann uns helfen, unser Endziel zu erreichen. Nach einer kleinen Pause wollen wir versuchen, den Weg der Automatisierung der EA-Tests mit neuem Elan fortzusetzen. Es scheint, dass es an der Zeit ist, zur Initialisierung von Instanzen von Handelsstrategien unter Verwendung von String-Konstanten zurückzukehren und ein System zur Speicherung von Optimierungsergebnissen aufzubauen.
Vielen Dank für Ihre Aufmerksamkeit! Bis bald!
Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/14574
- 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.