English Русский 中文 Español 日本語 Português
preview
Kontinuierliche Walk-Forward-Optimierung (Teil 8): Programmverbesserungen und Korrekturen

Kontinuierliche Walk-Forward-Optimierung (Teil 8): Programmverbesserungen und Korrekturen

MetaTrader 5Tester | 14 Dezember 2020, 09:09
361 0
Andrey Azatskiy
Andrey Azatskiy

Einführung

Das Programm wurde aufgrund von Kommentaren und Wünschen von Nutzern und Lesern dieser Artikelserie geändert. Dieser Artikel enthält eine neue Version des Autooptimierers. Diese Version implementiert gewünschte Funktionen und bietet weitere Verbesserungen, die ich bei der Arbeit mit dem Programm gefunden habe. Optimierungen werden jetzt um ein Vielfaches schneller durchgeführt als in der Vorgängerversion. Dies ist das Ergebnis der Entscheidung, ohne Mutexe zu arbeiten und einige andere Aktionen zu vermeiden, die den Prozess der Berichterstellung verlangsamten. Jetzt kann die Optimierung für eine Reihe von Assets verwendet werden. Außerdem wurde das Problem mit dem zum Zeitpunkt der Optimierung belegten Speicher behoben.  

Die vorherigen Artikel innerhalb dieser Serie: 

  1. Kontinuierliche Walk-Forward-Optimierung (Teil 1): Arbeiten mit Optimierungsberichten 
  2. Kontinuierliche Rolloptimierung (Teil 2): Mechanismus zur Erstellung eines Optimierungsberichts für einen beliebigen Roboter
  3. Kontinuierliche Walk-Forward-Optimierung (Teil 3): Anpassen eines Roboters an die automatische Optimierung
  4. Kontinuierliche Walk-Forward-Optimierung (Teil 4): Optimierungsmanager (automatische Optimierung)
  5. Kontinuierliche Walk-Forward-Optimierung (Teil 5): Projektübersicht Auto-Optimizer und Erstellen einer GUI
  6. Kontinuierliche Walk-Forward-Optimierung (Teil 6): Logikteil und die Struktur des Auto-Optimizers 
  7. Kontinuierliche Walk-Forward-Optimierung (Teil 7): Einbinden des logischen Teils des Auto-Optimizer mit Grafiken und Steuerung


Hinzufügen der automatischen Datumsvervollständigung

Die vorherige Programmversion hatte eine schrittweise Eingabe von Datumsangaben für Vorwärts- und Vergangenheitsoptimierungen, was unbequem war. Diesmal habe ich eine automatische Eingabe der gewünschten Zeitbereiche implementiert. Die Details der Funktionalität können wie folgt beschrieben werden. Das gewählte Zeitintervall soll automatisch in eine Vorwärts- und eine historische Optimierung aufgeteilt werden. Der Schritt für beide Optimierungstypen ist fest vorgegeben und wird vor der Aufteilung in Intervalle eingestellt. Jeder neue Vorwärtsbereich muss am nächsten Tag beginnen, der auf den vorherigen Bereich folgt. Die Verschiebung der historischen Intervalle (die sich überschneiden) ist gleich dem Schritt der Vorwärtsfenster. Im Gegensatz zu den historischen Optimierungen überschneiden sich die Forward-Optimierungen nicht, und sie implementieren eine kontinuierliche Handelsgeschichte. 

Um diese Aufgabe zu implementieren, habe ich mich entschieden, diese Funktionalität in ein separates Grafikfenster zu übertragen und es unabhängig und nicht direkt mit der Hauptschnittstelle verbunden zu machen. Als Ergebnis haben wir die folgende Hierarchie von Objekten.

 


Besprechen wir, wie diese Funktionalität zusammenhängt und sehen wir uns ihre Implementierungsbeispiele an. Beginnen wir mit der grafischen Oberfläche der erstellten Erweiterung, d. h. alles auf dem Chart ab dem AutoFillInDateBorders-Objekt, das das Grafikfenster darstellt, und darunter. Das Bild zeigt die GUI-Elemente, das XAML-Markup und die Felder aus dem ViewModel-Teil, der durch die Klasse AutoFillInDateBordersVM repräsentiert wird. 

Wie Sie sehen können, besteht die GUI aus drei Hauptbereichen. Dazu gehören zwei Kalender für die Eingabe des Anfangs- und Enddatums des Optimierungszeitraums, Tabellen für die Angabe der Grenzen der Vorwärts- und historischen Intervalle sowie die Schaltfläche "Setzen", auf die ein Klick den angegebenen Bereich auf die entsprechenden historischen und Vorwärtsfenster aufteilt. Die Tabelle im Screenshot enthält drei doppelte Zeilen, tatsächlich sind es aber nur zwei Zeilen: Die erste ist für den historischen Datumsbereich zuständig, die zweite legt den Vorwärtsbereich fest.

Der 'Value' in der Tabelle ist der Schritt des entsprechenden Optimierungstyps in Tagen. Wenn z. B. 'Value' des historischen Intervalls 360 Tage und 'Value' des Vorwärtsintervalls 90 ist, bedeutet dies, dass das in den Kalendern angegebene Zeitintervall in historische Optimierungsintervalle von 360 Tagen und Vorwärtsintervalle von 90 Tagen unterteilt wird. Der Beginn des jeweils nächsten historischen Optimierungsfensters wird um den Vorwärtsintervallschritt verschoben.  

class AutoFillInDateBordersM : IAutoFillInDateBordersM {     private AutoFillInDateBordersM() { }     private static AutoFillInDateBordersM instance;     public static AutoFillInDateBordersM Instance()     {         if (instance == null)             instance = new AutoFillInDateBordersM();         return instance;     }     public event Action<List<KeyValuePair<OptimisationType, DateTime[]>>> DateBorders;     public void Calculate(DateTime From, DateTime Till, uint history, uint forward)     {         if (From >= Till)             throw new ArgumentException("Date From must be less then date Till");         List<KeyValuePair<OptimisationType, DateTime[]>> data = new List<KeyValuePair<OptimisationType, DateTime[]>>();         OptimisationType type = OptimisationType.History;         DateTime _history = From;         DateTime _forward = From.AddDays(history + 1);         DateTime CalcEndDate()         {             return type == OptimisationType.History ? _history.AddDays(history) : _forward.AddDays(forward);         }            while (CalcEndDate() <= Till)         {             DateTime from = type == OptimisationType.History ? _history : _forward;             data.Add(new KeyValuePair<OptimisationType, DateTime[]>(type, new DateTime[2] { from, CalcEndDate() }));             if (type == OptimisationType.History)                 _history = _history.AddDays(forward + 1);             else                 _forward = _forward.AddDays(forward + 1);             type = type == OptimisationType.History ? OptimisationType.Forward : OptimisationType.History;         }         if (data.Count == 0)             throw new ArgumentException("Can`t create any date borders with set In sample (History) step");         DateBorders?.Invoke(data);     } }

Die Modellklasse für die Fensterdaten ist ein Objekt geschrieben nach dem Singletone-Muster. Dies ermöglicht die Interaktion des ViewModel-Teils des Hauptfensters mit dem Datenmodell unter Umgehung des Grafikfensters der Erweiterung. Von den interessanten Methoden enthält das Objekt nur "Calculate", das die Datumsbereiche berechnet, und ein Ereignis, das nach Abschluss der obigen Prozedur aufgerufen wird. Das Ereignis erhält als Parameter die Sammlung gepaarter Werte, bei der der Schlüssel der Typ des analysierten Intervalls ist (Vorwärts- oder historische Optimierung) und der Wert ein Array aus zwei DateTime-Werten ist. Das erste Datum gibt den Beginn des ausgewählten Intervalls an, das zweite das Ende.

Die Methode zur Berechnung von Datumsbereichen berechnet diese in einer Schleife, wobei abwechselnd der Typ des berechneten Fensters (vorwärts oder historisch) geändert wird. Zunächst wird der historische Fenstertyp als Startpunkt aller Berechnungen festgelegt. Die anfänglichen Datumswerte für jeden der Fenstertypen werden ebenfalls vor dem Schleifenstart gesetzt. Bei jeder Iteration einer Schleife wird die Extremgrenze des gewählten Fenstertyps mit Hilfe der geschachtelten Funktion berechnet und dieser Wert dann mit dem Extrembereichsdatum verifiziert. Wird dieses Datum überschritten, so ist dies eine Schleifenausgangsbedingung. In einer Schleife wird ein Optimierungsfensterbereich gebildet. Dann wird das nächste Anfangsdatum des Fensters und der Typumschalter des Fensters aktualisiert.

Nach allen Operationen wird, wenn keine Fehler aufgetreten sind, ein Event mit dem übergebenen Datumsbereich aufgerufen. Alle weiteren Aktionen werden von der Klasse ausgeführt. Die Ausführung der obigen Methode wird durch den Callback für den Tastendruck "Setzen" ausgelöst.   

Die Datenmodell-Factory für unsere Erweiterung ist auf die einfachste Weise implementiert:

class AutoFillInDateBordersCreator {     public static IAutoFillInDateBordersM Model => AutoFillInDateBordersM.Instance(); }

Wenn wir die statische Eigenschaft "Model" aufrufen, beziehen wir uns im Grunde immer auf dieselbe Instanz des Datenmodellobjekts, und dann casten wir es auf einen Schnittstellentyp. Lassen Sie uns diese Tatsache im ViewModel-Teil unseres Hauptfensters nutzen.

public AutoOptimiserVM() {     ...     AutoFillInDateBordersCreator.Model.DateBorders += Model_DateBorders;     .... } ~AutoOptimiserVM() {     ...     AutoFillInDateBordersCreator.Model.DateBorders -= Model_DateBorders;     .... }

Sowohl im Konstruktor als auch im Destruktor des ViewModel-Objekts des Hauptfensters haben wir die Möglichkeit, keinen Zeiger auf eine Instanz dieser Klasse zu speichern, sondern sie über eine statische Datenmodellfabrik aufzurufen. Achten Sie darauf, dass der ViewModel-Teil des Hauptfensters mit der besprochenen Klasse arbeitet, ohne zu wissen, dass er mit dieser Klasse arbeitet. Denn Referenzen auf dieses Objekt werden nirgends erwähnt, außer im Konstruktor und Destruktor der Klasse. Der Callback, der das erwähnte Ereignis abonniert, leert zunächst alle zuvor eingegebenen Datumsbereiche und fügt dann in einer Schleife neue Datumsbereiche hinzu, die durch das Ereignis erhalten wurden, einen nach dem anderen. Die Methode, die Datumsbereiche zur Sammlung hinzufügt, ist auch auf der ViewModel-Seite der grafischen Hauptschnittstelle implementiert. Sie sieht wie folgt aus: 

void _AddDateBorder(DateTime From, DateTime Till, OptimisationType DateBorderType) {         try     {         DateBorders border = new DateBorders(From, Till);         if (!DateBorders.Where(x => x.BorderType == DateBorderType).Any(y => y.DateBorders == border))         {             DateBorders.Add(new DateBordersItem(border, _DeleteDateBorder, DateBorderType));         }     }     catch (Exception e)     {         System.Windows.MessageBox.Show(e.Message);     } }

Die Erstellung eines DateBorder-Objekts ist in eine 'try - catch'-Konstruktion verpackt. Dies geschieht, weil im Objektkonstruktor eine Ausnahme auftreten kann und diese irgendwie behandelt werden muss. Ich habe auch die Methode ClearDateBorders hinzugefügt: 

ClearDateBorders = new RelayCommand((object o) => {     DateBorders.Clear(); });

Sie ermöglicht das schnelle Löschen aller eingegebenen Datumsbereiche. In der vorherigen Version musste jedes Datum einzeln gelöscht werden, was bei einer großen Anzahl von Daten unbequem war. Die Schaltflächen des Hauptfensters der grafischen Benutzeroberfläche, die die beschriebenen Neuerungen aufrufen, wurden in derselben Zeile wie die bereits vorhandenen Steuerelemente für den Datumsbereich hinzugefügt. 

Ein Klick auf Autoset löst einen Callback aus, der die Methode Open auf der Instanz der Klasse SubFormKeeper aufruft. Diese Klasse wurde als Wrapper geschrieben, der den Erstellungsprozess des verschachtelten Fensters kapselt. Dadurch werden unnötige Eigenschaften und Felder im ViewModel des Hauptfensters eliminiert, und es wird verhindert, dass wir direkt auf das erstellte Hilfsfenster zugreifen, da es nicht direkt interagiert werden soll. 

class SubFormKeeper {     public SubFormKeeper(Func<Window> createWindow, Action<Window> subscribe_events = null, Action<Window> unSubscribe_events = null);     public void Open();     public void Close(); }

Wenn Sie sich die Klassensignatur ansehen, können Sie anhand der öffentlichen Methoden erkennen, dass sie genau die aufgeführten Möglichkeiten bietet. Außerdem werden alle Hilfsfenster der Auto-Optimierung in dieser speziellen Klasse verpackt.  


Neue Funktionen und Fehlerbehebungen in der Bibliothek für die Arbeit mit Optimierungsergebnissen

Dieser Teil des Artikels beschreibt Änderungen in der Bibliothek für die Arbeit mit Optimierungsberichten - "ReportManager.dll". Neben der Einführung eines nutzerdefinierten Koeffizienten sorgt die neue Funktion für ein schnelleres Entladen von Optimierungsberichten aus dem Terminal. Sie behebt auch einen Fehler bei der Datensortierung.    

  • Einführung eines nutzerdefinierten Optimierungskoeffizienten

Einer der Verbesserungsvorschläge in Kommentaren zu früheren Artikeln war die Möglichkeit, einen nutzerdefinierten Koeffizienten für die Filterung von Optimierungsergebnissen zu verwenden. Um diese Option zu implementieren, musste ich einige Änderungen an bestehenden Objekten vornehmen. Um jedoch alte Berichte zu unterstützen, kann die Klasse, die Optimierungsdaten liest, sowohl mit Berichten arbeiten, die einen nutzerdefinierten Koeffizienten haben, als auch mit solchen, die in früheren Versionen des Programms erzeugt wurden. Deshalb blieb das Berichtsformat unverändert. Es hat einen zusätzlichen Parameter - ein Feld zur Angabe des nutzerdefinierten Koeffizienten.

Die Enumeration "SortBy" hat jetzt den neuen Parameter "Custom", und das entsprechende Feld wurde der Struktur "Coefficients" hinzugefügt. Dadurch wird der Koeffizient zu den Objekten hinzugefügt, die für das Speichern von Daten zuständig sind, aber nicht zu den Objekten, die Daten entladen und lesen. Das Schreiben der Daten wird durch zwei Methoden und eine Klasse mit statischen Methoden durchgeführt, die von MQL5 zum Speichern von Berichten verwendet wird.

public static void AppendMainCoef(double customCoef,                                   double payoff,                                   double profitFactor,                                   double averageProfitFactor,                                   double recoveryFactor,                                   double averageRecoveryFactor,                                   int totalTrades,                                   double pl,                                   double dd,                                   double altmanZScore) {     ReportItem.OptimisationCoefficients.Custom = customCoef;     ... }

Zunächst wurde ein neuer Parameter, der den nutzerdefinierten Koeffizienten identifiziert, der Methode AppendMainCoef hinzugefügt. Dann wird er zur Struktur ReportWriter.ReportItem hinzugefügt, wie andere übergebene Koeffizienten. Wenn Sie nun versuchen, das alte Projekt mit der neuen Bibliothek "ReportManager.dll" zu kompilieren, erhalten Sie eine Ausnahme, da sich die Signatur der Methode AppendMainCoef geändert hat. Dieser Fehler kann behoben werden, indem das Objekt, das die Daten entlädt, ein bisschen bearbeitet wird - wir werden etwas später mit dem MQL5-Code fortfahren.

Um eine korrekte Kompilierung mit der aktuellen dll-Version zu ermöglichen, ersetzen Sie den "History Manager" im Include-Verzeichnis durch einen neuen, der unten in diesem Artikel angehängt ist - das reicht aus, um Roboter mit den alten und neuen Methoden zu kompilieren.   

Außerdem habe ich die Signatur der Methode Write geändert, die nun keine Ausnahme mehr auswirft, sondern eine Fehlermeldung zurückgibt. Dies wurde hinzugefügt, weil das Programm nicht mehr den benannten Mutex verwendet, der den Datenentladevorgang erheblich verlangsamte, aber in der alten Version der Entladeklasse für die Erstellung von Berichten erforderlich war. Ich habe jedoch die Methode, die Daten unter Verwendung eines Mutex schreibt, nicht gelöscht, um die Kompatibilität mit dem zuvor implementierten Datenexportformat zu wahren. 

Damit ein neuer Datensatz in der Berichtsdatei erscheint, müssen wir ein neues <Item/>-Tag mit dem Attribut Name gleich "Custom" erstellen. 

WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.Custom.ToString(), new Dictionary<string, string> { { "Name", "Custom" } });

Eine weitere geänderte Methode ist OptimisationResultsExtentions.ReportWriter: hier wurde eine ähnliche Zeile hinzugefügt, die das <Item/>-Tag mit dem Parameter für den nutzerdefinierten Koeffizienten hinzufügt. 

Betrachten wir nun das Hinzufügen von nutzerdefinierten Koeffizienten zu den Daten und zum MQL-Robotercode. Betrachten wir zunächst die alte Version der Funktionalität zum Herunterladen von Daten, in der sich der Code, der mit der Klasse ReportWriter arbeitet, in der Klasse CXmlHistoryWriter in der Datei XmlHistoryWriter.mqh befindet. Um nutzerdefinierte Koeffizienten zu unterstützen, wurde ein Verweis auf die folgende Signatur erstellt: 

typedef double(*TCustomFilter)();

Das 'private' Feld in der obigen Klasse speichert diese Funktion.

class CXmlHistoryWriter   { private:    const string      _path_to_file,_mutex_name;    CReportCreator    _report_manager;    TCustomFilter     custom_filter;    void              append_bot_params(const BotParams  &params[]);//    void              append_main_coef(PL_detales &pl_detales,                                       TotalResult &totalResult);//    //double            get_average_coef(CoefChartType type);    void              insert_day(PLDrawdown &day,ENUM_DAY_OF_WEEK day);//    void              append_days_pl();// public:                      CXmlHistoryWriter(string file_name,string mutex_name,                      CCCM *_comission_manager, TCustomFilter filter);//                      CXmlHistoryWriter(string mutex_name,CCCM *_comission_manager, TCustomFilter filter);                     ~CXmlHistoryWriter(void) {_report_manager.Clear();} //    void              Write(const BotParams &params[],datetime start_test,datetime end_test);//   };

Der Wert dieses 'private' Feldes wird vom Klassenkonstruktor eingetragen. Weiterhin wird in der Methode append_main_coef beim Aufruf der statischen Methode "ReportWriter::AppendMainCoef" aus der dll-Bibliothek die übergebene Funktion durch ihren Zeiger aufgerufen und so der nutzerdefinierte Koeffizientenwert erhalten.

    Die Klasse wird nicht direkt verwendet, da es einen Wrapper gibt, der früher, im dritten Artikel, beschrieben wurde - es ist die Klasse CAutoUploader.

class CAutoUploader   { private:    datetime          From,Till; // Testing start and end dates    CCCM              *comission_manager; // Commission manager    BotParams         params[]; // List of parameters    string            mutexName; // Mutex name    TCustomFilter     custom_filter; public:                      CAutoUploader(CCCM *comission_manager, string mutexName, BotParams &params[],                                    TCustomFilter filter);                      CAutoUploader(CCCM *comission_manager, string mutexName, BotParams &params[]);    virtual          ~CAutoUploader(void);    virtual void      OnTick(); // Calculating testing start and end dates   };

In dieser Klasse habe ich zusätzlich zum bisherigen Konstruktor einen neuen Konstruktor hinzugefügt, der einen Zeiger auf die Funktion nimmt, die einen nutzerdefinierten Koeffizienten zurückgibt. Diese Konstruktoren speichern auch in einer Variablen eine Referenz auf die gewünschte Funktion, in der sie gespeichert ist. 
 

double EmptyCustomCoefCallback() {return 0;} //+------------------------------------------------------------------+ //| Constructor                                                       | //+------------------------------------------------------------------+ CAutoUploader::CAutoUploader(CCCM *_comission_manager,string _mutexName,BotParams &_params[], TCustomFilter filter) : comission_manager(_comission_manager),    mutexName(_mutexName),    From(0),    Till(0),    custom_filter(filter)   {    CopyBotParams(params,_params);   } //+------------------------------------------------------------------+ //| Constructor                                                       | //+------------------------------------------------------------------+ CAutoUploader::CAutoUploader(CCCM *_comission_manager,string _mutexName,BotParams &_params[]) : comission_manager(_comission_manager),    mutexName(_mutexName),    From(0),    Till(0),    custom_filter(EmptyCustomCoefCallback)   {    CopyBotParams(params,_params);   }

Die Funktion "EmptyCustomCoefCallback" wurde erstellt, um die alte Version des Konstruktors zu speichern. Diese Funktion gibt Null als nutzerdefinierten Koeffizienten zurück. Wenn der vorherige Konstruktor dieser Klasse aufgerufen wird, übergeben Sie genau diese Funktion an die Klasse CXmlHistoryWriter. Wenn wir ein Beispiel aus Artikel 4 nehmen, können wir dem Roboter einen nutzerdefinierten Koeffizienten wie folgt hinzufügen:

//+------------------------------------------------------------------+ //|                                                     SimpleMA.mq5 | //|                        Copyright 2019, MetaQuotes Software Corp. | //|                                             https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2019, MetaQuotes Software Corp." #property link      "https://www.mql5.com" #property version   "1.00" #include <Trade/Trade.mqh> #include <History manager/AutoLoader.mqh> // Include CAutoUploader #define TESTER_ONLY input int ma_fast = 10; // MA fast input int ma_slow = 50; // MA slow input int _sl_ = 20; // SL input int _tp_ = 60; // TP input double _lot_ = 1; // Lot size // Comission and price shift (Article 2) input double _comission_ = 0; // Comission input int _shift_ = 0; // Shift int ma_fast_handle,ma_slow_handle; const double tick_size = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE); CTrade trade; CAutoUploader * auto_optimiser;// Pointer to CAutoUploader class (Article 3) CCCM _comission_manager_;// Comission manager (Article 2) double CulculateMyCustomCoef() {    return 0; } //+------------------------------------------------------------------+ //| Expert initialization function                                   | //+------------------------------------------------------------------+ int OnInit()   { //--- ...    // Add Instance CAutoUploader class (Article3)    auto_optimiser = new CAutoUploader(&_comission_manager_,"SimpleMAMutex",params,CulculateMyCustomCoef); //---    return(INIT_SUCCEEDED);   }   double OnTester()   {    return(CulculateMyCustomCoef());   } //+------------------------------------------------------------------+

Hier habe ich die Implementierung bis auf den Teil, der mit der Einführung des nutzerdefinierten Koeffizienten verbunden ist, entfernt, um hier nicht zu viel Code bereitzustellen. Der vollständige Code ist im Anhang verfügbar. Zunächst ist es notwendig, eine Funktion zu erstellen, die den nutzerdefinierten Koeffizienten berechnen wird. Im obigen Beispiel gibt die Funktion Null zurück und hat keine Implementierung, aber die nutzerdefinierte Koeffizientenberechnung muss in ihr beschrieben werden. Die Berechnung wird also nicht im OnTester-Callback durchgeführt - stattdessen wird die beschriebene Funktion aufgerufen. Bei der Erstellung der Klasse CAutoUploader müssen wir nur eine neue Konstruktorüberladung aufrufen und den nutzerdefinierten Koeffizienten darin angeben. Damit ist das Hinzufügen eines nutzerdefinierten Koeffizienten abgeschlossen. 

  • Schnelleres Hochladen von Optimierungspassdaten im neuen Daten-Upload-Format

Die Entscheidung, den Mutex auszuschließen, hat die Datenladegeschwindigkeit erhöht, aber diese Erhöhung ist nicht ausreichend. In der vorherigen Version mussten wir bei jedem neuen Datensatz eine Reihe von Operationen durchführen, um Daten zu einer Datei hinzuzufügen:  

  1. Lesen einer Datei  
  2. Speichern der gelesenen Daten im RAM 
  3. Hinzufügen eines neuen Optimierungsdurchgangs zu den gelesenen Daten in den Speicher 
  4. Löschen der alten Datei 
  5. Anlegen einer neuen, sauberen Datei anstelle der alten Datei 
  6. Speichern des gesamten Datenfeldes in die erstellte Datei 

Dies ist die Standardprozedur der verwendeten Klasse XmlDocument aus der C#-Standardbibliothek. Diese Prozedur ist zeitaufwendig. Außerdem steigt der Zeitaufwand für diese Operation mit zunehmender Größe der Datei. In der Vorgängerversion mussten wir dies in Kauf nehmen, da wir nicht alle Daten an einem Ort sammeln konnten. Stattdessen haben wir die Daten nach Abschluss jeder Optimierung gespeichert. In der aktuellen Implementierung werden die Daten mit Hilfe von Frames akkumuliert, und so können wir alle Daten auf einmal in das gewünschte Format konvertieren. Dies wird mit der zuvor geschriebenen Methode "OptimisationResultsExtentions.ReportWriter" realisiert. Dies ist eine Erweiterungsmethode für das Array der Optimierungsdurchläufe. Im Gegensatz zu ReportWriter.Write fügt diese Methode die Daten nicht in eine Datei ein, sondern legt eine Datei an und schreibt alle Optimierungsdurchläufe zeilenweise in diese Datei. So kann das Datenarray, das beim Schreiben mit ReportWriter.Write mehrere Minuten benötigte, nun in wenigen Sekunden geschrieben werden.  

 Es wurde ein Wrapper in der Klasse ReportWriter erstellt, um die Verwendung der Methode OptimisationResultsExtentions.ReportWriter aus MQL5 zu ermöglichen. 

public class ReportWriter
{
    private static ReportItem ReportItem;
    private static List<OptimisationResult> ReportData = new List<OptimisationResult>();
    public static void AppendToReportData(string symbol, int tf,
                                          ulong StartDT, ulong FinishDT)
    {
        ReportItem.Symbol = symbol;
        ReportItem.TF = tf;
        ReportItem.DateBorders = new DateBorders(StartDT.UnixDTToDT(), FinishDT.UnixDTToDT());

        ReportData.Add(ReportItem);
        ClearReportItem();
    }
    public static void ClearReportItem()
    {
        ReportItem = new ReportItem();
    }
    public static void ClearReportData() { ReportData.Clear(); }
    public static string WriteReportData(string pathToBot, string currency, double balance,
                                         int laverage, string pathToFile)
    {
        try
        {
            ReportData.ReportWriter(pathToBot, currency, balance, laverage, pathToFile);
            ClearReportData();
        }
        catch (Exception e)
        {
            return e.Message;
        }
        ClearReportData();
        return "";
    }
}

In der Klasse ReportWriter haben wir das Feld ReportData angelegt, in dem eine Sammlung von ReportItem-Elementen gespeichert wird, es handelt sich also um eine Sammlung von Optimierungsdurchläufen. Die Idee ist, alle benötigten Daten von MQL5 in die Struktur ReportItem zu schreiben, wobei die im ersten Artikel beschriebenen Methoden verwendet werden. Dann fügen Sie sie durch den Aufruf der Methode AppendToReportData zur Sammlung der Optimierungspässe hinzu. Auf diese Weise wird die erforderliche Datensammlung auf der C#-Seite gebildet. Sobald alle Optimierungsdurchläufe der Sammlung hinzugefügt wurden, rufen Sie die Methode WriteReportData auf, die mit der Methode OptimisationResultsExtentions.ReportWriter schnell den Optimierungsbericht bildet.

  • Fehlerbehebung

Leider ist mir in der vorherigen Programmversion ein Fehler unterlaufen, den ich erst recht spät bemerkt habe. Dieser Fehler hängt mit dem Sortiermechanismus der Optimierungen zusammen, der im ersten Artikel beschrieben wurde. Da die Sortierung der Daten nach mehreren Kriterien erfolgen kann, muss man festlegen, welche dieser Kriterien maximiert und welche minimiert werden sollen. Zum Beispiel würde niemand die Anzahl der verlorenen Positionen maximieren. 

Um Missverständnisse zu vermeiden, hat die Optimierungssortierung eine etwas andere Richtungsbedeutung: 

  • Absteigend - von den besten zu den schlechtesten Parametern
  • Aufsteigend - von den schlechtesten zu den besten Parametern

Damit die Methode der Datensortierung bestimmen kann, welche Kriterien maximiert und welche minimiert werden sollen, wurde eine eigene Methode erstellt, die entsprechende Variablen zurückgibt. Die bisherige Implementierung der Methode sah wie folgt aus: 

private static SortMethod GetSortMethod(SortBy sortBy) {     switch (sortBy)     {         case SortBy.Payoff: return SortMethod.Increasing;         case SortBy.ProfitFactor: return SortMethod.Increasing;         case SortBy.AverageProfitFactor: return SortMethod.Increasing;         case SortBy.RecoveryFactor: return SortMethod.Increasing;         case SortBy.AverageRecoveryFactor: return SortMethod.Increasing;         case SortBy.PL: return SortMethod.Increasing;         case SortBy.DD: return SortMethod.Decreasing;         case SortBy.AltmanZScore: return SortMethod.Decreasing;         case SortBy.TotalTrades: return SortMethod.Increasing;         case SortBy.Q_90: return SortMethod.Decreasing;         case SortBy.Q_95: return SortMethod.Decreasing;         case SortBy.Q_99: return SortMethod.Decreasing;         case SortBy.Mx: return SortMethod.Increasing;         case SortBy.Std: return SortMethod.Decreasing;         case SortBy.MaxProfit: return SortMethod.Increasing;         case SortBy.MaxDD: return SortMethod.Decreasing;         case SortBy.MaxProfitTotalTrades: return SortMethod.Increasing;         case SortBy.MaxDDTotalTrades: return SortMethod.Decreasing;         case SortBy.MaxProfitConsecutivesTrades: return SortMethod.Increasing;         case SortBy.MaxDDConsecutivesTrades: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_Mn: return SortMethod.Increasing;         case SortBy.AverageDailyDD_Mn: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_Mn: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_Mn: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_Tu: return SortMethod.Increasing;         case SortBy.AverageDailyDD_Tu: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_Tu: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_Tu: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_We: return SortMethod.Increasing;         case SortBy.AverageDailyDD_We: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_We: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_We: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_Th: return SortMethod.Increasing;         case SortBy.AverageDailyDD_Th: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_Th: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_Th: return SortMethod.Decreasing;         case SortBy.AverageDailyProfit_Fr: return SortMethod.Increasing;         case SortBy.AverageDailyDD_Fr: return SortMethod.Decreasing;         case SortBy.AverageDailyProfitTrades_Fr: return SortMethod.Increasing;         case SortBy.AverageDailyDDTrades_Fr: return SortMethod.Decreasing;         default: throw new ArgumentException($"Unaxpected Sortby variable {sortBy}");     } }

Die aktuelle Implementierung sieht wie folgt aus:

private static OrderBy GetSortingDirection(SortBy sortBy) {     switch (sortBy)     {         case SortBy.Custom: return OrderBy.Ascending;         case SortBy.Payoff: return OrderBy.Ascending;         case SortBy.ProfitFactor: return OrderBy.Ascending;        case SortBy.AverageProfitFactor: return OrderBy.Ascending;         case SortBy.RecoveryFactor: return OrderBy.Ascending;         case SortBy.AverageRecoveryFactor: return Or-derBy.Ascending;         case SortBy.PL: return OrderBy.Ascending;         case SortBy.DD: return OrderBy.Ascending;         case SortBy.AltmanZScore: return OrderBy.Descending;         case SortBy.TotalTrades: return OrderBy.Ascending;         case SortBy.Q_90: return OrderBy.Ascending;         case SortBy.Q_95: return OrderBy.Ascending;         case SortBy.Q_99: return OrderBy.Ascending;         case SortBy.Mx: return OrderBy.Ascending;         case SortBy.Std: return OrderBy.Descending;         case SortBy.MaxProfit: return OrderBy.Ascending;         case SortBy.MaxDD: return OrderBy.Ascending;         case SortBy.MaxProfitTotalTrades: return OrderBy.Ascending;         case SortBy.MaxDDTotalTrades: return OrderBy.Descending;         case SortBy.MaxProfitConsecutivesTrades: return OrderBy.Ascending;         case SortBy.MaxDDConsecutivesTrades: return OrderBy.Descending;         case SortBy.AverageDailyProfit_Mn: return OrderBy.Ascending;         case SortBy.AverageDailyDD_Mn: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_Mn: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_Mn: return OrderBy.Descending;         case SortBy.AverageDailyProfit_Tu: return OrderBy.Ascending;         case SortBy.AverageDailyDD_Tu: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_Tu: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_Tu: return OrderBy.Descending;         case SortBy.AverageDailyProfit_We: return OrderBy.Ascending;         case SortBy.AverageDailyDD_We: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_We: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_We: return OrderBy.Descending;         case SortBy.AverageDailyProfit_Th: return OrderBy.Ascending;         case SortBy.AverageDailyDD_Th: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_Th: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_Th: return OrderBy.Descending;         case SortBy.AverageDailyProfit_Fr: return OrderBy.Ascending;         case SortBy.AverageDailyDD_Fr: return OrderBy.Descending;         case SortBy.AverageDailyProfitTrades_Fr: return OrderBy.Ascending;         case SortBy.AverageDailyDDTrades_Fr: return OrderBy.Descending;         default: throw new ArgumentException($"Unaxpected Sortby variable {sortBy}");     } }

Wie Sie aus dem Code ersehen können, hat sich die Sortierrichtung für die ausgewählten Koeffizienten geändert. In der vorherigen Implementierung waren sie als absteigend sortiert gekennzeichnet. Ich habe jedoch nicht berücksichtigt, dass diese Daten einen negativen Wert haben und dass sie in aufsteigender, nicht in absteigender Reihenfolge sortiert werden sollten. Um die Logik zu verstehen, sehen Sie sich bitte den folgenden Code an, der die Sortierung für jeden der übergebenen Werte implementiert:  

// If the minimum is below zero, shift all data by the negative minimum value
if (mm.Min < 0) {     value += Math.Abs(mm.Min);     mm.Max += Math.Abs(mm.Min); } // If the maximum is greater than zero, calculate if (mm.Max > 0) {     // Calculate the coefficient according to the sorting method     if (GetSortingDirection(item.Key) == OrderBy.Descending)     {         // Calculate the coefficient to sort in descending order         data.SortBy += (1 - value / mm.Max) * coef;     }     else     {         // Calculate the coefficient to sort in ascending order         data.SortBy += value / mm.Max * coef;     } }

'Value' ist ein numerischer Wert eines bestimmten Koeffizienten. Prüfen Sie vor dem Sortieren der Daten, ob der Minimalwert aus dem Array des zum Sortieren ausgewählten Koeffizienten negativ ist. Ist dies der Fall, konvertieren Sie diese Werte in die positive Ebene, indem Sie sie um den Wert des minimalen Koeffizienten nach oben verschieben. So erhalten wir ein Array mit Werten im Bereich [0 ; (Max + |Min|)]. Bei der Berechnung des resultierenden Koeffizienten, nach dem die endgültige Sortierung durchgeführt wird, schieben wir das Datenarray in den Bereich [0; 1], indem wir jeden i-ten Wert durch den Maximalwert aus dem Sortierdatenarray dividieren. Wenn die Sortiermethode absteigend ist, dann subtrahieren wir den resultierenden Wert von eins, wodurch das Array der resultierenden Gewichte invertiert wird. Das ist der Grund, warum die vorherige Version der Datensortierung nicht korrekt ist: Aufgrund der implementierten Multifaktor-Sortierlogik haben wir einfach das Array der Gewichte umgekehrt, was für die im obigen Code markierten Koeffizienten nicht erforderlich war. Die Sortiermethode wird im ersten Artikel ausführlicher beschrieben. Der Einfachheit halber wurden der Name der Methode und der Rückgabetyp in passendere geändert, was aber die Anwendungslogik in keiner Weise beeinflusst.  

Der zweite Fehler war der Teil des Codes, der das Array mit den Optimierungsergebnissen in dem Fall sortiert, wenn nur ein Sortierkriterium ausgewählt wurde. Die vorherige Implementierung sah wie folgt aus:

if (order == OrderBy.Ascending)     return results.OrderBy(x => x.GetResult(sortingFlags.ElementAt(0))); else     return results.OrderByDescending(x => x.GetResult(sortingFlags.ElementAt(0)));

Der aktuelle sieht so aus:

if (order == GetSortingDirection(sortingFlags.ElementAt(0)))     return results.OrderBy(x => x.GetResult(sortingFlags.ElementAt(0))); else     return results.OrderByDescending(x => x.GetResult(sortingFlags.ElementAt(0)));

Die bisherige Version berücksichtigte nicht die mit der Methode GetSortingDirection angegebenen Richtungen. Die neue sortiert nach diesem Kriterium. Wenn wir z. B. die absteigende Sortierung auswählen (beste Ergebnisse oben), dann wird für SortBy.PL die gewünschte absteigende Sortierung durchgeführt, und der höchste Wert steht oben. Für den Parameter SortBy.MaxDDTotalTrades (Gesamtzahl der unrentablen Geschäfte) wird jedoch der kleinste Wert oben stehen, und das Array wird in aufsteigender Reihenfolge sortiert. Dadurch bleibt die logische Struktur erhalten. Wenn wir z. B. SortBy.MaxDDTotalTrades als Kriterium wählen, hätten wir nach der bisherigen Sortierlogik die schlechtesten Durchläufe erhalten. 

Automatisches Laden von Roboterparametern und neue Schreibregeln für Expert Advisors

Die neue Parameterladelogik wird in der Datei "AutoUploader2.mqh" bereitgestellt. Nach der Beschreibung des Mechanismus folgt ein Beispiel auf der Grundlage des im vierten Artikel vorgestellten Expert Advisors. 

class CAutoUploader2   { private:                      CAutoUploader2() {}    static CCCM       comission_manager;    static datetime   From,Till;    static TCustomFilter on_tester;    static TCallback on_tick,           on_tester_deinit;    static TOnTesterInit on_tester_init;    static string     frame_name;    static long       frame_id;    static string     file_name;    static bool       FillInData(Data &data);    static void       UploadData(const Data &data, double custom_coef, const BotParams &params[]); public:    static void       OnTick();    static double     OnTester();    static int        OnTesterInit();    static void       OnTesterDeinit();    static void       SetUploadingFileName(string name);    static void       SetCallback(TCallback callback, ENUM_CALLBACK_TYPE type);    static void       SetCustomCoefCallback(TCustomFilter custom_filter_callback);    static void       SetOnTesterInit(TOnTesterInit on_tester_init_callback);    static void       AddComission(string symbol,double comission,double shift);    static double     GetComission(string symbol,double price,double volume);    static void       RemoveComission(string symbol);   }; datetime CAutoUploader2::From = 0; datetime CAutoUploader2::Till = 0; TCustomFilter CAutoUploader2:: EmptyCustomCoefCallback; TCallback CAutoUploader2:: EmptyCallback; TOnTesterInit CAutoUploader2:: EmptyOnTesterInit; TCallback CAutoUploader2:: EmptyCallback; CCCM CAutoUploader2::comission_manager; string CAutoUploader2::frame_name = "AutoOptomiserFrame"; long CAutoUploader2::frame_id = 1; string CAutoUploader2::file_name = MQLInfoString(MQL_PROGRAM_NAME)+"_Report.xml";  

Die neue Klasse hat nur statische Methoden. Dadurch muss sie nicht instanziiert werden, was den EA-Entwicklungsprozess durch Entfernen von unnötigem Code vereinfacht. Diese Klasse hat eine Reihe von statischen Feldern, einschließlich Datumsbereiche ähnlich der vorher verwendeten Klasse, für Details lesen Sie bitte den dritten Artikel), Funktionsreferenzen für Testabschluss-Callbacks, Optimierungs-Frames und neuen Tick-Ankunfts-Callback, eine Kommissionsmanager-Klasse (für Details lesen Sie bitte Artikel #2), Framename und id und den Namen der Datei für das Herunterladen der Optimierungsergebnisse.     

Um den Auto-Optimierer einzubinden, fügen Sie dem EA einen Verweis auf die Datei hinzu, in der einige der Callbacks bereits definiert sind. Wenn der EA einen der Callbacks verwendet, die in dieser Datei definiert sind, besteht die einfachste Lösung darin, eine Funktion mit der Signatur des verwendeten Callbacks und seiner Implementierung zu erstellen und sie dann zu Funktionsreferenzen für Callbacks hinzuzufügen, unter Verwendung spezieller statischer Funktionen.

#ifndef CUSTOM_ON_TESTER double OnTester() { return CAutoUploader2::OnTester(); } #endif #ifndef CUSTOM_ON_TESTER_INIT int OnTesterInit() { return CAutoUploader2::OnTesterInit(); } #endif #ifndef CUSTOM_ON_TESTER_DEINIT void OnTesterDeinit() { CAutoUploader2::OnTesterDeinit(); } #endif #ifndef CUSTOM_ON_TICK void OnTick() { CAutoUploader2::OnTick(); } #endif

Jeder der spezifischen Rückrufe ist in eine Präprozessor-Bedingung verpackt, wodurch seine Definition in dieser Datei durch die Definition der entsprechenden Präprozessor-Bedingung vermieden werden kann. Details zur Implementierung werden in einem weiteren Beispiel beschrieben. 

Wenn Sie sich entscheiden, diese Callbacks selbst zu beschreiben, vergessen Sie nicht, die statischen Methoden der Klasse CAutoUploader2 aufzurufen (wie in diesem Codeschnipsel geschehen) am Anfang des definierten Callbacks. Dies ist für einen korrekten Betrieb des Mechanismus der Berichtserstellung erforderlich. 

Um einen nutzerdefinierten Callback für das Herunterladen von Daten zu aktivieren (wenn Sie keinen eigenen Callback implementiert haben), übergeben Sie einen Zeiger auf die Funktion, die die Implementierungsbeschreibung enthält, an die statische Methode CAutoUploader2::SetCustomCoefCallback. Um Provisionen zu verwalten, verwenden Sie eine der folgenden Methoden. 

static void       AddComission(string symbol,double comission,double shift); static double     GetComission(string symbol,double price,double volume); static void       RemoveComission(string symbol);

Dies ist alles über die Funktionalität. Lassen Sie uns nun sehen, wie es funktioniert.

int CAutoUploader2::OnTesterInit(void) { return on_tester_init(); }

Der Expert Advisor ruft im OnTesterInit-Callback die Methode CAutoUploader2::OnTesterInit auf (wenn die Optimierung gestartet wurde), wo er den übergebenen Funktionszeiger oder eine leere Funktion aufruft, wenn diese standardmäßig ersetzt wurde.

void CAutoUploader2::OnTick(void)   {    if(MQLInfoInteger(MQL_OPTIMIZATION)==1 ||       MQLInfoInteger(MQL_TESTER)==1)      {       if(From == 0)          From = iTime(_Symbol,PERIOD_M1,0);       Till=iTime(_Symbol,PERIOD_M1,0);      }    on_tick();   }

Dann wird bei jedem Tick die reale Zeit des Optimierungsstarts in den entsprechenden Variablen gespeichert. Dann ruft der EA die Methode on_tick auf, die als Callback für die Ankunft eines neuen Ticks gesendet wurde, oder einen leeren Standard-Callback. Optimierungszeit wird nur eingespart, wenn der EA im Tester läuft. 

double CAutoUploader2::OnTester(void)   {    double ret = on_tester();    Data data[1];    if(!FillInData(data[0]))       return ret;    if(MQLInfoInteger(MQL_OPTIMIZATION)==1)      {       if(!FrameAdd(frame_name, frame_id, ret, data))          Print(GetLastError());      }    else       if(MQLInfoInteger(MQL_TESTER)==1)         {          BotParams params[];          UploadData(data[0], ret, params, false);         }    return ret;   }

Wenn die Tester-Operation abgeschlossen ist, wird die statische Methode CAutoUploader2::OnTester im OnTester-Callback aufgerufen, wobei die Frames gespeichert werden (wenn es sich um eine Optimierung handelt), oder die Frames in eine Datei geschrieben werden (wenn es sich um einen Test handelt). Handelt es sich um einen Test, dann endet der Prozess beim aktuellen Schritt und das Terminal wird über den in der Konfigurationsdatei übergebenen Befehl geschlossen. Handelt es sich jedoch um einen Optimierungsprozess, dann wird der folgende Endschritt vorgeführt:

input bool close_terminal_after_finishing_optimisation = false; // MetaTrader Auto Optimiser param (must be false if you run it  from terminal) void CAutoUploader2::OnTesterDeinit(void)   {    ResetLastError();    if(FrameFilter(frame_name,frame_id))      {       ulong pass;       string name;       long id;       double coef_value;       Data data[];       while(FrameNext(pass,name,id,coef_value,data))         {          string parameters_list[];          uint params_count;          BotParams params[];          if(FrameInputs(pass,parameters_list,params_count))            {             for(uint i=0; i<params_count; i++)               {                string arr[];                StringSplit(parameters_list[i],'=',arr);                BotParams item;                item.name = arr[0];                item.value = arr[1];                ADD_TO_ARR(params,item);               }            }          else             Print("Can`t get params");          UploadData(data[0], coef_value, params, true);         }       CheckRetMessage(ReportWriter::WriteReportData(get_path_to_expert(),                       CharArrayToString(data[0].currency),                       data[0].balance,                       data[0].laverage,                       TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+file_name));      }    else      {       Print("Can`t select apropriate frames. Error code = " + IntegerToString(GetLastError()));       ResetLastError();      }    on_tester_deinit();    if(close_terminal_after_finishing_optimisation)      {       if(!TerminalClose(0))         {          Print("===================================");          Print("Can`t close terminal from OnTesterDeinit error number: " +                IntegerToString(GetLastError()) +                " Close it by hands");          Print("===================================");         }      }    ExpertRemove();   }

Der letzte Optimierungsschritt ist der Aufruf der statischen Methode CAutoUploader2::OnTesterDeinit(). In dieser Methode werden alle gespeicherten Frames gelesen und die endgültige Datei mit dem Optimierungsbericht gebildet. Zunächst wird der vorherige Fehler zurückgesetzt und die Frames nach Namen und IDs gefiltert. Dann wird jeder Frame in einer Schleife gelesen und seine gespeicherten Daten erhalten, die dann in eine Datei geschrieben werden.

Nach dem Lesen der Daten lesen Sie die Eingabeparameter des EA für diesen Optimierungsdurchlauf und fügen die erhaltenen Informationen in die Sammlung der statischen Klasse auf der C#-Seite ein. Nach dem Verlassen der Schleife schreiben Sie die gebildete Sammlung in eine Datei mit dem Methodenaufruf ReportWriter::WriteReportData. Anschließend wird der übergebene nutzerdefinierte Callback oder eine leere Standardreferenz aufgerufen. Dieser Ansatz hat ein Problem: Damit der Auto-Optimierer arbeiten kann, muss er das Terminal neu starten können, wofür das Terminal zunächst heruntergefahren werden sollte.

Bisher wurde das Flag in der Konfigurationsdatei auf true gesetzt, um dieses Problem zu lösen. Dies ist jedoch bei der Arbeit mit Frames nicht möglich, da deren endgültige Verarbeitung erst nach dem Stoppen der Optimierung beginnt, und wenn das erforderliche Flag der Konfigurationsdatei auf true gesetzt ist, können wir sie nicht verarbeiten, da das Terminal heruntergefahren wird, bevor die Methode OnTerderDeinit abgeschlossen ist. Um das Problem zu lösen, habe ich eine Eingabevariable hinzugefügt, die dem Expert Advisor zusammen mit der Include-Datei hinzugefügt wird. Diese Variable wird vom Auto-Optimierer geändert und sollte nicht manuell oder im Code geändert werden. Wenn sie auf true gesetzt ist, dann wird die Methode zum Schließen des Terminals von MQL5 aufgerufen, ansonsten wird das Terminal nicht geschlossen. Nach allen beschriebenen Situationen wird der EA, der Frames verarbeitet, aus dem Chart entfernt. 

Die Methode UploadData fungiert sowohl als Methode, die Daten zur Sammlung hinzufügt, als auch als Methode, die einen bestimmten Testerpass in eine Datei herunterlädt, wenn es sich um einen Test und nicht um eine Optimierung handelt. 

void CAutoUploader2::UploadData(const Data &data, double custom_coef, const BotParams &params[], bool is_appent_to_collection)   {    int total = ArraySize(params);    for(int i=0; i<total; i++)       ReportWriter::AppendBotParam(params[i].name,params[i].value);    ReportWriter::AppendMainCoef(custom_coef,data.payoff,data.profitFactor,data.averageProfitFactor,                                 data.recoveryFactor,data.averageRecoveryFactor,data.totalTrades,                                 data.pl,data.dd,data.altmanZScore);    ReportWriter::AppendVaR(data.var_90,data.var_95,data.var_99,data.mx,data.std);    ReportWriter::AppendMaxPLDD(data.max_profit,data.max_dd,                                data.totalProfitTrades,data.totalLooseTrades,                                data.consecutiveWins,data.consequtiveLoose);    ReportWriter::AppendDay(MONDAY,data.averagePl_mn,data.averageDd_mn,                            data.numberProfitTrades_mn,data.numberLooseTrades_mn);    ReportWriter::AppendDay(TUESDAY,data.averagePl_tu,data.averageDd_tu,                            data.numberProfitTrades_tu,data.numberLooseTrades_tu);    ReportWriter::AppendDay(WEDNESDAY,data.averagePl_we,data.averageDd_we,                            data.numberProfitTrades_we,data.numberLooseTrades_we);    ReportWriter::AppendDay(THURSDAY,data.averagePl_th,data.averageDd_th,                            data.numberProfitTrades_th,data.numberLooseTrades_th);    ReportWriter::AppendDay(FRIDAY,data.averagePl_fr,data.averageDd_fr,                            data.numberProfitTrades_fr,data.numberLooseTrades_fr);    if(is_appent_to_collection)      {       ReportWriter::AppendToReportData(_Symbol,                                        data.tf,                                        data.startDT,                                        data.finishDT);       return;      }    CheckRetMessage(ReportWriter::Write(get_path_to_expert(),                                        CharArrayToString(data.currency),                                        data.balance,                                        data.laverage,                                        TerminalInfoString(TERMINAL_COMMONDATA_PATH)+"\\"+file_name,                                        _Symbol,                                        data.tf,                                        data.startDT,                                        data.finishDT));   }

Wenn das Flag is_appent_to_collection true ist, wird der Durchlauf einfach zur Sammlung hinzugefügt. Wenn es false ist, dann wird der aktuelle Durchlauf in eine Datei hochgeladen. Aus dem obigen Code ist ersichtlich, dass das Flag nur dann gleich true ist, wenn wir Frames auslesen und sie zum schnellen Herunterladen des Berichts in eine Sammlung hinzufügen. Wenn wir den Expert Advisor im Testmodus ausführen, dann wird diese Methode mit dem Parameter false aufgerufen, was bedeutet, dass der Bericht in einer Datei gespeichert werden soll.  

Lassen Sie uns nun sehen, wie wir einen Link zum Herunterladen von Optimierungsberichten mit der neuen Logik hinzufügen können. Betrachten Sie die zuvor erstellte Datei mit einem Expert Advisor aus dem vierten Artikel. Das Einbinden der neuen Methode (abgesehen vom Verweis auf die Include-Datei) nimmt nur 3 Codezeilen in Anspruch, statt der 16 Zeilen aus dem Beispiel in Artikel 4. Was die für das Herunterladen der Daten verwendeten Callbacks betrifft, so hat der EA jetzt die Implementierung des "OnTick"-Callbacks, während alle anderen Callbacks ("OnTester", "OnTesterInit", "OnTesterDeinit") in der Include-Datei implementiert sind. 

//+------------------------------------------------------------------+ //|                                                     SimpleMA.mq5 | //|                        Copyright 2019, MetaQuotes Software Corp. | //|                                             https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2019, MetaQuotes Software Corp." #property link      "https://www.mql5.com" #property version   "1.00" #include <Trade/Trade.mqh> #define CUSTOM_ON_TICK // Tell to uploading system that we implement OnTick callback ourself #include <History manager/AutoUpLoader2.mqh> // Include CAutoUploader #define TESTER_ONLY input int ma_fast = 10; // MA fast input int ma_slow = 50; // MA slow input int _sl_ = 20; // SL input int _tp_ = 60; // TP input double _lot_ = 1; // Lot size // Comission and price shift (Article 2) input double _comission_ = 0; // Comission input int _shift_ = 0; // Shift int ma_fast_handle,ma_slow_handle; const double tick_size = SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE); CTrade trade; //+------------------------------------------------------------------+ //| Custom coeffifient`s creator                                     | //+------------------------------------------------------------------+ double CulculateMyCustomCoef()   {    return 0;   } //+------------------------------------------------------------------+ //| Expert initialization function                                   | //+------------------------------------------------------------------+ int OnInit()   { //--- ...    CAutoUploader2::SetCustomCoefCallback(CulculateMyCustomCoef);    CAutoUploader2::AddComission(_Symbol,_comission_,_shift_); //---    return(INIT_SUCCEEDED);   } //+------------------------------------------------------------------+ //| Expert tick function                                             | //+------------------------------------------------------------------+ void OnTick()   {    CAutoUploader2::OnTick(); // If CUSTOM_ON_TICK was defined    ...   } //+------------------------------------------------------------------+

Rot zeigt das Hinzufügen der Daten-Downloading-Schnittstelle mit der neuen Methode. Wie Sie im Beispiel sehen können, ist der OnTester-Callback noch in der Daten-Downloading-Datei implementiert. Um die Berechnung des nutzerdefinierten Koeffizienten zu ermöglichen, übergeben wir ihm die Methode CulculateMyCustomCoef, die die nutzerdefinierte Logik für die Implementierung dieses Callbacks enthalten soll. Die Implementierung des OnTick-Callbacks verbleibt im Roboter. Dazu wird die Variable CUSTOM_ON_TICK vor einem Verweis auf die Datei definiert, in der der Ablauf des Datendownloads beschrieben ist. Sie können die angehängte Datei verwenden, um die EA-Implementierung genauer zu studieren sowie sie mit der Standardimplementierung und mit einer Implementierung der vorherigen Datendownload-Methode zu vergleichen. 

Änderungen in der Optimierungsstartmethode und andere Verbesserungen

Die neue Version hat eine Reihe von weiteren Verbesserungen. Eine davon ist die Möglichkeit, Optimierungen für mehrere Kühlstellen zu planen.

  • Optimierungszeitplan für die übergebene Kühlstellenliste

 

Diese Verbesserung ermöglicht eine Zeitersparnis durch die Durchführung von Optimierungen an einer Reihe von Assets. Die geplanten Aufgaben werden Tag und Nacht ausgeführt, bis die angegebene Liste endet. Um diese Funktion zu aktivieren, musste ich den in früheren Artikeln beschriebenen Startprozess der Optimierung ändern. Bisher hat das ViewModel nach dem Drücken der Schaltfläche "Start / Stop" die Aufgabe sofort an die Datenmodellmethode weitergeleitet, die einen kompletten Zyklus vom Start der Optimierung bis zum Speichern der Ergebnisse aufrief. Jetzt rufen wir zuerst die Methode auf, die in einer Schleife die übergebene Liste der Parameter durchläuft, und dann startet sie die Optimierungen und speichert sie im entsprechenden Verzeichnis.    

public async void StartOptimisation(OptimiserInputData optimiserInputData, bool isAppend, string dirPrefix, List<string> assets) {     if (assets.Count == 0)     {         ThrowException("Fill in asset name");         OnPropertyChanged("ResumeEnablingTogle");         return;     }     await Task.Run(() =>     {         try         {             if (optimiserInputData.OptimisationMode == ENUM_OptimisationMode.Disabled &&                assets.Count > 1)             {                 throw new Exception("For test there must be selected only one asset");             }             StopOptimisationTougle = false;             bool doWhile()             {                 if (assets.Count == 0 || StopOptimisationTougle)                     return false;                 optimiserInputData.Symb = assets.First();                 LoadingOptimisationTougle = assets.Count == 1;                 assets.Remove(assets.First());                 return true;             }             while (doWhile())             {                 var data = optimiserInputData; // Copy input data                 StartOptimisation(data, isAppend, dirPrefix);             }         }         catch (Exception e)         {             LoadingOptimisationTougle = true;             OnPropertyChanged("ResumeEnablingTogle");м             ThrowException?.Invoke(e.Message);         }     }); }

Nachdem Sie die Liste der Assets übergeben und auf Vollständigkeit geprüft haben, fahren Sie mit dem asynchronen Teil dieser Methode fort. Rufen Sie in einer Schleife die zuvor besprochenen Methode Optimierungsstart auf, die nun synchron ist und somit auf den Abschluss des Optimierungsprozesses wartet. Da sich die übergebene Struktur für die Optimierungsparameter in der Optimiererklasse ändern kann, kopieren Sie diese vor dem Start jeder neuen Optimierung und füttern Sie jede neue Optimierung mit den Ausgangsdaten.

Die Bedingung der Fortsetzung sowie der Ersetzung der Kühlstelle, an der die Optimierung durchgeführt wird, wird von der verschachtelten doWhile()-Funktion ausgeführt. Im Funktionskörper wird die Ausgangsbedingung der Schleife geprüft, der Wert der nächsten Anlage zugewiesen und dann die zuletzt zugewiesene Anlage aus der Liste gelöscht. Bei jeder neuen Schleifeniteration wird also zuerst die Anlage angegeben, für die die Optimierung durchgeführt wird, und dann wird die Optimierung ausgeführt, und so weiter, bis die Liste leer ist oder bis ein Optimierungsabschluss-Signal gesendet wird. In früheren Implementierungen konnte der Optimierungsprozess durch einfaches Beenden des laufenden Prozesses zwangsweise beendet werden. In der aktuellen Implementierung würde der Prozess jedoch zur nächsten Iteration wechseln, anstatt anzuhalten. Deshalb wurden entsprechende Anpassungen an der Optimierungsabbruchmethode vorgenommen. 

/// <summary> /// Complete optimization from outside the optimizer /// </summary> public void StopOptimisation() {     StopOptimisationTougle = true;     LoadingOptimisationTougle = true;     Optimiser.Stop();     var processes = System.Diagnostics.Process.GetProcesses().Where(x => x.ProcessName == "metatester64");     foreach (var item in processes)         item.Kill(); } bool StopOptimisationTougle = false;

Wenn wir nun die Optimierung stoppen, schalten wir einfach dieses Flag auf true. Die Asset-Schleife sieht das Flag und verlässt die laufenden Iterationen. Außerdem müssen wir nach dem Umleiten des Optimierungsstopp-Prozesses auf die Optimierer-Klasse die laufenden Tester-Prozesse beenden, da diese beim dringenden Schließen des Terminals oft einfrieren und bis zum Neustart des Rechners in einem laufenden Zustand bleiben. 

Zu diesem Zweck wird das zusätzliche Flag LoadingOptimisationTougle verwendet. Dieses Flag gibt an, ob die aktuell durchgeführte Optimierung in die grafische Oberfläche geladen werden soll, wie es früher implementiert war. Um den Prozess zu beschleunigen, ist dieses Flag immer false, bis der Prozess zwangsweise gestoppt wird, oder bis das letzte Element aus der übergebenen Liste der Assets erreicht ist. Und erst danach, wenn wir den Optimierungsprozess beenden, werden die Daten in die grafische Oberfläche geladen. 

  • Speichern einer Konfigurationsdatei mit den Startparametern der Optimierung und Löschen des Speichers der geladenen Optimierungen

Die Möglichkeit, bei einer wiederholten Optimierung neue Durchgänge an die vorherigen anzuhängen, anstatt sie neu durchzuführen, ist bereits seit der ersten Version des Programms vorhanden. Um jedoch einen reibungslosen Ablauf zu gewährleisten, sollten neue Optimierungen mit den gleichen Parametern gestartet werden. Zu diesem Zweck habe ich die Option eingeführt, die Parameter von zuvor durchgeführten Optimierungen im gleichen Verzeichnis zu speichern, in dem auch die Optimierungsergebnisse gespeichert sind. In der GUI wurde eine separate Schaltfläche hinzugefügt, die es ermöglicht, diese Einstellungen für eine neue Konfiguration hochzuladen.   

Die folgende Methode der Klasse AutoOptimiserVM wird nach einem Klick auf die Schaltfläche ausgelöst:

private void SetBotParams()
{
    if (string.IsNullOrEmpty(SelectedOptimisation))
        return;

    try
    {
        Status = "Filling bot params";
        OnPropertyChanged("Status");
        Progress = 100;
        OnPropertyChanged("Progress");

        var botParams = model.GetBotParamsFromOptimisationPass(OptimiserSettings.First(x => x.Name == "Available experts").SelectedParam,
                                                                       SelectedOptimisation);
        for (int i = 0; i < BotParams.Count; i++)
        {
            if (!botParams.Any(x => x.Variable == BotParams[i].Vriable))
                continue;

            BotParams[i] = new BotParamsData(botParams.First(x => x.Variable == BotParams[i].Vriable));
        }
    }
    catch (Exception e)
    {
        MessageBox.Show(e.Message);
    }

    Status = null;
    OnPropertyChanged("Status");
    Progress = 0;
    OnPropertyChanged("Progress")
}

Zuerst fordern wir die Liste der EA-Parameter aus dem Datenmodell an. Dann Schleife durch alle in die GUI geladenen Parameter und Prüfung, ob der Parameter in der Liste der empfangenen Parameter vorhanden ist. Wenn der Parameter gefunden wurde, wird er durch einen neuen Wert ersetzt. Das Datenmodell Methode, das die korrekten Parameter der Einstellungsdatei zurückgibt, liest diese aus einem in der ComboBox ausgewählten Verzeichnis, in dem die Datei unter dem Namen "OptimizationSettings.set" gespeichert ist. Diese Datei wird von der Methode, die die Optimierung startet, nach Beendigung dieses Prozesses erzeugt. 

  • Entfernen von zuvor geladenen Optimierungspässen aus dem Speicher

Es wurde auch eine Option hinzugefügt, um Optimierungspässe nach dem Laden zu löschen. Sie benötigen zu viel Platz im RAM. Wenn der Computer über wenig Arbeitsspeicher verfügt, können mehrere Vorwärts- und historische Tests ihn merklich verlangsamen. Um den Ressourcenverbrauch zu minimieren, wurde die Duplizierung von Daten auf Vorwärts- und historischen Optimierungsdurchläufen entfernt. Sie werden jetzt nur noch im Datenmodell gespeichert. Eine spezielle Schaltfläche "Clear loaded results" (Geladene Ergebnisse löschen) wurde zur GUI hinzugefügt, die auf die Methode ClearResults aus dem Datenmodell verweist. 

void ClearOptimisationFields()
{
    if (HistoryOptimisations.Count > 0)
        dispatcher.Invoke(() => HistoryOptimisations.Clear());
    if (ForwardOptimisations.Count > 0)
        dispatcher.Invoke(() => ForwardOptimisations.Clear());
    if (AllOptimisationResults.AllOptimisationResults.Count > 0)
    {
        AllOptimisationResults.AllOptimisationResults.Clear();
        AllOptimisationResults = new ReportData
        {
            AllOptimisationResults = new Dictionary<DateBorders, List<OptimisationResult>>()
        };
    }

    GC.Collect();
}
public void ClearResults()
{
    ClearOptimisationFields();
    OnPropertyChanged("AllOptimisationResults");
    OnPropertyChanged("ClearResults");
}

Die genannte Methode bezieht sich auf die private Methode ClearOptimisationFields, die Sammlungen von Optimierungsberichten in der Klasse AutoOptimiserM leert. Da wir aber C# verwenden, in dem der Speicher nicht manuell, sondern automatisch verwaltet wird, ist es auch notwendig, den Speicher von allen gelöschten Objekten zu leeren, um das Löschen des Arrays und der Daten aus dem Speicher anzuwenden. Dies kann durch Aufruf der statischen Methode Collect der Klasse Garbage Collector (GC) erfolgen. Nach den durchgeführten Aktionen werden die zuvor vorhandenen Objekte aus dem Arbeitsspeicher gelöscht.

  • Generierung einer *.set-Datei des gewünschten Passes.

Nachdem wir uns die generierten Optimierungspässe angesehen haben, müssen wir eine *.set-Datei generieren, um die gewünschten Parameter in einen Expert Advisor einzugeben. Zuvor mussten wir die gefundenen Parameter manuell eingeben oder eine Datei aus dem Tester bilden, indem wir mit einem Doppelklick auf die ausgewählte Optimierungszeile einen Test starteten.

Die Tabelle oberhalb der Schaltfläche enthält eine Schlüssel-Wert-Liste, in der die Optimierungsparameter gespeichert sind. Mit einem Klick auf die Schaltfläche rufen Sie eine Methode aus dem Datenmodell auf, der die Liste aus der obigen Tabelle übergeben wird.

public void SaveBotParams(IEnumerable<KeyValuePair<string, string>> data, string path)
{
    SetFileManager setFileManager = new SetFileManager(path, true)
    {
        Params = data.Select(x => new ParamsItem { Variable = x.Key, Value = x.Value }).ToList()
    };

    setFileManager.SaveParams();
}

Diese Methode erhält den Pfad zur erstellten Datei, wandelt das Schlüsselwert-Array in eine Struktur mit EA-Parametern und speichert diese unter dem angegebenen Pfad. Der Pfad zur Datei wird über die Standard-Schnittstelle zum Speichern von Dateien aus dem ViewModel gesetzt.


Schlussfolgerung

Der Artikel kam viel später heraus als ich geplant hatte, aber ich hoffe, dass er für alle, die dieses Projekt verfolgt haben, interessant und nützlich ist. Ich werde ihn weiter entwickeln und weitere Verbesserungsideen umsetzen. Eine davon ist, die automatische Filterung von Optimierungen hinzuzufügen, die Suche nach den besten Parametern basierend auf den retrospektiven Ergebnissen von Walk-Forward-Optimierungen zu implementieren, sowie die Sammlung eines Portfolios von durchgeführten Optimierungen zu ermöglichen. Liebe Leser, wenn dieses Projekt für Sie interessant ist, werde ich seine Entwicklung fortsetzen. Das Projekt in seinem aktuellen Zustand ist einsatzbereit und ermöglicht es, den Prozessor zu 100% auszulasten und mit mehreren Anlagen gleichzeitig zu optimieren, wobei sowohl Walk-Forward- als auch reguläre historische Optimierungen verwendet werden.


Der Anhang enthält das vollständige Auto-Optimizer-Projekt mit einem Test-Expert Advisor, der in Artikel 4 besprochen wurde. Wenn Sie den EA verwenden möchten, kompilieren Sie das Auto-Optimizer-Projekt und den Test-Roboter. Kopieren Sie dann ReportManager.dll (wie im ersten Artikel beschrieben) in das Verzeichnis MQL5/Libraries, und Sie können mit dem Testen des EA beginnen. Wie Sie den Auto-Optimierer mit Ihren Expert Advisors verbinden, erfahren Sie in den Artikeln 3 und 4 dieser Serie.

Hier ist die Beschreibung des Kompilierungsprozesses für all diejenigen, die nicht mit VisualStudio gearbeitet haben. Das Projekt kann in VisualStudio auf verschiedene Arten kompiliert werden, hier sind drei davon:

  1. Am einfachsten ist es, STRG+UMSCHALT+B zu drücken.
  2. Eine visuellere Methode ist das Klicken auf das grüne Feld im Editor — dadurch wird die Anwendung im Code-Debug-Modus gestartet und die Kompilierung durchgeführt (wenn der Kompilierungsmodus Debuggen ausgewählt ist).
  3. Eine andere Möglichkeit ist die Verwendung des Befehls Build aus dem Menü.

    Das kompilierte Programm befindet sich dann im Ordner MetaTrader Auto Optimiser/bin/Debug (oder MetaTrader Auto Optimiser/bin/Release — abhängig von der gewählten Kompilierungsmethode).



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

    Beigefügte Dateien |
    Data.zip (142.39 KB)
    Grundlegende Mathematik hinter dem Forex-Handel Grundlegende Mathematik hinter dem Forex-Handel
    Der Artikel zielt darauf ab, die Hauptmerkmale des Forex-Handels so einfach und schnell wie möglich zu beschreiben sowie einige grundlegende Ideen mit Anfängern zu beschreiben. Er versucht auch, die quälendsten Fragen in der Trading-Community zu beantworten und zeigt die Entwicklung eines einfachen Indikators.
    Fortschrittliches Resampling und Auswahl von CatBoost-Modellen durch die Brute-Force-Methode Fortschrittliches Resampling und Auswahl von CatBoost-Modellen durch die Brute-Force-Methode
    Dieser Artikel beschreibt einen der möglichen Ansätze zur Datentransformation mit dem Ziel, die Verallgemeinerbarkeit des Modells zu verbessern, und erörtert auch die Stichprobenziehung und Auswahl von CatBoost-Modellen.
    Parallele Partikelschwarmoptimierung Parallele Partikelschwarmoptimierung
    Der Artikel beschreibt eine Methode zur schnellen Optimierung unter Verwendung des Partikelschwarm-Algorithmus. Er stellt auch die Implementierung der Methode in MQL vor, die sowohl im Single-Thread-Modus innerhalb eines Expert Advisors als auch in einem parallelen Multi-Thread-Modus als Add-on, das auf lokalen Tester-Agenten läuft, verwendet werden kann.
    Zeitreihen in der Bibliothek DoEasy (Teil 51): Zusammengesetzte Standardindikatoren für mehrere Symbole und Perioden Zeitreihen in der Bibliothek DoEasy (Teil 51): Zusammengesetzte Standardindikatoren für mehrere Symbole und Perioden
    Der Artikel vervollständigt die Entwicklung von Objekten der Standardindikatoren für mehrere Symbole und Perioden. Anhand des Standardindikators Ichimoku Kinko Hyo analysieren wir beispielsweise die Erstellung von zusammengesetzten, nutzerdefinierten Indikatoren, die über gezeichnete Hilfspuffer zur Anzeige von Daten auf dem Chart verfügen.