English Русский 日本語 Português
preview
Entwicklung eines Expertenberaters für mehrere Währungen (Teil 9): Sammeln von Optimierungsergebnissen für einzelne Handelsstrategie-Instanzen

Entwicklung eines Expertenberaters für mehrere Währungen (Teil 9): Sammeln von Optimierungsergebnissen für einzelne Handelsstrategie-Instanzen

MetaTrader 5Tester | 16 September 2024, 10:40
22 0
Yuriy Bykov
Yuriy Bykov

Einführung

In den vorangegangenen Artikeln haben wir bereits eine Menge interessanter Dinge umgesetzt. Wir haben eine Handelsstrategie oder mehrere Handelsstrategien, die wir in den EA implementieren können. Außerdem haben wir eine Struktur für die Verbindung vieler Instanzen von Handelsstrategien in einem einzigen EA entwickelt, Werkzeuge für die Verwaltung des maximal zulässigen Drawdowns hinzugefügt, Möglichkeiten für die automatische Auswahl von Strategieparametersätzen für ihre beste Arbeit in einer Gruppe untersucht und gelernt, wie man einen EA aus Gruppen von Strategieinstanzen und sogar aus Gruppen verschiedener Gruppen von Strategieinstanzen zusammenstellt. Der Wert der bereits erzielten Ergebnisse wird jedoch erheblich steigen, wenn es uns gelingt, sie miteinander zu kombinieren.

Versuchen wir, eine allgemeine Struktur im Rahmen des Artikels zu skizzieren: Einzelne Handelsstrategien werden in den Input eingespeist, während der Output ein fertiger EA ist, der ausgewählte und gruppierte Kopien der ursprünglichen Handelsstrategien verwendet, die die besten Handelsergebnisse liefern.

Nachdem wir einen groben Fahrplan erstellt haben, wollen wir uns einige Abschnitte davon genauer ansehen, analysieren, was wir für die Umsetzung der ausgewählten Etappe benötigen, und uns dann an die eigentliche Umsetzung machen.


Wichtigste Etappen

Lassen Sie uns die wichtigsten Phasen auflisten, die wir bei der Entwicklung des EA durchlaufen müssen:

  1. Umsetzung einer Handelsstrategie. Wir entwickeln die von CVirtualStrategy abgeleitete Klasse, die die Handelslogik der Eröffnung, Aufrechterhaltung und Schließung virtueller Positionen und Aufträge implementiert. Das haben wir in den ersten vier Teilen der Serie getan.

  2. Optimierung der Handelsstrategie. Wir wählen gute Inputs für eine Handelsstrategie aus, die bemerkenswerte Ergebnisse zeigen. Wenn keine gefunden werden, kehren wir zu Punkt 1 zurück.
    In der Regel ist es für uns bequemer, die Optimierung für ein Symbol und einen Zeitrahmen durchzuführen. Bei der genetischen Optimierung müssen wir sie höchstwahrscheinlich mehrmals mit verschiedenen Optimierungskriterien, einschließlich einiger unserer eigenen, durchführen. Eine Brute-Force-Optimierung ist nur bei Strategien mit einer sehr geringen Anzahl von Parametern möglich. Selbst in unserer Modellstrategie ist eine erschöpfende Suche zu aufwendig. Wenn wir also über Optimierung sprechen, sollten wir weitergehen.
    Ich werde die genetische Optimierung in den MetaTrader 5 Strategietester einbauen. Der Optimierungsprozess wurde in den Artikeln nicht detailliert beschrieben, da er ziemlich standardisiert ist.

  3. Clustering von Gruppen. Dieser Schritt ist nicht zwingend erforderlich, spart aber Zeit für den nächsten Schritt. Hier reduzieren wir die Anzahl der Parametersätze von Handelsstrategie-Instanzen erheblich, aus denen wir geeignete Gruppen auswählen. Dies wird im sechsten Teil beschrieben.

  4. Auswahl von Gruppen von Parametersätzen. Wir führen auf der Grundlage der Ergebnisse der vorangegangenen Phase eine Optimierung durch, bei der wir
    die am besten kompatiblen Parametersätze der Handelsstrategie-Instanzen auswählen, die die besten Ergebnisse liefern. Auch dies wird hauptsächlich im sechsten und siebten Teil beschrieben.

  5. Auswahl von Gruppen aus Gruppen von Parametersätzen. Wir kombinieren nun die Ergebnisse der vorangegangenen Stufe in Gruppen nach dem gleichen Prinzip wie bei der Kombination der Sätze von Einzelinstanzparametern.

  6. Iteration durch Symbole und Zeitrahmen. Wir wiederholen die Schritte 2 - 5 für alle gewünschten Symbole und Zeitrahmen. Vielleicht ist es möglich, zusätzlich zu einem Symbol und einem Zeitrahmen eine separate Optimierung für bestimmte Klassen anderer Inputs für einige Handelsstrategien durchzuführen. 

  7. Andere Strategien. Wenn Sie andere Handelsstrategien im Sinn haben, wiederholen Sie die Schritte 1 bis 6 für jede dieser Strategien.

  8. Zusammenbau des EA. Wir fassen alle besten Gruppen, die für verschiedene Handelsstrategien, Symbole, Zeitrahmen und andere Parameter gefunden wurden, in einem endgültigen EA zusammen.

Jede Stufe erzeugt nach Abschluss einige Daten, die gespeichert und in den nächsten Stufen verwendet werden müssen. Bisher haben wir provisorische, improvisierte Mittel verwendet, die bequem genug sind, um sie ein- oder zweimal zu nutzen, aber nicht besonders bequem für den wiederholten Gebrauch.

Zum Beispiel haben wir die Optimierungsergebnisse nach der zweiten Stufe in einer Excel-Datei gespeichert, dann die fehlenden Spalten manuell ergänzt und sie dann, nachdem wir sie als CSV-Datei gespeichert hatten, in der dritten Stufe verwendet.

Die Ergebnisse der dritten Stufe haben wir entweder direkt aus der Oberfläche des Strategietesters übernommen oder sie noch einmal in Excel-Dateien gespeichert, dort weiterverarbeitet und dann wieder die Ergebnisse aus der Tester-Interface verwendet.

Wir haben die fünfte Stufe nicht tatsächlich durchgeführt, sondern nur die Möglichkeit ihrer Durchführung vermerkt. Deshalb kam es nie zur Verwirklichung.

Für all diese empfangenen Daten möchten wir eine einzige Speicher- und Verwendungsstruktur einrichten. 


Optionen für die Umsetzung

Die wichtigsten Daten, die wir speichern und verwenden müssen, sind die Optimierungsergebnisse mehrerer EAs. Wie Sie wissen, zeichnet der Strategietester alle Optimierungsergebnisse in einer separaten Cache-Datei mit der Endung *.opt auf, die dann im Tester oder sogar im Tester eines anderen MetaTrader 5 Terminals wieder geöffnet werden kann. Der Dateiname wird aus dem Hash ermittelt, der aus dem Namen des optimierten EA und den Optimierungsparametern berechnet wird. Auf diese Weise gehen keine Informationen über die bereits erfolgten Durchgänge (passes) verloren, wenn die Optimierung nach einer vorzeitigen Unterbrechung oder nach einer Änderung des Optimierungskriteriums fortgesetzt wird.

Daher ist eine der in Betracht gezogenen Optionen die Verwendung von Optimierungs-Cache-Dateien zur Speicherung von Zwischenergebnissen. Es gibt eine gute Bibliothek von fxsaber, die es uns ermöglicht, auf alle gespeicherten Informationen von MQL5-Programmen zuzugreifen.

Mit zunehmender Anzahl der durchgeführten Optimierungen steigt jedoch auch die Anzahl der Dateien mit ihren Ergebnissen. Um nicht durcheinander zu kommen, müssen wir uns eine zusätzliche Struktur für die Speicherung und die Arbeit mit diesen Cache-Dateien einfallen lassen. Wenn die Optimierung nicht auf einem Server durchgeführt wird, ist eine Synchronisierung oder Speicherung aller Cache-Dateien an einem Ort erforderlich. Darüber hinaus benötigen wir für die nächste Stufe noch einige Verarbeitungsschritte, um die erzielten Optimierungsergebnisse in den EA der nächsten Stufe zu exportieren.

Dann wollen wir uns die Speicherung aller Ergebnisse in der Datenbank ansehen. Auf den ersten Blick würde die Umsetzung dieses Vorhabens viel Zeit in Anspruch nehmen. Diese Arbeit kann jedoch in kleinere Schritte unterteilt werden, und wir können die Ergebnisse sofort nutzen, ohne auf die vollständige Umsetzung zu warten. Dieser Ansatz ermöglicht auch eine größere Freiheit bei der Wahl des geeignetsten Mittels zur Zwischenverarbeitung der gespeicherten Ergebnisse. Zum Beispiel können wir einen Teil der Verarbeitung einfachen SQL-Abfragen zuweisen, etwas wird in MQL5 berechnet, und etwas in Python- oder R-Programmen. Wir können verschiedene Verarbeitungsmöglichkeiten ausprobieren und die am besten geeignete auswählen.

MQL5 bietet integrierte Funktionen für die Arbeit mit der SQLite-Datenbank. Es gab auch Implementierungen von Bibliotheken von Drittanbietern, die z. B. die Arbeit mit MySQL ermöglichen. Es ist noch nicht klar, ob die Fähigkeiten von SQLite für uns ausreichen werden, aber höchstwahrscheinlich wird diese Datenbank für unsere Bedürfnisse ausreichend sein. Wenn dies nicht ausreicht, werden wir über eine Migration zu einem anderen DBMS nachdenken.


Beginnen wir mit dem Entwurf der Datenbank

Zunächst müssen wir die Entitäten identifizieren, deren Informationen wir speichern wollen. Ein Testlauf ist natürlich einer davon. Die Felder dieser Entität umfassen Testeingabedatenfelder und Testergebnisfelder. Im Allgemeinen können sie als separate Einheiten unterschieden werden. Das Wesentliche der Eingabedaten kann in noch kleinere Einheiten zerlegt werden: den EA, die Optimierungseinstellungen und die EA-Single-Pass-Parameter. Doch lassen wir uns weiterhin vom Grundsatz des geringsten Aufwands leiten. Für den Anfang reicht eine Tabelle mit Feldern für die Durchgangs-Ergebnisse, wie wir sie in früheren Artikeln verwendet haben, und ein oder zwei Textfelder, in denen die notwendigen Informationen über die Durchgangs-Inputs untergebracht werden.

Eine solche Tabelle kann mit der folgenden SQL-Abfrage erstellt werden:

CREATE TABLE passes (
    id                    INTEGER  PRIMARY KEY AUTOINCREMENT,
    pass                  INT,	-- pass index

    inputs                TEXT, -- pass input values
    params                TEXT, -- additional pass data

    initial_deposit       REAL, -- pass results...
    withdrawal            REAL,
    profit                REAL,
    gross_profit          REAL,
    gross_loss            REAL,
    max_profittrade       REAL,
    max_losstrade         REAL,
    conprofitmax          REAL,
    conprofitmax_trades   REAL,
    max_conwins           REAL,
    max_conprofit_trades  REAL,
    conlossmax            REAL,
    conlossmax_trades     REAL,
    max_conlosses         REAL,
    max_conloss_trades    REAL,
    balancemin            REAL,
    balance_dd            REAL,
    balancedd_percent     REAL,
    balance_ddrel_percent REAL,
    balance_dd_relative   REAL,
    equitymin             REAL,
    equity_dd             REAL,
    equitydd_percent      REAL,
    equity_ddrel_percent  REAL,
    equity_dd_relative    REAL,
    expected_payoff       REAL,
    profit_factor         REAL,
    recovery_factor       REAL,
    sharpe_ratio          REAL,
    min_marginlevel       REAL,
    deals                 REAL,
    trades                REAL,
    profit_trades         REAL,
    loss_trades           REAL,
    short_trades          REAL,
    long_trades           REAL,
    profit_shorttrades    REAL,
    profit_longtrades     REAL,
    profittrades_avgcon   REAL,
    losstrades_avgcon     REAL,
    complex_criterion     REAL,
    custom_ontester       REAL,
    pass_date             DATETIME DEFAULT (datetime('now') ) 
                                   NOT NULL
);

Erstellen wir die Hilfsklasse CDatabase, die Methoden für die Arbeit mit der Datenbank enthalten wird. Wir können es statisch machen, denn wir brauchen nicht viele Instanzen in einem Programm, eine einzige reicht aus. Da wir derzeit planen, alle Informationen in einer Datenbank zu sammeln, können wir den Namen der Datenbankdatei im Quellcode fest vorgeben. 

Diese Klasse enthält das Feld s_db zum Speichern des offenen Datenbank-Handles. Die Methode Open() zum Öffnen der Datenbank setzt diesen Wert. Wenn die Datenbank zum Zeitpunkt des Öffnens noch nicht erstellt wurde, wird sie durch Aufruf der Methode Create() erstellt. Nach dem Öffnen können wir einzelne SQL-Abfragen an die Datenbank mit der Methode Execute() oder Massen-SQL-Abfragen in einer einzigen Transaktion mit der Methode ExecuteTransaction() ausführen. Zum Schluss schließen wir die Datenbank mit der Methode Close().

Wir können auch ein kurzes Makro deklarieren, das es uns ermöglicht, den langen Klassennamen CDatabase durch den kürzeren DB zu ersetzen.

#define DB CDatabase

//+------------------------------------------------------------------+
//| Class for handling the database                                  |
//+------------------------------------------------------------------+
class CDatabase {
   static int        s_db;          // DB connection handle
   static string     s_fileName;    // DB file name
public:
   static bool       IsOpen();      // Is the DB open?

   static void       Create();      // Create an empty DB
   static void       Open();        // Opening DB
   static void       Close();       // Closing DB

   // Execute one query to the DB
   static bool       Execute(string &query);

   // Execute multiple DB queries in one transaction
   static bool       ExecuteTransaction(string &queries[]);
};

int    CDatabase::s_db       =  INVALID_HANDLE;
string CDatabase::s_fileName = "database.sqlite";


Bei der Methode zur Erstellung der Datenbank erstellen wir einfach ein Array mit SQL-Abfragen zur Erstellung von Tabellen und führen sie in einer Transaktion aus:

//+------------------------------------------------------------------+
//| Create an empty DB                                               |
//+------------------------------------------------------------------+
void CDatabase::Create() {
   // Array of DB creation requests 
   string queries[] = {
      "DROP TABLE IF EXISTS passes;",

      "CREATE TABLE passes ("
      "id                    INTEGER  PRIMARY KEY AUTOINCREMENT,"
      "pass                  INT,"
      "inputs                TEXT,"
      "params                TEXT,"
      "initial_deposit       REAL,"
      "withdrawal            REAL,"
      "profit                REAL,"
      "gross_profit          REAL,"
      "gross_loss            REAL,"
      ...
      "pass_date             DATETIME DEFAULT (datetime('now') ) NOT NULL"
      ");"
      ,
   };

   // Execute all requests
   ExecuteTransaction(queries);
}


Die Methode zum Öffnen der Datenbank „open“ versucht zunächst, eine vorhandene Datenbankdatei zu öffnen. Wenn sie nicht vorhanden ist, wird sie erstellt und geöffnet. Danach wird die Datenbankstruktur durch Aufruf der Methode Create() erstellt: 

//+------------------------------------------------------------------+
//| Is the DB open?                                                  |
//+------------------------------------------------------------------+
bool CDatabase::IsOpen() {
   return (s_db != INVALID_HANDLE);
}
...

//+------------------------------------------------------------------+
//| Open DB                                                          |
//+------------------------------------------------------------------+
void CDatabase::Open() {
// Try to open an existing DB file
   s_db = DatabaseOpen(s_fileName, DATABASE_OPEN_READWRITE | DATABASE_OPEN_COMMON);

// If the DB file is not found, try to create it when opening
   if(!IsOpen()) {
      s_db = DatabaseOpen(s_fileName,
                          DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE |
                          DATABASE_OPEN_COMMON);

      // Report an error in case of failure
      if(!IsOpen()) {
         PrintFormat(__FUNCTION__" | ERROR: %s open failed with code %d",
                     s_fileName, GetLastError());
         return;
      }

      // Create the database structure
      Create();
   }
   PrintFormat(__FUNCTION__" | Database %s opened successfully", s_fileName);
}


Bei der Methode zur Ausführung mehrerer Abfragen durch ExecuteTransaction() erstellen wir eine Transaktion und beginnen mit der Ausführung aller SQL-Abfragen in einer Schleife, eine nach der anderen. Wenn bei der Ausführung der nächsten Anfrage ein Fehler auftritt, unterbrechen wir die Schleife, melden den Fehler und brechen alle vorherigen Anfragen innerhalb dieser Transaktion ab. Wenn keine Fehler auftreten, bestätigen wir die Transaktion:

//+------------------------------------------------------------------+
//| Execute multiple DB queries in one transaction                   |
//+------------------------------------------------------------------+
bool CDatabase::ExecuteTransaction(string &queries[]) {
// Open a transaction
   DatabaseTransactionBegin(s_db);

   bool res = true;
// Send all execution requests
   FOREACH(queries, {
      res &= Execute(queries[i]);
      if(!res) break;
   });

// If an error occurred in any request, then
   if(!res) {
      // Report it
      PrintFormat(__FUNCTION__" | ERROR: Transaction failed, error code=%d", GetLastError());
      // Cancel transaction
      DatabaseTransactionRollback(s_db);
   } else {
      // Otherwise, confirm transaction
      DatabaseTransactionCommit(s_db);
      PrintFormat(__FUNCTION__" | Transaction done successfully");
   }
   return res;
}


Wir speichern die Änderungen in der Datei Database.mqh des aktuellen Ordners.


Modifizierung des EA zur Erfassung von Optimierungsdaten

Wenn nur Agenten auf dem lokalen Computer im Optimierungsprozess verwendet werden, können wir das Speichern der Durchgangsergebnisse in der Datenbank entweder in OnTester() oder OnDeinit() veranlassen. Bei der Verwendung von Agenten in einem lokalen Netzwerk oder im MQL5-Cloud-Netzwerk wird es sehr schwierig sein, wenn überhaupt, die Ergebnisse zu speichern. Glücklicherweise bietet MQL5 eine großartige Standardmethode, um beliebige Informationen von Testagenten zu erhalten, egal wo sie sich befinden, indem Datenrahmen erstellt, gesendet und empfangen werden.

Dieser Mechanismus ist in der Referenz und im AlgoBook hinreichend detailliert beschrieben. Um sie zu verwenden, müssen wir drei zusätzliche Ereignisbehandlungen zu den optimierten hinzufügen: OnTesterInit(), OnTesterPass() und OnTesterDeinit()

Die Optimierung wird immer von einem MetaTrader 5-Terminal aus gestartet, das wir im Folgenden bedingt als Hauptterminal bezeichnen werden. Wenn ein EA mit solchen Ereignisbehandlungen vom Hauptterminal aus zur Optimierung gestartet wird, wird ein neues Chart im Hauptterminal geöffnet und eine weitere Instanz des EA auf diesem Chart gestartet, bevor die EA-Instanzen an Testagenten verteilt werden, um normale Optimierungsdurchläufe mit verschiedenen Parametersätzen durchzuführen.

Diese Instanz wird in einem speziellen Modus gestartet: Die Standardfunktionen OnInit(), OnTick() und OnDeinit() werden dabei nicht ausgeführt. Stattdessen werden nur diese drei neuen Handler ausgeführt. Dieser Modus hat sogar einen eigenen Namen - der Modus des Sammelns von Daten der Optimierungsergebnisse. Falls erforderlich, können wir in den EA-Funktionen überprüfen, ob der EA in diesem Modus läuft, indem wir die Funktion MQLInfoInteger() wie folgt aufrufen:

// Check if the EA is running in data frame collection mode 
bool isFrameMode = MQLInfoInteger(MQL_FRAME_MODE); 

Wie die Namen schon andeuten, wird OnTesterInit() im Frame-Collection-Modus einmal vor der Optimierung ausgeführt, OnTesterPass() wird jedes Mal ausgeführt, wenn einer der Testagenten seinen Durchlauf beendet hat, während OnTesterDeinit() einmal ausgeführt wird, nachdem alle geplanten Optimierungsdurchläufe abgeschlossen sind oder wenn die Optimierung unterbrochen wird.

Die EA-Instanz, die auf dem Hauptterminalchart im Frame-Sammelmodus gestartet wird, ist für das Sammeln von Datenframes von allen Testagenten zuständig. Datenrahmen („Data frame“) ist nur ein bequemer Name, um den Datenaustausch zwischen Testagenten und dem EA im Hauptterminal zu beschreiben. Er bezeichnet einen Datensatz mit einem Namen und einer numerischen ID, den der Testagent erstellt und nach Abschluss eines einzelnen Optimierungsdurchgangs an das Hauptterminal gesendet hat. 

Es ist zu beachten, dass es sinnvoll ist, Datenrahmen nur in den EA-Instanzen zu erstellen, die im normalen Modus auf den Testagenten arbeiten, und Datenrahmen nur in der EA-Instanz im Hauptterminal zu sammeln und zu verarbeiten, die im Rahmensammelmodus arbeitet. Beginnen wir also mit dem Erstellung der „Frames“ bzw. Rahmen.

Wir können die Erstellung von Frames im EA in OnTester() oder in einer beliebigen Funktion oder Methode platzieren, die von OnTester() aus aufgerufen wird. Die Ereignisbehandlung wird nach Abschluss des Durchlaufs gestartet, und wir können in ihm die Werte aller statistischen Merkmale des abgeschlossenen Durchlaufs abrufen und ggf. den Wert des Nutzerkriteriums für die Bewertung der Durchlaufergebnisse berechnen.

Wir haben derzeit den Code, der ein nutzerdefiniertes Kriterium berechnet, das den voraussichtlichen Gewinn anzeigt, der bei einem maximal erreichbaren Drawdown von 10 % erzielt werden könnte:

//+------------------------------------------------------------------+
//| Test results                                                     |
//+------------------------------------------------------------------+
double OnTester(void) {
// Maximum absolute drawdown
   double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD);

// Profit
   double profit = TesterStatistics(STAT_PROFIT);

// The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_
   double coeff = fixedBalance_ * 0.1 / balanceDrawdown;

// Recalculate the profit
   double fittedProfit = profit * coeff;

   return fittedProfit;
}


Verschieben wir diesen Code aus der EA-Datei SimpleVolumesExpertSingle.mq5 in die neue Methodenklasse CVirtualAdvisor, während der EA mit der Rückgabe des Ergebnisses des Methodenaufrufs beschäftigt ist:

//+------------------------------------------------------------------+
//| Test results                                                     |
//+------------------------------------------------------------------+
double OnTester(void) {
   return expert.Tester();
}


Beim Verschieben sollten wir bedenken, dass wir die Variable fixedBalance_ innerhalb der Methode nicht mehr verwenden können, da sie in einem anderen EA möglicherweise nicht mehr vorhanden ist. Sein Wert kann jedoch von der statischen Klasse CMoney durch Aufruf der Methode CMoney::FixedBalance() ermittelt werden. Auf dem Weg dorthin werden wir eine weitere Änderung an der Berechnung unseres Nutzerkriteriums vornehmen. Nachdem wir den voraussichtlichen Gewinn ermittelt haben, berechnen wir ihn pro Zeiteinheit neu, zum Beispiel den Gewinn pro Jahr. Auf diese Weise können wir die Ergebnisse von Durchgängen über unterschiedlich lange Zeiträume grob vergleichen.

Dazu müssen wir uns das Startdatum des Tests im EA merken. Fügen wir die neue Eigenschaft m_fromDate hinzu, die dazu dient, die aktuelle Zeit im Konstruktor des EA-Objekts zu speichern.

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...

   datetime          m_fromDate;

public:
   ...
   virtual double    Tester() override;         // OnTester event handler
   ...
};


//+------------------------------------------------------------------+
//| OnTester event handler                                           |
//+------------------------------------------------------------------+
double CVirtualAdvisor::Tester() {
// Maximum absolute drawdown
   double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD);

// Profit
   double profit = TesterStatistics(STAT_PROFIT);

// The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_
   double coeff = CMoney::FixedBalance() * 0.1 / balanceDrawdown;

// Calculate the profit in annual terms
   long totalSeconds = TimeCurrent() - m_fromDate;
   double fittedProfit = profit * coeff * 365 * 24 * 3600 / totalSeconds ;

// Perform data frame generation on the test agent
   CTesterHandler::Tester(fittedProfit,
                          ~((CVirtualStrategy *) m_strategies[0]));

   return fittedProfit;
}


Später werden wir vielleicht mehrere nutzerdefinierte Optimierungskriterien erstellen, und dann wird dieser Code wieder an eine neue Stelle verschoben werden. Aber lassen wir uns jetzt nicht von dem umfangreichen Thema der Untersuchung verschiedener Fitnessfunktionen zur Optimierung von EAs ablenken und lassen wir den Code so, wie er ist.

Die EA-Datei SimpleVolumesExpertSingle.mq5 erhält nun neue Ereignisbehandlungen durch OnTesterInit(), OnTesterPass() und OnTesterDeinit(). Da die Logik dieser Funktionen nach unserem Plan für alle EAs gleich sein soll, werden wir ihre Implementierung zunächst auf die EA-Ebene (CVirtualAdvisor-Klassenobjekt) herunterschrauben.

Es ist zu beachten, dass die Funktion OnInit(), in der die EA-Instanz erstellt wird, nicht ausgeführt wird, wenn der EA im Hauptterminal im Frame-Sammelmodus gestartet wird. Um die Erstellung/Löschung einer EA-Instanz nicht zu neuen Ereignisbehandlungen hinzuzufügen, sollten die Methoden zur Behandlung dieser Ereignisse in der Klasse CVirtualAdvisor statisch sein. Dann müssen wir den folgenden Code in den EA einfügen:  

//+------------------------------------------------------------------+
//| Initialization before starting optimization                      |
//+------------------------------------------------------------------+
int OnTesterInit(void) {
   return CVirtualAdvisor::TesterInit();
}

//+------------------------------------------------------------------+
//| Actions after completing the next optimization pass              |
//+------------------------------------------------------------------+
void OnTesterPass() {
   CVirtualAdvisor::TesterPass();
}

//+------------------------------------------------------------------+
//| Actions after optimization is complete                           |
//+------------------------------------------------------------------+
void OnTesterDeinit(void) {
   CVirtualAdvisor::TesterDeinit();
}


Eine weitere Änderung, die wir für die Zukunft vornehmen können, ist die Abschaffung des separaten Aufrufs der Methode CVirtualAdvisor::Add() zum Hinzufügen von Handelsstrategien zum EA, nachdem dieser erstellt wurde. Stattdessen werden wir die Informationen über die Strategien sofort an den Konstruktor des EA übertragen, während dieser die Methode Add() selbständig aufruft. Dann kann diese Methode aus dem öffentlichen Teil entfernt werden.

Mit diesem Ansatz sieht die EA-Initialisierungsfunktion OnInit() wie folgt aus:

int OnInit() {
   CMoney::FixedBalance(fixedBalance_);

// Create an EA handling virtual positions
   expert = new CVirtualAdvisor(
      new CSimpleVolumesStrategy(
         symbol_, timeframe_,
         signalPeriod_, signalDeviation_, signaAddlDeviation_,
         openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
         maxCountOfOrders_, 0), // One strategy instance
      magic_, "SimpleVolumesSingle", true);

   return(INIT_SUCCEEDED);
}

Wir speichern die Änderungen in der Datei SimpleVolumesExpertSingle.mq5 des aktuellen Ordners.

Ändern der EA-Klasse

Um ein Überladen der EA-Klasse CVirtualAdvisor zu vermeiden, verschieben wir den Code der Ereignisbehandler TesterInit, TesterPass und OnTesterDeinit in die separate Klasse CTesterHandler, in der wir statische Methoden zur Behandlung der einzelnen Ereignisse erstellen. In diesem Fall müssen wir der Klasse CVirtualAdvisor ungefähr den gleichen Code wie in der Haupt-EA-Datei hinzufügen:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
   ...

public:
   ...
   static int        TesterInit();     // OnTesterInit event handler
   static void       TesterPass();     // OnTesterDeinit event handler
   static void       TesterDeinit();   // OnTesterDeinit event handler
};


//+------------------------------------------------------------------+
//| Initialization before starting optimization                      |
//+------------------------------------------------------------------+
int CVirtualAdvisor::TesterInit() {
   return CTesterHandler::TesterInit();
}

//+------------------------------------------------------------------+
//| Actions after completing the next optimization pass              |
//+------------------------------------------------------------------+
void CVirtualAdvisor::TesterPass() {
   CTesterHandler::TesterPass();
}

//+------------------------------------------------------------------+
//| Actions after optimization is complete                           |
//+------------------------------------------------------------------+
void CVirtualAdvisor::TesterDeinit() {
   CTesterHandler::TesterDeinit();
}


Wir lassen uns auch einige Ergänzungen am Code des EA-Objektkonstruktors vornehmen. Verschiebung aller Aktionen aus dem Konstruktor in die neue Initialisierungsmethode Init() mit Blick auf zukünftige Verbesserungen. Dies ermöglicht es uns, mehrere Konstruktoren mit verschiedenen Parametersätzen hinzuzufügen, die alle dieselbe Initialisierungsmethode verwenden, nachdem die Parameter ein wenig vorverarbeitet wurden.

Fügen wir Konstruktoren hinzu, deren erstes Argument entweder ein Strategie-Objekt oder ein Strategie-Gruppen-Objekt sein wird. Dann können wir dem EA direkt im Konstruktor Strategien hinzufügen. In diesem Fall brauchen wir die Methode Add() in der EA-Funktion OnInit() nicht mehr aufzurufen.

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   ...

   datetime          m_fromDate;

public:
                     CVirtualAdvisor(CVirtualStrategy *p_strategy, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false); // Constructor
                     CVirtualAdvisor(CVirtualStrategyGroup *p_group, ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false); // Constructor
   void              CVirtualAdvisor::Init(CVirtualStrategyGroup *p_group,
                                           ulong p_magic = 1,
                                           string p_name = "",
                                           bool p_useOnlyNewBar = false
                                          );
   ...
};

...

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(CVirtualStrategy *p_strategy,
                                 ulong p_magic = 1,
                                 string p_name = "",
                                 bool p_useOnlyNewBar = false
                                ) {
   CVirtualStrategy *strategies[] = {p_strategy};
   Init(new CVirtualStrategyGroup(strategies), p_magic, p_name, p_useOnlyNewBar);
};

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(CVirtualStrategyGroup *p_group,
                                 ulong p_magic = 1,
                                 string p_name = "",
                                 bool p_useOnlyNewBar = false
                                ) {
   Init(p_group, p_magic, p_name, p_useOnlyNewBar);
};

//+------------------------------------------------------------------+
//| EA initialization method                                         |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Init(CVirtualStrategyGroup *p_group,
                           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" : "")
                        );

   m_fromDate = TimeCurrent();

   Add(p_group);
   delete p_group;
};

Wir speichern die Änderungen in der VirtualExpert.mqh des aktuellen Ordners.


Klasse zur Behandlung von Optimierungsereignissen

Konzentrieren wir uns nun direkt auf die Durchführung von Aktionen, die vor dem Start, nach Abschluss des Durchlaufs und nach Abschluss der Optimierung durchgeführt werden. Wir erstellen die Klasse CTesterHandler und fügen ihr Methoden zur Behandlung der erforderlichen Ereignisse sowie einige Hilfsmethoden hinzu, die im geschlossenen Teil der Klasse untergebracht sind: 

//+------------------------------------------------------------------+
//| Optimization event handling class                                |
//+------------------------------------------------------------------+
class CTesterHandler {
   static string     s_fileName;                   // File name for writing frame data
   static void       ProcessFrames();              // Handle incoming frames
   static string     GetFrameInputs(ulong pass);   // Get pass inputs
public:
   static int        TesterInit();     // Handle the optimization start in the main terminal
   static void       TesterDeinit();   // Handle the optimization completion in the main terminal
   static void       TesterPass();     // Handle the completion of a pass on an agent in the main terminal

   static void       Tester(const double OnTesterValue,
                            const string params);  // Handle completion of tester pass for agent
};

string CTesterHandler::s_fileName = "data.bin";    // File name for writing frame data


Die Ereignishandler für das Hauptterminal sehen sehr einfach aus, da wir den Hauptcode in Hilfsfunktionen verschieben werden:

//+------------------------------------------------------------------+
//| Handling the optimization start in the main terminal             |
//+------------------------------------------------------------------+
int CTesterHandler::TesterInit(void) {
// Open / create a database
   DB::Open();

// If failed to open it, we do not start optimization
   if(!DB::IsOpen()) {
      return INIT_FAILED;
   }

// Close a successfully opened database
   DB::Close();

   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Handling the optimization completion in the main terminal        |
//+------------------------------------------------------------------+
void CTesterHandler::TesterDeinit(void) {
// Handle the latest data frames received from agents
   ProcessFrames();

// Close the chart with the EA running in frame collection mode
   ChartClose();
}

//+--------------------------------------------------------------------+
//| Handling the completion of a pass on an agent in the main terminal |
//+--------------------------------------------------------------------+
void CTesterHandler::TesterPass(void) {
// Handle data frames received from the agent
   ProcessFrames();
}


Die Aktionen, die nach Abschluss eines Durchgangs durchgeführt werden, existieren in zwei Versionen:

  • Für das Testmittel. Dort werden nach der Passage die notwendigen Informationen gesammelt und ein Datenrahmen erstellt, der an das Hauptterminal gesendet wird. Diese Aktionen werden in der Ereignishandhabung Tester() gesammelt.

  • Für das Hauptterminal. Hier können wir Datenrahmen von Testagenten empfangen, die im Rahmen empfangenen Informationen analysieren und in die Datenbank eingeben. Diese Aktionen werden in der Ereignisbehandlung durch TesterPass() gesammelt.

Die Generierung eines Datenrahmens für den Testagenten sollte im EA erfolgen, und zwar innerhalb von OnTester. Da wir seinen Code auf die EA-Objektebene (in die Klasse CVirtualAdvisor) verschoben haben, müssen wir hier die Methode CTesterHandler::Tester() hinzufügen. Als Methodenparameter übergeben wir den neu berechneten Wert des nutzerdefinierten Optimierungskriteriums und einen String, der die Parameter der Strategie beschreibt, die im optimierten EA verwendet wurde. Um eine solche Zeichenkette zu bilden, werden wir die bereits erstellte ~ (Tilde) für die Objekte der Klasse CVirtualStrategy verwenden.

//+------------------------------------------------------------------+
//| OnTester event handler                                           |
//+------------------------------------------------------------------+
double CVirtualAdvisor::Tester() {
// Maximum absolute drawdown
   double balanceDrawdown = TesterStatistics(STAT_EQUITY_DD);

// Profit
   double profit = TesterStatistics(STAT_PROFIT);

// The ratio of possible increase in position sizes for the drawdown of 10% of fixedBalance_
   double coeff = CMoney::FixedBalance() * 0.1 / balanceDrawdown;

// Calculate the profit in annual terms
   long totalSeconds = TimeCurrent() - m_fromDate;
   double fittedProfit = profit * coeff * 365 * 24 * 3600 / totalSeconds ;

// Perform data frame generation on the test agent
   CTesterHandler::Tester(fittedProfit,
                          ~((CVirtualStrategy *) m_strategies[0]));

   return fittedProfit;
}


In der Methode CTesterHandler::Tester() gehen wir selbst alle möglichen Namen der verfügbaren statistischen Merkmale durch, ermitteln deren Werte, wandeln sie in Strings um und fügen diese Strings dem Array stats hinzu. Warum mussten wir reelle numerische Merkmale in Zeichenketten umwandeln? Nur so konnten sie in einem Rahmen mit einer Stringbeschreibung der Strategieparameter übergeben werden. In einem Frame können wir entweder ein Array von Werten eines der einfachen Typen (Strings gelten nicht) oder eine vorab erstellte Datei mit beliebigen Daten übergeben. Um zu vermeiden, dass zwei verschiedene Frames gesendet werden müssen (einer mit Zahlen, der andere mit Zeichenketten aus einer Datei), werden wir alle Daten in Zeichenketten umwandeln, sie in eine Datei schreiben und deren Inhalt in einem Frame senden: 

//+------------------------------------------------------------------+
//| Handling completion of tester pass for agent                     |
//+------------------------------------------------------------------+
void CTesterHandler::Tester(double custom,   // Custom criteria
                            string params    // Description of EA parameters in the current pass
                           ) {
// Array of names of saved statistical characteristics of the pass
   ENUM_STATISTICS statNames[] = {
      STAT_INITIAL_DEPOSIT,
      STAT_WITHDRAWAL,
      STAT_PROFIT,
      ... 
   };

// Array for values of statistical characteristics of the pass as strings
   string stats[];
   ArrayResize(stats, ArraySize(statNames));

// Fill the array of values of statistical characteristics of the pass
   FOREACH(statNames, stats[i] = DoubleToString(TesterStatistics(statNames[i]), 2));

// Add the custom criterion value to it
   APPEND(stats, DoubleToString(custom, 2));

// Screen the quotes in the description of parameters just in case
   StringReplace(params, "'", "\\'");

// Open the file to write data for the frame
   int f = FileOpen(s_fileName, FILE_WRITE | FILE_TXT | FILE_ANSI);

// Write statistical characteristics
   FOREACH(stats, FileWriteString(f, stats[i] + ","));

// Write a description of the EA parameters
   FileWriteString(f, StringFormat("'%s'", params));

// Close the file
   FileClose(f);

// Create a frame with data from the recorded file and send it to the main terminal
   if(!FrameAdd("", 0, 0, s_fileName)) {
      PrintFormat(__FUNCTION__" | ERROR: Frame add error: %d", GetLastError());
   }
}


Betrachten wir schließlich eine Hilfsmethode, die Datenrahmen akzeptiert und die Informationen daraus in der Datenbank speichert. Bei dieser Methode erhalten wir in einer Schleife alle eingehenden Bilder, die zum aktuellen Zeitpunkt noch nicht bearbeitet wurden. Von jedem Frame erhalten wir Daten in Form eines Zeichenarrays und wandeln sie in eine Zeichenkette um. Anschließend wird eine Zeichenkette mit den Namen und Werten der Parameter des Durchlaufs mit dem angegebenen Index gebildet. Wir verwenden die erhaltenen Werte, um eine SQL-Abfrage zu erstellen, mit der wir eine neue Zeile in die Tabelle der Durchgänge in unserer Datenbank einfügen. Fügen wir die erstellte SQL-Abfrage zum SQL-Abfrage-Array hinzu.

Nachdem wir alle aktuell empfangenen Datenrahmen auf diese Weise bearbeitet haben, führen wir alle SQL-Abfragen aus dem Array in einer einzigen Transaktion aus.

//+------------------------------------------------------------------+
//| Handling incoming frames                                         |
//+------------------------------------------------------------------+
void CTesterHandler::ProcessFrames(void) {
// Open the database
   DB::Open();

// Variables for reading data from frames
   string   name;      // Frame name (not used)
   ulong    pass;      // Frame pass index
   long     id;        // Frame type ID (not used) 
   double   value;     // Single frame value (not used)
   uchar    data[];    //  Frame data array as a character array

   string   values;    // Frame data as a string
   string   inputs;    // String with names and values of pass parameters
   string   query;     // A single SQL query string
   string   queries[]; // SQL queries for adding records to the database


// Go through frames and read data from them
   while(FrameNext(pass, name, id, value, data)) {
      // Convert the array of characters read from the frame into a string
      values = CharArrayToString(data);

      // Form a string with names and values of the pass parameters
      inputs = GetFrameInputs(pass);

      // Form an SQL query from the received data
      query = StringFormat("INSERT INTO passes "
                           "VALUES (NULL, %d, %s,\n'%s',\n'%s');",
                           pass, values, inputs,
                           TimeToString(TimeLocal(), TIME_DATE | TIME_SECONDS));

      // Add it to the SQL query array
      APPEND(queries, query);
   }

// Execute all requests
   DB::ExecuteTransaction(queries);

// Close the database
   DB::Close();
}


Die Hilfsmethode GetFrameInputs() zur Bildung einer Zeichenkette mit den Namen und Werten der Eingabevariablen des Durchlaufs wurde aus dem AlgoBook übernommen und für unsere Bedürfnisse leicht ergänzt.

Wir speichern den erhaltenen Code in der Datei TesterHandler.mqh des aktuellen Ordners.


Operationskontrolle

Um die Funktionsweise zu testen, lassen wir die Optimierung mit einer kleinen Anzahl von Parametern laufen, die über einen relativ kurzen Zeitraum iteriert werden. Nachdem der Optimierungsprozess abgeschlossen ist, können wir die Ergebnisse im Strategietester und in der erstellten Datenbank betrachten.

Abb. 1. Optimierungsergebnisse im Strategie-Tester

Abb. 2. Optimierungsergebnisse in der Datenbank 

Wie wir sehen können, stimmen die Ergebnisse in der Datenbank mit den Ergebnissen im Testgerät überein: Bei gleicher Sortierung nach Nutzerkriterien beobachten wir in beiden Fällen die gleiche Reihenfolge der Gewinnwerte. Der beste Durchgang (pass) zeigt, dass der erwartete Gewinn bei einer Ersteinlage von 10.000 USD und einem maximal erreichbaren Drawdown von 10 % der Ersteinlage (1000 USD) innerhalb eines Jahres 5.000 USD übersteigen kann. Im Moment sind wir jedoch weniger an den quantitativen Eigenschaften der Optimierungsergebnisse interessiert als an der Tatsache, dass sie nun in einer Datenbank gespeichert werden können.


Schlussfolgerung

Wir sind also unserem Ziel einen Schritt näher gekommen. Es ist uns gelungen, die Ergebnisse der durchgeführten Optimierungen der EA-Parameter in unserer Datenbank zu speichern. Auf diese Weise haben wir die Grundlage für die weitere automatisierte Umsetzung der zweiten Stufe der EA-Entwicklung geschaffen.

Hinter den Kulissen sind noch viele Fragen offen. Viele Dinge mussten auf die Zukunft verschoben werden, da ihre Umsetzung mit erheblichen Kosten verbunden wäre. Aber mit den aktuellen Ergebnissen können wir die Richtung der weiteren Projektentwicklung klarer formulieren.

Die implementierte Speicherung funktioniert derzeit nur für einen Optimierungsprozess in dem Sinne, dass wir Informationen über die Durchgänge speichern, aber es ist immer noch schwierig, Gruppen von Zeichenfolgen, die sich auf einen Optimierungsprozess beziehen, daraus zu extrahieren. Zu diesem Zweck müssen wir Änderungen an der Datenbankstruktur vornehmen, was jetzt sehr einfach ist. In Zukunft werden wir versuchen, den Start mehrerer sequenzieller Optimierungsprozesse mit vorheriger Zuweisung verschiedener Optionen für die zu optimierenden Parameter zu automatisieren.

Vielen Dank für Ihre Aufmerksamkeit! Bis bald!



Übersetzt aus dem Russischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/ru/articles/14680

Beigefügte Dateien |
Advisor.mqh (4.4 KB)
Database.mqh (13.37 KB)
Interface.mqh (3.21 KB)
Macros.mqh (2.28 KB)
Money.mqh (4.61 KB)
NewBarEvent.mqh (11.52 KB)
Receiver.mqh (1.79 KB)
Strategy.mqh (1.73 KB)
TesterHandler.mqh (17.5 KB)
VirtualAdvisor.mqh (22.65 KB)
VirtualOrder.mqh (39.52 KB)
VirtualReceiver.mqh (17.43 KB)
Hinzufügen von Trailing-Stop mit Parabolic SAR Hinzufügen von Trailing-Stop mit Parabolic SAR
Wenn wir eine Handelsstrategie entwickeln, müssen wir verschiedene Optionen für einen schützenden Stopp testen. Hier bietet sich ein dynamisches Nachziehen des dem Kurs folgenden Stop-Loss-Levels an. Der beste Kandidat dafür ist der Parabolic SAR-Indikator. Es ist schwierig, sich etwas Einfacheres und visuell Klareres vorzustellen.
Neuronale Netze leicht gemacht (Teil 86): U-förmiger Transformator Neuronale Netze leicht gemacht (Teil 86): U-förmiger Transformator
Wir untersuchen weiterhin Algorithmen für die Zeitreihenprognose. In diesem Artikel werden wir eine andere Methode besprechen: den U-förmigen Transformator.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Neuronale Netze leicht gemacht (Teil 85): Multivariate Zeitreihenvorhersage Neuronale Netze leicht gemacht (Teil 85): Multivariate Zeitreihenvorhersage
In diesem Artikel möchte ich Ihnen eine neue komplexe Methode zur Zeitreihenprognose vorstellen, die die Vorteile von linearen Modellen und Transformer harmonisch vereint.