Kontinuierliche Walk-Forward-Optimierung (Teil 1): Arbeiten mit Optimierungsberichten

Andrey Azatskiy | 20 Januar, 2020

Einführung

In den vorangegangenen Artikeln (Optimierungsmanagement (Teil I) und Optimierungsmanagement (Teil II)) haben wir einen Mechanismus zur Einleitung der Optimierung im Terminal durch einen Drittprozess betrachtet. Dies ermöglicht es, einen bestimmten Optimierungsmanager zu erstellen, der den Prozess ähnlich wie ein Handelsalgorithmus, der einen bestimmten Handelsprozess implementiert, d.h. in einem vollautomatischen Modus ohne Benutzereingriff, implementieren kann. Die Idee ist es, einen Algorithmus zu schaffen, der den gleitenden Optimierungsprozess verwaltet, bei dem Vorwärts- und historische Perioden um ein vorgegebenes Intervall verschoben werden und sich gegenseitig überlappen.

Dieser Ansatz der Algorithmusoptimierung kann eher als Strategierobustheitstest denn als reine Optimierung dienen, obwohl er beide Rollen erfüllt. Im Ergebnis können wir feststellen, ob ein Handelssystem stabil ist, und können optimale Kombinationen von Indikatoren für das System bestimmen. Da der beschriebene Prozess verschiedene Roboter-Koeffizienten-Filtrierungs- und optimale Kombinationsauswahlmethoden beinhalten kann, die wir in jedem der Zeitintervalle (die mehrfach sein können) überprüfen müssen, kann der Prozess kaum manuell implementiert werden. Außerdem können wir so auf Fehler stoßen, die mit der Datenübertragung oder anderen Fehlern im Zusammenhang mit dem menschlichen Faktor verbunden sind. Deshalb werden einige Werkzeuge benötigt, die den Optimierungsprozess von außen ohne unser Zutun steuern würden. Das erstellte Programm erfüllt die gesetzten Ziele. Für eine strukturiertere Darstellung wurde der Prozess der Programmerstellung in mehrere Artikel aufgeteilt, von denen jeder einen bestimmten Bereich des Programmerstellungsprozesses abdeckt.

Dieser Teil beschäftigt sich mit der Erstellung eines Toolkits für die Arbeit mit den Optimierungsberichten, für den Import aus dem Terminal sowie für die Filterung und Sortierung der erhaltenen Daten. Um eine bessere Struktur der Präsentation zu gewährleisten, werden wir das Dateiformat *xml verwenden. Die Daten aus solch einer Datei können sowohl von Menschen als auch von Programmen gelesen werden. Darüber hinaus können die Daten innerhalb der Datei in Blöcken gruppiert werden und so schneller und einfacher auf die benötigten Informationen zugegriffen werden.

Unser Programm ist ein in C# geschriebener Prozess eines Drittanbieters und muss ähnlich wie MQL5-Programme erstellte *xml-Dokumente erstellen und lesen. Daher wird der Reporterstellungsblock als DLL implementiert, die sowohl in MQL5 als auch in C# Code verwendet werden kann. Um einen MQL5-Code zu entwickeln, benötigen wir also eine Bibliothek. Wir werden zuerst den Erstellungsprozess der Bibliothek beschreiben, während der nächste Artikel eine Beschreibung des MQL5-Codes enthält, der mit der erstellten Bibliothek arbeitet und Optimierungsparameter generiert. Diese Parameter werden wir im aktuellen Artikel besprechen.

Berichtsstruktur und erforderliche Kennzahlen

Wie bereits in den vorhergehenden Artikeln gezeigt, kann MetaTrader 5 den Bericht der Optimierungsdurchgänge selbstständig laden, jedoch bietet er nicht so viele Informationen wie der Bericht, der auf der Registerkarte Backtest nach Abschluss eines Tests mit einem bestimmten Satz von Parametern generiert wird. Um einen größeren Spielraum bei der Arbeit mit den Optimierungsdaten zu haben, sollte der Bericht viele der auf dieser Registerkarte angezeigten Daten enthalten, sowie die Möglichkeit bieten, dem Bericht weitere benutzerdefinierte Daten hinzuzufügen. Für diese Zwecke werden wir unsere eigenen generierten Berichte anstelle des Standardberichts laden. Beginnen wir mit der Definition von drei Datentypen, die für unser Programm benötigt werden:


<Optimisation_Report Created="06.10.2019 10:39:02">
        <Optimiser_Settings>
                <Item Name="Bot">StockFut\StockFut.ex5</Item>
                <Item Name="Deposit" Currency="RUR">100000</Item>
                <Item Name="Leverage">1</Item>
        </Optimiser_Settings>

Die Parameter werden in den Block "Item" geschrieben, die jeweils ein eigenes Attribut "Name" haben. Die Kontowährung wird für das Attribut "Währung" eingetragen. 

Auf dieser Grundlage sollte die Dateistruktur 2 Hauptabschnitte enthalten: Testereinstellungen und die Beschreibung der Optimierungsdurchgänge. Für den ersten Abschnitt müssen wir drei Parameter beibehalten:

  1. Roboterpfad relativ zum Experten-Ordner
  2. Kontowährung und Einlage
  3. Hebel des Kontos

 Der zweite Abschnitt enthält eine Folge von Blöcken mit Optimierungsergebnissen, von denen jeder einen Abschnitt mit Koeffizienten sowie einen Satz von Roboterparametern enthält. 

<Optimisation_Results>
                <Result Symbol="SBRF Splice" TF="1" Start_DT="1481327340" Finish_DT="1512776940">
                        <Coefficients>
                                <VaR>
                                        <Item Name="90">-1055,18214207419</Item>
                                        <Item Name="95">-1323,65133343373</Item>
                                        <Item Name="99">-1827,30841143882</Item>
                                        <Item Name="Mx">-107,03475</Item>
                                        <Item Name="Std">739,584549199836</Item>
                                </VaR>
                                <Max_PL_DD>
                                        <Item Name="Profit">1045,9305</Item>
                                        <Item Name="DD">-630</Item>
                                        <Item Name="Total Profit Trades">1</Item>
                                        <Item Name="Total Lose Trades">1</Item>
                                        <Item Name="Consecutive Wins">1</Item>
                                        <Item Name="Consecutive Lose">1</Item>
                                </Max_PL_DD>
                                <Trading_Days>
                                        <Mn>
                                                <Item Name="Profit">0</Item>
                                                <Item Name="DD">0</Item>
                                                <Item Name="Number Of Profit Trades">0</Item>
                                                <Item Name="Number Of Lose Trades">0</Item>
                                        </Mn>
                                        <Tu>
                                                <Item Name="Profit">0</Item>
                                                <Item Name="DD">0</Item>
                                                <Item Name="Number Of Profit Trades">0</Item>
                                                <Item Name="Number Of Lose Trades">0</Item>
                                        </Tu>
                                        <We>
                                                <Item Name="Profit">1045,9305</Item>
                                                <Item Name="DD">630</Item>
                                                <Item Name="Number Of Profit Trades">1</Item>
                                                <Item Name="Number Of Lose Trades">1</Item>
                                        </We>
                                        <Th>
                                                <Item Name="Profit">0</Item>
                                                <Item Name="DD">0</Item>
                                                <Item Name="Number Of Profit Trades">0</Item>
                                                <Item Name="Number Of Lose Trades">0</Item>
                                        </Th>
                                        <Fr>
                                                <Item Name="Profit">0</Item>
                                                <Item Name="DD">0</Item>
                                                <Item Name="Number Of Profit Trades">0</Item>
                                                <Item Name="Number Of Lose Trades">0</Item>
                                        </Fr>
                                </Trading_Days>
                                <Item Name="Payoff">1,66020714285714</Item>
                                <Item Name="Profit factor">1,66020714285714</Item>
                                <Item Name="Average Profit factor">0,830103571428571</Item>
                                <Item Name="Recovery factor">0,660207142857143</Item>
                                <Item Name="Average Recovery factor">-0,169896428571429</Item>
                                <Item Name="Total trades">2</Item>
                                <Item Name="PL">415,9305</Item>
                                <Item Name="DD">-630</Item>
                                <Item Name="Altman Z Score">0</Item>
                        </Coefficients>
                        <Item Name="_lot_">1</Item>
                        <Item Name="USymbol">SBER</Item>
                        <Item Name="Spread_in_percent">3.00000000</Item>
                        <Item Name="UseAutoLevle">false</Item>
                        <Item Name="max_per">174</Item>
                        <Item Name="comission_stock">0.05000000</Item>
                        <Item Name="shift_stock">0.00000000</Item>
                        <Item Name="comission_fut">4.00000000</Item>
                        <Item Name="shift_fut">0.00000000</Item>
                </Result>
        </Optimisation_Results>
</Optimisation_Report>

Innerhalb des Blocks Optimierungsergebnisse sehen wir mehrere Ergebnis-Blöcke, von denen jeder den i-ten Optimierungsdurchgang darstellt. Jeder der Ergebnis-Blöcke enthält 4 Attribute:

Dies sind die Einstellungen des Testers, die je nach dem Zeitintervall, in dem die Optimierung durchgeführt wird, variieren. Jeder der Roboterparameter wird in den Block Item mit dem Attribut Name als eindeutiger Wert geschrieben, anhand dessen er identifiziert werden kann. Die Roboterkoeffizienten werden in den Koeffizienten-Block geschrieben. Nicht gruppierbare Koeffizienten werden direkt im Item-Block aufgelistet. Andere Koeffizienten werden in Blöcke unterteilt:

  1. 90 - quantile 90
  2. 95 - quantile 95
  3. 99 - quantile 99
  4. Mx - Mathematische Erwartung
  5. Std - Standardabweichung
  • Max_PL_DD
  1. Profit - Gesamtgewinn
  2. DD - Drawdown insgesamt
  3. Total Profit Trades - Gesamtzahl der profitablen Positionen
  4. Total Lose Trades - Gesamtzahl der verlorenen Positionen
  5. Consecutive Wins - aufeinanderfolgende Gewinnpositionen
  6. Conecutive Lose - aufeinanderfolgende Verlustpositionen
  • Trading_Days - Handelsberichte nach Tagen 
  1. Gewinn - durchschnittlicher Gewinn pro Tag
  2. DD - durchschnittliche Verluste pro Tag
  3. Number Of Profit Trades - Anzahl der profitablen Positionen
  4. Number Of Lose Trades - Anzahl der verlorenen Positionen

Als Ergebnis erhalten wir eine Liste mit den Koeffizienten der Optimierungsergebnisse, die die Testergebnisse vollständig beschreiben. Zur Filterung und Auswahl der Roboterparameter gibt es nun eine vollständige Liste der benötigten Koeffizienten, die uns eine effiziente Bewertung der Roboterleistung ermöglichen. 

Die Wrapperklasse des Optimierungsberichts, die Klasse, die die Optimierungsdaten speichert, sowie die Struktur der Optimierungsergebnisse sind in C#.

Beginnen wir mit der Struktur, die die Daten für einen bestimmten Optimierungsdurchgang speichert. 

public struct ReportItem
{
    public Dictionary<string, string> BotParams; // List of robot parameters
    public Coefficients OptimisationCoefficients; // Robot coefficients
    public string Symbol; // Symbol
    public int TF; // Timeframe
    public DateBorders DateBorders; // Date range
}

Alle Roboterkoeffizienten werden im Wörterbuch im Format von Zeichenketten gespeichert. Die Datei mit den Roboterparametern speichert den Datentyp nicht, daher eignet sich hier das Zeichenkettenformat am besten. Die Liste der Roboterkoeffizienten wird in einer anderen Struktur bereitgestellt, ähnlich wie andere Blöcke, die im *xml-Optimierungsbericht gruppiert sind. Auch die Handelsberichte nach Tagen werden im Dictionary gespeichert.

public Dictionary<DayOfWeek, DailyData> TradingDays;

Die DayOfWeek und das Wörterbuch müssen immer die Aufzählung von 5 Tagen (von Montag bis Freitag) als Schlüssel enthalten, ähnlich wie die *xml-Datei. Die interessanteste Klasse in der Datenspeicherstruktur ist DateBorders. Ähnlich wie die Daten innerhalb einer Struktur gruppiert werden, die Felder enthält, die jeden der Datumsparameter beschreiben, werden auch in der Struktur DateBorders Zeitspannen gespeichert. 

public class DateBorders : IComparable
{
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="from">Range beginning date</param>
    /// <param name="till">Range ending date</param>
    public DateBorders(DateTime from, DateTime till)
    {
        if (till <= from)
            throw new ArgumentException("Date 'Till' is less or equal to date 'From'");

        From = from;
        Till = till;
    }
    /// <summary>
    /// From
    /// </summary>
    public DateTime From { get; }
    /// <summary>
    /// To
    /// </summary>
    public DateTime Till { get; }
}

Für einen vollwertigen Betrieb mit den Zeitspannen benötigen wir die Möglichkeit, zwei Zeitspannen zu erstellen. Dazu überschreiben Sie 2 Operatoren "==" und "!=". 

Die Gleichheitskriterien werden durch die Gleichheit der beiden Daten in den beiden durchlaufenen Spannen bestimmt, d.h. das Anfangsdatum stimmt mit dem Handelsbeginn der zweiten Spanne überein (während das gleiche auch für das Handelsende gilt). Da der Objekttyp jedoch eine 'Klasse' ist, kann er gleich Null sein, sodass wir zunächst die Vergleichbarkeit mit Null vorsehen müssen. Verwenden wir dazu das Schlüsselwort ist. Nachher können wir Parameter miteinander vergleichen, ansonsten, wenn wir versuchen, mit Null zu vergleichen, wird "null reference exception" zurückgegeben.

#region Equal
/// <summary>
/// The equality comparison operator
/// </summary>
/// <param name="b1">Element 1</param>
/// <param name="b2">Element 2</param>
/// <returns>Result</returns>
public static bool operator ==(DateBorders b1, DateBorders b2)
{
    bool ans;
    if (b2 is null && b1 is null) ans = true;
    else if (b2 is null || b1 is null) ans = false;
    else ans = b1.From == b2.From && b1.Till == b2.Till;

    return ans;
}
/// <summary>
/// The inequality comparison operator
/// </summary>
/// <param name="b1">Element 1</param>
/// <param name="b2">Element 2</param>
/// <returns>Comparison result</returns>
public static bool operator !=(DateBorders b1, DateBorders b2) => !(b1 == b2);
#endregion

Um den Ungleichheitsoperator zu überladen, brauchen wir die oben beschriebenen Prozeduren nicht mehr zu schreiben, während sie alle bereits im Operator "==" geschrieben sind. Die nächste Funktion, die wir implementieren müssen, ist die Sortierung der Daten nach Zeiträumen, deshalb müssen wir die Operatoren ">", "<", ">=", "<=" überladen.

#region (Grater / Less) than
/// <summary>
/// Comparing: current element is greater than the previous one
/// </summary>
/// <param name="b1">Element 1</param>
/// <param name="b2">Element 2</param>
/// <returns>Result</returns>
public static bool operator >(DateBorders b1, DateBorders b2)
{
    if (b1 == null || b2 == null)
        return false;

    if (b1.From == b2.From)
        return (b1.Till > b2.Till);
    else
        return (b1.From > b2.From);
}
/// <summary>
/// Comparing: current element is less than the previous one
/// </summary>
/// <param name="b1">Element 1</param>
/// <param name="b2">Element 2</param>
/// <returns>Result</returns>
public static bool operator <(DateBorders b1, DateBorders b2)
{
    if (b1 == null || b2 == null)
        return false;

    if (b1.From == b2.From)
        return (b1.Till < b2.Till);
    else
        return (b1.From < b2.From);
}
#endregion

Wenn einer der dem Operator übergebenen Parameter gleich Null ist, wird der Vergleich unmöglich, daher wird False zurückgegeben. Andernfalls wird der Vergleich Schritt für Schritt durchgeführt. Wenn die erste Zeitspanne übereinstimmt, vergleichen wir ihn mit der zweiten Zeitspanne. Wenn sie nicht gleich sind, vergleichen wir ihn mit der ersten Zeitspanne. Wenn wir also die Vergleichslogik am Beispiel des "größer als"-Operators beschreiben, ist das größere Intervall dasjenige, das zeitlich älter ist als das vorherige, entweder um das Startdatum oder um das Enddatum (wenn die Startdaten gleich sind). Die Vergleichslogik "kleiner als" ist ähnlich wie der Vergleich "größer als". 

Die nächsten Operatoren, die überladen werden müssen, um die Sortieroption zu aktivieren, sind "größer oder gleich" und "kleiner oder gleich". 

#region Equal or (Grater / Less) than
/// <summary>
/// Greater than or equal comparison
/// </summary>
/// <param name="b1">Element 1</param>
/// <param name="b2">Element 2</param>
/// <returns>Result</returns>
public static bool operator >=(DateBorders b1, DateBorders b2) => (b1 == b2 || b1 > b2);
/// <summary>
/// Less than or equal comparison
/// </summary>
/// <param name="b1">Element 1</param>
/// <param name="b2">Element 2</param>
/// <returns>Result</returns>
public static bool operator <=(DateBorders b1, DateBorders b2) => (b1 == b2 || b1 < b2);
#endregion

Wie man sieht, erfordert das Überladen des Operators keine Beschreibung der internen Vergleichslogik. Stattdessen verwenden wir die bereits überladenen Operatoren == und >, <. Wie vom Visual Studio während der Kompilierung vorgeschlagen müssen wir jedoch zusätzlich zur Überladung dieser Operatoren einige Funktionen überladen, die von der Basisklasse "object" geerbt wurden.

#region override base methods (from object)
/// <summary>
/// Overloading of equality comparison
/// </summary>
/// <param name="obj">Element to compare to</param>
/// <returns></returns>
public override bool Equals(object obj)
{
    if (obj is DateBorders other)
        return this == other;
    else
        return base.Equals(obj);
}
/// <summary>
/// Cast the class to a string and return its hash code
/// </summary>
/// <returns>String hash code</returns>
public override int GetHashCode()
{
    return ToString().GetHashCode();
}
/// <summary>
/// Convert the current class to a string
/// </summary>
/// <returns>String From date - To date</returns>
public override string ToString()
{
    return $"{From}-{Till}";
}
#endregion
/// <summary>
/// Compare the current element with the passed one
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public int CompareTo(object obj)
{
    if (obj == null) return 1;

    if (obj is DateBorders borders)
    {
        if (this == borders)
            return 0;
        else if (this < borders)
            return -1;
        else
            return 1;
    }
    else
    {
        throw new ArgumentException("object is not DateBorders");
    }
}

Die Prüfmethode auf Gleichheit: Überladen Sie es entweder mit dem überladenen Operator == (wenn das übergebene Objekt vom Typ DateBorders ist) oder mit der Basisimplementierung der Methode.

Die Methode ToString: überladen Sie es als String-Repräsentation zweier durch einen Bindestrich getrennter Daten. Dies hilft uns, die Methode GetHashCode zu überladen.

Die Methode GetHashCode: überladen Sie sie, indem Sie das Objekt zuerst in eine Zeichenkette überführen und dann den Hash-Code dieser Zeichenkette zurückgeben. Wenn eine neue Klasseninstanz in C# erzeugt wird, ist ihr Hashcode unabhängig vom Inhalt der Klasse eindeutig. Das heißt, wenn wir die Methode nicht überladen und zwei Instanzen der Klasse DateBorders mit dem gleichen Von- und Bis-Datum innerhalb erstellen, werden sie trotz identischem Inhalt unterschiedliche Hashcodes haben. Diese Regel gilt nicht für Zeichenketten, da C# einen Mechanismus zur Verfügung stellt, der die Erzeugung neuer Instanzen der Klasse "String" verhindert, wenn die Zeichenkette bereits vorher erzeugt wurde — daher werden ihre Hashcodes bei identische Zeichenketten übereinstimmen. Mit Hilfe der Methode ToString, die überladen wird, und unter Verwendung des String-Hash-Codes bieten wir das Verhalten unserer Klassen-Hash-Codes ähnlich dem von String an. Jetzt, wenn wir die Methode IEnumerable.Distinct verwenden, können wir garantieren, dass die Logik des Empfangs der eindeutigen Liste von Datumsbereichen korrekt sein wird, da diese Methode auf den Hashcodes der verglichenen Objekte basiert.

Implementierung des IComparable Interfaces, von dem unsere Klasse geerbt wird, implementieren wir die Methode CompareTo, die die aktuelle Klasseninstanz mit der übergebenen vergleicht. Ihre Implementierung ist einfach und verwendet das Überladen von zuvor überladenen Operatoren. 

Nachdem wir die erforderlichen Überladungen implementiert haben, können wir mit dieser Klasse effizienter arbeiten. Wir können:

Da wir eine rollierende Optimierung mit Backtests und Vorwärtstests implementieren, müssen wir eine Methode zum Vergleich von Zeitspannen, die davor (Historic) oder danach (Forward) liegen, erstellen.

/// <summary>
/// Method for comparing forward and historical optimizations
/// </summary>
/// <param name="History">Array of historical optimization</param>
/// <param name="Forward">Array of forward optimizations</param>
/// <returns>Sorted list historical - forward optimization</returns>
public static Dictionary<DateBorders, DateBorders> CompareHistoryToForward(List<DateBorders> History, List<DateBorders> Forward)
{
    // array of comparable optimizations
    Dictionary<DateBorders, DateBorders> ans = new Dictionary<DateBorders, DateBorders>();

    // Sort the passed parameters
    History.Sort();
    Forward.Sort();

    // Create a historical optimization loop
    int i = 0;
    foreach (var item in History)
    {
if(ans.ContainsKey(item))
       	    continue;

        ans.Add(item, null); // Add historical optimization
        if (Forward.Count <= i)
            continue; // If the array of forward optimization is less than the index, continue the loop

        // Forward optimization loop
        for (int j = i; j < Forward.Count; j++)
        {
            // If the current forward optimization is contained in the results array, skip
            if (ans.ContainsValue(Forward[j]) ||
                Forward[j].From < item.Till)
            {
                continue;
            }

            // Compare forward and historical optimization
            ans[item] = Forward[j];
            i = j + 1;
            break;
        }
    }

    return ans;
}

Wie Sie sehen können, ist die Methode statisch. Dies geschieht, um sie als reguläre Funktion, ohne Bindung an eine bestimmte Klasseninstanz, zur Verfügung zu stellen. Zunächst einmal sortiert sie die übergebenen Zeitspannen in aufsteigender Reihenfolge. So können wir in der nächsten Schleife sicher wissen, dass alle zuvor übergebenen Intervalle kleiner oder gleich den nächsten sind. Dann implementieren wir zwei Schleifen: foreach für historische Intervalle und eine geschachtelte Schleife für Vorwärtsintervalle.

Zu Beginn der historischen Datenschleife fügen wir immer historische Bereiche (Schlüssel) mit Ergebnissen in die Sammlung ein, sowie vorübergehend Null an Stelle von Vorwärtsintervallen setzen. Die Vorwärts-Ergebnisschleife beginnt mit dem i-ten Parameter. Damit wird verhindert, dass die Schleife mit bereits verwendeten Elementen der Vorwärtsliste wiederholt wird. D.h. das Vorwärtsintervall sollte immer dem historischen folgen, d.h. es sollte > als das historische sein. Deshalb implementieren wir die Schleife nach Vorwärtsintervallen, falls in der übergebenen Liste eine Vorwärtsperiode für das allererste historische Intervall vorhanden ist, die dem allerersten historischen Intervall vorausgeht. Es ist besser, die Idee in einer Tabelle zu visualisieren:

Historisch Vorwärts
Von Bis Von Bis
10.03.2016 09.03.2017 12.12.2016 09.03.2017
10.06.2016 09.06.2017 10.03.2017 09.06.2017
10.09.2016 09.09.2017 10.06.2017 09.09.2017

Das erste historische Intervall endet also am 09.03.2017, und das erste Vorwärtsintervall beginnt am 12.12.2016, was nicht korrekt ist. Deshalb überspringen wir es in der Schleife des Vorwärtsintervalls aufgrund der Bedingung. Auch überspringen wir das Vorwärtsintervall, das im resultierenden Wörterbuch enthalten ist. Wenn die j-ten Vorwärtsdaten noch nicht im resultierenden Wörterbuch vorhanden sind und das Anfangsdatum des Vorwärtsintervalls >= aktuelles historisches Intervallenddatum ist, speichern wir den empfangenen Wert und verlassen die Vorwärtsintervallschleife, da der gewünschte Wert bereits gefunden wurde. Vor dem Verlassen weisen wir der Variablen i (die Variable, die den Beginn der Vorwärtslisten-Iterationen bedeutet) den Wert des auf das ausgewählte Intervall folgenden Vorwärtsintervalls zu. Dies wird getan, da das aktuelle Intervall (aufgrund der anfänglichen Sortierung der Daten) nicht mehr benötigt wird.

Eine Überprüfung vor der historischen Optimierung stellt sicher, dass alle historischen Optimierungen eindeutig sind. Somit ergibt sich im resultierenden Wörterbuch die folgende Liste:

Schlüssel Wert
10.03.2016-09.3.2017 10.03.2017-09.06.2017
10.06.2016-09.06.2017 10.06.2017-09.09.2017
10.09.2016-09.09.2017 Null

Wie aus den dargestellten Daten ersichtlich ist, wird das erste Vorwärtsintervall verworfen und für das letzte historische Intervall kein Intervall gefunden, da kein solches überschritten wurde. Basierend auf dieser Logik wird das Programm die Daten der historischen und der Vorwärtsintervalle vergleichen und verstehen, welches der historischen Intervalle Optimierungsparameter für Vorwärtstests liefern sollte.

Um einen effizienten Betrieb mit einem bestimmten Optimierungsergebnis zu ermöglichen, habe ich eine Wrapper-Struktur für die Struktur ReportItem erstellt, die eine Reihe von zusätzlichen Methoden und überladenen Operatoren enthält. Im Wesentlichen enthält der Wrappper zwei Felder:

/// <summary>
/// Optimization pass report
/// </summary>
public ReportItem report;
/// <summary>
/// Sorting factor
/// </summary>
public double SortBy;

Das erste Feld wurde oben beschrieben. Das zweite Feld ist so angelegt, dass eine Sortierung nach mehreren Werten, z.B. Gewinn und Wiedergewinnungsfaktor, möglich ist. Der Sortiermechanismus wird später beschrieben, aber die Idee ist, diese Werte in einen einzigen Wert umzuwandeln und in dieser Variable zu speichern. 

Die Struktur enthält auch das Überladen der Typkonvertierung:

/// <summary>
/// The operator of implicit type conversion from optimization pass to the current type
/// </summary>
/// <param name="item">Optimization pass report</param>
public static implicit operator OptimisationResult(ReportItem item)
{
    return new OptimisationResult { report = item, SortBy = 0 };
}
/// <summary>
/// The operator of explicit type conversion from current to the optimization pass structure
/// </summary>
/// <param name="optimisationResult">current type</param>
public static explicit operator ReportItem(OptimisationResult optimisationResult)
{
    return optimisationResult.report;
}

Als Ergebnis können wir den Typ von ReportItem implizit auf seinen Wrapper casten und dann den Wrapper von ReportItem explizit auf das Handelsberichtelement casten. Dies kann effizienter sein als das sequentielle Eintragen in die Felder. Da alle Felder in der Struktur ReportItem in Kategorien unterteilt sind, kann es vorkommen, dass wir einen langen Code benötigen, um einen gewünschten Wert zu erhalten. Um Platz zu sparen und einen universelleren Getter (Abfragefunktion) zu schaffen, wurde eine spezielle Methode geschaffen. Dieser erhält die angeforderten Daten des Roboterkoeffizienten über die übergebene Enum SourtBy aus dem obigen GetResult(SortBy resultType). Die Implementierung ist einfach, aber zu lang und wird daher hier nicht besprochen. Die Methode iteriert über die übergebene Enum in einem Switch-Konstrukt und gibt den Wert des angeforderten Koeffizienten zurück. Da die meisten Koeffizienten vom Typ double sind und da dieser Typ alle anderen numerischen Typen enthalten kann, werden Koeffizientenwerte in double umgewandelt.

Für diesen Wrappertyp sind auch Vergleichsoperatorüberlastungen implementiert worden:

/// <summary>
/// Overloading of the equality comparison operator
/// </summary>
/// <param name="result1">Parameter 1 to compare</param>
/// <param name="result2">Parameter 2 to compare</param>
/// <returns>Comparison result</returns>
public static bool operator ==(OptimisationResult result1, OptimisationResult result2)
{
    foreach (var item in result1.report.BotParams)
    {
        if (!result2.report.BotParams.ContainsKey(item.Key))
            return false;
        if (result2.report.BotParams[item.Key] != item.Value)
            return false;
    }

    return true;
}
/// <summary>
 /// Overloading of the inequality comparison operator
/// </summary>
/// <param name="result1">Parameter 1 to compare</param>
/// <param name="result2">Parameter 2 to compare</param>
/// <returns>Comparison result</returns>
public static bool operator !=(OptimisationResult result1, OptimisationResult result2)
{
    return !(result1 == result2);
}
/// <summary>
/// Overloading of the basic type comparison operator
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
    if (obj is OptimisationResult other)
    {
        return this == other;
    }
    else
        return base.Equals(obj);
}

Die Elemente von Optimierungen, die die gleichen Namen und Werte von Roboterparametern enthalten, werden als gleichwertig betrachtet. Wenn wir also zwei Optimierungsdurchgänge vergleichen müssen, haben wir bereits die gebrauchsfertigen, überladenen Operatoren. Diese Struktur enthält auch eine Methode, die Daten in eine Datei schreibt. Wenn sie existiert, werden die Daten einfach in die Datei eingefügt. Eine Erklärung des Datenschreibelements und der Methodenimplementierung wird weiter unten besprochen.

Erstellen einer Datei zur Speicherung des Optimierungsberichts

Wir werden mit den Optimierungsberichten arbeiten und sie nicht nur im Terminal, sondern auch im erstellten Programm schreiben. Deshalb fügen wir die Methode zur Erstellung von Optimierungsberichten in diese DLL ein. Lassen Sie uns auch mehrere Methoden für das Schreiben der Daten in eine Datei anbieten, d.h. das Schreiben eines Datenarrays in eine Datei ermöglichen, sowie das Hinzufügen eines separaten Elements zu einer bestehenden Datei (wenn die Datei nicht existiert, sollte sie erstellt werden). Die letzte Methode wird in das Terminal importiert und wird in C#-Klassen verwendet. Betrachten wir zunächst die implementierten Methoden zum Schreiben von Berichtsdateien mit den Funktionen, die mit dem Hinzufügen von Daten zu einer Datei verbunden sind. Zu diesem Zweck wurde die Klasse ReportWriter angelegt. Die vollständige Implementierung der Klasse ist in der angehängten Projektdatei verfügbar. Ich werde hier nur die interessantesten Methoden zeigen. Beschreiben wir zunächst, wie diese Klasse funktioniert. 

Sie enthält nur statische Methoden: dies ermöglicht den Export ihrer Methoden nach MQL5. Für den gleichen Zweck ist die Klasse mit einem Modifikator für den öffentlichen Zugriff markiert. Diese Klasse enthält ein statisches Feld vom Typ ReportItem und eine Reihe von Methoden, die ihr abwechselnd Koeffizienten und EA-Parameter hinzufügen.

/// <summary>
/// temporary data keeper
/// </summary>
private static ReportItem ReportItem;
/// <summary>
/// clearing the temporary data keeper
/// </summary>
public static void ClearReportItem()
{
    ReportItem = new ReportItem();
}

Eine weitere Methode ist ClearReportItem(). Sie erstellt die Feldinstanz neu. In diesem Fall verlieren wir den Zugriff auf die vorherige Instanz dieses Objektes: es wird gelöscht und der Datensicherungsprozess startet erneut. Die Methoden zum Hinzufügen von Daten sind nach Blöcken gruppiert. Hier sind die Signaturen dieser Methoden.  

/// <summary>
/// Add robot parameters
/// </summary>
/// <param name="name">Parameter name</param>
/// <param name="value">Parameter value</param>
public static void AppendBotParam(string name, string value);

/// <summary>
/// Add the main list of coefficients
/// </summary>
/// <param name="payoff"></param>
/// <param name="profitFactor"></param>
/// <param name="averageProfitFactor"></param>
/// <param name="recoveryFactor"></param>
/// <param name="averageRecoveryFactor"></param>
/// <param name="totalTrades"></param>
/// <param name="pl"></param>
/// <param name="dd"></param>
/// <param name="altmanZScore"></param>
public static void AppendMainCoef(double payoff,
                                  double profitFactor,
                                  double averageProfitFactor,
                                  double recoveryFactor,
                                  double averageRecoveryFactor,
                                  int totalTrades,
                                  double pl,
                                  double dd,
                                  double altmanZScore);

/// <summary>
/// Add VaR
/// </summary>
/// <param name="Q_90"></param>
/// <param name="Q_95"></param>
/// <param name="Q_99"></param>
/// <param name="Mx"></param>
/// <param name="Std"></param>
public static void AppendVaR(double Q_90, double Q_95,
                             double Q_99, double Mx, double Std);

/// <summary>
/// Add total PL / DD and associated values
/// </summary>
/// <param name="profit"></param>
/// <param name="dd"></param>
/// <param name="totalProfitTrades"></param>
/// <param name="totalLoseTrades"></param>
/// <param name="consecutiveWins"></param>
/// <param name="consecutiveLose"></param>
public static void AppendMaxPLDD(double profit, double dd,
                                 int totalProfitTrades, int totalLoseTrades,
                                 int consecutiveWins, int consecutiveLose);

/// <summary>
/// Add a specific day
/// </summary>
/// <param name="day"></param>
/// <param name="profit"></param>
/// <param name="dd"></param>
/// <param name="numberOfProfitTrades"></param>
/// <param name="numberOfLoseTrades"></param>
public static void AppendDay(int day,
                             double profit, double dd,
                             int numberOfProfitTrades,
                             int numberOfLoseTrades);

Die Methode, die die Handelsstatistik nach Tagen unterteilt hinzufügt, sollte für jeden der 5 Handelstage aufgerufen werden. Wenn wir sie für einen der Tage nicht hinzufügen, wird die geschriebene Datei in der Zukunft nicht mehr gelesen. Sobald die Daten in das Datenspeicherfeld hinzugefügt wurden, können wir mit der Aufzeichnung des Feldes fortfahren. Zuvor ist zu prüfen, ob die Datei existiert und gegebenenfalls zu erstellen. Es wurden einige Methoden zur Erstellung der Datei hinzugefügt.

/// <summary>
/// The method creates the file if it has not been created
/// </summary>
/// <param name="pathToBot">Path to the robot</param>
/// <param name="currency">Deposit currency</param>
/// <param name="balance">Balance</param>
/// <param name="leverage">Leverage</param>
/// <param name="pathToFile">Path to file</param>
private static void CreateFileIfNotExists(string pathToBot, string currency, double balance, int leverage, string pathToFile)
{
    if (File.Exists(pathToFile))
        return;
    using (var xmlWriter = new XmlTextWriter(pathToFile, null))
    {
        // set document format
        xmlWriter.Formatting = Formatting.Indented;
        xmlWriter.IndentChar = '\t';
        xmlWriter.Indentation = 1;

        xmlWriter.WriteStartDocument();

        // Create document root
        #region Document root
        xmlWriter.WriteStartElement("Optimisation_Report");

        // Write the creation date
        xmlWriter.WriteStartAttribute("Created");
        xmlWriter.WriteString(DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"));
        xmlWriter.WriteEndAttribute();

        #region Optimiser settings section 
        // Optimizer settings
        xmlWriter.WriteStartElement("Optimiser_Settings");

        // Path to the robot
        WriteItem(xmlWriter, "Bot", pathToBot);
        // Deposit
        WriteItem(xmlWriter, "Deposit", balance.ToString(), new Dictionary<string, string> { { "Currency", currency } });
        // Leverage
        WriteItem(xmlWriter, "Leverage", leverage.ToString());

        xmlWriter.WriteEndElement();
        #endregion

        #region Optimization results section
        // the root node of the optimization results list
        xmlWriter.WriteStartElement("Optimisation_Results");
        xmlWriter.WriteEndElement();
        #endregion

        xmlWriter.WriteEndElement();
        #endregion

        xmlWriter.WriteEndDocument();
        xmlWriter.Close();
    }
}

/// <summary>
/// Write element to a file
/// </summary>
/// <param name="writer">Writer</param>
/// <param name="Name">Element name</param>
/// <param name="Value">Element value</param>
/// <param name="Attributes">Attributes</param>
private static void WriteItem(XmlTextWriter writer, string Name, string Value, Dictionary<string, string> Attributes = null)
{
    writer.WriteStartElement("Item");

    writer.WriteStartAttribute("Name");
    writer.WriteString(Name);
    writer.WriteEndAttribute();

    if (Attributes != null)
    {
        foreach (var item in Attributes)
        {
            writer.WriteStartAttribute(item.Key);
            writer.WriteString(item.Value);
            writer.WriteEndAttribute();
        }
    }

    writer.WriteString(Value);

    writer.WriteEndElement();
}

Ich stelle hier auch die Implementierung der Methode WriteItem zur Verfügung, die den sich wiederholenden Code zum Hinzufügen eines letzten Elements mit Daten und elementspezifischen Attributen zu einer Datei enthält. Die Methode CreateFileIfNotExists prüft, ob die Datei existiert, erzeugt ggf. die Datei und beginnt mit der Bildung der minimal erforderlichen Dateistruktur. 

Erst erzeugt sie die Wurzel der Datei, d.h. das <Optimization_Report/>-Tag, in dem sich alle Unterstrukturen der Datei befinden. Dann wird Dateierzeugungsdaten gefüllt — dies ist für die weitere komfortable Arbeit mit Dateien implementiert. Danach erstellen wir einen Knoten mit unveränderten Optimierungseinstellungen und geben diese an. Dann erstellen wir einen Abschnitt, der die Optimierungsergebnisse speichert und sofort schließt. Als Ergebnis haben wir eine leere Datei mit der minimal erforderlichen Formatierung. 


<Optimisation_Report Created="24.10.2019 19:10:08">
        <Optimiser_Settings>
                <Item Name="Bot">Path to bot</Item>
                <Item Name="Deposit" Currency="Currency">1000</Item>
                <Item Name="Leverage">1</Item>
        </Optimiser_Settings>
        <Optimisation_Results />
</Optimisation_Report>

Somit werden wir in der Lage sein, diese Datei mit der Klasse XmlDocument zu lesen. Dies ist die nützlichste Klasse zum Lesen und Bearbeiten von bestehenden Xml-Dokumenten. Wir werden genau diese Klasse verwenden, um Daten zu bestehenden Dokumenten hinzuzufügen. Wiederholte Operationen werden als separate Methoden implementiert und somit werden wir in der Lage sein, Daten effizienter zu einem bestehenden Dokument hinzuzufügen:

/// <summary>
/// Writing attributes to a file
/// </summary>
/// <param name="item">Node</param>
/// <param name="xmlDoc">Document</param>
/// <param name="Attributes">Attributes</param>
private static void FillInAttributes(XmlNode item, XmlDocument xmlDoc, Dictionary<string, string> Attributes)
{
    if (Attributes != null)
    {
        foreach (var attr in Attributes)
        {
            XmlAttribute attribute = xmlDoc.CreateAttribute(attr.Key);
            attribute.Value = attr.Value;
            item.Attributes.Append(attribute);
        }
    }
}

/// <summary>
/// Add section
/// </summary>
/// <param name="xmlDoc">Document</param>
/// <param name="xpath_parentSection">xpath to select parent node</param>
/// <param name="sectionName">Section name</param>
/// <param name="Attributes">Attribute</param>
private static void AppendSection(XmlDocument xmlDoc, string xpath_parentSection,
                                  string sectionName, Dictionary<string, string> Attributes = null)
{
    XmlNode section = xmlDoc.SelectSingleNode(xpath_parentSection);
    XmlNode item = xmlDoc.CreateElement(sectionName);

    FillInAttributes(item, xmlDoc, Attributes);

    section.AppendChild(item);
}

/// <summary>
/// Write item
/// </summary>
/// <param name="xmlDoc">Document</param>
/// <param name="xpath_parentSection">xpath to select parent node</param>
/// <param name="name">Item name</param>
/// <param name="value">Value</param>
/// <param name="Attributes">Attributes</param>
private static void WriteItem(XmlDocument xmlDoc, string xpath_parentSection, string name,
                              string value, Dictionary<string, string> Attributes = null)
{
    XmlNode section = xmlDoc.SelectSingleNode(xpath_parentSection);
    XmlNode item = xmlDoc.CreateElement(name);
    item.InnerText = value;

    FillInAttributes(item, xmlDoc, Attributes);

    section.AppendChild(item);
}

Die erste Methode FillInAttributes trägt die Attribute für den übergebenen Knoten ein, WriteItem schreibt ein Element in die über XPath spezifizierte Sektion, während AppendSection eine Sektion innerhalb einer anderen Sektion hinzufügt, die über einen mit Xpath übergebenen Pfad spezifiziert wird. Diese Code-Blöcke werden oft verwendet, wenn Daten zu einer Datei hinzugefügt werden. Die Datenschreibmethode ist ziemlich langwierig und in Blöcke unterteilt.

/// <summary>
/// Write trading results to a file
/// </summary>
/// <param name="pathToBot">Path to the bot</param>
/// <param name="currency">Deposit currency</param>
/// <param name="balance">Balance</param>
/// <param name="leverage">Leverage</param>
/// <param name="pathToFile">Path to file</param>
/// <param name="symbol">Symbol</param>
/// <param name="tf">Timeframe</param>
/// <param name="StartDT">Trading start dare</param>
/// <param name="FinishDT">Trading end date</param>
public static void Write(string pathToBot, string currency, double balance,
                         int leverage, string pathToFile, string symbol, int tf,
                         ulong StartDT, ulong FinishDT)
{
    // Create the file if it does not yet exist
    CreateFileIfNotExists(pathToBot, currency, balance, leverage, pathToFile);
            
    ReportItem.Symbol = symbol;
    ReportItem.TF = tf;

    // Create a document and read the file using it
    XmlDocument xmlDoc = new XmlDocument();
    xmlDoc.Load(pathToFile);

    #region Append result section
    // Write a request to switch to the optimization results section 
    string xpath = "Optimisation_Report/Optimisation_Results";
    // Add a new section with optimization results
    AppendSection(xmlDoc, xpath, "Result",
                  new Dictionary<string, string>
                  {
                      { "Symbol", symbol },
                      { "TF", tf.ToString() },
                      { "Start_DT", StartDT.ToString() },
                      { "Finish_DT", FinishDT.ToString() }
                  });
    // Add section with optimization results
    AppendSection(xmlDoc, $"{xpath}/Result[last()]", "Coefficients");
    // Add section with VaR
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients", "VaR");
    // Add section with total PL / DD
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients", "Max_PL_DD");
    // Add section with trading results by days
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients", "Trading_Days");
    // Add section with trading results on Monday
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Mn");
    // Add section with trading results on Tuesday
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Tu");
    // Add section with trading results on Wednesday
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "We");
    // Add section with trading results on Thursday
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Th");
    // Add section with trading results on Friday
    AppendSection(xmlDoc, $"{xpath}/Result[last()]/Coefficients/Trading_Days", "Fr");
    #endregion

    #region Append Bot params
    // Iterate through bot parameters
    foreach (var item in ReportItem.BotParams)
    {
        // Write the selected robot parameter
        WriteItem(xmlDoc, "Optimisation_Report/Optimisation_Results/Result[last()]",
                  "Item", item.Value, new Dictionary<string, string> { { "Name", item.Key } });
    }
    #endregion

    #region Append main coef
    // Set path to node with coefficients
    xpath = "Optimisation_Report/Optimisation_Results/Result[last()]/Coefficients";

    // Save coefficients
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.Payoff.ToString(), new Dictionary<string, string> { { "Name", "Payoff" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.ProfitFactor.ToString(), new Dictionary<string, string> { { "Name", "Profit factor" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.AverageProfitFactor.ToString(), new Dictionary<string, string> { { "Name", "Average Profit factor" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.RecoveryFactor.ToString(), new Dictionary<string, string> { { "Name", "Recovery factor" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.AverageRecoveryFactor.ToString(), new Dictionary<string, string> { { "Name", "Average Recovery factor" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.TotalTrades.ToString(), new Dictionary<string, string> { { "Name", "Total trades" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.PL.ToString(), new Dictionary<string, string> { { "Name", "PL" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.DD.ToString(), new Dictionary<string, string> { { "Name", "DD" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.AltmanZScore.ToString(), new Dictionary<string, string> { { "Name", "Altman Z Score" } });
    #endregion

    #region Append VaR
    // Set path to node with VaR
    xpath = "Optimisation_Report/Optimisation_Results/Result[last()]/Coefficients/VaR";

    // Save VaR results
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Q_90.ToString(), new Dictionary<string, string> { { "Name", "90" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Q_95.ToString(), new Dictionary<string, string> { { "Name", "95" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Q_99.ToString(), new Dictionary<string, string> { { "Name", "99" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Mx.ToString(), new Dictionary<string, string> { { "Name", "Mx" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.VaR.Std.ToString(), new Dictionary<string, string> { { "Name", "Std" } });
    #endregion

    #region Append max PL and DD
    // Set path to node with total PL / DD
    xpath = "Optimisation_Report/Optimisation_Results/Result[last()]/Coefficients/Max_PL_DD";

    // Save coefficients
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.Profit.Value.ToString(), new Dictionary<string, string> { { "Name", "Profit" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.DD.Value.ToString(), new Dictionary<string, string> { { "Name", "DD" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.Profit.TotalTrades.ToString(), new Dictionary<string, string> { { "Name", "Total Profit Trades" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.DD.TotalTrades.ToString(), new Dictionary<string, string> { { "Name", "Total Lose Trades" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.Profit.ConsecutivesTrades.ToString(), new Dictionary<string, string> { { "Name", "Consecutive Wins" } });
    WriteItem(xmlDoc, xpath, "Item", ReportItem.OptimisationCoefficients.MaxPLDD.DD.ConsecutivesTrades.ToString(), new Dictionary<string, string> { { "Name", "Consecutive Lose" } });
    #endregion

    #region Append Days
    foreach (var item in ReportItem.OptimisationCoefficients.TradingDays)
    {
        // Set path to specific day node
        xpath = "Optimisation_Report/Optimisation_Results/Result[last()]/Coefficients/Trading_Days";
        // Select day
        switch (item.Key)
        {
            case DayOfWeek.Monday: xpath += "/Mn"; break;
            case DayOfWeek.Tuesday: xpath += "/Tu"; break;

            case DayOfWeek.Wednesday: xpath += "/We"; break;
            case DayOfWeek.Thursday: xpath += "/Th"; break;
            case DayOfWeek.Friday: xpath += "/Fr"; break;
        }

        // Save results
        WriteItem(xmlDoc, xpath, "Item", item.Value.Profit.Value.ToString(), new Dictionary<string, string> { { "Name", "Profit" } });
        WriteItem(xmlDoc, xpath, "Item", item.Value.DD.Value.ToString(), new Dictionary<string, string> { { "Name", "DD" } });
        WriteItem(xmlDoc, xpath, "Item", item.Value.Profit.Trades.ToString(), new Dictionary<string, string> { { "Name", "Number Of Profit Trades" } });
        WriteItem(xmlDoc, xpath, "Item", item.Value.DD.Trades.ToString(), new Dictionary<string, string> { { "Name", "Number Of Lose Trades" } });
    }
    #endregion

    // Rewrite the file with the changes
    xmlDoc.Save(pathToFile);

    // Clear the variable which stored results written to a file
    ClearReportItem();
}

Zuerst laden wir das gesamte Dokument in den Speicher und dann werden die Abschnitte hinzugefügt. Betrachten wir das Xpath-Anforderungsformat, das den Pfad zum Wurzelknoten übergibt.  

$"{xpath}/Result[last()]/Coefficients"

Die Variable xpath enthält den Pfad zu dem Knoten, in dem die Elemente des Optimierungsdurchlaufs gespeichert sind. In diesem Knoten werden die Knoten der Optimierungsergebnisse gespeichert, die als Array von Strukturen dargestellt werden können. Das Konstrukt Result[last()] selektiert das letzte Element des Arrays, wonach der Pfad an den geschachtelten Knoten /Coefficients übergeben wird. Nach dem beschriebenen Prinzip wählen wir den gewünschten Knoten mit den Ergebnissen der Optimierungen aus. 

Der nächste Schritt ist Hinzufügen von Roboterparametern: In der Schleife fügen wir die Parameter direkt in das Ergebnisverzeichnis ein. Dann fügen wir eine Anzahl von Koeffizienten in das Koeffizientenverzeichnis ein. Diese Addition ist in Blöcke unterteilt. Als Ergebnis speichern wir die Ergebnisse und Löschen das Zwischengespeicherte. Als Ergebnis erhalten wir eine Datei mit der Liste der Parameter und Optimierungsergebnisse. Um Threads während asynchroner Operationen, die von verschiedenen Prozessen gestartet werden, zu trennen (so wird die Optimierung im Tester bei Verwendung mehrerer Prozessoren durchgeführt), wurde eine weitere Schreibmethode erstellt, die Threads durch benannte Mutexe trennt.

/// <summary>
/// Write to file while locking using a named mutex
/// </summary>
/// <param name="mutexName">Mutex name</param>
/// <param name="pathToBot">Path to the bot</param>
/// <param name="currency">Deposit currency</param>
/// <param name="balance">Balance</param>
/// <param name="leverage">Leverage</param>
/// <param name="pathToFile">Path to file</param>
/// <param name="symbol">Symbol</param>
/// <param name="tf">Timeframe</param>
/// <param name="StartDT">Trading start dare</param>
/// <param name="FinishDT">Trading end date</param>
/// <returns></returns>
public static string MutexWriter(string mutexName, string pathToBot, string currency, double balance,
                                 int leverage, string pathToFile, string symbol, int tf,
                                 ulong StartDT, ulong FinishDT)
{
    string ans = "";
    // Mutex lock
    Mutex m = new Mutex(false, mutexName);
    m.WaitOne();
    try
    {
        // write to file
        Write(pathToBot, currency, balance, leverage, pathToFile, symbol, tf, StartDT, FinishDT);
    }
    catch (Exception e)
    {
        // Catch error if any
        ans = e.Message;
    }

    // Release the mutex
    m.ReleaseMutex();
    // Return error text
    return ans;
}

Diese Methode schreibt Daten mit der vorherigen Methode, aber der Schreibvorgang wird durch eine Mutex und von einem Try-Catch-Block umhüllt. Letzterer ermöglicht die Mutex-Freigabe auch im Fehlerfall. Andernfalls kann der Prozess einfrieren und die Optimierung kann nicht fortgesetzt werden. Diese Methoden werden auch in der Struktur OptimisationResult in der Methode WriteResult verwendet.

/// <summary>
/// The method adds current parameter to the existing file or creates a new file with the current parameter
/// </summary>
/// <param name="pathToBot">Relative path to the robot from the Experts folder</param>
/// <param name="currency">Deposit currency</param>
/// <param name="balance">Balance</param>
/// <param name="leverage">Leverage</param>
/// <param name="pathToFile">Path to file</param>
public void WriteResult(string pathToBot,
                        string currency, double balance,
                        int leverage, string pathToFile)
{
    try
    {
        foreach (var param in report.BotParams)
        {
            ReportWriter.AppendBotParam(param.Key, param.Value);
        }
        ReportWriter.AppendMainCoef(GetResult(ReportManager.SortBy.Payoff),
                                    GetResult(ReportManager.SortBy.ProfitFactor),
                                    GetResult(ReportManager.SortBy.AverageProfitFactor),
                                    GetResult(ReportManager.SortBy.RecoveryFactor),
                                    GetResult(ReportManager.SortBy.AverageRecoveryFactor),
                                    (int)GetResult(ReportManager.SortBy.TotalTrades),
                                    GetResult(ReportManager.SortBy.PL),
                                    GetResult(ReportManager.SortBy.DD),
                                    GetResult(ReportManager.SortBy.AltmanZScore));

        ReportWriter.AppendVaR(GetResult(ReportManager.SortBy.Q_90), GetResult(ReportManager.SortBy.Q_95),
                               GetResult(ReportManager.SortBy.Q_99), GetResult(ReportManager.SortBy.Mx),
                               GetResult(ReportManager.SortBy.Std));

        ReportWriter.AppendMaxPLDD(GetResult(ReportManager.SortBy.ProfitFactor), GetResult(ReportManager.SortBy.MaxDD),
                                  (int)GetResult(ReportManager.SortBy.MaxProfitTotalTrades),
                                  (int)GetResult(ReportManager.SortBy.MaxDDTotalTrades),
                                  (int)GetResult(ReportManager.SortBy.MaxProfitConsecutivesTrades),
                                  (int)GetResult(ReportManager.SortBy.MaxDDConsecutivesTrades));


        foreach (var day in report.OptimisationCoefficients.TradingDays)
        {
            ReportWriter.AppendDay((int)day.Key, day.Value.Profit.Value, day.Value.Profit.Value,
                                   day.Value.Profit.Trades, day.Value.DD.Trades);
        }

        ReportWriter.Write(pathToBot, currency, balance, leverage, pathToFile, report.Symbol, report.TF,
                           report.DateBorders.From.DTToUnixDT(), report.DateBorders.Till.DTToUnixDT());
    }
    catch (Exception e)
    {
        ReportWriter.ClearReportItem();
        throw e;
    }
}

Bei dieser Methode speichern wir abwechselnd Optimierungsergebnisse in einen Zwischenspeicher und rufen dann die Methode Write auf, um sie in einer bestehenden Datei zu speichern oder eine neue Datei zu erstellen, falls sie noch nicht erstellt wurde. 

Die beschriebene Methode zum Schreiben der gewonnenen Daten wird benötigt, um Informationen in eine vorbereitete Datei einzufügen. Es gibt eine weitere Methode, die besser geeignet ist, wenn eine Datenreihe geschrieben werden soll. Die Methode wurde als Erweiterung für die Schnittstelle IEnumerable<OptimisationResult> entwickelt. Jetzt können wir Daten für alle Listen speichern, die von der entsprechenden Schnittstelle geerbt wurden. 

public static void ReportWriter(this IEnumerable<OptimisationResult> results, string pathToBot,
                                string currency, double balance,
                                int leverage, string pathToFile)
{
    // Delete the file if it exists
    if (File.Exists(pathToFile))
        File.Delete(pathToFile);

    // Create writer 
    using (var xmlWriter = new XmlTextWriter(pathToFile, null))
    {
        // Set document format
        xmlWriter.Formatting = Formatting.Indented;
        xmlWriter.IndentChar = '\t';
        xmlWriter.Indentation = 1;

        xmlWriter.WriteStartDocument();

        // The root node of the document
        xmlWriter.WriteStartElement("Optimisation_Report");

        // Write attributes
        WriteAttribute(xmlWriter, "Created", DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"));

        // Write optimizer settings to file
        #region Optimiser settings section 
        xmlWriter.WriteStartElement("Optimiser_Settings");

        WriteItem(xmlWriter, "Bot", pathToBot); // path to the robot
        WriteItem(xmlWriter, "Deposit", balance.ToString(), new Dictionary<string, string> { { "Currency", currency } }); // Currency and deposit
        WriteItem(xmlWriter, "Leverage", leverage.ToString()); // Leverage

        xmlWriter.WriteEndElement();
        #endregion

        // Write optimization results to the file
        #region Optimisation result section
        xmlWriter.WriteStartElement("Optimisation_Results");

        // Loop through optimization results
        foreach (var item in results)
        {
            // Write specific result
            xmlWriter.WriteStartElement("Result");

            // Write attributes of this optimization pass
            WriteAttribute(xmlWriter, "Symbol", item.report.Symbol); // Symbol
            WriteAttribute(xmlWriter, "TF", item.report.TF.ToString()); // Timeframe
            WriteAttribute(xmlWriter, "Start_DT", item.report.DateBorders.From.DTToUnixDT().ToString()); // Optimization start date
            WriteAttribute(xmlWriter, "Finish_DT", item.report.DateBorders.Till.DTToUnixDT().ToString()); // Optimization end date

            // Write optimization result
            WriteResultItem(item, xmlWriter);

            xmlWriter.WriteEndElement();
        }

        xmlWriter.WriteEndElement();
        #endregion

        xmlWriter.WriteEndElement();

        xmlWriter.WriteEndDocument();
        xmlWriter.Close();
    }
}

Die Methode schreibt Optimierungsberichte in eine Datei, einen nach dem anderen, bis das Array keine Daten mehr enthält. Wenn die Datei am übergebenen Pfad bereits existiert, wird sie durch eine neue ersetzt. Zuerst erstellen wir etwas zum Schreiben der Datei und konfigurieren es. Dann schreiben wir, der bereits bekannten Dateistruktur folgend, nacheinander Optimierungseinstellungen und Optimierungsergebnisse. Wie aus dem obigen Codeauszug ersichtlich ist, werden die Ergebnisse in eine Schleife geschrieben, die die Elemente der Kollektion durchläuft, auf deren Instanz die beschriebene Methode aufgerufen wurde. Innerhalb der Schleife wird das Schreiben der Daten an die Methode delegiert, die für das Schreiben von Daten eines bestimmten Elements in die Datei erstellt wurde.

/// <summary>
/// Write a specific optimization pass
/// </summary>
/// <param name="resultItem">Optimization pass value</param>
/// <param name="writer">Writer</param>
private static void WriteResultItem(OptimisationResult resultItem, XmlTextWriter writer)
{
    // Write coefficients
    #region Coefficients
    writer.WriteStartElement("Coefficients");

    // Write VaR
    #region VaR
    writer.WriteStartElement("VaR");

    WriteItem(writer, "90", resultItem.GetResult(SortBy.Q_90).ToString()); // Quantile 90
    WriteItem(writer, "95", resultItem.GetResult(SortBy.Q_95).ToString()); // Quantile 95
    WriteItem(writer, "99", resultItem.GetResult(SortBy.Q_99).ToString()); // Quantile 99
    WriteItem(writer, "Mx", resultItem.GetResult(SortBy.Mx).ToString()); // Average for PL
    WriteItem(writer, "Std", resultItem.GetResult(SortBy.Std).ToString()); // Standard deviation for PL

    writer.WriteEndElement();
    #endregion

    // Write PL / DD parameters - extreme points
    #region Max PL DD
    writer.WriteStartElement("Max_PL_DD");
    WriteItem(writer, "Profit", resultItem.GetResult(SortBy.MaxProfit).ToString()); // Total profit
    WriteItem(writer, "DD", resultItem.GetResult(SortBy.MaxDD).ToString()); // Total loss
    WriteItem(writer, "Total Profit Trades", ((int)resultItem.GetResult(SortBy.MaxProfitTotalTrades)).ToString()); // Total number of winning trades
    WriteItem(writer, "Total Lose Trades", ((int)resultItem.GetResult(SortBy.MaxDDTotalTrades)).ToString()); // Total number of losing trades
    WriteItem(writer, "Consecutive Wins", ((int)resultItem.GetResult(SortBy.MaxProfitConsecutivesTrades)).ToString()); // Winning trades in a row 
    WriteItem(writer, "Consecutive Lose", ((int)resultItem.GetResult(SortBy.MaxDDConsecutivesTrades)).ToString()); // Losing trades in a row
    writer.WriteEndElement();
    #endregion

    // Write trading results by days
    #region Trading_Days

    // The method writing trading results
    void AddDay(string Day, double Profit, double DD, int ProfitTrades, int DDTrades)
    {
        writer.WriteStartElement(Day);

        WriteItem(writer, "Profit", Profit.ToString()); // Profits
        WriteItem(writer, "DD", DD.ToString()); // Losses
        WriteItem(writer, "Number Of Profit Trades", ProfitTrades.ToString()); // Number of profitable trades
        WriteItem(writer, "Number Of Lose Trades", DDTrades.ToString()); // Number of losing trades

        writer.WriteEndElement();
    }

    writer.WriteStartElement("Trading_Days");

    // Monday
    AddDay("Mn", resultItem.GetResult(SortBy.AverageDailyProfit_Mn),
                 resultItem.GetResult(SortBy.AverageDailyDD_Mn),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Mn),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Mn));
    // Tuesday
    AddDay("Tu", resultItem.GetResult(SortBy.AverageDailyProfit_Tu),
                 resultItem.GetResult(SortBy.AverageDailyDD_Tu),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Tu),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Tu));
    // Wednesday
    AddDay("We", resultItem.GetResult(SortBy.AverageDailyProfit_We),
                 resultItem.GetResult(SortBy.AverageDailyDD_We),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_We),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_We));
    // Thursday
    AddDay("Th", resultItem.GetResult(SortBy.AverageDailyProfit_Th),
                 resultItem.GetResult(SortBy.AverageDailyDD_Th),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Th),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Th));
    // Friday
    AddDay("Fr", resultItem.GetResult(SortBy.AverageDailyProfit_Fr),
                 resultItem.GetResult(SortBy.AverageDailyDD_Fr),
                 (int)resultItem.GetResult(SortBy.AverageDailyProfitTrades_Fr),
                 (int)resultItem.GetResult(SortBy.AverageDailyDDTrades_Fr));

    writer.WriteEndElement();
    #endregion

    // Write other coefficients
    WriteItem(writer, "Payoff", resultItem.GetResult(SortBy.Payoff).ToString());
    WriteItem(writer, "Profit factor", resultItem.GetResult(SortBy.ProfitFactor).ToString());
    WriteItem(writer, "Average Profit factor", resultItem.GetResult(SortBy.AverageProfitFactor).ToString());
    WriteItem(writer, "Recovery factor", resultItem.GetResult(SortBy.RecoveryFactor).ToString());
    WriteItem(writer, "Average Recovery factor", resultItem.GetResult(SortBy.AverageRecoveryFactor).ToString());
    WriteItem(writer, "Total trades", ((int)resultItem.GetResult(SortBy.TotalTrades)).ToString());
    WriteItem(writer, "PL", resultItem.GetResult(SortBy.PL).ToString());
    WriteItem(writer, "DD", resultItem.GetResult(SortBy.DD).ToString());
    WriteItem(writer, "Altman Z Score", resultItem.GetResult(SortBy.AltmanZScore).ToString());

    writer.WriteEndElement();
    #endregion

    // Write robot coefficients
    #region Bot params
    foreach (var item in resultItem.report.BotParams)
    {
        WriteItem(writer, item.Key, item.Value);
    }
    #endregion
}

Die Implementierung der Methode, die Daten in eine Datei schreibt, ist sehr einfach, obwohl sie ziemlich lang ist. Nach dem Erstellen der entsprechenden Abschnitte und dem Füllen der Attribute fügt die Methode Daten über VaR des durchgeführten Optimierungsdurchlaufs und Werte, die den maximalen Gewinn und die maximale Inanspruchnahme charakterisieren, hinzu. Es wurde eine geschachtelte Funktion erstellt, um für jeden der Tage Optimierungsergebnisse für ein bestimmtes Datum zu schreiben, die 5 Mal aufgerufen wird. Danach werden Koeffizienten ohne Gruppierung und Wurzelparameter hinzugefügt. Da die beschriebene Prozedur in einer Schleife für jedes der Elemente durchgeführt wird, werden die Daten erst nach dem Aufruf der Methode xmlWriter.Close() in die Datei geschrieben (dies geschieht in der Hauptschreibmethode). Damit ist dies die schnellste Erweiterungsmethode zum Schreiben eines Datenarrays, im Vergleich zu den bisher betrachteten Methoden. Wir haben Prozeduren betrachtet, die mit dem Schreiben von Daten in eine Datei zusammenhängen. Nun gehen wir zum nächsten logischen Teil der Beschreibung über, d.h. dem Lesen von Daten aus der resultierenden Datei.

Lesen der Optimierungsberichtsdatei

Wir müssen die Dateien lesen, um die empfangenen Informationen zu verarbeiten und anzuzeigen. Daher ist ein geeigneter Mechanismus erforderlich, die Daten zu lesen. Dieser ist als eigene Klasse implementiert:

public class ReportReader : IDisposable
    {
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="path">Path to file</param>
        public ReportReader(string path);

        /// <summary>
        /// Binary number format provider
        /// </summary>
        private readonly NumberFormatInfo formatInfo = new NumberFormatInfo { NumberDecimalSeparator = "." };

        #region DataKeepers
        /// <summary>
        /// Presenting the report file in OOP format
        /// </summary>
        private readonly XmlDocument document = new XmlDocument();

        /// <summary>
        /// Collection of document nodes (rows in excel table)
        /// </summary>
        private readonly System.Collections.IEnumerator enumerator;
        #endregion

        /// <summary>
        /// The read current report item
        /// </summary>
        public ReportItem? ReportItem { get; private set; } = null;

        #region Optimiser settings
        /// <summary>
        /// Path to the robot
        /// </summary>
        public string RelativePathToBot { get; }

        /// <summary>
        /// Balance
        /// </summary>
        public double Balance { get; }

        /// <summary>
        /// Currency
        /// </summary>
        public string Currency { get; }

        /// <summary>
        /// Leverage
        /// </summary>
        public int Leverage { get; }
        #endregion

        /// <summary>
        /// File creation date
        /// </summary>
        public DateTime Created { get; }

        /// <summary>
        /// File reader method
        /// </summary>
        /// <returns></returns>
        public bool Read();

        /// <summary>
        /// The method receiving the item by its name (the Name attribute)
        /// </summary>
        /// <param name="Name"></param>
        /// <returns></returns>
        private string SelectItem(string Name) => $"Item[@Name='{Name}']";

        /// <summary>
        /// Get the trading result value for the selected day
        /// </summary>
        /// <param name="dailyNode">Node of this day</param>
        /// <returns></returns>
        private DailyData GetDay(XmlNode dailyNode);

        /// <summary>
        /// Reset the quote reader
        /// </summary>
        public void ResetReader();

        /// <summary>
        /// Clear the document
        /// </summary>
        public void Dispose() => document.RemoveAll();
    }

Betrachten wir die Struktur im Detail. Die Klasse wird von der Schnittstelle iDisposable abgeleitet. Dies ist keine Pflichtbedingung, sondern dient der Vorsicht. Nun enthält die beschriebene Klasse die erforderliche Methode Dispasable, die das Objekt Document löscht. Das Objekt speichert die in den Speicher geladene Optimierungsergebnisdatei.

Der Ansatz ist bequem, weil beim Erzeugen einer Instanz die von der oben genannten Schnittstelle geerbte Klasse in das Konstrukt 'using' eingehüllt werden sollte, das automatisch die angegebene Methode aufruft, wenn sie über die Strukturblockgrenzen 'using' hinausgeht. Das bedeutet, dass das gelesene Dokument nicht lange im Speicher gehalten wird und somit die geladene Speichermenge reduziert wird.

Die Klasse für das zeilenweise Lesen des Dokuments verwendet Enumeratoren, die vom gelesenen Dokument erhalten wurden. Die gelesenen Werte werden in die Spezialeigenschaft geschrieben und damit der Zugriff auf die Daten ermöglicht. Außerdem werden bei der Instanziierung der Klasse folgende Daten gefüllt: Eigenschaften, die die Haupteigenschaften Optimierungseinstellungen, Dateierstellungsdatum und -zeit angeben. Um den Einfluss der OS-Lokalisierungseinstellungen (sowohl beim Schreiben als auch beim Lesen der Datei) zu eliminieren, wird die doppelte Genauigkeitszahl Delimiterformat angegeben. Beim ersten Lesen der Datei sollte die Klasse auf den Listenanfang zurückgesetzt werden. Dazu verwenden wir die Methode ResetReader, die Enumerator auf den Listenanfang zurücksetzt. Der Klassenkonstruktor ist so implementiert, dass alle erforderlichen Eigenschaften ausgefüllt werden und die Klasse für die weitere Verwendung vorbereitet wird.

public ReportReader(string path)
{
    // load the document
    document.Load(path);

    // Get file creation date
    Created = DateTime.ParseExact(document["Optimisation_Report"].Attributes["Created"].Value, "dd.MM.yyyy HH:mm:ss", null);
    // Get enumerator
    enumerator = document["Optimisation_Report"]["Optimisation_Results"].ChildNodes.GetEnumerator();

    // Parameter receiving function
    string xpath(string Name) { return $"/Optimisation_Report/Optimiser_Settings/Item[@Name='{Name}']"; }

    // Get path to the robot
    RelativePathToBot = document.SelectSingleNode(xpath("Bot")).InnerText;

    // Get balance and deposit currency
    XmlNode Deposit = document.SelectSingleNode(xpath("Deposit"));
    Balance = Convert.ToDouble(Deposit.InnerText.Replace(",", "."), formatInfo);
    Currency = Deposit.Attributes["Currency"].Value;

    // Get leverage
    Leverage = Convert.ToInt32(document.SelectSingleNode(xpath("Leverage")).InnerText);
}

Zuerst lädt es lädt das übergebene Dokument und trägt dessen Erstellungsdatum ein. Der Enumerator, der bei der Instanziierung der Klasse erhalten wurden, gehört zu den unter der Sektion Optimisation_Report/Optimisation_Results liegenden Kindknoten des Dokuments, d.h. zu den Knoten mit dem Tag <Result/>. Um die gewünschten Konfigurationsparameter des Optimierers zu erhalten, wird der Pfad zu dem gewünschten Dokumentenknoten mit Markup Expfad angegeben. Ein Analogon dieser eingebauten Funktion mit einem kürzeren Pfad ist die Methode SelectItem, die den Pfad zu einem Item unter den Dokumentenknoten mit dem Tag <Item/> entsprechend seinem Attribut Name angibt. Die Methode GetDay konvertiert den übergebenen Dokumentenknoten in die entsprechende Struktur des täglichen Handelsberichts. Die letzte Methode in dieser Klasse ist die Lesemethode der Daten. Ihre Implementierung in Kurzform ist nachfolgend dargestellt.   

public bool Read()
{
    if (enumerator == null)
        return false;

    // Read the next item
    bool ans = enumerator.MoveNext();
    if (ans)
    {
        // Current node
        XmlNode result = (XmlNode)enumerator.Current;
        // current report item
        ReportItem = new ReportItem[...]

        // Fill the robot parameters
        foreach (XmlNode item in result.ChildNodes)
        {
            if (item.Name == "Item")
                ReportItem.Value.BotParams.Add(item.Attributes["Name"].Value, item.InnerText);
        }

    }
    return ans;
}

Der versteckter Codeteil enthält die Operation der Instanziierung des Optimierungsreports und das Füllen der Reportfelder mit den gelesenen Daten. Diese Operation beinhaltet ähnliche Aktionen, die das Stringformat in das gewünschte Format konvertieren. In einer weiteren Schleife werden die Roboterparameter mit den zeilenweise aus der Datei gelesenen Daten gefüllt. Diese Operation wird nur durchgeführt, wenn die Zeile der zu vervollständigenden Datei nicht erreicht wurde. Das Ergebnis der Operation ist die Rückgabe einer Information, ob die Zeile gelesen wurde oder nicht. Sie dient auch als Hinweis auf das Erreichen des Dateiendes.

Multifaktorielles Filtern und Sortieren des Optimierungsberichts

Um die Ziele zu erreichen, habe ich zwei Enumerationen erstellt, die die Sortierrichtung angeben (SortMethod und OrderBy). Sie sind ähnlich und wahrscheinlich könnte nur eine von ihnen ausreichen. Um die Filter- und Sortiermethoden zu trennen, wurden jedoch zwei Enumerationen statt einer erstellt. Der Zweck der Aufzählungen ist es, die aufsteigende oder absteigende Reihenfolge anzuzeigen. Der Typ der Koeffizienten mit dem übergebenen Wert wird durch Flags angezeigt. Der Zweck ist es, die Vergleichsbedingung zu setzen.    

/// <summary>
/// Filtering type
/// </summary>
[Flags]
public enum CompareType
{
    GraterThan = 1, // greater than
    LessThan = 2, // less than
    EqualTo = 4 // equal
}

Die Art der Koeffizienten, nach denen die Daten gefiltert und sortiert werden können, wird durch die oben genannte Enumeration OrderBy erfasst. Sortier- und Filterverfahren sind als Methoden zur Erweiterung von Kollektionen implementiert, die von der Schnittstelle IEnumerable<OptimisationResult> geerbt werden. In der Filtermethode prüfen wir jeden der Koeffizienten Punkt für Punkt, ob er die angegebenen Kriterien erfüllt, und verwerfen die Optimierungsdurchläufe, in denen einer der Koeffizienten die Kriterien nicht erfüllt. Zum Filtern der Daten verwenden wir die in der Schnittstelle IEnumerable enthaltene Schleife Where. Die Methode ist wie folgt implementiert.

/// <summary>
/// Optimization filtering method
/// </summary>
/// <param name="results">Current collection</param>
/// <param name="compareData">Collection of coefficients and filtering types</param>
/// <returns>Filtered collection</returns>
public static IEnumerable<OptimisationResult> FiltreOptimisations(this IEnumerable<OptimisationResult> results,
                                                                  IDictionary<SortBy, KeyValuePair<CompareType, double>> compareData)
{
    // Result sorting function
    bool Compare(double _data, KeyValuePair<CompareType, double> compareParams)
    {
        // Comparison result
        bool ans = false;
        // Comparison for equality
        if (compareParams.Key.HasFlag(CompareType.EqualTo))
        {
            ans = compareParams.Value == _data;
        }
        // Comparison for 'greater than current'
        if (!ans && compareParams.Key.HasFlag(CompareType.GraterThan))
        {
            ans = _data > compareParams.Value;
        }
        // Comparison for 'less than current'
        if (!ans && compareParams.Key.HasFlag(CompareType.LessThan))
        {
            ans = _data < compareParams.Value;
        }

        return ans;
    }
    // Sorting condition
    bool Sort(OptimisationResult x)
    {
        // Loop through passed sorting parameters
        foreach (var item in compareData)
        {
            // Compare the passed parameter with the current one
            if (!Compare(x.GetResult(item.Key), item.Value))
                return false;
        }

        return true;
    }

    // Filtering
    return results.Where(x => Sort(x));
}

In der Methode sind zwei Funktionen implementiert, jede von ihnen führt einen eigenen Teil der Datenfilterung aus. Betrachten wir sie, beginnend mit der letzten Funktion:

Zum Sortieren der Daten wird die Methode 'Where' verwendet. Sie erzeugt automatisch eine Liste von passenden Bedingungen, die als Ergebnis der Ausführung der Erweiterungsmethode zurückgegeben wird.  

Die Datenfilterung ist recht einfach zu verstehen. Bei der Sortierung können Schwierigkeiten auftreten. Betrachten wir den Sortiermechanismus an einem Beispiel. Angenommen, wir haben die Parameter Profit Factor und Recovery Factor. Wir müssen die Daten nach diesen beiden Parametern sortieren. Wenn wir zwei Sortieriterationen nacheinander durchführen, erhalten wir immer noch Daten, die nach dem letzten Parameter sortiert sind. Wir müssen diese Werte in irgendeiner Weise vergleichen.

Gewinn Profit Faktor Erholungsfaktor
5000 1 9
15000 1.2 5
-11000 0.5 -2
0 0 0
10000 2 5
7000 1 4

Diese beiden Koeffizienten sind nicht innerhalb ihrer Randwerte normiert. Sie haben auch einen sehr großen Wertebereich relativ zueinander. Logischerweise müssen wir sie zunächst unter Beibehaltung ihrer Reihenfolge normieren. Der Standardweg, um die Daten in eine normalisierte Form zu bringen, ist, jeden von ihnen durch den Maximalwert in der Reihe zu dividieren: So erhalten wir eine Reihe von Werten, die im Bereich [0;1] variieren. Aber zuerst müssen wir die Extrema dieser Reihe von Werten finden, die in der Tabelle dargestellt sind.


Profit Faktor  Erholungsfaktor
Min  0 -2  
Max  2 9

Wie aus der Tabelle ersichtlich ist, hat der Recovery-Faktor negative Werte und daher ist der obige Ansatz hier nicht geeignet. Um diesen Effekt zu eliminieren, verschieben wir einfach die gesamte Reihe um einen negativen Modulo-Wert. Nun können wir den normalisierten Wert jedes einzelnen Parameters berechnen.

Gewinn Profit Faktor Erholungsfaktor Normalisierte Summe
5000 0.5 1  0.75
15000 0.6 0.64  0.62
-11000 0.25 0  0.13
0 0 0.18  0.09
10000 1 0.64  0.82
7000 0.5 0.55  0.52

Nun, da wir alle Koeffizienten in der normierten Form haben, können wir die gewichtete Summe verwenden, in der das Gewicht gleich eins geteilt durch n ist (hier ist n die Anzahl der zu gewichtenden Faktoren). Als Ergebnis erhalten wir eine normierte Spalte, die als Sortierkriterium verwendet werden kann. Wenn einer der Koeffizienten absteigend sortiert werden soll, müssen wir diesen Parameter von eins subtrahieren und somit den größten und den kleinsten Koeffizienten vertauschen.

Der Code, der diesen Mechanismus implementiert, wird in zwei Methoden dargestellt, von denen die erste die Sortierreihenfolge (aufsteigend oder absteigend) angibt und die zweite Methode den Sortiermechanismus implementiert. Die erste der Methoden, SortMethod GetSortMethod(SortBy sortBy), ist recht einfach, also gehen wir gleich zur zweiten Methode über.

public static IEnumerable<OptimisationResult> SortOptimisations(this IEnumerable<OptimisationResult> results,
                                                                OrderBy order, IEnumerable<SortBy> sortingFlags,
                                                                Func<SortBy, SortMethod> sortMethod = null)
{
    // Get the unique list of flags for sorting
    sortingFlags = sortingFlags.Distinct();
    // Check flags
    if (sortingFlags.Count() == 0)
        return null;
    // If there is one flag, sort by this parameter
    if (sortingFlags.Count() == 1)
    {
        if (order == OrderBy.Ascending)
            return results.OrderBy(x => x.GetResult(sortingFlags.ElementAt(0)));
        else
            return results.OrderByDescending(x => x.GetResult(sortingFlags.ElementAt(0)));
    }

    // Form minimum and maximum boundaries according to the passed optimization flags
    Dictionary<SortBy, MinMax> Borders = sortingFlags.ToDictionary(x => x, x => new MinMax { Max = double.MinValue, Min = double.MaxValue });

    #region create Borders min max dictionary
    // Loop through the list of optimization passes
    for (int i = 0; i < results.Count(); i++)
    {
        // Loop through sorting flags
        foreach (var item in sortingFlags)
        {
            // Get the value of the current coefficient
            double value = results.ElementAt(i).GetResult(item);
            MinMax mm = Borders[item];
            // Set the minimum and maximum values
            mm.Max = Math.Max(mm.Max, value);
            mm.Min = Math.Min(mm.Min, value);
            Borders[item] = mm;
        }
    }
    #endregion

    // The weight of the weighted sum of normalized coefficients
    double coef = (1.0 / Borders.Count);

    // Convert the list of optimization results to the List type array
    // Since it is faster to work with
    List<OptimisationResult> listOfResults = results.ToList();
    // Loop through optimization results
    for (int i = 0; i < listOfResults.Count; i++)
    {
        // Assign value to the current coefficient
        OptimisationResult data = listOfResults[i];
        // Zero the current sorting factor
        data.SortBy = 0;
        // Loop through the formed maximum and minimum borders
        foreach (var item in Borders)
        {
            // Get the current result value
            double value = listOfResults[i].GetResult(item.Key);
            MinMax mm = item.Value;

            // If the minimum is below zero, shift all data by the negative minimum value
            if (mm.Min < 0)
            {
                value += Math.Abs(mm.Min);
                mm.Max += Math.Abs(mm.Min);
            }

            // If the maximum is greater than zero, calculate
            if (mm.Max > 0)
            {
                // Calculate the coefficient according to the sorting method
                if ((sortMethod == null ? GetSortMethod(item.Key) : sortMethod(item.Key)) == SortMethod.Decreasing)
                {
                    // Calculate the coefficient to sort in descending order
                    data.SortBy += (1 - value / mm.Max) * coef;
                }
                else
                {
                    // Calculate the coefficient to sort in ascending order
                    data.SortBy += value / mm.Max * coef;
                }
            }
        }
        // Replace the value of the current coefficient with the sorting parameter
        listOfResults[i] = data;
    }

    // Sort according to the passed sorting type
    if (order == OrderBy.Ascending)
        return listOfResults.OrderBy(x => x.SortBy);
    else
        return listOfResults.OrderByDescending(x => x.SortBy);
}

Soll nach einem Parameter sortiert werden, so ist ohne auf die Normierung der Reihe zurückzugreifen zu sortieren. Dann wird sofort das Ergebnis zurückgegeben. Soll nach mehreren Parametern sortiert werden, so wird zunächst ein Wörterbuch bestehend aus Maximal- und Minimalwert der betrachteten Reihe erzeugt. Dies ermöglicht eine Beschleunigung der Berechnungen, da wir sonst bei jeder Iteration Parameter abfragen müssten. Dies würde viel mehr Schleifen erzeugen, als wir in dieser Implementierung berücksichtigt haben.

Dann wird für die gewichtete Summe ein Gewicht gebildet und eine Operation zur Normierung einer Reihe auf ihre Summe durchgeführt. Auch hier werden wieder zwei Schleifen verwendet, die oben beschriebenen Operationen werden in der internen Schleife durchgeführt. Die resultierende gewichtete Summe wird zu der SortBy Variablen des entsprechenden Array-Elements addiert. Am Ende dieser Operation, wenn der resultierende Koeffizient, der für die Sortierung verwendet werden soll, bereits gebildet wurde, wird die zuvor beschriebene Sortiermethode über die Standardmethode List<T>.OrderBy bzw. List<T> verwendet. OrderByDescending   — wenn absteigend sortiert werden soll. Die Sortiermethode für einzelne Mitglieder der gewichteten Summe wird durch einen delegate festgelegt, der als einer der Funktionsparameter übergeben wird. Wenn dieser "delegate" als parametrisierter Standardwert belassen wird, wird die zuvor beschriebene Methode verwendet; andernfalls wird der übergebene Delegierte verwendet.
  

Schlussfolgerung

Wir haben einen Mechanismus geschaffen, der in Zukunft aktiv innerhalb unserer Anwendung genutzt werden soll. Neben dem Entladen und Lesen von xml-Dateien eines benutzerdefinierten Formats, in dem die strukturierte Informationen über durchgeführte Tests gespeichert ist, enthält der Mechanismus C#-Kollektionserweiterungsmethoden, die zum Sortieren und Filtern von Daten verwendet werden. Wir haben den Multi-Faktor-Sortiermechanismus implementiert, der im Standard-Terminal-Tester nicht verfügbar ist. Einer der Vorteile der Sortiermethode ist die Möglichkeit, eine Reihe von Faktoren zu berücksichtigen. Ihr Nachteil ist jedoch, dass die Ergebnisse nur innerhalb der gegebenen Reihe verglichen werden können. Das bedeutet, dass die gewichtete Summe des gewählten Zeitintervalls nicht mit anderen Intervallen verglichen werden kann, da jedes von ihnen eine individuelle Koeffizientenreihe verwendet. In den nächsten Artikeln werden wir uns mit der Umrechnungsmethode der Algorithmen beschäftigen, um die Anwendung oder einen automatisierten Optimierer für die Algorithmen zu ermöglichen, sowie mit der Erstellung eines solchen automatisierten Optimierers.