Kontinuierliche Walk-Forward-Optimierung (Teil 7): Einbinden des logischen Teils des Auto-Optimizer mit Grafiken und Steuerung

Andrey Azatskiy | 7 August, 2020

In diesem Artikel betrachten wir, wie der logische Teil des Programms mit seiner grafischen Darstellung verbunden ist. Wir werden den gesamten laufenden Optimierungsprozess von Anfang an betrachten und alle Stufen bis zur Klasse des Auto-Optimizers analysieren. Wir werden auch sehen, wie der logische Programmteil mit der Darstellung verbunden ist, sowie Methoden zur Verwaltung von Grafiken aus dem Anwendungscode betrachten. 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
  6. Kontinuierliche Walk-Forward-Optimierung (Teil 6): Logikteil und die Struktur des Auto-Optimizers

Die Klasse ViewModel und die Interaktion mit der Grafikebene

Wie bereits früher erwähnt, ist ViewModel das Bindeglied zwischen dem grafischen Teil der Anwendung und der Software-Implementierung der Logik. Es ist die grafische Darstellung des Programms, die die Aufrufe der Anwendungslogik und die grafische Reaktion auf die Rückrufe des logischen Teils der Anwendung implementiert. Dementsprechend entspricht eine 'public' Eigenschaft aus dem ViewModel-Teil jedem editierbaren Feld im grafischen Teil der Anwendung. Diese Eigenschaften können entweder Getter (Abrufen von Werten) sein, in diesem Fall können sie von der Grafik aus nicht geändert werden, oder Setter (Zuweisen von Werten), die das Überschreiben des hinter dieser Eigenschaft verborgenen Objekts ermöglichen. In früheren Teilen haben wir uns bereits ausführlich mit der Datenbindungstechnologie befasst. Daher werde ich hier nur einige wenige Beispiele anführen. 

Textfelder werden über Eigenschaften verbunden, die sowohl Schreib- als auch Lesezugriff haben. Betrachten Sie als Beispiel ein Feld, das den Namen eines Handelssymbols angibt, an dem eine Optimierung durchgeführt wird. Das XAML-Markup für dieses Feld ist äußerst einfach.

<TextBox Width="100"          IsEnabled="{Binding EnableMainTogles, UpdateSourceTrigger=PropertyChanged}"          Text="{Binding AssetName}"/>

Zusätzlich zur Einstellung der Breite des Textfensters hat es auch die Felder IsEnabled und Text. Das erste legt fest, ob das Feld zur Bearbeitung zur Verfügung steht. Wenn es auf true gesetzt wird, wird das Feld zur Bearbeitung verfügbar. Wenn es auf false gesetzt ist, wird das Feld gesperrt. Das Feld "Text" enthält den in dieses Feld eingegebenen Text. Dann gibt es gegenüber jedem Feld eine Konstruktion in geschweiften Klammern. Ihr Inhalt legt die Verbindung des Objekts mit einer bestimmten 'public' Eigenschaft aus der Klasse ViewModel fest, die nach dem Parameter "Binding" angegeben wird.

Darauf können eine Reihe von Parametern folgen. Beispielsweise gibt der Parameter "UpdateSourceTrigger" die Methode zur Aktualisierung des grafischen Teils dieser Anwendung an. Der in unserem Beispiel verwendete Wert (PropertyChanged) gibt an, dass der grafische Teil nur dann aktualisiert wird, wenn das Ereignis OnPropertyChanged von der Klasse ViewModel mit einem nach den Parameter "Binding" angegebenen übergebenen Namen (in unserem Fall "EnableMainTogles") ausgelöst wird.

Wenn der Parameter "Text" nicht an eine Zeichenkette, sondern, sagen wir, an einen 'double' Parameter gebunden ist, sind in diesem Feld nur Zahlen erlaubt. Wenn er an einen 'int'-Typ gebunden ist, sind nur ganze Zahlen erlaubt. Mit anderen Worten, diese Implementierung erlaubt es, Anforderungen für den eingegebenen Werttyp festzulegen.

Im ViewModel-Teil werden die Felder wie folgt dargestellt:

Der Parameter IsEnabled:

/// <summary> /// If the switch = false, then the most important fields are not available /// </summary> public bool EnableMainTogles { get; private set; } = true;

und der Parameter Text:

/// <summary> /// Name of the asset selected for tests / optimization /// </summary> public string AssetName { get; set; }

Wie man sehen kann, haben beide sowohl schreibenden als auch lesenden Zugriff auf Daten. Der einzige Unterschied besteht darin, dass die Eigenschaften EnableMainTogles nur Schreibzugriff von der Klasse AutoOptimiserVM (d.h. von sich selbst) bietet, so dass sie von außen nicht bearbeitet werden kann.

Wenn wir irgendeine Datenkollektion betrachten, wie z.B. eine Liste der Ergebnisse der Vorwärtsoptimierung, dann entspricht sie einer Eigenschaft, die eine Liste von Werten enthält. Betrachten wir eine Tabelle mit den Ergebnissen von Vorwärtsdurchläufen:

<ListView ItemsSource="{Binding ForwardOptimisations}"           SelectedIndex="{Binding SelectedForwardItem}"           v:ListViewExtention.DoubleClickCommand="{Binding StartTestForward}">     <ListView.View>         <GridView>             <GridViewColumn Header="Date From"                             DisplayMemberBinding="{Binding From}"/>             <GridViewColumn Header="Date Till"                             DisplayMemberBinding="{Binding Till}"/>             <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>

Wie aus dem Markup ersichtlich ist, ist die Tabelle vom Typ ListView eine Referenz der Tabellenklasse selbst. Darauf folgt die Erzeugung eines Rasters, in dem die Daten gespeichert werden, und eine Spalte mit den Daten. Indem ich die Erstellung einer Klassenreferenz erwähnte, bezog ich mich auf die Klasse ListView. Dieses scheinbar einfache XAML-Markup steht für einen ziemlich komplexen und gut durchdachten Mechanismus, der die Beschreibung von Klassen und die Arbeit mit den Klassenobjekten mit Hilfe der Markup-Sprache ermöglicht. Alle Felder, die wir mit der Klasse AutoOptimiserVM verknüpfen, sind genau die Eigenschaften dieser Klassen. In dem obigen Beispiel mit der Tabelle haben wir es mit drei Klassen zu tun:

Die Eigenschaft ItemsSource der Klasse ListView zeigt eine Kollektion von Elementen an, aus denen die Tabelle besteht. Nachdem diese Eigenschaft mit einer Kollektion aus ViewModel verbunden wurde, haben wir eine Art DataContext für die Klasse Window, die innerhalb unserer Tabelle operiert. Da wir von einer Tabelle sprechen, muss die Tabelle, die die Sammlung repräsentiert, aus Klassen bestehen, die 'public' Eigenschaften für jede der Tabellen haben. Nachdem wir die Eigenschaft ItemsSource mit einer Eigenschaft aus ViewModel, die eine Tabelle mit Daten darstellt, gebunden haben, können wir jede der Spalten mit dem gewünschten Spaltenwert aus der gegebenen Tabelle binden. Außerdem hat die Tabelle eine Verbindung der Eigenschaft SelectedIndex mit der Eigenschaft SelectedForwardItem aus ViewModel. Dies ist erforderlich, damit ViewModel weiß, welche Zeile der Nutzer in dieser Tabelle ausgewählt hat.

Im ViewModel-Teil wird die Eigenschaft, mit der die präsentierte Tabelle gebunden ist, wie folgt implementiert:

/// <summary> /// Selected forward tests /// </summary> public ObservableCollection<ReportItem> ForwardOptimisations { get; } = new ObservableCollection<ReportItem>();

Die Klasse ObservableCollection aus der C#-Standardbibliothek ist ein Objekt, das die Grafiken über Änderungen benachrichtigt. Das liegt daran, dass die Klasse bereits über das erwähnte Ereignis verfügt und es jedes Mal aufruft, wenn die Liste ihrer Elemente aktualisiert wird. Im Übrigen handelt es sich um eine standardmäßige Datensammlung.

Die Eigenschaft SelectedForwardItem erfüllt mehrere Funktionen: Sie speichert Daten zur ausgewählten Tabellenzeile und dient als Zeilenauswahl-Callback.

/// <summary> /// Selected forward pass /// </summary> private int _selectedForwardItem; public int SelectedForwardItem {     get => _selectedForwardItem;     set     {         _selectedForwardItem = value;         if (value > -1)         {             FillInBotParams(model.ForwardOptimisations[value]);             FillInDailyPL(model.ForwardOptimisations[value]);             FillInMaxPLDD(model.ForwardOptimisations[value]);         }     } } 

Da die Eigenschaft als Callback verwendet wird, aufgrund dessen die Spezifikation der Reaktion auf das Setzen eines Wertes (in unserem Beispiel) erwartet wird, muss der Setter die Implementierung dieser Reaktion enthalten und als Funktion dienen. Aus diesem Grund wird der Eigenschaftswert in einer 'private' Variablen gespeichert. Um einen Wert aus dieser Variablen zu erhalten, greifen wir direkt auf sie vom Getter aus zu. Um einen Wert zu setzen, weist der Setter den in 'value' gespeicherten Wert zu. Die Variable 'value' ist nicht betitelt und dient als ein bestimmter Alias für den gesetzten Wert, der von der Sprache C# bereitgestellt wird. Wenn 'value' größer als -1 ist, füllen wir andere Bezugstabellen in der Registerkarte Ergebnisse, die entsprechend der ausgewählten Zeile aktualisiert werden. Dies sind die Tabellen mit den Parametern des Handelsroboters, dem durchschnittlichen Gewinn, den Verlusten für einen Wochentag und den höchsten/niedrigsten GV-Werten. Die Prüfung, die in der 'if'-Bedingung durchgeführt wird, ist erforderlich, denn wenn der Index des ausgewählten Tabellenelements -1 ist, bedeutet dies, dass die Tabelle leer ist und somit keine Notwendigkeit besteht, die Bezugstabellen zu füllen. Die Implementierung der aufgerufenen Methoden ist im Code der Klasse AutoOptimiserVM verfügbar.

Hier ist die Implementierung der Klasse, die die Zeile mit dem Optimierungsergebnis beschreibt.

/// <summary> /// Class - a wrapper for a report item (for a graphical interval) /// </summary> class ReportItem {     /// <summary>     /// Constructor     /// </summary>     /// <param name="item">Item</param>     public ReportItem(OptimisationResult item)     {         result = item;     }     /// <summary>     /// Report item     /// </summary>     private readonly OptimisationResult result;     public DateTime From => result.report.DateBorders.From;     public DateTime Till => result.report.DateBorders.Till;     public double SortBy => result.SortBy;     public double Payoff => result.report.OptimisationCoefficients.Payoff;     public double ProfitFactor => result.report.OptimisationCoefficients.ProfitFactor;     public double AverageProfitFactor => result.report.OptimisationCoefficients.AverageProfitFactor;     public double RecoveryFactor => result.report.OptimisationCoefficients.RecoveryFactor;     public double AverageRecoveryFactor => result.report.OptimisationCoefficients.AverageRecoveryFactor;     public double PL => result.report.OptimisationCoefficients.PL;     public double DD => result.report.OptimisationCoefficients.DD;     public double AltmanZScore => result.report.OptimisationCoefficients.AltmanZScore;     public int TotalTrades => result.report.OptimisationCoefficients.TotalTrades;     public double VaR90 => result.report.OptimisationCoefficients.VaR.Q_90;     public double VaR95 => result.report.OptimisationCoefficients.VaR.Q_95;     public double VaR99 => result.report.OptimisationCoefficients.VaR.Q_99;     public double Mx => result.report.OptimisationCoefficients.VaR.Mx;     public double Std => result.report.OptimisationCoefficients.VaR.Std; }

Die Klasse wird hier bereitgestellt, um eine Zeichenkettendarstellung eines Optimierungsdurchgangs in einem Code zu demonstrieren. Jede Tabellenspalte ist mit einer entsprechenden Eigenschaft der spezifischen Klasseninstanz verknüpft. Die Klasse selbst ist eine Hülle für die Struktur OptimisationResult, die im ersten Artikel betrachtet wurde.

Alle Schaltflächen oder Doppelklicks auf eine Tabellenzeile sind durch die Command-Eigenschaft von ViewModel verbunden, deren Grundtyp ICommand ist. Wir haben diese Technologie bereits in früheren Artikeln über die Erstellung der grafischen Oberfläche betrachtet. 

Die Klasse ViewModel und Interaktion mit dem Datenmodell

Beginnen wir dieses Kapitel mit den Start- und Stopp-Rückrufen der Optimierung, die in der gleichen Schaltfläche zusammengefasst sind. 


Durch Klicken auf die Schaltfläche StartStop wird die Methode _StartStopOptimisation aus der Klasse AutoOptimiserVM aufgerufen. Außerdem gibt es zwei Alternativen: Stoppen der Optimierung und Starten der Optimierung. Wie aus dem Diagramm ersichtlich ist, führen wir, wenn die Eigenschaft IsOptimisationInProcess der Optimiererklasse den Wert true zurückgibt, den ersten Teil der Logik aus und fordern die Methode StopOptimisation von der Datenmodellklasse an. Die Methode leitet diesen Aufruf dann an den Optimierer um. Wenn die Optimierung nicht gestartet wurde, wird die Methode StartOptimisation von der Datenmodellklasse aufgerufen. Die Methode ist asynchron, und daher wird der Betrieb der aufgerufenen Start-Methode auch nach Abschluss der _StartStopOptimierung fortgesetzt. 

Wir haben die Kette der Aufrufe betrachtet, die beim Aufruf der Methode ausgeführt wird. Betrachten wir nun den Codeblock, der die Verbindung dieser Methodenaufrufe mit dem grafischen Teil und Modell mit ViewModel beschreibt. Die grafischen XAML-Markups sind nicht schwierig und werden daher hier nicht vorgestellt. Was den ViewModel-Teil betrifft, so sind die Eigenschaften und die Methode, die für den Start der Optimierung verantwortlich sind, wie folgt:

private void _StartStopOptimisation(object o) {     if (model.Optimiser.IsOptimisationInProcess)     {         model.StopOptimisation();     }     else     {         EnableMainTogles = false;         OnPropertyChanged("EnableMainTogles");         Model.OptimisationManagers.OptimiserInputData optimiserInputData = new Model.OptimisationManagers.OptimiserInputData         {             Balance = Convert.ToDouble(OptimiserSettings.Find(x => x.Name == "Deposit").SelectedParam),             BotParams = BotParams?.Select(x => x.Param).ToList(),             CompareData = FilterItems.ToDictionary(x => x.Sorter, x => new KeyValuePair<CompareType, double>(x.CompareType, x.Border)),             Currency = OptimiserSettings.Find(x => x.Name == "Currency").SelectedParam,             ExecutionDelay = GetEnum<ENUM_ExecutionDelay>(OptimiserSettings.Find(x => x.Name == "Execution Mode").SelectedParam),             Laverage = Convert.ToInt32(OptimiserSettings.Find(x => x.Name == "Laverage").SelectedParam),             Model = GetEnum<ENUM_Model>(OptimiserSettings.Find(x => x.Name == "Optimisation model").SelectedParam),             OptimisationMode = GetEnum<ENUM_OptimisationMode>(OptimiserSettings.Find(x => x.Name == "Optimisation mode").SelectedParam),             RelativePathToBot = OptimiserSettings.Find(x => x.Name == "Available experts").SelectedParam,             Symb = AssetName,             TF = GetEnum<ENUM_Timeframes>(OptimiserSettings.Find(x => x.Name == "TF").SelectedParam),             HistoryBorders = (DateBorders.Any(x => x.BorderType == OptimisationType.History) ?                             DateBorders.Where(x => x.BorderType == OptimisationType.History)                             .Select(x => x.DateBorders).ToList() :                             new List<DateBorders>()),             ForwardBorders = (DateBorders.Any(x => x.BorderType == OptimisationType.Forward) ?                             DateBorders.Where(x => x.BorderType == OptimisationType.Forward)                             .Select(x => x.DateBorders).ToList() :                             new List<DateBorders>()),             SortingFlags = SorterItems.Select(x => x.Sorter)         };         model.StartOptimisation(optimiserInputData, FileWritingMode == "Append", DirPrefix);     } } /// <summary> /// Callback for the graphical interface - run optimization / test /// </summary> public ICommand StartStopOptimisation { get; }

Wie aus dem Code und dem Diagramm ersichtlich ist, ist die Methode in zwei Zweige der 'If-Else'-Bedingung unterteilt. Der erste Zweig stoppt den Optimierungsprozess, wenn er läuft. Der zweite Zweig startet den Prozess, wenn er nicht läuft.

Zum Zeitpunkt des Optimierungsstarts sperren wir die Hauptfelder der graphischen Oberfläche, indem wir EnableMainTogles = false setzen, und fahren dann mit der Bildung der Eingabeparameter fort. Um eine Optimierung zu starten, müssen wir die Struktur OptimistionInputData erstellen, die aus den Sammlungen OptimiserSettings, BotParams, FilterItems, SorterItems und DateBorders gefüllt wird. Die Werte gelangen über den bereits erwähnten Datenbindungsmechanismus direkt von der grafischen Oberfläche in diese Strukturen. Nach Abschluss der Bildung dieser Struktur führen wir die zuvor besprochene Methode StartOptimierung für eine Instanz der Datenmodellklasse aus. Die Eigenschaft StartStopOptimierung im Konstruktor.

// Callback of optimization start/stop buttons StartStopOptimisation = new RelayCommand(_StartStopOptimisation);

Sie wird von der Instanz der Klasse RelayCommand instanziiert, die die Schnittstelle ICommand implementiert, die zum Binden von ViewModel-Befehlen mit der Command-Eigenschaft aus dem grafischen Teil der Anwendung erforderlich ist.

Nachdem alle Optimierungen durchgeführt und Tabellen in der Ergebnis-Registerkarte gebildet worden sind (oder nachdem sie mit der Schaltfläche Laden und durch Auswahl einer Optimierung aus der Liste hochgeladen worden sind), können Sie einen Test des ausgewählten Optimierungsdurchlaufs in jedem der erforderlichen Zeitintervalle starten, indem Sie auf den gewünschten Optimierungsdurchlauf doppelklicken. 

private void _StartTest(List<OptimisationResult> results, int ind) {     try     {         Model.OptimisationManagers.OptimiserInputData optimiserInputData = new Model.OptimisationManagers.OptimiserInputData         {             Balance = Convert.ToDouble(OptimiserSettingsForResults_fixed.First(x => x.Key == "Deposit").Value),             Currency = OptimiserSettingsForResults_fixed.First(x => x.Key == "Currency").Value,             ExecutionDelay = GetEnum<ENUM_ExecutionDelay>(OptimiserSettingsForResults_changing.First(x => x.Name == "Execution Mode").SelectedParam),             Laverage = Convert.ToInt32(OptimiserSettingsForResults_fixed.First(x => x.Key == "Laverage").Value),             Model = GetEnum<ENUM_Model>(OptimiserSettingsForResults_changing.First(x => x.Name == "Optimisation model").SelectedParam),             OptimisationMode = ENUM_OptimisationMode.Disabled,             RelativePathToBot = OptimiserSettingsForResults_fixed.First(x => x.Key == "Expert").Value,             ForwardBorders = new List<DateBorders>(),             HistoryBorders = new List<DateBorders> { new DateBorders(TestFrom, TestTill) },             Symb = OptimiserSettingsForResults_fixed.First(x => x.Key == "Symbol").Value,             TF = (ENUM_Timeframes)Enum.Parse(typeof(ENUM_Timeframes), OptimiserSettingsForResults_fixed.First(x => x.Key == "TF").Value),             SortingFlags = null,             CompareData = null,             BotParams = results[ind].report.BotParams.Select(x => new ParamsItem { Variable = x.Key, Value = x.Value }).ToList()         };         model.StartTest(optimiserInputData);     }     catch (Exception e)     {         System.Windows.MessageBox.Show(e.Message);     } }

Dann erstellen wir eine Struktur mit Eingabeparametern und starten einen Test. Im Falle eines Fehlers während des Methodenausführungsprozesses zeigen wir eine Fehlermeldung in einer MessageBox an. Die Implementierung der Methode wurde bereits besprochen. Lassen Sie uns jedoch noch einmal einen Blick auf die Instantiierung von Eigenschaften werfen, die diesen Callback enthalten. Wir haben drei verschiedene Tabellen:

Daher wurden drei Rückrufe angelegt. Dies ist für eine korrekte Verarbeitung der einzelnen Tabellendaten erforderlich. 

/// <summary>
/// Run a test from a table with forward tests
/// </summary>
public ICommand StartTestForward { get; }
/// <summary>
/// Run a test from a table with historical tests
/// </summary>
public ICommand StartTestHistory { get; }
/// <summary>
/// Run a test from a table with optimization results
/// </summary>
public ICommand StartTestReport { get; }

Ihre Implementierung erfolgt über das Setzen von Lambda-Funktionen:

StartTestReport = new RelayCommand((object o) => {     _StartTest(model.AllOptimisationResults.AllOptimisationResults[ReportDateBorders[SelectedReportDateBorder]], SelecterReportItem); }); // Callback for the test start upon the event of double-clicking on the table with historical tests StartTestHistory = new RelayCommand((object o) => {     _StartTest(model.HistoryOptimisations, SelectedHistoryItem); }); // Callback for the test start upon the event of double-clicking on the table with historical tests StartTestForward = new RelayCommand((object o) => {     _StartTest(model.ForwardOptimisations, SelectedForwardItem); });

Dieser Ansatz ermöglicht die Erstellung der benötigten Liste mit Optimierungsergebnissen, die für die Ermittlung der Roboterparameter verwendet wird, die der Algorithmus an eine Datei übergibt (für Details siehe Teil 3 dieser Artikelserie). 

Nachdem der Optimierungsprozess beendet ist und die besten Ergebnisse ausgewählt und anhand von historischen und Vorwärtsdaten getestet wurden, wird eine Liste mit allen Optimierungsdurchläufen gespeichert. Aufgrund dieses Prozesses kann der Nutzer die Operationslogik des ausgewählten Optimierers überprüfen und auch andere Durchläufe manuell auswählen, indem er Filter- und Sortierfaktoren ändert. Es besteht daher die Möglichkeit, den eingebauten Mechanismus zum Filtern von Optimierungsergebnissen zu verwenden und diese nach mehreren Kriterien gleichzeitig zu sortieren. Dieser Mechanismus ist im Datenmodell implementiert, aber die Eingabeparameter für den Mechanismus werden in der Klasse ViewModel generiert.

/// <summary> /// Sort reports /// </summary> /// <param name="o"></param> private void _SortResults(object o) {     if (ReportDateBorders.Count == 0)         return;     IEnumerable<SortBy> sortFlags = SorterItems.Select(x => x.Sorter);     if (sortFlags.Count() == 0)         return;     if (AllOptimisations.Count == 0)         return;     model.SortResults(ReportDateBorders[SelectedReportDateBorder], sortFlags); } public ICommand SortResults { get; } /// <summary> /// Filtering reports /// </summary> /// <param name="o"></param> private void _FilterResults(object o) {     if (ReportDateBorders.Count == 0)         return;     IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData =         FilterItems.ToDictionary(x => x.Sorter, x => new KeyValuePair<CompareType, double>(x.CompareType, x.Border));     if (compareData.Count() == 0)         return;     if (AllOptimisations.Count == 0)         return;     model.FilterResults(ReportDateBorders[SelectedReportDateBorder], compareData); } public ICommand FilterResults { get; }

Diese beiden Methoden haben ähnliche Implementierungen. Sie prüfen das Vorhandensein von Datenfilterparametern (d.h., dass die Tabelle nicht leer ist) und leiten ihre Ausführung an die Datenmodellklasse um. Beide Methoden der Datenmodellklasse leiten die Ausführung an die im ersten Artikel beschriebene entsprechende Erweiterungsmethode um.

Die Sortiermethode hat die folgenden Signaturen:

public static IEnumerable<OptimisationResult> SortOptimisations(this IEnumerable<OptimisationResult> results,                                                                         OrderBy order, IEnumerable<SortBy> sortingFlags,                                                                         Func<SortBy, SortMethod> sortMethod = null)

Die Filtermethode:

public static IEnumerable<OptimisationResult> FiltreOptimisations(this IEnumerable<OptimisationResult> results,                                                                   IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData)

Dies wird in einem asynchronen Modus durchgeführt, um Grafiksperren während der Sortierung (die je nach Datenmenge mehr als eine Sekunde dauern kann) zu vermeiden.

Wenn wir über das Sortieren von Daten sprechen, lassen Sie uns die Implementierung der Verbindung zwischen zwei Datensortiertabellen und der Datenfilterung betrachten. Im automatischen Optimierer haben sowohl die Registerkarte Ergebnisse als auch die Registerkarte Einstellungen (Hauptregister) einen Bereich mit der Datensortierung und Filterung von Tabellendaten - das ist es, worüber wir sprechen.

   

Im obigen Screenshot ist dieser Bereich auf der Registerkarte Optimierungsergebnisse markiert. Die Idee ist, dass, wenn wir in diesem Bereich irgendeinen Sortierparameter hinzufügen und dann zu einem anderen Register wechseln (in unserem Beispiel das Register Einstellungen), derselbe Mehrwert im selben Bereich erscheint. Wenn wir nun diesen Wert aus diesem Bereich auf der Registerkarte Einstellungen mit entfernen und dann wieder auf die Registerkarte mit den Optimierungsergebnissen wechseln, werden wir sehen, dass der Wert auch aus dieser Registerkarte entfernt wurde. Das liegt daran, dass beide Tabellen mit derselben Eigenschaft verknüpft sind.

Sortierungstabellen sind mit der folgenden Eigenschaft verknüpft:

/// <summary> /// Selected sorting options /// </summary> public ObservableCollection<SorterItem> SorterItems { get; } = new ObservableCollection<SorterItem>();

Die Filtertabellen:   

/// <summary> /// Selected filters /// </summary> public ObservableCollection<FilterItem> FilterItems { get; } = new ObservableCollection<FilterItem>();

Die Klassen, die die Zeilen in diesen Tabellen beschreiben, haben einige sich wiederholende Felder und werden in derselben Datei wie das ViewModel betitelt.

/// <summary> /// Wrapper class for enum SortBy (for graphical interval) /// </summary> class SorterItem {     /// <summary>     /// Constructor     /// </summary>     /// <param name="sorter">Sort parameter</param>     /// <param name="deleteItem">Delete from list callback</param>     public SorterItem(SortBy sorter, Action<object> deleteItem)     {         Sorter = sorter;         Delete = new RelayCommand((object o) => deleteItem(this));      }      /// <summary>      /// Sort element      /// </summary>      public SortBy Sorter { get; }      /// <summary>      /// Item delete callback      /// </summary>      public ICommand Delete { get; } } /// <summary> /// Wrapper class for enum SortBy and CompareType flags (for GUI) /// </summary> class FilterItem : SorterItem {     /// <summary>     /// Constructor     /// </summary>     /// <param name="sorter">Sort element</param>     /// <param name="deleteItem">Deletion callback</param>     /// <param name="compareType">Comparison method</param>     /// <param name="border">Comparable value</param>     public FilterItem(SortBy sorter, Action<object> deleteItem,                       CompareType compareType, double border) : base(sorter, deleteItem)     {         CompareType = compareType;         Border = border;     }     /// <summary>     /// Comparison type     /// </summary>     public CompareType CompareType { get; }     /// <summary>     /// Comparable value     /// </summary>     public double Border { get; } }

Die Klasse SorterItem ist ein Objekt, das Tabellenzeilen der ausgewählten Parameter zum Sortieren präsentiert. Zusätzlich zum Sortierparameter enthält sie auch die Eigenschaft, die auf den callback zum Löschen dieses speziellen Parameters aus der Liste zeigt. Bitte beachten Sie, dass dieser callback extern über delegate gesetzt wird. Die Datenfilterklasse ist von der Sortierklasse abgeleitet: Es ist nicht nötig, bereits implementierte Felder doppelt zu schreiben, da wir sie einfach von der Basisklasse erben können. Zusätzlich zu dem zuvor betrachteten Parametersatz verfügt sie über einen Datenvergleichstyp mit einem Schwellenwert und diesem Schwellenwert selbst.

Das Vorhandensein von Löschmethoden in der Klasse, die eine Zeile präsentiert, erlaubt es, wie in der aktuellen Implementierung, neben jeder Zeile eine Schaltfläche Löschen hinzuzufügen. Dies ist bequem für Nutzer und hat eine interessante Implementierung. Die Löschmethoden sind außerhalb der Klassen implementiert. Sie werden als Delegierte festgelegt, da sie Zugriff auf Datensammlungen benötigen, die sich in der Klasse befinden, die ViewModel darstellt. Ihre Implementierung ist recht einfach und wird daher hier nicht vorgestellt. Diese Methoden rufen nur die Methode Delete für die gewünschte Datensammlungsinstanz auf.

Nach Abschluss einiger Ereignisse, die eine Reaktion der grafischen Schicht erfordern, wird das OnPropertyChanged-Ereignis aufgerufen. Das Callback-Ereignis in der Klasse, die ViewModel repräsentiert, ist wie folgt implementiert:

private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e) {     // The test has completed, or you need to resume the availability of the buttons locked at the optimization or test start     if (e.PropertyName == "StopTest" ||         e.PropertyName == "ResumeEnablingTogle")     {         // button accessibility switch = true         EnableMainTogles = true;         // Reset status and progress         Status = "";         Progress = 0;         // Notify the GUI of changes         dispatcher.Invoke(() =>         {             OnPropertyChanged("EnableMainTogles");             OnPropertyChanged("Status");             OnPropertyChanged("Progress");         });     }     // Changed the list of passed optimization passes     if (e.PropertyName == "AllOptimisationResults")     {         dispatcher.Invoke(() =>         {             // Clear the previously saved optimization passes and add new ones             ReportDateBorders.Clear();             foreach (var item in model.AllOptimisationResults.AllOptimisationResults.Keys)             {                 ReportDateBorders.Add(item);             }             // Select the very first date             SelectedReportDateBorder = 0;             // Fill in the fixed settings of the tester in accordance with the settings of the uploaded results             ReplaceBotFixedParam("Expert", model.AllOptimisationResults.Expert);             ReplaceBotFixedParam("Deposit", model.AllOptimisationResults.Deposit.ToString());             ReplaceBotFixedParam("Currency", model.AllOptimisationResults.Currency);             ReplaceBotFixedParam("Laverage", model.AllOptimisationResults.Laverage.ToString());             OnPropertyChanged("OptimiserSettingsForResults_fixed");         });         // Notify when data loading is complete         System.Windows.MessageBox.Show("Report params where updated");     }     // Filter or sort optimization passes     if (e.PropertyName == "SortedResults" ||         e.PropertyName == "FilteredResults")     {         dispatcher.Invoke(() =>         {             SelectedReportDateBorder = SelectedReportDateBorder;         });     }     // Updated forward optimization data     if (e.PropertyName == "ForwardOptimisations")     {         dispatcher.Invoke(() =>         {             ForwardOptimisations.Clear();             foreach (var item in model.ForwardOptimisations)             {                 ForwardOptimisations.Add(new ReportItem(item));             }         });     }     // Updated historical optimization data     if (e.PropertyName == "HistoryOptimisations")     {         dispatcher.Invoke(() =>         {             HistoryOptimisations.Clear();             foreach (var item in model.HistoryOptimisations)             {                 HistoryOptimisations.Add(new ReportItem(item));             }         });     }     // Save (*.csv) file with optimization/test results     if (e.PropertyName == "CSV")     {         System.Windows.MessageBox.Show("(*.csv) File saved");     } }

Alle Bedingungen in diesem Callback überprüfen die Eigenschaft PropertyName aus dem Eingabeparameter "e". Die erste Bedingung ist erfüllt, wenn der Test abgeschlossen ist und das Datenmodell aufgefordert wird, die GUI zu entsperren. Wenn diese Bedingung ausgelöst wird, entsperren wir die GUI, setzen den Status des Fortschrittsbalkens und die Fortschrittsbalken auf die Anfangswerte zurück. Beachten Sie, dass dieses Ereignis im Kontext des sekundären Threads aufgerufen werden kann, und die Grafikbenachrichtigung (OnPropertyChanged-Ereignisaufruf) muss immer im Kontext des primären Threads erfolgen, d.h. im selben Thread mit einer GUI. Um Fehler zu vermeiden, rufen Sie daher dieses Event aus der Dispatcher-Klasse auf. Der Dispatcher erlaubt den Zugriff auf die GUI aus dem Thread-Kontext dieses Fensters.

Die nächste Bedingung wird aufgerufen, sobald das Datenmodell die Liste aller durchgeführten Optimierungen aktualisiert. Um die Auswahl von Optimierungslisten über eine Combobox zu ermöglichen, müssen wir diese mit entsprechenden Optimierungsdaten füllen. Dies geschieht durch diesen Codeteil. Er füllt auch feste Tester-Parameter:

Danach zeigt es eine MessageBox an, die darüber informiert, dass die Aktualisierung der Parameter und der Tabellen mit den Berichten des Optimierungsdurchlaufs abgeschlossen ist.

Sobald das Filtern oder Sortieren abgeschlossen ist, wird die entsprechende Bedingung ausgelöst. Um ihre Implementierung zu verstehen, besprechen wir jedoch die Implementierung der Eigenschaft SelectedReportDateBorder.

#region Selected optimisation date border index keeper private int _selectedReportDateBorder; public int SelectedReportDateBorder {     get => _selectedReportDateBorder;     set     {         AllOptimisations.Clear();         if (value == -1)         {             _selectedReportDateBorder = 0;             return;         }         _selectedReportDateBorder = value;         if (ReportDateBorders.Count == 0)             return;         List<OptimisationResult> collection = model.AllOptimisationResults.AllOptimisationResults[ReportDateBorders[value]];         foreach (var item in collection)         {             AllOptimisations.Add(new ReportItem(item));         }     } } #endregion

Der Setter-Teil aktualisiert die AllOptimisations-Sammlung in der Klasse ViewModel, und daher macht der Code in der Bedingung jetzt Sinn. Mit anderen Worten, indem wir den Parameter SelectedReportDateBorder auf sich selbst setzen, vermeiden wir einfach die Duplizierung dieser Schleife. 

Bedingungen, die sich auf die Aktualisierung der Tabellen Forward und Historical beziehen, dienen der gleichen Rolle wie die vorherige Bedingung, d.h. der Datensynchronisation zwischen ViewModel und Modell. Diese Synchronisation ist notwendig, da wir uns nicht direkt auf die Strukturen beziehen können, auf denen das Datenmodell arbeitet, da entsprechende Klassen zur Beschreibung von Tabellenzeilen benötigt werden, wobei jede Spalte durch eine Eigenschaft repräsentiert wird. Diese Klassen werden als Wrapper für die im Datenmodell verwendeten Strukturen angelegt. Die Klasse ReportItem wird für Tabellen mit Optimierungsergebnissen verwendet, was im vorigen Kapitel betrachtet wurde.

Schlussfolgerung

Dieser Artikel geht dem letzten innerhalb der Artikelserie voraus, die sich mit der Walk-Forward-Optimierung und einem Auto-Optimizer befasst, der diesen Prozess implementiert. Wir haben die Struktur der wichtigsten Teile der erstellten Anwendung besprochen. Der erste Artikel beschrieb den Teil der Anwendung, der für die Arbeit mit Berichten und deren Speicherung in Xml-Dateien verantwortlich ist. Der zweite und dritte Teil enthielten eine Beschreibung, wie ein Bericht für den Auto-Optimizer generiert wird und wie ein Expert Advisor mit der Schnittstelle des Berichtsladeprogramms verbunden werden kann, die im ersten Artikel beschrieben wurde. Der vierte Teil enthielt Anweisungen zur Programmverwendung: Zu diesem Zeitpunkt haben wir die erforderlichen Schritte für die Verbindung jedes Roboters mit dem Autooptimierer in Betracht gezogen.

In den Teilen 5, 6 und 7 haben wir das Auto-Optimierungsprogramm besprochen, das den Prozess steuert. Wir begannen mit seinem grafischen Teil (fünfter Artikel), untersuchten dann seine Operationslogik (sechster Artikel) und die Verbindung zwischen ihnen (aktueller Artikel). In den Kommentaren zum fünften Artikel fügten Anwender einige Vorschläge bezüglich der Nutzeroberfläche der Anwendung hinzu. Die interessantesten von ihnen wurden bereits umgesetzt.

Der vorliegende Teil enthält diese Verbesserungen nicht, da die primäre Idee darin bestand, die bisherige Arbeit zu beschreiben. Der nächste Artikel (der der letzte sein wird) enthält die angegebenen Verbesserungen und beschreibt, wie Sie Ihren eigenen Optimierer erstellen können. Mit Optimizer meine ich die Logik der laufenden Optimierungen. Die aktuelle Optimiererlogik wurde bereits früher besprochen (hauptsächlich im vierten Artikel). Daher wird im letzten Artikel eine Anweisung gegeben, wie eine ähnliche Logik erstellt werden kann. Wir werden die vorhandene Optimierungslogik als Grundlage verwenden und einen Schritt-für-Schritt-Vorgang zur Erstellung Ihres eigenen Optimierers in Betracht ziehen.

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.