Kontinuierliche Walk-Forward-Optimierung (Teil 5): Projektübersicht Auto-Optimizer und Erstellen einer GUI

Andrey Azatskiy | 18 Juni, 2020

Einführung

In den vorhergehenden Artikeln haben wir sowohl den Teil des Projekts betrachtet, der sich direkt auf den Terminal bezieht, als auch den Teil, der die allgemeine Anwendung des Projekts beschreibt. Der vorherige Artikel war dem Zeitplan für den gesamten Artikelzyklus voraus. Dies geschah aus zwei Gründen. Erstens dient er als Anleitung zur Benutzung der Anwendung. Zweitens veranschaulicht er die Idee und Logik der App-Erstellung, wobei er weiß, was zum Verständnis des Codes beiträgt.

Die Artikel sind unter den folgenden Links verfügbar:

  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)

Frühere Artikel, aus denen eine Reihe von Entwicklungen entlehnt wurden, sind unter den folgenden Links verfügbar:

  1. Optimierungsmanagement (Teil I): Erstellen einer GUI
  2. Optimierungsmanagement (Teil II): Erstellen der Schlüsselobjekte und der Add-on-Logik

Der aktuelle Artikel bietet eine Beschreibung der Projektstruktur in der Visual Studio-IDE und ihrer Komponenten. Dieser Teil ist der Erstellung der GUI der App gewidmet. Sie berücksichtigt auch die Struktur des verwalteten Verzeichnisses, in dem die Optimierungen gespeichert sind, sowie Änderungen im Optimierungsprozess der Klassen des vorherigen Projekts.


Überblick über die Projektstruktur

Da dieser Teil des Artikels auch dem C# gewidmet ist, beginnen wir mit der Betrachtung seiner Dateistruktur:

Die unten angehängte Lösung enthält zwei Projekte. Eines wurde im ersten Artikel besprochen, das zweite wurde in späteren Artikeln analysiert. Dieses Projekt ist der Auto-Optimierer.


Da das Projekt über eine GUI verfügt, wird diesmal wieder der MVVM-Ansatz (ModelViewViewModel) verwendet. Die Projektvorlage ist in entsprechende Abschnitte unterteilt. Da die Projektlogik im Modellteil implementiert werden soll, befinden sich die Klassen, die nicht mit dem grafischen Teil des Projekts zusammenhängen, im Unterverzeichnis Model und sind weiter in Verzeichnisse unterteilt.

Beginnen wir mit den Objekten aus der vorherigen Artikelserie, die modifiziert wurden. Diese Beschreibung wird auch für all jene nützlich sein, die mit dem vorherigen Teil nicht vertraut sind. 


Erstellen des grafischen Teils der Anwendung

Lassen Sie uns zur grafischen Oberfläche übergehen. Zuvor haben wir eine Methode zur Erstellung eines Add-ons für den MetaTrader 5 in der Sprache C# und Möglichkeiten zur Kombination seiner Funktionen mit einem Expert Advisor unter Verwendung einer DLL und des OnTimer-Callbacks in Betracht gezogen. In der aktuellen Implementierung wird der Auto-Optimierer außerhalb des Terminals implementiert. Jetzt läuft es als externer Optimierungsmanager und imitiert die Arbeit eines Händlers, der Optimierungen einführt und die Ergebnisse verarbeitet. Außerdem können wir durch die Vermeidung gleichzeitiger Optimierungen auf mehreren Terminals, die auf demselben Computer laufen, und durch die Implementierung des automatischen Optimierers als separate Anwendung absolut auf alle Terminals zugreifen, die auf dem Computer installiert sind, einschließlich des Computers, auf dem der Optimierer läuft. Dieser Computer konnte im vorherigen Projekt nicht verwendet werden.

Aus diesem Grund ist das aktuelle Projekt nicht vollständig als DLL implementiert, sondern wird nun in eine DLL und eine ausführbare Datei des Auto-Optimierer-Projekts unterteilt.


Wie aus dem obigen Screenshot ersichtlich, besteht das Projektfenster aus einem Kopfzeile, einem Fußzeile und einem TabControl mit zwei Registerkarten: Settings (Einstellungen) und Reports (Berichte). Der Kopfteil und die Fußzeile des Fensters werden nicht verändert, unabhängig davon, welche Registerkarte im Mittelteil ausgewählt ist, wodurch ein einfacher Zugang zu allen Bedienelementen, die sich in diesen Teilen befinden, ermöglicht wird.

Der Kopfteil des Fensters wird durch das folgende XAML-Markup erstellt:

<Grid>
        <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <WrapPanel Margin="2">
            <Label Content="Optimisation:"/>
            <ComboBox Width="200"
                      ItemsSource="{Binding SelectedOptimisationNames,UpdateSourceTrigger=PropertyChanged}"
                      SelectedItem="{Binding SelectedOptimisation}"
                      SelectedIndex="0"
                      IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/>
            <Button Content="Load" 
                    Margin="2,0,0,0"
                    Width="34"
                    Command="{Binding LoadResults}"
                    IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/>
        </WrapPanel>

        <WrapPanel HorizontalAlignment="Right" 
                   Margin="2"
                   Grid.Column="1">
            <Label Content="Terminal:"/>
            <ComboBox Width="200"
                      SelectedIndex="{Binding SelectedTerminalIndex}"
                      ItemsSource="{Binding Terminals,UpdateSourceTrigger=LostFocus}"
                      IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/>
        </WrapPanel>
</Grid>


Der Grid-Container, der alle im betrachteten Bereich verfügbaren Steuerelemente enthält, ist in 2 Spalten unterteilt. Die folgenden Elemente werden der ersten Spalte hinzugefügt: Parametername (Optimierung), Combo-Box mit einer Liste der verfügbaren Optimierungen, sowie die Optimierungen Laden-Button. Die zweite Spalte enthält den Parameternamen und eine Dropdown-Liste mit den IDs der verfügbaren Terminals. 

Der Grid-Container, der die grafische Fensterfußzeile (mit einer Fortschrittsanzeige) darstellt, ist ähnlich aufgebaut:

<Grid Grid.Row="2">
        <Grid.ColumnDefinitions>
                <ColumnDefinition Width="150"/>
                <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <Label Content="{Binding Status, UpdateSourceTrigger=PropertyChanged}"/>
        <ProgressBar Grid.Column="1"
                     Value="{Binding Progress, UpdateSourceTrigger=PropertyChanged}"
                     Minimum="0"
                     Maximum="100"/>
</Grid>

Er teilt seine Fläche in 2 Teile, wobei die Größe des ersten Teils begrenzt wird. Infolgedessen wird der größte Teil der Container von der Fortschrittsanzeige verwendet. Außerdem passt sich die Breite der Fortschrittsanzeige an, falls sich die Breite des gesamten Fensters ändert. Alle drei Komponenten werden entsprechend den XAML-Markup-Regeln im Container <Window/> platziert.

<Window x:Class="Metatrader_Auto_Optimiser.AutoOptimiser"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:vm="clr-namespace:Metatrader_Auto_Optimiser.View_Model"
        xmlns:v="clr-namespace:Metatrader_Auto_Optimiser.View"
        mc:Ignorable="d"
        Title="Auto Optimiser" Height="500" Width="1200"
        MinHeight="500" MinWidth="1200">

    <Window.DataContext>
        <vm:AutoOptimiserVM/>
    </Window.DataContext>


    ...


</Window>

 Dieser Container definiert Namensraumreferenzen:

Außerdem werden die folgenden Fenstergrößen eingestellt: die minimale Größe und die Ausgangsgröße, mit der das Fenster beim Programmstart geöffnet wird. Dann wird DataContext für die grafische Oberfläche installiert, wobei der oben erwähnte Alias für den Namensraum, der ViewModel enthält, verwendet wird. 

Der zentrale Teil des Panels besteht aus einem Element TabControl mit 2 Registern. Er dient als Hauptteil, als "Body" unseres grafischen Elements. Der Aufbau der Registerkarte "Settings" (Einstellungen) ist wie folgt:


Diese Registerkarte ist ebenfalls in drei Teile unterteilt. Im oberen Teil der Registerkarte befindet sich ein Bedienfeld, in dem die Parameter des zu speichernden Auto-Optimierungsberichts eingestellt werden können. Es umfasst auch die Auswahl des Asset-Namens und eine Schaltfläche zum Aktualisieren der *set-Datei. Der mittlere Teil der Registerkarte "Settings" enthält die Optimierungseinstellungen und Optionen zur Auswahl der Filter- und Sortierparameter während des Auto-Optimierungsprozesses. Der letzte Teil ermöglicht die Einstellung von Expert Advisor-Parametern und die Auswahl von Optimierungs- und Vorwärtsdaten. Der Einfachheit halber befindet sich das Element GridSplitter zwischen den ersten beiden Teilen. Durch Ziehen können Sie die Größe dieser Registerkarten ändern. Dies ist besonders praktisch, wenn Sie Optimierungsparameter für einen Roboter mit einer langen Liste von Eingaben füllen müssen.

Betrachten wir im Detail den Code des Markups der ersten der drei Komponenten der Registerkarte "Settings":

<Grid>
        <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>

        <WrapPanel HorizontalAlignment="Left"
                VerticalAlignment="Bottom">
        <Label Content="Select Optimiser:"/>
        <ComboBox Width="150"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"
                ItemsSource="{Binding Optimisers}"
                SelectedIndex="{Binding SelectedOptimiserIndex}"/>
        <Button Content="GUI"
                Command="{Binding ShowOptimiserGUI}"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"/>
        <Label Content="Directory prefix:"/>
        <TextBox Width="150"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"
                Text="{Binding DirPrefix}"/>
        <ComboBox Width="100" 
                Margin="2,0,0,0"
                SelectedIndex="0"
                ItemsSource="{Binding FileFillingType}"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"
                SelectedItem="{Binding FileWritingMode}"/>
        <Label Content="Asset name:"/>
        <TextBox Width="100"
                IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"
                Text="{Binding AssetName}"/>
        <Button Content="Update (*.set) file"
                Margin="2,0,0,0"
                IsEnabled="{Binding EnableMainTogles}"
                Command="{Binding UpdateSetFile}"/>
        </WrapPanel>
        <Button Content="Start/Stop"
                Grid.Column="2"
                Margin="2"
                Command="{Binding StartStopOptimisation}"/>

</Grid>

Der beschriebene Teil enthält die Aufteilung in zwei Spalten. Die Breite der ersten Spalte kann dynamisch verändert werden; die zweite Spaltenbreite ist fest und beträgt 100 Pixel. Die erste Spalte enthält alle Bedienelemente, die sich im Panel befinden. Sie alle sind im WrapPanel enthalten, so dass die Elemente nacheinander angeordnet werden können. Zuerst kommen die Steuerelemente, die für die Auswahl und Einrichtung des Auto-Optimierers verantwortlich sind. Danach folgen Parameter, die sich auf die Benennung des Ordners mit dem Optimierungsbericht beziehen, sowie auf die Art und Weise der Berichtserstellung (Umschreiben, Anhängen). Der letzte Teil ist die Angabe des Asset-Namens, der für die Optimierung verwendet wird, und eine Schaltfläche zum Aktualisieren der *set-Datei mit Roboterparametern. Die Spalte mit der festen Breite ist mit der Schaltfläche "Start/Stop" belegt, die als Punkt zum Starten und Stoppen der Optimierung dient. 

Der zweite Teil der Registerkarte "Settings" ist in 2 Teile gegliedert.


Die erste enthält ListView mit der Liste der Einstellparameter des Optimierers. Hier entsprechen die Namen und Werte der Parameter den Einstellungsfeldern des Optimierers im Terminal. Der zweite Teil enthält die Spezifikation der Datensortierung und der Filterkoeffizienten. Die Spalten haben auch das Element GridSplitter, das die beschriebenen Bereiche trennt. Der Code zur Erstellung der Elemente ist einfach, daher werde ich ihn hier nicht zur Verfügung stellen. Der vollständige Code ist unten angehängt. Der untere Teil der Registerkarte ist dem oberen völlig ähnlich, mit der einzigen Ausnahme, dass der rechte Teil, der die Optimierungsdaten enthält, in zwei Teile geteilt ist. Der erste enthält Steuerelemente zum Hinzufügen von Daten zu einer Liste. Die zweite wird für die Anzeige der erstellten Liste verwendet.

Das letzte Element der grafischen Oberfläche ist die Registerkarte "Results", auf der die Ergebnisse der Optimierungen sowie die Ergebnisse von Vorwärts- und historischen Tests eingesehen werden können.  


Wie aus dem beigefügten Bild ersichtlich ist, hat die Registerkarte eine interessantere Struktur als die vorherige. Es ist in zwei Teile geteilt und wird durch das Element GridSplitter getrennt, wodurch die Teile in ihrer Größe verändert werden können, was eine detailliertere Untersuchung der Optimierungsergebnisse ermöglicht. Der obere Teil enthält zwei ineinander verschachtelte Elemente der TabItem in einer Gruppe. Der Reiter "Selected pass", in dem sich Vorwärts- und historische Tests befinden, ist nicht so interessant wie der Reiter "Optimisations", aber wir werden später darauf zurückkommen.

Im unteren Teil des Tabs befinden sich zwei Felder, die durch einen vertikalen GridSplitter getrennt sind. Der erste dient zur Angabe von Daten und Modi für einen Test, der aus einer der Tabellen im oberen Teil ausgewählt wurde, und der andere zeigt eine Reihe von Variablen, die zur leichteren Anzeige und zum leichteren Lesen in Tabellen zusammengefasst sind. Es enthält auch die Liste der Parameter des ausgewählten Optimierungsdurchlaufs (Registerkarte "Bot Params").

Der Vergleich der Markup-Elemente mit seinen Ergebnissen im Register "Optimisations" hat die folgende Struktur:


Ähnlich wie "Selected pass" hat diese Registerkarte die Schaltfläche "Save to (*csv)", die die Ergebnisse aller abgeschlossenen Optimierungen für das ausgewählte Datum in einer Datei speichert. Es gibt zwei weitere Schaltflächen zum Sortieren und Filtern der Daten in der Tabelle, die die Ergebnisse aller Optimierungen anzeigt. Die Struktur der Ergebnisse Tabelle ist ähnlich wie die Tabellen in den Registern "Selected pass.History" und "Selected pass.Forward". Der Teil des Markups, der die Tabellendaten erstellt, ist unten dargestellt:

<ListView ItemsSource="{Binding AllOptimisations}"
          SelectedIndex="{Binding SelecterReportItem}"
          v:ListViewExtention.DoubleClickCommand="{Binding StartTestReport}">
        <ListView.View>
                <GridView>
                        <GridViewColumn Header="Date From" DisplayMemberBinding="{Binding From}"/>
                        <GridViewColumn Header="Date Till" DisplayMemberBinding="{Binding Till}"/>
                        <GridViewColumn Header="Sort by" DisplayMemberBinding="{Binding SortBy}"/>
                        <GridViewColumn Header="Payoff" DisplayMemberBinding="{Binding Payoff}"/>
                        <GridViewColumn Header="Profit pactor" DisplayMemberBinding="{Binding ProfitFactor}"/>
                        <GridViewColumn Header="Average Profit Factor" DisplayMemberBinding="{Binding AverageProfitFactor}"/>
                        <GridViewColumn Header="Recovery factor" DisplayMemberBinding="{Binding RecoveryFactor}"/>
                        <GridViewColumn Header="Average Recovery Factor" DisplayMemberBinding="{Binding AverageRecoveryFactor}"/>
                        <GridViewColumn Header="PL" DisplayMemberBinding="{Binding PL}"/>
                        <GridViewColumn Header="DD" DisplayMemberBinding="{Binding DD}"/>
                        <GridViewColumn Header="Altman Z score" DisplayMemberBinding="{Binding AltmanZScore}"/>
                        <GridViewColumn Header="Total trades" DisplayMemberBinding="{Binding TotalTrades}"/>
                        <GridViewColumn Header="VaR 90" DisplayMemberBinding="{Binding VaR90}"/>
                        <GridViewColumn Header="VaR 95" DisplayMemberBinding="{Binding VaR95}"/>
                        <GridViewColumn Header="VaR 99" DisplayMemberBinding="{Binding VaR99}"/>
                        <GridViewColumn Header="Mx" DisplayMemberBinding="{Binding Mx}"/>
                        <GridViewColumn Header="Std" DisplayMemberBinding="{Binding Std}"/>
                </GridView>
        </ListView.View>
</ListView>

TabItem der Filter für Optimierungsergebnisse und Sortierparameter enthält, ist völlig identisch mit demselben Element im Register "Settings". Obwohl sie im Markup getrennt sind, ist ViewModel so angeordnet, dass Änderungen in einem von ihnen sofort im anderen gerendert werden. Der Rendering-Mechanismus der Änderungen wird im nächsten Artikel behandelt.  

Wie aus diesem Abschnitt hervorgeht, ist das Markup der grafischen Oberfläche recht einfach. Ich habe nicht die entsprechenden visuellen Effekte im Programm vorgesehen, weil die Hauptaufgabe die Funktionalität war. Wenn Sie die Anwendung verschönern möchten, bearbeiten Sie die Datei App.xaml, die als zentraler Projektspeicher dient. 


Aus der Artikelserie "Optimierungsmanagement" entlehnte Klassen und ihre Modifikationen

In diesem Projekt habe ich die Objekte verwendet, die zuvor für die Reihe "Optimierungsmanagement" erstellt wurden. Ich werde keine detaillierte Beschreibung der einzelnen Objekte geben, da sie in den oben genannten Artikeln verfügbar sind. Auf einige von ihnen wollen wir jedoch näher eingehen, insbesondere auf diejenigen, die im Rahmen dieser Projekte geändert wurden. Die vollständige Liste der ausgeliehenen Objekte lautet wie folgt:

Die letzten vier Objekte aus der Liste können als proprietäre API für die Arbeit mit dem Terminal aus C#-Code betrachtet werden. Die in diesem Teil des Artikels beschriebenen Änderungen waren nur interner Natur. Mit anderen Worten, die externe Schnittstelle für die Arbeit mit diesen Klassen (öffentliche Methoden und Eigenschaften) blieb in ihrer Signatur unverändert. Dementsprechend wird auch dann, wenn Sie die früheren Implementierungen dieser Objekte im vorherigen Projekt durch neue ersetzen, das Projekt kompiliert und funktioniert. 

Das erste der Objekte mit der modifizierten Struktur ist die Klasse Config. Die Klasse präsentiert eine Tabelle, die im entsprechenden Abschnitt der Terminal-Dokumentation beschrieben wird. Es enthält alle Tabellenfelder in seinen Eigenschaften. Durch Ändern einer Eigenschaft ändern Sie den Wert eines bestimmten Schlüssels in einem bestimmten Abschnitt der Terminal-Initialisierungsdatei. Die Initialisierungsdateien *.ini stellen ein gebräuchliches Format dar. Der Kernel des Windows-Betriebssystems stellt Funktionen für die Arbeit mit diesem Format zur Verfügung. Wir haben zwei davon in unseren C#-Code importiert. In der vorherigen Implementierung dieser Klasse wurden die verwendeten Methoden direkt in die Klasse Config importiert. In der aktuellen Implementierung sind die Methoden in einer separaten Klasse IniFileManager implementiert.

class IniFileManager
{
    private const int SIZE = 1024; //Maximum size (for reading the value from the file)
        
    public static string GetParam(string section, string key, string path)
    {
        //To get the value
        StringBuilder buffer = new StringBuilder(SIZE);

        //Get value to buffer
        if (GetPrivateProfileString(section, key, null, buffer, SIZE, path) == 0)
            ThrowCErrorMeneger("GetPrivateProfileStrin", Marshal.GetLastWin32Error(), path);

        //Return the received value
        return buffer.Length == 0 ? null : buffer.ToString();
    }
    /// <summary>
    /// Return error
    /// </summary>
    /// <param name="methodName">Method name</param>
    /// <param name="er">Error code</param>
    private static void ThrowCErrorMeneger(string methodName, int er, string path)
    {
        if (er > 0)
        {
            if (er == 2)
            {
                if (!File.Exists(path))
                    throw new Exception($"{path} - File doesn1t exist");
            }
            else
            {
                throw new Exception($"{methodName} error {er} " +
                    $"See System Error Codes (https://docs.microsoft.com/en-us/windows/desktop/Debug/system-error-codes) for details");
            }
        }
    }

    public static void WriteParam(string section, string key, string value, string path)
    {
        //Write value to the INI-file
        if (WritePrivateProfileString(section, key, value, path) == 0)
            ThrowCErrorMeneger("WritePrivateProfileString", Marshal.GetLastWin32Error(), path);
    }
}

Die resultierende Config-Datei enthält nur die in der Konfigurationsdatei enthaltenen Felder. Die vollständige Beschreibung dieses Objekts wurde in früheren Artikeln zum Optimierungsmanagement gegeben.  

Die nächste modifizierte Klasse ist TerminalManager. Der Klasseninhalt bleibt unverändert. Wir werden die Klassenoperationsmethode und Komponenten nicht besprechen, da es sich um eine geliehene Klasse handelt. Die Klasse spielt jedoch eine wichtige Rolle in der Anwendung, da sie den Terminalbetrieb startet und stoppt. Hier ist also der vollständige Code der Klassenimplementierung.   

class TerminalManager
{
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="TerminalChangeableDirectory">
    /// Path to the directory with mutable files (the one in AppData)
    /// </param>
    public TerminalManager(DirectoryInfo TerminalChangeableDirectory) :
        this(TerminalChangeableDirectory, new DirectoryInfo(File.ReadAllText(TerminalChangeableDirectory.GetFiles().First(x => x.Name == "origin.txt").FullName)), false)
    {
    }
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="TerminalChangeableDirectory">
    /// Path to the directory with mutable files
    /// </param>
    /// <param name="TerminalInstallationDirectory">
    /// Path to the terminal folder
    /// </param>
    public TerminalManager(DirectoryInfo TerminalChangeableDirectory, DirectoryInfo TerminalInstallationDirectory, bool isPortable)
    {
        this.TerminalInstallationDirectory = TerminalInstallationDirectory;
        this.TerminalChangeableDirectory = TerminalChangeableDirectory;

        TerminalID = TerminalChangeableDirectory.Name;

        CheckDirectories();

        Process.Exited += Process_Exited;

        Portable = isPortable;
    }
    /// <summary>
    /// Destructor
    /// </summary>
    ~TerminalManager()
    {
        Close();
        Process.Exited -= Process_Exited;
    }
    /// <summary>
    /// Terminal startup process
    /// </summary>
    private readonly System.Diagnostics.Process Process = new System.Diagnostics.Process();
    /// <summary>
    /// Running process completion event
    /// </summary>
    public event Action<TerminalManager> TerminalClosed;

    #region Terminal start Arguments
    /// <summary>
    /// Login for start - flag /Login
    /// </summary>
    public uint? Login { get; set; } = null;
    /// <summary>
    /// Platform launch under a certain profile. 
    /// The profile must be created in advance and located in the /profiles/charts/ folder of the trading platform
    /// </summary>
    public string Profile { get; set; } = null;
    /// <summary>
    /// Config file as a /Config object
    /// </summary>
    public Config Config { get; set; } = null;
    /// <summary>
    /// Flag of terminal launch in /portable mode
    /// </summary>
    private bool _portable;
    public bool Portable
    {
        get => _portable;
        set
        {
            _portable = value;
            if (value && !TerminalInstallationDirectory.GetDirectories().Any(x => x.Name == "MQL5"))
            {
                WindowStyle = System.Diagnostics.ProcessWindowStyle.Minimized;

                if (Run())
                {
                    System.Threading.Thread.Sleep(1000);
                    Close();
                }
                WaitForStop();
                WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
            }
        }
    }
    /// <summary>
    /// window style of the launched process
    /// </summary>
    public System.Diagnostics.ProcessWindowStyle WindowStyle { get; set; } = System.Diagnostics.ProcessWindowStyle.Normal;
    #endregion

    #region Terminal directories
    /// <summary>
    /// Path to terminal installation folder
    /// </summary>
    public DirectoryInfo TerminalInstallationDirectory { get; }
    /// <summary>
    /// Path to terminal folder with variable files
    /// </summary>
    public DirectoryInfo TerminalChangeableDirectory { get; }
    /// <summary>
    /// Path to the MQL5 folder
    /// </summary>
    public DirectoryInfo MQL5Directory => (Portable ? TerminalInstallationDirectory : TerminalChangeableDirectory).GetDirectory("MQL5");
    #endregion

    /// <summary>
    /// Terminal ID folder name in AppData directory
    /// </summary>
    public string TerminalID { get; }
    /// <summary>
    /// Flag of whether the terminal is currently running or not
    /// </summary>
    public bool IsActive => Process.StartInfo.FileName != "" && !Process.HasExited;

    #region .ex5 files relative paths
    /// <summary>
    /// List of full EA names
    /// </summary>
    public List<string> Experts => GetEX5FilesR(MQL5Directory.GetDirectory("Experts"));
    /// <summary>
    /// List of full indicator names
    /// </summary>
    public List<string> Indicators => GetEX5FilesR(MQL5Directory.GetDirectory("Indicators"));
    /// <summary>
    /// List of full script names
    /// </summary>
    public List<string> Scripts => GetEX5FilesR(MQL5Directory.GetDirectory("Scripts"));
    #endregion

    /// <summary>
    /// Terminal launch
    /// </summary>
    public bool Run()
    {
        if (IsActive)
            return false;
        // Set path to the terminal
        Process.StartInfo.FileName = Path.Combine(TerminalInstallationDirectory.FullName, "terminal64.exe");
        Process.StartInfo.WindowStyle = WindowStyle;
        // Set data for terminal launch (if any data were set)
        if (Config != null && File.Exists(Config.Path))
            Process.StartInfo.Arguments = $"/config:{Config.Path} ";
        if (Login.HasValue)
            Process.StartInfo.Arguments += $"/login:{Login.Value} ";
        if (Profile != null)
            Process.StartInfo.Arguments += $"/profile:{Profile} ";
        if (Portable)
            Process.StartInfo.Arguments += "/portable";

        // Notify the process of the need to call an Exit event after closing the terminal
        Process.EnableRaisingEvents = true;

        // Run the process and save the launch status to the IsActive variable
        return Process.Start();
    }
    /// <summary>
    /// Wait for the terminal operation to complete
    /// </summary>
    public void WaitForStop()
    {
        if (IsActive)
            Process.WaitForExit();
    }
    /// <summary>
    /// Stop the process
    /// </summary>
    public void Close()
    {
        if (IsActive)
            Process.Kill();
    }
    /// <summary>
    /// Wait for the terminal operation to complete for a certain time
    /// </summary>
    public bool WaitForStop(int miliseconds)
    {
        if (IsActive)
            return Process.WaitForExit(miliseconds);
        return true;
    }
    /// <summary>
    /// Search for files with the Ex5 extension 
    /// Search is performed recursively - files are searched in the specified folder and in all subfolders
    /// </summary>
    /// <param name="path">Path to the folder where search begins</param>
    /// <param name="RelativeDirectory">Folder relative to which oath is returned</param>
    /// <returns>List of paths to the found files</returns>
    private List<string> GetEX5FilesR(DirectoryInfo path, string RelativeDirectory = null)
    {
        if (RelativeDirectory == null)
            RelativeDirectory = path.Name;
        string GetRelevantPath(string pathToFile)
        {
            string[] path_parts = pathToFile.Split('\\');
            int i = path_parts.ToList().IndexOf(RelativeDirectory) + 1;
            string ans = path_parts[i];

            for (i++; i < path_parts.Length; i++)
            {
                ans = Path.Combine(ans, path_parts[i]);
            }

            return ans;
        }
    
        List<string> files = new List<string>();
        IEnumerable<DirectoryInfo> directories = path.GetDirectories();

        files.AddRange(path.GetFiles("*.ex5").Select(x => GetRelevantPath(x.FullName)));

        foreach (var item in directories)
            files.AddRange(GetEX5FilesR(item, RelativeDirectory));

        return files;
    }
    /// <summary>
    /// Terminal closing event
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void Process_Exited(object sender, EventArgs e)
    {
       TerminalClosed?.Invoke(this);
    }
    /// <summary>
    /// Check the correctness of the passed terminal path
    /// </summary>
    private void CheckDirectories()
    {
        if (!TerminalInstallationDirectory.Exists)
            throw new ArgumentException("PathToTerminalInstallationDirectory doesn`t exists");
        if (!TerminalChangeableDirectory.Exists)
            throw new ArgumentException("PathToTerminalChangeableDirectory doesn`t exists");
        if (!TerminalInstallationDirectory.GetFiles().Any(x => x.Name == "terminal64.exe"))
            throw new ArgumentException($"Can`t find terminal (terminal64.exe) in the instalation folder {TerminalInstallationDirectory.FullName}");
    }
}

Jetzt implementiert die Klasse die Schnittstelle ITerminalManager nicht mehr (wie beim letzten Mal). Ich beschloss, bei der Implementierung der beschriebenen Anwendung auf Unit-Tests zu verzichten, um den Entwicklungsprozess zu beschleunigen und die Anzahl der Projekte zu minimieren. Infolgedessen sind für dieses Objekt keine Schnittstellen erforderlich.

Die nächste Modifikation betrifft eine neue Methode zur Bestimmung, ob das Terminal läuft oder nicht. In der vorherigen Version erhielt die Eigenschaft einen Wert aus der Methode Run (der in denen ein falscher Wert zugewiesen wurde) und aus dem Optimierungsabschluss callback. Es war jedoch keine sehr gute Entscheidung, und es könnte manchmal nicht funktionieren. Daher habe ich die Eigenschaftsabfrage über IsActive überarbeitet. Jetzt greift die Abfrage direkt auf die Eigenschaft HasExited des Objekts Process zu. Ein Versuch, vor dem ersten Start auf die Eigenschaft zuzugreifen, erzeugt jedoch eine Fehlermeldung. Ich habe die Klassenspezifika von Prozess studiert und festgestellt, dass, wenn Sie den Prozess über das beschriebene Objekt starten, seine Eigenschaft StartInfo.FileName mit einem Pfad zur ausführbaren Datei gefüllt ist. Vor dem ersten Start ist er gleich einem leeren Wert (""). Aus diesem Grund sieht die Abfrage durch IsActive seltsam aus. Zuerst prüft die Abfrage , ob der Name existiert, und dann prüft er die Eigenschaft Process.HasExited. Mit anderen Worten, wir gehen standardmäßig davon aus, dass das Terminal geschlossen ist und nur über unsere TerminalManager-Klasse gestartet werden kann. Wenn StartInfo.FileName == "" ist, geben sie daher false zurück (dies bedeutet, dass das Terminal nicht läuft). Wenn das Terminal jemals gestartet wurde, vergleichen Sie den Wert der Eigenschaft HasExited. Der Eigenschaftswert ändert sich jedes Mal, wenn das Terminal gestartet wird, wenn es von unserem Objekt aus gestartet wird und wenn es abgeschaltet wird. Aufgrund dieser Funktion sollten Sie das Terminal immer geschlossen haben, wenn Sie den Auto-Optimierer verwenden. 

Die Beschreibung wird durch das letzte Objekt mit einer modifizierten internen Struktur vervollständigt. Es handelt sich um die Klasse SetFileManager und ihre Methode UpdateParams.

/// <summary>
/// Clear all recorded data in Params and load data from the required file
/// </summary>
public virtual void UpdateParams()
{
    _params.Clear();

    using (var file = FileInfo.OpenText())
    {
        string line;
        while ((line = file.ReadLine()) != null)
        {
            if (line[0].CompareTo(';') != 0 && line[0].CompareTo('#') != 0)
            {
                string[] key_value = line.Replace(" ", "").Split('=');
                string[] value_data = key_value[1].Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries);

                ParamsItem item = new ParamsItem
                {
                    Variable = key_value[0],
                    Value = (value_data.Length > 0 ? value_data[0] : null),
                    Start = (value_data.Length > 1 ? value_data[1] : null),
                    Step = (value_data.Length > 2 ? value_data[2] : null),
                    Stop = (value_data.Length > 3 ? value_data[3] : null),
                    IsOptimize = (value_data.Length > 4 ? value_data[4].CompareTo("Y") == 0 : false)
                };

                _params.Add(item);
            }
        }
    }
}

Änderungen in dieser Klasse betreffen nur eine Methode, und daher werde ich hier nicht den vollständigen Klassencode angeben. Während der Anwendungstests habe ich festgestellt, dass die vom Terminal für den Optimierer generierte *.set-Datei mit Roboterparametern für einige der Parameter manchmal halb leer sein kann. Beispielsweise kann das Terminal das Feld Value füllen und den Anfangs- oder Endwert für die Optimierung nicht zuweisen. Dies hängt vom Parametertyp ab. Beispielsweise füllen String-Parameter nur die Value-Felder. Der Zweck von implementieren der Änderungen im untenstehenden Code war es, das oben genannte Problem zu vermeiden.


Die Datenverzeichnisstruktur

In früheren Artikeln haben wir bereits das lokale Verzeichnis "Data" erwähnt, in dem Optimierungsberichte und andere vom automatischen Optimierer erstellte Arbeitsdateien gespeichert werden. Nun ist es an der Zeit, das Verzeichnis eingehender zu betrachten. Das Verzeichnis Data wird beim Start des Terminals in der Nähe der ausführbaren Datei erstellt. Das Verzeichnis wird nur erstellt, wenn es zur Startzeit des Auto-Optimierers noch nicht existiert. Andernfalls wird sein Pfad in der entsprechenden Eigenschaft der unteren Klasse gespeichert. Das Verzeichnis dient gleichzeitig als Arbeitsordner und als Speicherort. Wenn Sie jemals auf Dateien zugreifen und sie speichern müssen, tun Sie dies in diesem Verzeichnis. Das folgende Objekt erstellt und speichert das Verzeichnis:

/// <summary>
/// The object describing the Data directory with the auto optimizer's mutable files.
/// </summary>
class WorkingDirectory
{
    /// <summary>
    /// Default constructor
    /// </summary>
    public WorkingDirectory()
    {
        // Create a root directory with mutable files
        WDRoot = new DirectoryInfo("Data");
        if (!WDRoot.Exists)
            WDRoot.Create();
        // Create a subdirectory with optimization reports
        Reports = WDRoot.GetDirectory("Reports", true);
    }
    /// <summary>
    /// Nested directory with optimization reports
    /// </summary>
    public DirectoryInfo Reports { get; }
    /// <summary>
    /// Root directory with mutable files and folders
    /// </summary>
    public DirectoryInfo WDRoot { get; }

    /// <summary>
    /// Get or create (if not previously created) a directory nested inside the Reports directory.
    /// The resulting directory stores the results of a particular optimization pass.
    /// </summary>
    /// <param name="Symbol">The symbol on which the optimization was performed</param>
    /// <param name="ExpertName">Robot name</param>
    /// <param name="DirectoryPrefix">Prefix added to the directory name</param>
    /// <param name="OptimiserName">The name of the use optimizer</param>
    /// <returns>
    /// Path to the directory with the optimization results.
    /// The name of the directory is formed as follows: public DirectoryInfo WDRoot { get; }
    /// {DirectoryPrefix} {OptimiserName} {ExpertName} {Symbol}
    /// </returns>
    public DirectoryInfo GetOptimisationDirectory(string Symbol, string ExpertName,
                                                  string DirectoryPrefix, string OptimiserName)
    {
        return Reports.GetDirectory($"{DirectoryPrefix} {OptimiserName} {ExpertName} {Symbol}", true);
    }

    /// <summary>
    /// Path to Data/Tester 
    /// Needed to temporarily move files from the terminal directory of the same name
    /// </summary>
    public DirectoryInfo Tester => WDRoot.GetDirectory("Tester", true);

}

Die Klasse dient als Manager für das beschriebene Verzeichnis. Das ist sehr praktisch, denn egal, wo sich die ausführbare Datei des Auto-Optimierers befindet, wir können immer den korrekten Pfad zum gewünschten Verzeichnis erhalten, indem wir auf die Eigenschaft WDRoot dieses Objekts zugreifen. In diesem Konstruktor erstellen wir das Datenverzeichnis, falls es noch nicht existiert. Andernfalls speichern wir seine Adresse in der obigen Eigenschaft. Außerdem speichern wir den Pfad zum Unterverzeichnis "Reports". Der übergebene Parameter true gibt an, dass das Verzeichnis, wenn es nicht existiert, erstellt werden soll. 


Als Ergebnis wird das Verzeichnis Data unmittelbar nach dem ersten Start erstellt. Nach dem Erstellen hat das Verzeichnis nur das leere Unterverzeichnis "Reports". Beim ersten Start einer Optimierung oder eines Tests wird das Unterverzeichnis Tester durch einen Aufruf der Eigenschaft geeignete Eigenschaft des beschriebenen Objekts erstellt. Die Konfigurationsdatei {Terminal ID}.ini wird durch Kopieren der Konfigurationsdatei erstellt, die Sie standardmäßig ausgewählt haben. Auf diese Weise vermeiden Sie ein Überschreiben der Quellkonfigurationsdatei. Das Verzeichnis Tester wird für ein temporäres Kopieren des Caches früher durchgeführter Optimierungen erstellt. Es ähnelt teilweise dem entsprechenden Tester-Verzeichnis, das unter den veränderbaren Terminalverzeichnissen verfügbar ist.

Das Verzeichnis enthält nur den Ordner "cache". Alle Dateien aus demselben Verzeichnis des ausgewählten Terminals werden in diesen cache-Ordner verschoben. Nach dem Ende des Optimierungsprozesses werden die Dateien an den vorherigen Speicherort zurückgegeben. Diese Operation stellt die Ausführung des Optimierungsprozesses sicher. Wenn das Terminalverzeichnis Dateien enthält, die den Optimierungsprozess beschreiben, lädt der Optimierer gemäß der Optimiererlogik die zuvor durchgeführten Optimierungen, anstatt einen neuen Prozess zu starten. Dies ist eine großartige Lösung, die eine Menge Zeit spart. Aber es ist für unsere Zwecke völlig ungeeignet. Da wir unsere eigene Kopie des Optimierungsberichts speichern, der für unseren automatischen Optimierer angepasst wurde (Artikel 3 und 1 der aktuellen Serie), müssen wir einen Bericht erstellen. Um einen Bericht zu erstellen, müssen wir den Optimierungsprozess starten. Deshalb emulieren wir die Abwesenheit dieser Dateien. Um dies zu tun, verschieben wir diese Dateien vorübergehend in unser lokales Verzeichnis. Nach erfolgreichem Abschluss des Optimierungsprozesses wird im Verzeichnis Reports mit der Methode GetOptimisationDirectory ein Unterverzeichnis erstellt.

 

In der obigen Abbildung zeigt color das Verzeichnispräfix, das in den Autooptimierungseinstellungen vor dem Start der Optimierung angegeben ist. Es ermöglicht die Unterscheidung zwischen verschiedenen Optimierungen desselben Expert Advisors. Jedes Verzeichnis speichert drei Dateien mit den Ergebnissen der durchgeführten Optimierungen:

Die Dateien haben eine ähnliche Struktur, die bereits im ersten Artikel dieser Serie beschrieben wurde. Wenn Sie in der GUI auf die Schaltfläche Laden klicken, lädt der Auto-Optimierer alle drei Dateien aus dem ausgewählten Verzeichnis in die entsprechenden Tabellen. Wenn eine der drei Dateien nicht gefunden wird oder alle Dateien nicht existieren, wird eine entsprechende Meldung erzeugt. Tabellen, die fehlenden Dateien entsprechen, werden leer angezeigt. 

Wenn Sie Optimierungsergebnisse aus dem auf einem Computer befindlichen Auto-Optimierungsprogramm in das auf einem anderen Computer befindliche Auto-Optimierungsprogramm verschieben müssen, kopieren Sie einfach das Verzeichnis Reports und verschieben Sie es in das entsprechende Verzeichnis auf dem zweiten Computer. Nach dem Start greift der Auto-Optimierer auf die gewünschten Verzeichnisse mit den Ergebnissen zu. Daher werden die Ergebnisse zum Herunterladen und zur weiteren Analyse zur Verfügung stehen.

Schlussfolgerung

In den ersten Artikeln dieser Serie haben wir das Erstellen und das Laden von Optimierungsberichten untersucht. Dann haben wir das Projekt der automatischen Optimierung in Erwägung gezogen. Im vorigen Artikel haben wir das fertige Projekt analysiert. Die Idee war, den letztendlichen Zweck dieser Reihe vorzustellen. Außerdem enthält der vorherige Artikel Anweisungen zur Verwendung des fertigen automatischen Optimierers. In diesem Artikel haben wir technische Aspekte bei der Implementierung des automatischen Optimierers betrachtet. Bevor wir uns der Analyse des logischen Teils des Projekts zuwenden, haben wir uns mit der grafischen Oberfläche und den Modifikationen in Dateien befasst, die aus früheren Artikelserien entlehnt wurden. Links zu den vorherigen Artikelserien finden Sie in der Einleitung zu diesem Artikel. Im nächsten Artikel werden wir uns mit der Umsetzung des logischen Teils des Programms befassen.

Der Anhang enthält das in Artikel 4 analysierte Auto-Optimierer-Projekt mit einem Handelsroboter. Um das Projekt zu verwenden, kompilieren Sie bitte die Auto-Optimierer-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 Optimierers mit Ihren Expertenberatern.