Kontinuierliche Walk-Forward-Optimierung (Teil 6): Logikteil und die Struktur des Auto-Optimizers

Andrey Azatskiy | 29 Juli, 2020

Einführung

Wir beschreiben weiterhin das Erstellen eines Auto-Optimizers, der die kontinuierliche Walk-Forward-Optimierung implementiert. Im vorherigen Artikel analysierten wir die grafische Oberfläche der resultierenden Anwendung, wobei wir jedoch ihren logischen Teil und ihre interne Struktur nicht berücksichtigt haben. Dies wird in diesem Artikel beschrieben. Die vorherigen Artikel innerhalb dieser Serie:

  1. Kontinuierliche Walk-Forward-Optimierung (Teil 1): Arbeiten mit Optimierungsberichten
  2. Kontinuierliche Walk-Forward-Optimierung (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

Wir werden UML-Diagramme verwenden, um die interne Struktur der Anwendung und die von der Anwendung während des Betriebs durchgeführten Aufrufe zu beschreiben. Bitte beachten Sie, dass der Zweck der Diagramme darin besteht, eine schematische Darstellung der Hauptobjekte und der Beziehungen zwischen ihnen zu liefern, nicht aber darin, jedes bestehende Objekt zu beschreiben.

Interne Struktur der Anwendung, ihre Beschreibung und die Erzeugung von Schlüsselobjekten

Wie im vorigen Artikel erwähnt, ist das Hauptmuster, das in dem resultierenden Programm verwendet wird, MVVM. Nach diesem Muster ist die gesamte Programmlogik in der Datenmodellklasse implementiert, die über eine separate Klasse, die die Objektrolle ViewModel implementiert, mit den Grafiken verbunden ist. Die Programmlogik wird weiter in eine Reihe von Klassen aufgeteilt, die elementare Entitäten sind. Die Hauptprogrammeinheiten, die ihre Logik und die Beziehung zwischen der Anmeldung und der Benutzeroberfläche beschreiben, sind im folgenden UML-Klassendiagramm dargestellt.    


Bevor wir das Diagramm betrachten, wollen wir uns die für verschiedene Objekttypen verwendete Farbangabe ansehen. Blau wird für die Grafikebene verwendet. Dies sind die Objekte, die das XAML-Markup mit allen darin verborgenen WPF-Mechanismen darstellen, die weder für den Endbenutzer noch für den Entwickler sichtbar sind. Lila wird für die Ebene verwendet, die die Anwendungsgrafik mit ihrer Logik verbindet. Mit anderen Worten, es ist die ViewModel-Schicht aus dem verwendeten MVVM-Modell. Rosa wird verwendet, um die Schnittstellen zu zeigen, die abstrakte Darstellungen von dahinter verborgenen Daten sind.

Die erste von ihnen (IMainModel) verbirgt eine spezifische Implementierung des Datenmodells. Gemäß der Hauptidee des MVVM-Musters muss das Datenmodell so unabhängig wie möglich sein, während ViewModel nicht von der spezifischen Implementierung dieses Modells abhängen sollte. Das zweite (IOptimiser) ist eine Schnittstelle der Optimierungslogik, denn gemäß einer der Programmideen können mehrere Optimierungen ausgeführt und Logiken ausgewählt werden, und der Benutzer kann sie durch Auswahl des entsprechenden Optimizers aus dem Kombinationsfeld ändern.

Braun wird für die Schicht verwendet, die das Datenmodell in grafischen Oberflächen darstellt. Wie Sie sehen können, gibt es im Diagramm zwei Datenmodelle: das erste bezieht sich auf den automatischen Optimizer selbst und das zweite auf die grafische Oberfläche des Optimizers. Gelb wird für den einzigen Optimierungsmanager verwendet, der zur Zeit existiert. Es kann jedoch mehrere Optimierungsmanager geben. Sie können auch Ihre eigene Optimierungslogik implementieren (die Methode zur Implementierung des Mechanismus wird in weiteren Artikeln betrachtet). Grün wird für Hilfsobjekte verwendet, die zur Erzeugung dienen und das Erstellen von Objekten implementieren, die im aktuellen Moment benötigt werden.

Betrachten wir ferner die Beziehungen zwischen den Objekten und dem Prozess ihrer Erstellung während der Einführung der Anwendung. Zuvor müssen wir die grafische Ebene und ihre Komponenten betrachten:


Dies sind die ersten fünf Objekte, die in der Abbildung gezeigt werden. Die Klasse AutoOptimiser wird beim Start der Anwendung zuerst instanziiert. Diese Klasse erstellt eine grafische Schnittstelle. Das XAML-Markup der grafischen Oberfläche enthält eine Referenz auf das AutoOptimiserVM-Objekt, das als ViewModel fungiert. Daher wird bei der Erstellung der grafischen Ebene auch die AutoOptimiserVM-Klasse erstellt, während die grafische Ebene vollständig im Besitz der Klasse ist. Dieses Objekt existiert so lange, bis es nach der Zerstörung der grafischen Oberfläche zerstört wird. Es ist mit der Klasse AutoOptimiser (unserem Fenster) über "Composition" verbunden, was den vollständigen Besitz und die Kontrolle über das Objekt impliziert.  

Die Klasse ViewModel muss Zugriff auf die Model-Klasse haben, aber die Datenmodell-Klasse muss unabhängig von ViewModel bleiben. Mit anderen Worten, sie muss nicht wissen, welche Klasse das Datenmodell bereitstellt. Stattdessen kennt die Klasse ViewModel die Modellschnittstelle, die eine Reihe von öffentlichen Methoden, Ereignissen und Eigenschaften enthält, die unser Mediator verwenden kann. Deshalb ist diese Klasse nicht direkt mit der Klasse MainModel verbunden, sondern mit ihrer Schnittstelle über die "Aggregations"-Beziehung, nach der die analysierte Klasse zu der Klasse gehört, die sie verwendet.

Einer der Unterschiede zwischen Aggregation und Komposition besteht jedoch darin, dass die analysierte Klasse zu mehr als einem Objekt gleichzeitig gehören kann und ihr Lebensdauerprozess nicht durch Containerobjekte gesteuert wird. Diese Aussage trifft voll und ganz auf die Klasse MainModel zu, da sie in ihrem statischen Konstruktor (der Klasse MainModelCreator) erstellt und gleichzeitig in ihr und in der Klasse AutoOptimiserVM gespeichert wird. Das Objekt wird zerstört, wenn die Anwendung ihre Arbeit abschließt. Das liegt daran, dass es ursprünglich in einer statischen Eigenschaft implementiert wurde, die erst beim Beenden der Anwendung gelöscht wird.   

Wir haben die Beziehung zwischen drei Schlüsselobjekten betrachtet: Model — View — ViewModel. Der Rest des Diagramms ist der Haupt-Geschäftslogik unserer Anwendung gewidmet. Es stellt die Beziehung zwischen den für den Optimierungsprozess verantwortlichen Objekten und dem Datenmodell-Objekt dar. Die für den Optimierungssteuerungsprozess verantwortlichen Objekte dienen als eine Art Controller, der die erforderlichen Prozesse startet und ihre Ausführung an einzelne Programmobjekte delegiert. Eines von ihnen ist der Optimizer. Der Optimizer ist auch ein Manager, der die Ausführung von Aufgaben an aufgabenorientierte Objekte delegiert, wie z. B. den Terminalstart oder die Generierung der für den Terminalstart erforderlichen Konfigurationsdatei. 


Während der Instanziierung der Klasse MainModel instanziieren wir auch die Klasse des Optimizers unter Verwendung des bereits bekannten Mechanismus der statischen Konstruktoren. Wie aus dem Diagramm ersichtlich ist, sollte die Klasse des Optimizers die Schnittstelle IOptimiser implementieren und eine Konstruktorklasse haben, die von OptimiserCreator abgeleitet ist — sie wird eine spezifische Instanz des Optimizers erzeugen. Dies ist für die Implementierung der dynamischen Substitution von Optimizern im Programmausführungsmodus erforderlich.

Jeder der Optimizer kann über eine individuelle Optimierungslogik verfügen. Die Logik des aktuellen Optimizers und die Implementierung von Optimizern werden in zukünftigen Artikeln ausführlich behandelt. Kommen wir nun zurück zur Architektur. Die Datenmodellklasse ist über die Assoziationsbeziehung mit der Basisklasse aller Modellkonstruktoren verbunden, d.h. die Datenmodellklasse verwendet die Konstruktoren von Optimizern, die auf ihre Basisklasse gegossen wurden, um eine bestimmte Instanz des Optimizers zu erzeugen.

Der angelegte Optimizer wird auf seinen Schnittstellentyp geprüft und im entsprechenden Feld der Klasse MainModel gespeichert. Durch die Verwendung von Abstraktion bei der Objekterstellung (Objektkonstruktoren) und Instanzerstellung (Optimizer) bieten wir also die Möglichkeit der dynamischen Substitution von Optimierern während des Programmablaufs. Der verwendete Ansatz wird als "Abstract Factory" bezeichnet. Seine Idee ist, dass sowohl das Produkt (die Klasse, die die Optimierungslogik implementiert) als auch seine Fabriken (Klassen, die das Produkt erzeugen) ihre eigene Abstraktion haben. Die Nutzerklasse muss nicht über die spezifische Implementierung der Logik beider Komponenten Bescheid wissen, aber sie muss in der Lage sein, ihre unterschiedlichen Implementierungen zu verwenden.

Als Beispiel aus dem wirklichen Leben können wir Sprudelwasser, Tee, Kaffee oder ähnliche Produkte sowie Fabriken, die sie herstellen, verwenden. Eine Person muss die spezifische Herstellungsmethode der Getränke nicht kennen, um sie zu trinken. Ebenso wenig braucht die Person eine bestimmte interne Struktur der Fabriken, die die Getränke herstellen, oder des Ladens, in dem sie verkauft werden, zu kennen. In diesem Beispiel:

In unserem Programm ist der Nutzer die Klasse MainModel.


Wenn Sie sich die Standardimplementierung des Optimizers ansehen, werden Sie sehen, dass sie auch eine grafische Oberfläche mit Einstellungen hat (die durch einen Klick auf die "GUI"-Schaltfläche neben der ComboBox aufgerufen wird, wo alle Optimizer aufgezählt werden). Im Diagramm der Klassen (und im Code) wird der grafische Teil der Optimierungseinstellungen "SimpleOptimiserSettings" genannt, während ViewModel und View "SimpleOptimiserVM" bzw. "SimpleOptimiserM" genannt werden. Wie aus dem Diagramm der Klassen ersichtlich ist, ist ViewModel der Optimierungseinstellungen vollständig im Besitz des grafischen Teils und somit über die "Composition"-Beziehung verbunden. Der View-Teil ist vollständig im Besitz des Optimizers und ist mit der Manager-Klasse über die "Composition"-Beziehung verbunden. Ein Teil des Datenmodells der Optimierungseinstellungen gehört sowohl dem Optimizer als auch dem ViewModel, weshalb er mit beiden eine "Aggregations"-Beziehung hat. Dies geschieht absichtlich, um dem Optimizer den Zugriff auf die im grafischen Datenmodell der Optimierungseinstellungen gespeicherten Einstellungen zu ermöglichen.      

Zur Vervollständigung des Kapitels stelle ich hier ein Sequenzdiagramm zur Verfügung, das den Instantiierungsprozess der oben besprochenen Objekte zeigt.


Das Diagramm sollte von oben nach unten gelesen werden. Der Startpunkt des angezeigten Prozesses ist Instance, die den Startzeitpunkt der Anwendung mit der Instanziierung der Grafikschicht des Hauptoptimierungsfensters zeigt. Während der Instanziierung instanziiert die grafische Oberfläche die Klasse SimpleOptimiserVM, da sie als DataContext des Hauptfensters deklariert ist. Während der Instanziierung ruft SimpleOptimiserVM die statische Eigenschaft MainModelCreator.Model auf, die wiederum das Objekt MainModel erzeugt und es auf den Schnittstellentyp IMainModel wirft.

Zum Zeitpunkt der Instanzierung der Klasse von MainModel wird eine Liste von Optimizer-Konstruktoren erstellt. Dies ist die in der ComboBox angezeigte Liste, die die Auswahl des gewünschten Optimizers ermöglicht. Nach der Instantiierung des Datenmodells wird der Klassenkonstruktor SimpleOptimiserVM aufgerufen, der die Methode ChangeOptimiser aus der durch den Schnittstellentyp IMainModel dargestellten Datenmodell aufruft. Die Methode ChangeOptimiser ruft die Methode Create() des ausgewählten Konstruktors des Optimizers auf. Da wir den Start der Anwendung betrachten, ist der ausgewählte Optimizer-Konstruktor der erste aus der angegebenen Liste. Durch Aufruf der Methode Create des gewünschten Optimizer-Konstruktors delegieren wir die Erstellung des spezifischen Typs des Optimizer an den Konstruktor. Er erstellt den Optimizer, gibt das Objekt Optimizer an den Schnittstellentyp zurück und übergibt es an das Datenmodell, wo es in der entsprechenden Eigenschaft gespeichert wird. Danach ist die Operation der Methode ChangeOptimiser abgeschlossen, und wir können zum Klassenkonstruktor SimpleOptimiserVM zurückkehren.

Die Klasse Model und der logische Programmteil

Wir haben die allgemeine Struktur der resultierenden Anwendung und den Prozess der Erstellung der Hauptobjekte zum Zeitpunkt der Einführung der Anwendung betrachtet. Nun wollen wir uns mit den Einzelheiten der logischen Implementierung befassen. Alle Objekte, die die Logik der erstellten Anwendung beschreiben, befinden sich im Verzeichnis "Model". In der Wurzel des Verzeichnisses befindet sich die Datei "MainModel.cs", die die Datenmodellklasse enthält, die der Ausgangspunkt für den Start der gesamten Ablauflogik der Anwendung ist. Ihre Implementierung enthält mehr als 1000 Codezeilen, daher werde ich hier nicht den gesamten Klassencode, sondern nur die Implementierungen der einzelnen Methoden bereitstellen. Die Klasse wird von der Schnittstelle IMainModel abgeleitet. Hier ist der Code der Schnittstelle, der ihre Struktur demonstriert.

/// <summary>
/// Data model interface of the main optimizer window
/// </summary>    
interface IMainModel : INotifyPropertyChanged
{
    #region Getters
    /// <summary>
    /// Selected optimizer
    /// </summary>
    IOptimiser Optimiser { get; }
    /// <summary>
    /// The list of names of terminals installed on the computer
    /// </summary>
    IEnumerable<string> TerminalNames { get; }
    /// <summary>
    /// The list of names of optimizers available for usage
    /// </summary>
    IEnumerable<string> OptimisatorNames { get; }
    /// <summary>
    /// The list of names of directories with saved optimizations (Data/Reports/*)
    /// </summary>
    IEnumerable<string> SavedOptimisations { get; }
    /// <summary>
    /// Structure with all passes of optimization results
    /// </summary>
    ReportData AllOptimisationResults { get; }
    /// <summary>
    /// Forward tests
    /// </summary>
    List<OptimisationResult> ForwardOptimisations { get; }
    /// <summary>
    /// Historical tests
    /// </summary>
    List<OptimisationResult> HistoryOptimisations { get; }
    #endregion

    #region Events
    /// <summary>
    /// Event of exception throw form the data model
    /// </summary>
    event Action<string> ThrowException;
    /// <summary>
    /// Optimization stop error
    /// </summary>
    event Action OptimisationStoped;
    /// <summary>
    /// Event of progress bar update form the data model
    /// </summary>
    event Action<string, double> PBUpdate;
    #endregion

    #region Methods
    /// <summary>
    /// Method loading previously saved optimization results
    /// </summary>
    /// <param name="optimisationName">The name of the required report</param>
    void LoadSavedOptimisation(string optimisationName);
    /// <summary>
    /// Method changing the previously selected terminal
    /// </summary>
    /// <param name="terminalName">ID of the requested terminal</param>
    /// <returns></returns>
    bool ChangeTerminal(string terminalName);
    /// <summary>
    /// Optimizer change method
    /// </summary>
    /// <param name="optimiserName">Optimizer name</param>
    /// <param name="terminalName">Terminal name</param>
    /// <returns></returns>
    bool ChangeOptimiser(string optimiserName, string terminalName = null);
    /// <summary>
    /// Optimization start
    /// </summary>
    /// <param name="optimiserInputData">Input data to launch optimization</param>
    /// <param name="IsAppend">Flag showing whether to add to existing data (if any) or overwrite them</param>
    /// <param name="dirPrefix">Prefix of the directory with optimizations</param>
    void StartOptimisation(OptimiserInputData optimiserInputData, bool IsAppend, string dirPrefix);
    /// <summary>
    /// Optimization stop from outside (by user)
    /// </summary>
    void StopOptimisation();
    /// <summary>
    /// Get robot parameters
    /// </summary>
    /// <param name="botName">Expert name</param>
    /// <param name="isUpdate">Flag whether file needs to be updated before reading</param>
    /// <returns>List of parameters</returns>
    IEnumerable<ParamsItem> GetBotParams(string botName, bool isUpdate);
    /// <summary>
    /// Saving selected optimizations to the (* .csv) file 
    /// </summary>
    /// <param name="pathToSavingFile">Path to the file to be saved</param>
    void SaveToCSVSelectedOptimisations(string pathToSavingFile);
    /// <summary>
    /// Saving optimizations for the transferred date to the (* csv) file 
    /// </summary>
    /// <param name="dateBorders">Date range borders</param>
    /// <param name="pathToSavingFile">Path to the file to be saved</param>
    void SaveToCSVOptimisations(DateBorders dateBorders, string pathToSavingFile);
    /// <summary>
    /// Start the testing process
    /// </summary>
    /// <param name="optimiserInputData">List of tester setup parameters</param>
    void StartTest(OptimiserInputData optimiserInputData);
    /// <summary>
    /// Start the sorting process
    /// </summary>
    /// <param name="borders">Date range borders</param>
    /// <param name="sortingFlags">Array of parameter names for sorting</param>
    void SortResults(DateBorders borders, IEnumerable<SortBy> sortingFlags);
    /// <summary>
    /// Filtering optimization results
    /// </summary>
    /// <param name="borders">Date range borders</param>
    /// <param name="compareData">Data filtering flags</param>
    void FilterResults(DateBorders borders, IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData);
    #endregion
}

Die Komponenten der Schnittstelle werden durch Direktiven #region abgegrenzt. Die Mitglieder der Schnittstelle sind also in typische Komponenten unterteilt. Wie Sie sehen können, verfügt sie über eine Reihe von Eigenschaften, die verschiedene Informationen aus den vom Datenmodell geregelten Bereichen an die grafische Schnittstelle liefern. Dabei handelt es sich jedoch nur um Getter, die den Zugriff auf Daten einschränken und nur das Lesen dieser Daten erlauben, ohne die Möglichkeit, das zu lesende Objekt zu verändern. Dies wird getan, um eine versehentliche Beschädigung des Datenmodells und der Logik aus dem ViewModel zu verhindern. Eines der interessanten Dinge an den Schnittstelleneigenschaften sind die Listen der Optimierungsergebnisse:

Diese Felder enthalten die Liste der Optimierungen, die in Tabellen auf der Registerkarte Ergebnisse unserer GUI angezeigt werden. Die Liste aller Optimierungsdurchläufe ist in einer speziell erstellten Struktur "ReportData" enthalten:

/// <summary>
/// Structure describing optimization results
/// </summary>
struct ReportData
{
    /// <summary>
    /// Dictionary with optimization passes
    /// key - date range
    /// value - list of optimization passes for the given range
    /// </summary>
    public Dictionary<DateBorders, List<OptimisationResult>> AllOptimisationResults;
    /// <summary>
    /// Expert and Currency
    /// </summary>
    public string Expert, Currency;
    /// <summary>
    /// Deposits
    /// </summary>
    public double Deposit;
    /// <summary>
    /// Leverage
    /// </summary>
    public int Laverage;
}

Zusätzlich zu den Optimierungsdaten beschreibt die Struktur die wichtigsten Optimierungseinstellungen, die für den Start von Tests (durch einen Doppelklick auf den ausgewählten Optimierungsdurchgang) und für den Vergleich von Optimierungsergebnissen beim Hinzufügen neuer Daten mit den zuvor optimierten Daten erforderlich sind.

Außerdem enthält das Datenmodell die Liste der auf dem Computer installierten Terminals, die Namen der zur Auswahl stehenden Optimizer (erzeugt aus den Konstrukteuren dieser Optimizer) und die Liste der zuvor gespeicherten Optimierungen (Namen der Verzeichnisse, die sich unter "Daten/Berichte" befinden). Der Zugriff auf den Optimizer selbst ist ebenfalls möglich.

Der umgekehrte Informationsaustausch (vom Modell zum View-Modell) erfolgt über die Ereignisse, auf die sich das ViewModel nach der Instantiierung des Datenmodells abonniert. Es gibt 4 solcher Ereignisse, 3 davon sind nutzerdefiniert, und eines wird von der Schnittstelle INotifyPropertyChanged abgeleitet. Eine Vererbung von der Schnittstelle INotifyPropertyChanged ist im Datenmodell nicht erforderlich. Aber es liegt nahe, weshalb in diesem Programm die Vererbung verwendet wird.

Eines der Ereignisse ist ThrowException. Ursprünglich wurde es erstellt, um eine Fehlermeldung an den grafischen Teil der Anwendung zu senden und diese dann anzuzeigen, da Sie die Grafiken nicht direkt aus dem Datenmodell heraus steuern sollten. Jetzt wird das Ereignis jedoch auch dazu verwendet, eine Reihe von Textnachrichten an Grafiken aus dem Datenmodell zu übergeben. Dabei handelt es sich nicht um Fehler, sondern um Text-Alerts. Bitte beachten Sie also, daß das Ereignis auch Meldungen weitergibt, die keine Fehler sind. 

Um die Methoden des Datenmodells zu betrachten, betrachten wir die Klasse, die diesen Programmteil implementiert. 

Das erste, was der Optimizer tut, wenn ein neuer Roboter ausgewählt wird, ist das Laden seiner Parameter. Dies geschieht durch die Methode "GetBotParams", die zwei mögliche Logiken implementiert. Sie kann die Konfigurationsdatei mit den Roboterparametern aktualisieren und kann sie einfach lesen. Sie kann auch rekursiv sein. 

/// <summary>
/// Get parameters for the selected EA
/// </summary>
/// <param name="botName">Expert name</param>
/// <param name="terminalName">Terminal name</param>
/// <returns>Expert parameters</returns>
public IEnumerable<ParamsItem> GetBotParams(string botName, bool isUpdate)
{
    if (botName == null)
        return null;

    FileInfo setFile = new FileInfo(Path.Combine(Optimiser
                                   .TerminalManager
                                   .TerminalChangeableDirectory
                                   .GetDirectory("MQL5")
                                   .GetDirectory("Profiles")
                                   .GetDirectory("Tester")
                                   .FullName, $"{Path.GetFileNameWithoutExtension(botName)}.set"));


    try
    {
        if (isUpdate)
        {
            if (Optimiser.TerminalManager.IsActive)
            {
                ThrowException("Wating for closing terminal");
                Optimiser.TerminalManager.WaitForStop();
            }
            if (setFile.Exists)
                setFile.Delete();

            FileInfo iniFile = terminalDirectory.Terminals
                                                .First(x => x.Name == Optimiser.TerminalManager.TerminalID)
                                                .GetDirectory("config")
                                                .GetFiles("common.ini").First();

            Config config = new Config(iniFile.FullName);

            config = config.DublicateFile(Path.Combine(workingDirectory.WDRoot.FullName, $"{Optimiser.TerminalManager.TerminalID}.ini"));

            config.Tester.Expert = botName;
            config.Tester.FromDate = DateTime.Now;
            config.Tester.ToDate = config.Tester.FromDate.Value.AddDays(-1);
            config.Tester.Optimization = ENUM_OptimisationMode.Disabled;
            config.Tester.Model = ENUM_Model.OHLC_1_minute;
            config.Tester.Period = ENUM_Timeframes.D1;
            config.Tester.ShutdownTerminal = true;
            config.Tester.UseCloud = false;
            config.Tester.Visual = false;

            Optimiser.TerminalManager.WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized;
            Optimiser.TerminalManager.Config = config;

            if (Optimiser.TerminalManager.Run())
                Optimiser.TerminalManager.WaitForStop();

            if (!File.Exists(setFile.FullName))
                return null;

            SetFileManager setFileManager = new SetFileManager(setFile.FullName, false);
            return setFileManager.Params;
        }
        else
        {
            if (!setFile.Exists)
                return GetBotParams(botName, true);

            SetFileManager setFileManager = new SetFileManager(setFile.FullName, false);
            if (setFileManager.Params.Count == 0)
                return GetBotParams(botName, true);

            return setFileManager.Params;
        }
    }
    catch (Exception e)
    {
        ThrowException(e.Message);
        return null;
    }
}

Zu Beginn der Methode erstellen wir eine objektorientierte Darstellung der Datei mit Roboterparametern unter Verwendung der Klasse FileInfo, die in der C#-Standardbibliothek verfügbar ist. Entsprechend den Standard-Terminaleinstellungen wird die Datei unter dem Verzeichnis MQL5/Profiles/Tester/{gewählter Robotername}.set gespeichert. Dies ist der Pfad, der zum Zeitpunkt der Erstellung einer objektorientierten Dateidarstellung gesetzt wird. Weitere Aktionen werden in das Konstrukt Try-Catch eingeschlossen, da die Gefahr besteht, dass bei Dateioperationen Fehler ausgelöst werden. Nun wird einer der möglichen Logikzweige in Abhängigkeit vom übergebenen Parameter isUpdate ausgeführt. Wenn isUpdate = true ist, müssen wir die Datei mit Einstellungen aktualisieren, bei denen ihre Werte auf die Standardwerte zurückgesetzt werden, und dann ihre Parameter lesen. Dieser Logikzweig wird ausgeführt, wenn wir im grafischen Teil der Anwendung auf "Datei aktualisieren (*.set)" klicken. Der bequemste Weg, die Datei mit Experteneinstellungen zu aktualisieren, ist, sie neu zu generieren.

Die Datei wird vom Strategietester generiert, wenn sie bei der Auswahl eines Roboters im Tester nicht vorhanden war. Daher brauchen wir nur den Tester nach dem Löschen der Datei neu zu starten, dann zu warten, bis die Datei generiert ist, den Tester zu schließen und seinen Standardwert zurückzugeben. Zuerst prüfen wir, ob das Terminal läuft. Wenn es läuft, dann zeigen wir die entsprechende Meldung an und warten auf die Fertigstellung. Dann prüfen wir, ob die Datei mit den Parametern existiert. Wenn es eine solche Datei gibt, löschen wir sie.

Dann füllen wir die Konfigurationsdatei für den Terminalstart, wobei wir die bereits bekannte Config verwenden, die in früheren Artikeln besprochen wurde. Dabei achten wir auf Daten, die in die Konfigurationsdatei geschrieben werden. Wir starten den Test im Terminal, aber das Startdatum des Tests wird 1 Tag früher als das Enddatum angegeben. Aus diesem Grund startet der Tester und erzeugt eine Datei mit den erforderlichen Einstellungen. Dann startet er den Test nicht und schließt seine Operation ab, woraufhin wir die Datei lesen können. Nachdem die Konfigurationsdatei erstellt und vorbereitet wurde, wird die Klasse TerminalManager verwendet, um den Prozess der Generierung von Einstellungsdateien zu starten (der Prozess wurde bereits früher besprochen). Sobald die Generierung der Datei abgeschlossen ist, verwenden wir die Klasse SetFileManager, um die Datei mit den Einstellungen zu lesen und ihren Inhalt zurückzugeben.

Wenn ein weiterer Logikzweig benötigt wird, nach dem die explizite Generierung einer Einstellungsdatei nicht erforderlich ist, verwenden Sie den zweiten Teil der Bedingung. Die Methode liest die Datei mit den EA-Einstellungen und gibt ihren Inhalt zurück, oder die Methode wird rekursiv mit dem Parameter isUpdate = true gestartet und damit der zuvor betrachtete Logikteil ausgeführt.

Eine weitere interessante Methode ist "StartOptimierung":

/// <summary>
/// Start optimizations
/// </summary>
/// <param name="optimiserInputData">Input data for the optimizer</param>
/// <param name="isAppend">Flag whether data should be added to a file?</param>
/// <param name="dirPrefix">Directory prefix</param>
public async void StartOptimisation(OptimiserInputData optimiserInputData, bool isAppend, string dirPrefix)
{
    if (string.IsNullOrEmpty(optimiserInputData.Symb) ||
        string.IsNullOrWhiteSpace(optimiserInputData.Symb) ||
        (optimiserInputData.HistoryBorders.Count == 0 && optimiserInputData.ForwardBorders.Count == 0))
    {
        ThrowException("Fill in asset name and date borders");
        OnPropertyChanged("ResumeEnablingTogle");
        return;
    }

    if (Optimiser.TerminalManager.IsActive)
    {
        ThrowException("Terminal already running");
        return;
    }

    if (optimiserInputData.OptimisationMode == ENUM_OptimisationMode.Disabled)
    {
        StartTest(optimiserInputData);
        return;
    }

    if (!isAppend)
    {
        var dir = workingDirectory.GetOptimisationDirectory(optimiserInputData.Symb,
                                                  Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot),
                                                  dirPrefix, Optimiser.Name);
        List<FileInfo> data = dir.GetFiles().ToList();
        data.ForEach(x => x.Delete());
        List<DirectoryInfo> dirData = dir.GetDirectories().ToList();
        dirData.ForEach(x => x.Delete());
    }

    await Task.Run(() =>
    {
        try
        {
            DirectoryInfo cachDir = Optimiser.TerminalManager.TerminalChangeableDirectory
                                                     .GetDirectory("Tester")
                                                     .GetDirectory("cache", true);
            DirectoryInfo cacheCopy = workingDirectory.Tester.GetDirectory("cache", true);
            cacheCopy.GetFiles().ToList().ForEach(x => x.Delete());
            cachDir.GetFiles().ToList()
                   .ForEach(x => x.MoveTo(Path.Combine(cacheCopy.FullName, x.Name)));

            Optimiser.ClearOptimiser();
            Optimiser.Start(optimiserInputData,
                Path.Combine(terminalDirectory.Common.FullName,
                $"{Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot)}_Report.xml"), dirPrefix);
        }
        catch (Exception e)
        {
            Optimiser.Stop();
            ThrowException(e.Message);
        }
    });
}

Diese Methode ist asynchron, und sie wird mit der Technologie async await geschrieben, die eine einfachere Deklaration asynchroner Methoden ermöglicht. Zuerst überprüfen wir den übergebenen Symbolnamen und die Optimierungsbereiche. Wenn einer davon fehlt, entsperren wir die gesperrte GUI (einige GUI-Schaltflächen sind gesperrt, wenn die Optimierung beginnt) und zeigen eine Fehlermeldung an, nach der die Funktionsausführung abgeschlossen sein sollte. Wir tun genau dasselbe, wenn das Terminal bereits läuft. Wenn anstelle der Optimierung ein Testmodus gewählt wurde, lenken wir die Ausführung des Prozesses auf die Methode um, die den Test startet.

Wenn Modus anhängen gewählt wurde, löschen wir alle Dateien in dem Verzeichnis mit den Optimierungen sowie alle Unterverzeichnisse. Dann fahren wir mit der Optimierung fort. Der Optimierungsprozess startet asynchron und blockiert daher die GUI nicht, während diese Aufgabe ausgeführt wird. Im Falle von Fehlern wird sie auch in ein Try-Catch-Konstrukt verpackt. Bevor der Prozess beginnt, kopieren wir alle Cache-Dateien früher durchgeführter Optimierungen in ein temporäres Verzeichnis, das im Arbeitsverzeichnis Data des Auto-Optimizers angelegt wurde. Dadurch wird sichergestellt, dass Optimierungen auch dann gestartet werden, wenn sie bereits früher gestartet wurden. Dann löschen wir den Optimizer, wenn alle Daten zuvor in lokale Variablen des Optimizers geschrieben wurden, und starten Sie den Optimierungsprozess. Einer der Optimierungs-Startparameter ist der Pfad zu der vom Roboter erzeugten Berichtsdatei. Wie bereits in Artikel 3 erwähnt, wird der Bericht mit dem Namen {Robotername}_Bericht.xml erzeugt. Im Auto-Optimizer wird dieser Name durch die folgende Zeile angegeben:

$"{Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot)}_Report.xml")

Sie erfolgt durch das Verketten vom Zeichenketten, wobei robot name aus dem Pfad zum Roboter gebildet wird, der als einer der Parameter der Optimierungsdatei angegeben ist. Der Optimierungsstoppprozess wird vollständig an die Klasse des Optimizers übergeben. Die Methode, die ihn implementiert, ruft einfach die StopOptimierungsmethode an einer Instanz der Optimizerklasse auf.

/// <summary>
/// Complete optimization from outside the optimizer
/// </summary>
public void StopOptimisation()
{
    Optimiser.Stop();
}

Tests werden nicht im Optimizer, sondern mit der in der Datenmodellklasse implementierten Methode gestartet.

/// <summary>
/// Run tests
/// </summary>
/// <param name="optimiserInputData">Input data for the tester</param>
public async void StartTest(OptimiserInputData optimiserInputData)
{
    // Check if the terminal is running
    if (Optimiser.TerminalManager.IsActive)
    {
        ThrowException("Terminal already running");
        return;
    }

    // Set the date range
    #region From/Forward/To
    DateTime Forward = new DateTime();
    DateTime ToDate = Forward;
    DateTime FromDate = Forward;

    // Check the number of passed dates. Maximum one historical and one forward
    if (optimiserInputData.HistoryBorders.Count > 1 ||
        optimiserInputData.ForwardBorders.Count > 1)
    {
        ThrowException("For test there must be from 1 to 2 date borders");
        OnPropertyChanged("ResumeEnablingTogle");
        return;
    }

    // If both historical and forward dates are passed
    if (optimiserInputData.HistoryBorders.Count == 1 &&
        optimiserInputData.ForwardBorders.Count == 1)
    {
        // Test the correctness of the specified interval
        DateBorders _Forward = optimiserInputData.ForwardBorders[0];
        DateBorders _History = optimiserInputData.HistoryBorders[0];

        if (_History > _Forward)
        {
            ThrowException("History optimization must be less than Forward");
            OnPropertyChanged("ResumeEnablingTogle");
            return;
        }

        // Remember the dates
        Forward = _Forward.From;
        FromDate = _History.From;
        ToDate = (_History.Till < _Forward.Till ? _Forward.Till : _History.Till);
    }
    else // If only forward or only historical data is passed
    {
        // Save and consider it a historical date (even if forward was passed)
        if (optimiserInputData.HistoryBorders.Count > 0)
        {
            FromDate = optimiserInputData.HistoryBorders[0].From;
            ToDate = optimiserInputData.HistoryBorders[0].Till;
        }
        else
        {
            FromDate = optimiserInputData.ForwardBorders[0].From;
            ToDate = optimiserInputData.ForwardBorders[0].Till;
        }
    }
    #endregion

    PBUpdate("Start test", 100);

    // Run test in the secondary thread
    await Task.Run(() =>
    {
        try
        {
            // Create a file with EA settings
            #region Create (*.set) file
            FileInfo file = new FileInfo(Path.Combine(Optimiser
                                             .TerminalManager
                                             .TerminalChangeableDirectory
                                             .GetDirectory("MQL5")
                                             .GetDirectory("Profiles")
                                             .GetDirectory("Tester")
                                             .FullName, $"{Path.GetFileNameWithoutExtension(optimiserInputData.RelativePathToBot)}.set"));

            List<ParamsItem> botParams = new List<ParamsItem>(GetBotParams(optimiserInputData.RelativePathToBot, false));

            // Fill the expert settings with those that were specified in the graphical interface
            for (int i = 0; i < optimiserInputData.BotParams.Count; i++)
            {
                var item = optimiserInputData.BotParams[i];

                int ind = botParams.FindIndex(x => x.Variable == item.Variable);
                if (ind != -1)
                {
                    var param = botParams[ind];
                    param.Value = item.Value;
                    botParams[ind] = param;
                }
            }

            // Save settings to a file
            SetFileManager setFile = new SetFileManager(file.FullName, false)
            {
                Params = botParams
            };
            setFile.SaveParams();
            #endregion

            // Create terminal config
            #region Create config file
            Config config = new Config(Optimiser.TerminalManager
                                                .TerminalChangeableDirectory
                                                .GetDirectory("config")
                                                .GetFiles("common.ini")
                                                .First().FullName);
            config = config.DublicateFile(Path.Combine(workingDirectory.WDRoot.FullName, $"{Optimiser.TerminalManager.TerminalID}.ini"));

            config.Tester.Currency = optimiserInputData.Currency;
            config.Tester.Deposit = optimiserInputData.Balance;
            config.Tester.ExecutionMode = optimiserInputData.ExecutionDelay;
            config.Tester.Expert = optimiserInputData.RelativePathToBot;
            config.Tester.ExpertParameters = setFile.FileInfo.Name;
            config.Tester.ForwardMode = (Forward == new DateTime() ? ENUM_ForvardMode.Disabled : ENUM_ForvardMode.Custom);
            if (config.Tester.ForwardMode == ENUM_ForvardMode.Custom)
                config.Tester.ForwardDate = Forward;OnPropertyChanged("StopTest");
            else
                config.DeleteKey(ENUM_SectionType.Tester, "ForwardDate");
            config.Tester.FromDate = FromDate;
            config.Tester.ToDate = ToDate;
            config.Tester.Leverage = $"1:{optimiserInputData.Laverage}";
            config.Tester.Model = optimiserInputData.Model;
            config.Tester.Optimization = ENUM_OptimisationMode.Disabled;
            config.Tester.Period = optimiserInputData.TF;
            config.Tester.ShutdownTerminal = false;
            config.Tester.Symbol = optimiserInputData.Symb;
            config.Tester.Visual = false;
            #endregion

            // Configure the terminal and launch it
            Optimiser.TerminalManager.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
            Optimiser.TerminalManager.Config = config;
            Optimiser.TerminalManager.Run();

            // Wait for the terminal to close
            Optimiser.TerminalManager.WaitForStop();
        }
        catch (Exception e)
        {
            ThrowException(e.Message);
        }

        OnPropertyChanged("StopTest");
    });
}

Nach der üblichen Überprüfung, ob das Terminal läuft, fahren wir mit der Festlegung der Termine für historische und Vorwärts-Tests fort. Sie können entweder einen historischen Zeitraum oder sowohl einen historischen als auch einen Vorwärts-Bereich festlegen. Wenn in den Einstellungen nur ein Vorwärtszeitraum angegeben ist, wird es als historisches Zeitraum behandelt. Zuerst deklarieren wir die Variablen, die Testdaten speichern (Vorwärts, letztes Testdatum, Testbeginndatum). Dann wird die Methode überprüft — wenn mehr als eine historische Bereichsgrenze oder mehr als eine Vorwärtstestgrenze überschritten wird, wird eine Fehlermeldung angezeigt. Dann erfolgt das Setzen der Intervallgrenzen — die Idee dieser Bedingung besteht darin, die vier bestandenen Daten (oder zwei, wenn nur der historische Zeitraum gesetzt werden soll) zwischen drei deklarierten Variablen zu setzen.

Der Testbeginn ist ebenfalls in ein Try-Catch-Konstrukt verpackt. Zuerst wird eine Datei mit den Roboterparametern erzeugt und die übergebenen Roboterparameter eingetragen. Dies geschieht unter Verwendung des zuvor besprochenen Objekts SetFileManager. Dann wird eine Konfigurationsdatei gemäß der Anweisung erstellt und der Testprozess gestartet. Warten Sie dann, bis das Terminal geschlossen wird. Sobald die Methode iher Aufgaben beendet hat, wird die Grafik benachrichtigt, dass der Test abgeschlossen ist. Dies muss über ein Ereignis erfolgen, da diese Methode asynchron ist und die Programmoperation nach ihrem Aufruf fortgesetzt wird, ohne auf den Abschluss der aufgerufenen Methode zu warten.

Was den Optimierungsprozess betrifft, so teilt der Optimizer dem Datenmodell den beendeten Optimierungsprozess ebenfalls über das Ereignis Abschluss des Optimierungsprozesses mit. Darauf wird im letzten Artikel näher eingegangen.

Schlussfolgerung

In früheren Artikeln haben wir den Prozess der Kombination von Algorithmen mit dem erstellten automatischen Optimizer und einigen seiner Teile eingehend analysiert. Wir haben uns bereits mit der Logik von Optimierungsberichten befasst und ihre Anwendung in Handelsalgorithmen erlebt. Im vorigen Artikel haben wir uns mit der graphischen Schnittstelle (dem View-Teil des Programms) und der Struktur von Projektdateien befasst.

Wir analysierten auch die interne Struktur des Projekts, die Interaktion zwischen den Klassen und den Start des Optimierungsprozesses aus der Sicht des Programms. Da das Programm mehrere Optimierungslogiken unterstützt, haben wir die implementierte Logik nicht im Detail betrachtet — es ist besser, die Logik in einem separaten Artikel als Beispiel für eine Optimizer-Implementierung zu beschreiben. Es werden zwei weitere Artikel kommen, in denen wir die Verbindung des logischen Teils mit den Grafiken analysieren, sowie den Implementierungsalgorithmus des Optimizers diskutieren und ein Beispiel für eine Optimizer-Implementierung besprechen.

Der Anhang enthält das in Artikel 4 analysierte Auto-Optimizer-Projekt mit einem Handelsroboter. Um das Projekt zu verwenden, kompilieren Sie bitte die Auto-Optimizer-Projektdatei und die Testroboter-Datei. 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. In den Artikeln 3 und 4 dieser Serie finden Sie Einzelheiten zur Verbindung des automatischen Optimizers mit Ihren Expertenberatern.

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 wird dann im Ordner MetaTrader Auto Optimiser/bin/Debug (oder MetaTrader Auto Optimiser/bin/Release — abhängig von der gewählten Kompilierungsmethode) abhängen.