English Русский 日本語 Português
preview
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 8): Belastungstest und Handhabung eines neuen Balkens

Entwicklung eines Expertenberaters für mehrere Währungen (Teil 8): Belastungstest und Handhabung eines neuen Balkens

MetaTrader 5Tester | 11 September 2024, 09:56
35 0
Yuriy Bykov
Yuriy Bykov

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

Beigefügte Dateien |
NewBarEvent.mqh (11.52 KB)
VirtualAdvisor.mqh (14.64 KB)
VirtualOrder.mqh (39.52 KB)
Brain Storm Optimierungsalgorithmus (Teil I): Clustering Brain Storm Optimierungsalgorithmus (Teil I): Clustering
In diesem Artikel befassen wir uns mit einer innovativen Optimierungsmethode namens BSO (Brain Storm Optimization), die von einem natürlichen Phänomen namens „Brainstorming“ inspiriert ist. Wir werden auch einen neuen Ansatz zur Lösung von multimodalen Optimierungsproblemen diskutieren, den die BSO-Methode anwendet. Es ermöglicht die Suche nach mehreren optimalen Lösungen, ohne dass die Anzahl der Teilpopulationen vorher festgelegt werden muss. Wir werden auch die Clustermethoden K-Means und K-Means++ betrachten.
Neuronale Netze leicht gemacht (Teil 83): Der „Conformer“-Algorithmus für räumlich-zeitliche kontinuierliche Aufmerksamkeitstransformation Neuronale Netze leicht gemacht (Teil 83): Der „Conformer“-Algorithmus für räumlich-zeitliche kontinuierliche Aufmerksamkeitstransformation
In diesem Artikel wird der Conformer-Algorithmus vorgestellt, der ursprünglich für die Wettervorhersage entwickelt wurde, die in Bezug auf Variabilität und Launenhaftigkeit mit den Finanzmärkten verglichen werden kann. Conformer ist eine komplexe Methode. Es kombiniert die Vorteile von Aufmerksamkeitsmodellen und gewöhnlichen Differentialgleichungen.
Neuronale Netze leicht gemacht (Teil 84): Umkehrbare Normalisierung (RevIN) Neuronale Netze leicht gemacht (Teil 84): Umkehrbare Normalisierung (RevIN)
Wir wissen bereits, dass die Vorverarbeitung der Eingabedaten eine wichtige Rolle für die Stabilität der Modellbildung spielt. Für die Online-Verarbeitung von „rohen“ Eingabedaten verwenden wir häufig eine Batch-Normalisierungsschicht. Aber manchmal brauchen wir ein umgekehrtes Verfahren. In diesem Artikel wird einer der möglichen Ansätze zur Lösung dieses Problems erörtert.
Neuronale Netze leicht gemacht (Teil 82): Modelle für gewöhnliche Differentialgleichungen (NeuralODE) Neuronale Netze leicht gemacht (Teil 82): Modelle für gewöhnliche Differentialgleichungen (NeuralODE)
In diesem Artikel werden wir eine andere Art von Modellen erörtern, die auf die Untersuchung der Dynamik des Umgebungszustands abzielen.