連続歩行順最適化(パート1):最適化レポートの使用

3 2月 2020, 12:25
Andrey Azatskiy
0
306

イントロダクション

前回の記事(最適化管理(パートI)最適化管理(パート2)では、サードパーティのプロセスを通じてターミナルで最適化を開始するメカニズムを検討しました。 これにより、特定のトレードプロセスを実装するトレードアルゴリズムと同様にプロセスを実装できる特定の最適化マネージャを作成できます。 このアイデアは、ヒストリーの期間が事前の間隔によってシフトされ、スライディング最適化プロセスを管理するアルゴリズムを作成することです。

アルゴリズムの最適化に対するこのアプローチは、両方の役割を果たしますが、純粋な最適化ではなく、戦略の堅牢性テストとして機能します。 結果として、トレードシステムが安定しているかどうかを調べることができ、システムのインジケータの最適な組み合わせを決定することができます。 説明したプロセスは、異なるロボット係数フィルターと最適な組み合わせ選択方法を伴う可能性があるため、各時間間隔(複数の場合がある)をチェックインする必要があるため、プロセスを手動で実施することはほとんどできません。 また、データ転送に関連するエラーや、人的要因に関連するその他のエラーが発生する可能性があります。 したがって、介入なしに外部から最適化プロセスを管理するツールが必要です。 今回作成されたプログラムは、目標を満たすことができます。 構造化されたプレゼンテーションでは、プログラム作成プロセスは複数の記事に分割され、各記事はプログラム作成プロセスの特定の領域を扱っています。

このパートは、最適化レポートを操作するためのツールキットの作成、ターミナルからのインポート、取得したデータのフィルタリングとソートに関するツールキットの作成です。 より良いプレゼンテーション構造を提供するために、*xmlファイル形式を使用します。 このファイルのデータは、人間とプログラムの両方で読み取ることができます。 さらに、データはファイル内のブロックにグループ化できるため、必要な情報に迅速かつ容易にアクセスできます。

プログラムはC#で書かれたサードパーティのプロセスであり、作成された*xml文書を作成してMQL5プログラムと同様に読み取る必要があります。 したがって、レポート作成ブロックは、MQL5とC#コードの両方で使用できるDLLとして実装されます。 したがって、MQL5コードを開発するためには、ライブラリが必要です。 まずライブラリ作成プロセスについて説明しますが、次の記事では作成されたライブラリで動作する MQL5 コードの説明を提供し、最適化パラメータを生成します。 現在の記事でパラメータを検討します。

レポート構造と必要比率

前の記事で既に示したように、MetaTrader5は最適化パスのレポートを個別にダウンロードできますが、特定のパラメータセットを使用してテストが完了した後に[バックテスト]タブで生成されたレポートほど多くの情報は提供されません。 最適化データの処理の範囲を拡張するには、このタブに表示されるデータの多くをレポートにインクルードするとともに、レポートにカスタム データを追加する可能性を提供する必要があります。 このために、標準レポートではなく、独自の生成されたレポートをダウンロードします。 まず、プログラムに必要な 3 つのデータ型の定義から始めましょう。

  • テスターの設定 (レポート全体の同じ設定)
  • トレーディングロボットの設定(最適化パスごとにユニーク)
  • トレード結果を記述する係数(最適化パスごとに一意)

<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>

パラメータは"Item"ブロックに書き込まれ、それぞれが独自の"Name"属性を持ちます。 資産通貨は"通貨"属性に書き込まれます。 

これに基づいて、ファイル構造には、テスター設定と最適化パスの説明の2つの主要なセクションが含まれている必要があります。 最初のセクションでは、次の 3 つのパラメータを保持する必要があります。

  1. エキスパートフォルダに対するロボットパス
  2. 資産通貨と資産
  3. アカウントレバレッジ

 2番目のセクションには、最適化結果を持つ一連のブロックが含まれ、それぞれに係数を持つセクションとロボットパラメータのセットが含まれます。 

<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>

Optimisation_Resultsブロックの中には、各ブロックに i 番目の最適化パスが含まれるResultブロックが繰り返されます。 Resultブロックには、次の 4 つの属性があります。

  • Symbol
  • TF
  • Start_DT
  • Finish_DT

最適化が実行される時間間隔によって異なるテスター設定です。 各ロボットパラメータは、識別可能な一意の値として、Name 属性を持つItemブロックに書き込まれます。 ロボット係数は係数ブロックに書き込まれます。グループ化できない係数はItemブロックに直接列挙されます。 その他の係数はブロックに分割されます。

  • VaR
  1. 90 - quantile 90
  2. 95 - quantile 95
  3. 99 - quantile 99
  4. Mx - math expectation
  5. Std - standard deviation
  • Max_PL_DD
  1. Profit - total profit
  2. DD - total drawdown
  3. Total Profit Trades - total number of profitable trades
  4. Total Lose Trades - total number of losing trades
  5. Consecutive Wins - winning trades in a row
  6. Consecutive Lose - losing trades in a row
  • Trading_Days - trading reports by days 
  1. Profit - average profit per day
  2. DD - average losses per day
  3. Number Of Profit Trades - number of profitable trades
  4. Number Of Lose Trades - number of losing trades

結果として、テスト結果を完全に記述した最適化結果の係数を含むリストが表示されます。 ロボットパラメータをフィルタリングして選択するために、ロボットの性能を効率的に評価できる必要な係数の完全なリストがあります。 

最適化レポートのラッパクラス、最適化日付を格納するクラス、および最適化の構造は C# で結果します。

まず、特定の最適化パスのデータを格納する構造体から始めましょう。 

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
}

すべてのロボット係数は、文字列形式でディクショナリに格納されます。 ロボットパラメータを持つファイルはデータのタイプを保存しないため、文字列フォーマットが最適です。 ロボット係数のリストは、*xml最適化レポートにグループ化された他のブロックと同様に、異なる構造で提供されます。 日別のトレードレポートもディクショナリに保存されます。

public Dictionary<DayOfWeek, DailyData> TradingDays;

DayOfWeek とディクショナリには、必ず 、*xml ファイルと同様に、キーとして 5 日 (月曜日から金曜日) の列挙体が含まれている必要があります。 データ格納構造体の中で最も興味深いクラスは DateBorders です。 日付パラメータを記述するフィールドを含む構造内でグループ化されるデータと同様に、日付範囲も DateBorders 構造体に格納されます。 

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; }
}

日付範囲を含む完全な関数を備えた操作では、2 つの日付範囲を作成する可能性が必要です。 このために、2 つの演算子 "=="および "!=" を上書きします。 

等値基準は、2つの渡された範囲の両方の日付の等値、すなわち、開始日付が2番目の範囲のトレード開始と一致することによって決定されます(同じことがトレードエンドにも適用されます)。 ただし、オブジェクト型は 'class' なので、null と等しくなる可能性があるため、まず null と比較する関数を提供する必要があります。 そのためにisキーワードを使いましょう。 その後、パラメータを互いに比較することができ、それ以外の場合、nullと比較しようとすると"null参照例外"が返されます。

#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

等値演算子をオーバーロードするために、上記のプロシージャを記述する必要はなくなりましたが、すべてが既に演算子 "==" で書かれています。 実装する必要がある次の関数は、期間別のデータソートであり、演算子">", "<", ">=", "<="をオーバーロードする必要があります。

#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

演算子に渡されたパラメータのいずれかがnullの場合、比較は不可能になるため、False が返されます。 それ以外の場合は、ステップバイステップで比較します。 最初の時間間隔が一致する場合は、2 番目の時間間隔で比較します。 等しくない場合は、最初の間隔で比較します。 したがって、比較ロジックをベースにした "Greater" 演算子の例を記述すると、より大きい間隔は、開始日または終了日 (開始日が等しい場合) によって前の時間よりも古いものです。 "less" 比較ロジックは "greater" 比較に似ています。 

並べ替えオプションを有効にするためにオーバーロードされる次の演算子は、「次に大きいか等しい」と「等しくない」です。 

#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

見てわかるように、演算子のオーバーロードは内部比較ロジックの記述を必要としません。 代わりに、既にオーバーロードされた演算子 == と >を使用します。 ただし、Visual Studio がコンパイル時に示すように、演算子のオーバーロードに加えて、"object" 基本クラスから継承された関数をオーバーロードする必要があります。

#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");
    }
}

Equals メソッド: オーバーロードするには、オーバーロード演算子 == (渡されたオブジェクトに DateBorders 型が指定されている場合)、またはメソッドの基本的な実装を使用します。.

ToString メソッド: ハイフンで区切られた 2 つの日付の文字列表現としてオーバーロードします。 GetHashCode メソッドをオーバーロードするのに役立ちます。

GetHashCode メソッド: 最初にオブジェクトを文字列にキャストし、次にこの文字列のハッシュ コードを返すことでオーバーロードします。 C# で新しいクラス インスタンスを作成すると、そのハッシュ コードはクラスの内容に関係なく一意になります。 つまり、メソッドをオーバーロードせず、同じ From と To の日付を含む DateBorders クラスの 2 つのインスタンスを作成すると、同じ内容それにも関わらずハッシュ コードが異なります。 C# は、文字列が以前に作成された場合に String クラスの新しいインスタンスを作成できないようにするメカニズムを提供するため、このルールは文字列には適用されません。 ToString メソッドのオーバーロードと文字列ハッシュ コードを使用して、String と同様のクラス ハッシュ コードの動作を提供します。 IEnumerable.Distinct メソッドを使用する場合、このメソッドは比較されたオブジェクトのハッシュ コードに基づいているため、日付範囲の一意のリストを受け取るロジックが正しいことを保証できます。

クラスが継承されるIComparable インターフェイスを実装する、 CompareTo現在のクラス インスタンスと渡されたインスタンスを比較するメソッドを実装します。 その実装は簡単で、以前にオーバーロードされた演算子のオーバーロードを使用します。 

必要なオーバーロードを実装したら、このクラスをより効率的に処理できます。 We can:

  • 2 つのインスタンスを等しいかどうかを比較する
  • 2 つのインスタンスを次より大きい/小さい値と比較する
  • 2 つのインスタンスを比較して、次の値以上の値を比較します。
  • 昇順/降順で並べ替え
  • 日付範囲のリストから一意の値を取得する
  • リストを降順に並べ替え、IComparable インターフェイスを使用する IEnumerable.Sort メソッドを使用します。

バックテストとフォワードテストを行うローリング最適化を実装しているので、ヒストリーと前方の間隔を比較するメソッドを作成する必要があります。

/// <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;
}

ご覧のとおり、このメソッドは静的です。 これは、特定のクラスインスタンスにバインドすることなく、通常の関数として使用できるようにするために行われます。 まず、渡された時間間隔を昇順に並べ替えます。 したがって、次のループでは、以前に渡されたすべての間隔が次の間隔以下であることを確認できます。 次に、ヒストリーの間隔に対してforeach、順方向の間隔にネストされたループの 2 つのループを実装します。

ヒストリーデータループの開始時に、常にヒストリー範囲 (キー) を結果付けのコレクションに追加し、転送間隔の代わりに一時的に null を設定します。 結果の転送ループは i番目のパラメータから始まります。 これより、前方リストの既に使用されている要素でループを繰り返し実行できなくなります。 前方の間隔は常にヒストリーに従う必要があります。つまり、ヒストリーよりも >でなければなりません。 そのため、ループを転送間隔で実装し、渡されたリストに最初のヒストリー区間の前に、最初のヒストリー間隔の転送期間がある場合に、最初のヒストリー間隔が先行します。 アイデアを表に視覚化する方が良いでしょう:

Historical Forward
From To From To
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

したがって、最初のヒストリー区間は 09.03.2017 で終了し、最初の転送間隔は 12.12.2016 で始まりますが、正しくはありません。 そのため、この条件により、前方の間隔ループでスキップします。 また、結果のディクショナリに含まれている転送間隔をスキップします。j 番目の転送データがまだ結果のディクショナリに存在せず、転送間隔の開始日付が >= 現在のヒストリカル・インターバル終了日付である場合は、必要な値が既に見つかったので、受信した値を保管し、転送間隔ループを終了します。 終了する前に、選択した変数の後に続く転送間隔の値を i 変数 (フォワード・リストの繰り返しが開始することを意味する変数) に割り当てます。 これは、(初期データの並べ替えに) 現在の間隔が不要になるためです。

ヒストリー最適化の前のチェックでは、すべてのヒストリー最適化が一意であることを確認します。 したがって、結果のディクショナリで次のリストが取得されます。

Key Value
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

提示されたデータからわかるように、最初の前方間隔は破棄され、そのような間隔が渡されていないので、直近のヒストリーの間隔は見つかりません。 このロジックに基づいて、プログラムはヒストリーと前方の間隔のデータを比較し、ヒストリーの間隔のどれが前方テストの最適化パラメータを提供すべきかを理解します。

特定の最適化結果で効率的な操作を可能にするために、追加メソッドとオーバーロードされた演算子を含む ReportItem 構造体のラッパ構造を作成しました。 基本的に、ラッパには次の 2 つのフィールドが含まれます。

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

最初のフィールドは、上記で説明しました。 2 番目のフィールドは、複数の値 (たとえば、利益係数やリカバリーファクターなど) による並べ替えを可能にするために作成されます。 ソートのメカニズムは後で説明しますが、値を 1 に変換し、この変数に格納するという考え方です。 

構造体には、型変換オーバーロードも含まれています。

/// <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;
}

その結果、ReportItem 型をラッパに暗黙的にキャストし、ReportItem ラッパを明示的にトレーディング レポート要素にキャストできます。 これは、フィールドの連続的なインプットよりも効率的です。 ReportItem 構造体のすべてのフィールドはカテゴリに分かれているので、必要な値を受け取るために長いコードが必要な場合があります。 スペースを節約し、より汎用的なゲッターを作成するために特別なメソッドが作成されました。 上記のGetResult(SortBy resultType)コードから、渡された列挙型 SourtBy を介してリクエストされたロボット比率データを受け取ります。 実装はシンプルですが、長すぎるため、ここでは示しません。 このメソッドは、渡された列挙型をスイッチで繰り返し処理し、リクエストされた係数の値を返します。 ほとんどの係数は倍精度浮動小数点型を持ち、この型には他のすべての数値型をインクルードすることができるため、係数値は倍精度浮動小数点数に変換されます。

このラッパ型には比較演算子のオーバーロードも実装されています。

/// <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);
}

ロボットパラメータの同じ名前と値を含む最適化の要素は等しいと見なされます。 したがって、2つの最適化パスを比較する必要がある場合は、既にすぐに使用できるオーバーロードされた演算子があります。 この構造体には、データをファイルに書き込むメソッドも含まれています。 存在する場合、データはファイルに追加されます。 データ書き込み要素とメソッド実装の説明を以下に示します。

最適化レポートを保存するファイルの作成

最適化レポートを扱い、ターミナルだけでなく作成したプログラムにも書き込みます。 そのため、最適化レポート作成メソッドをこの.dll に追加します。 また、ファイルへのデータ書き込みのメソッドを提供しましょう、すなわち、ファイルへのデータ配列の書き込みを有効にするだけでなく、既存のファイルに別の要素を追加することができます(ファイルが存在しない場合は作成する必要があります)。 直近のメソッドはターミナルにインポートされ、C# クラスで使用します。 実装されたレポートファイル書き込みメソッドを、ファイルにデータを追加して接続された関数で検討してみましょう。 このためにレポートライター クラスが作成されました。 クラス全体の実装は、添付されたプロジェクト ファイルで使用できます。 ここでは、最も興味深いメソッドだけを紹介します。 まず、このクラスの仕組みを説明しましょう。 

静的メソッドのみを含みます: MQL5へのメソッドのエクスポートを可能にします。 同じ目的に、クラスはパブリック アクセス修飾子でマークされます。 このクラスには、ReportItem 型の静的フィールドと、係数とEAパラメータを交互に追加するメソッドがあります。

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

もう 1 つのメソッドは、ClearReportItem()です。 フィールド インスタンスを再作成します。 この場合、このオブジェクトの前のインスタンスへのアクセスが失われます。消去され、データ保存プロセスが再び開始されます。 データ追加メソッドはブロックごとにグループ化されます。 メソッドのシグネチャを次に示します。  

/// <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);

日別に分類されたトレード統計を追加するメソッドは、5トレード日のそれぞれに対して呼び出されるべきです。 1 日のうちに追加しないと、書き込まれたファイルは今後読み取られるわけではありません。 データストレージフィールドにデータを追加したら、フィールドの記録に進むことができます。 この前に、ファイルが存在するかどうかを確認し、必要に応じて作成します。 ファイルを作成するためのメソッドが追加されました。

/// <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();
}

また、データと要素固有の属性を持つ直近の要素をファイルに追加するための繰り返しコードを含むWriteItemメソッドの実装もここで示します。 CreateFileIfNotExists ファイル作成メソッドは、ファイルが存在するかどうかをチェックし、ファイルを作成し、必要最小限のファイル構造の形成を開始します。 

まず、ファイルルート、すなわち<Optimization_Report/>タグを作成し、その中にファイルのすべての子構造が配置されます。 ファイル作成データが埋まる - ファイルを使ってさらに便利なタスクに実装されます。 その後、変更されていないオプティマイザ設定を持つノードを作成し、指定します。 次に、最適化結果を保存するセクションを作成し、すぐに閉じます。 その結果、必要最小限のフォーマットを持つ空のファイルがあります。 


<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>

したがって、XmlDocument クラスを使用してこのファイルを読み取ることができるでしょう。 これは、既存の Xml ドキュメントの読み取りと編集に最も役立つクラスです。 このクラスを使用して、既存のドキュメントにデータを追加します。 繰り返しの操作は別々のメソッドとして実装されるため、終了ドキュメントにデータをより効率的に追加できます。

/// <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);
}

最初のメソッド FillInAttributes は渡されたノードの属性を埋め、WriteItem は XPath を介して指定されたセクションに項目を書き込み、AppendSection は Xpath を使用して渡されたパスを介して指定されたセクションを別のセクション内に追加します。 コード ブロックは、ファイルにデータを追加するときによく使用します。 データ書き込みメソッドは長く、ブロックに分割されます。

/// <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();
}

まず、ドキュメント全体をメモリに読み込み、次にセクションを追加します。 ルートノードへのパスを渡すXpathリクエスト形式を考えてみましょう。  

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

xpath変数には、最適化パス要素が格納されるノードへのパスが含まれます。 このノードには、構造体の配列として表示できる最適化結果ノードが格納されます。 Result[last()]コンストラクトは配列の直近の要素を選択し、その後パスはネストされた/Coefficientsノードに渡されます。 説明した原則に従って、最適化の結果を含む必要なノードを選択します。 

次のステップはロボットパラメータの追加です。つまり、ループ内で、結果ディレクトリに直接パラメータを追加します。 次に、係数ディレクトリに係数の数を追加します。 この追加はブロックに分けられます。 結果として、結果を保存し、一時ストレージをクリアします。 その結果、パラメータと最適化結果のリストを含むファイルが取得されます。 異なるプロセスから起動された非同期操作の間にスレッドを分離するために (複数のプロセッサを使用する場合にテスターの最適化を実行する方法です)、別の書き込みメソッドが作成され、名前付きミューテックスを使用してスレッドを分離します。

/// <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;
}

このメソッドは、前のメソッドを使用してデータを書き込みますが、書き込みプロセスはミューテックスと try-catch ブロックでラップされます。 直近のは、エラーの場合でもミューテックスのインプットを有効にします。 そうしないと、プロセスがフリーズし、最適化が続行できなくなる可能性があります。 このメソッドは、WriteResult メソッドの最適化結果構造体でも使用します。

/// <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;
    }
}

このメソッドでは、最適化結果を一時記憶域に追加し、Writeメソッドを呼び出して既存のファイルに保存するか、まだ作成されていない場合は新しいファイルを作成します。 

準備されたファイルに情報を追加するには、取得したデータを書き込むメソッドが必要です。 データ系列を書く必要がある場合に適した別のメソッドがあります。 このメソッドは、IEnumerable<OptimisationResult> インターフェイスの拡張として開発されました。 適切なインターフェイスから継承されたすべてのリストのデータを保存できます。 

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();
    }
}

このメソッドは、最適化レポートを、配列にデータがなくなるまで 1 つずつファイルに書き込みます。 渡されたパスにファイルが既に存在する場合は、新しいパスに置き換えられます。 まず、ファイル ライターを作成し、構成します。 次に、既に知られているファイル構造に従って、オプティマイザの設定最適化結果を1つずつ書き込みます。 上記のコード抽出からわかるように、結果は、記述されたメソッドが呼び出されたインスタンスで、コレクションの要素をループするループで記述されます。 ループ内では、データ書き込みは、ファイルに特定の要素のデータを書き込むために作成されたメソッドに委任されます。

/// <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
}

データをファイルに書き込むメソッドの実装は簡単ですが、かなり長いです。 適切なセクションを作成し、属性を埋めた後、メソッドは実行された最適化パスのVaR上のデータと最大利益とドローダウンを特徴付ける値にデータを追加します。 特定の日付の最適化結果 (5 回) を各日に書き込むために、ネストした関数が作成されました。 その後、ルートパラメータおよびグルーピングを使用しない係数が追加されます。 記述されたプロシージャは要素ごとに 1 つのループで実行されるため、xmlWriter.Close()メソッドが呼び出されるまでデータはファイルに書き込まれません (書き込みメソッドで行われます)。 したがって、以前に検討されたメソッドと比較して、データ配列を書き込む最も速い拡張メソッドです。 ファイルへのデータ書き込みに関連するステップを検討しました。 次に、論理部分、つまり結果のファイルからデータを読み取る部分に移ります。

最適化レポート・ファイルの読み取り

受信した情報を処理し、表示するためにファイルを読み取る必要があります。 したがって、適切なファイル読み取りメカニズムが必要です。 これは、別のクラスとして実装されます。

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();
    }

構造を詳しく見てみましょう。 このクラスはiDisposableインターフェイスから継承されます。 必須の条件ではありませんが、予防措置として行われます。 describe クラスには、ドキュメントオブジェクトをクリアするために必要なディスタブルメソッドがあります。 このオブジェクトは、メモリに読み込まれた最適化結果ファイルを格納します。

インスタンスを作成する際、上記のインターフェイスから継承されたクラスを 'using' 構造体にラップする必要があり、'using'構造体ブロックを超えたときに指定されたメソッドを自動的に呼び出す必要があるため、このアプローチは便利です。 読み取りドキュメントがメモリ内に長く保持されないため、読み込まれたメモリ量が減少することを意味します。

行単位のドキュメント リーダー クラスは、読み取りドキュメントから受信した列挙子を使用します。 読み取り値は特殊プロパティに書き込まれるため、データへのアクセスを提供します。 また、クラスのインスタンス化中に、メインオプティマイザ設定ファイル作成日時を指定するプロパティのデータもインプットされます。 OS のローカリゼーション設定の影響を排除するために (書き込み時とファイルの読み取り時の両方)、倍精度の数値区切り書式が示されます。 ファイルを初めて読み込むときは、クラスをリストの先頭にリセットする必要があります。 このために、列挙子をリストの先頭にリセットするResetReaderメソッドを使用します。 クラスコンストラクタは、必要なすべてのプロパティを埋め、クラスをさらに使用できるように実装されています。

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);
}

まず、渡されたドキュメントを読み込み、作成日をインプットします。 クラスのインスタンス化中に取得される列挙子は、セクションOptimisation_Report/Optimisation_Resultsつまりタグ <Result/>を持つノードに配置されているドキュメントの子ノードに属します。 必要なオプティマイザの設定パラメータを取得するには、xpathマークアップを使用して、必要なドキュメント ノードへのパスを指定します。 短いパスを持つこの組み込み関数の類似点は SelectItem メソッドで、タグを持つドキュメント ノード間のアイテムへのパスを示します<Item/>Name属性に従います。 GetDay メソッドは、渡されたドキュメント ノードをデイリートレードレポートの適切な構造に変換します。 このクラスの直近のメソッドは、データ リーダー メソッドです。 ここでは簡潔な形での実装を以下に示します。   

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;
}

隠しコード部分には、最適化レポートのインスタンス化操作とレポート フィールドの読み取りデータのインプットが含まれます。 この操作には、文字列形式を必要な形式に変換する同様のアクションが含まれます。 さらにループは、ファイルから行ごとに読み取られたデータを使用してロボットパラメータを埋めます。 この操作は、完了したファイル行に達していない場合にのみ実行されます。 操作の結果は、行が読み取られたかどうかを示す結果です。 また、ファイルのトレーリングストップに到達するインジケータとしても機能します。

最適化レポートの多要素フィルタリングと並べ替え

この目的を達成するために、ソートの方向を示す2つの列挙を作成しました(SortMethdとOrderBy)。 これらは似ていて、おそらくそのうちの1つだけで十分です。 ただし、フィルタ処理と並べ替えメソッドを分離するために、1 つの列挙型ではなく 2 つの列挙が作成されました。 列挙体の目的は、昇順または降順です。 渡された値を持つ係数の比率タイプはフラグで示されます。 目的は比較条件を設定することです。    

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

データをフィルタ処理および並べ替えることができる係数の種類は、前述の列挙 OrderBy で記述されます。 並べ替えメソッドとフィルタ処理メソッドは、IEnumerable <OptimisationResult> インターフェイスから継承されたコレクションを展開するメソッドとして実装されます。 フィルタリングメソッドでは、各係数項目を項目ごとにチェックし、指定された基準を満たすかどうかを確認し、いずれかの係数が基準を満たさない最適化パスを拒否します。 データをフィルタ処理するには、IEnumerable インターフェイスに含まれる Where ループを使用します。 このメソッドは次のように実装されます。

/// <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));
}

メソッド内に実装される 2 つの関数は、それぞれがデータ フィルタ処理タスクの独自の部分を実行します。 最終的な関数から始めてみましょう:

  • 比較 — その目的は、渡された値を KeyValuePair として提示し、メソッドで指定された値を比較することです。 より大きい/より小さい、等しい比較に加えて、他の条件をチェックする必要があるかもしれません。 このために、フラグを利用します。 フラグは 1 ビットで、int フィールドは 8 ビットを格納します。 したがって、int フィールドのフラグを同時に 8 つまで同時に設定または削除できます。 フラグは、複数のループや巨大な条件を作成する必要なく、順番にチェックすることができ、したがって、3つの条件があります。 さらに、後で検討するグラフィカルインタフェースでは、必要な比較パラメータを設定するためにフラグを使用することも便利です。 この関数のフラグを順番にチェックし、データがフラグに対応しているかどうかをチェックします。  
  • ソート: 前のメソッドとは異なり、このメソッドは 1 つではなく、複数の書き込みパラメータをチェックするように設計されています。 フィルタ処理に渡されたすべてのフラグを項目ごとのループで実行し、前述の関数を使用して、選択したパラメータが指定した条件を満たしているかどうかを調べます。 "スイッチケース"演算子を使用せずにループ内の特定の選択された項目の値へのアクセスを有効にするには、前述の OptimisationResult.GetResult(OrderBy item) メソッドを使用します。 渡された値がリクエストされた値と一致しない場合は、false を返し、不適切な値を破棄します。

データを並べ替えるには、'Where' メソッドを使用します。 このメソッドは、適切な条件の一覧を自動的に生成し、拡張メソッドの実行結果として返します。  

データフィルタリングは理解しやすいです。 並べ替えでは困難が発生する可能性があります。 例を用いてソートメカニズムを考えてみましょう。 たとえば、プロフィットファクターとリカバリファクターのパラメータがあるとします。 2 つのパラメータでデータを並べ替える必要があります。 2 つの並べ替え繰り返しを 1 つずつ実行した場合でも、直近のパラメータで並べ替えられたデータが受け取られます。 値を何らかの方法で比較する必要があります。

利益 プロフィットファクター リカバリーファクター
5000 1 9
15000 1.2 5
-11000 0.5 -2
0 0 0
10000 2 5
7000 1 4

2 つの係数は、境界値内では正規化されません。 また、互いに相対的な値の広い範囲があります。 論理的には、まずシーケンスを維持しながら正規化する必要があります。 正規化された形式にデータを取り込む標準的な方法は、各データを系列の最大値で除算することです。 しかし、まず、この一連の値の極値を見つける必要があります。


プロフィットファクター  リカバリーファクター
Min  0 -2  
Max  2 9

表からわかるように、リカバリーファクターは負の値があるので、上記のアプローチはここでは適していません。 この効果を排除するために、剰余を取った負の値で系列全体をシフトします。 各パラメータの正規化値を計算できます。

利益 プロフィットファクター リカバリーファクター 正規化された合計
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

正規化された形式のすべての係数が得られたので、加重付けされた合計を使用できます。 その結果、ソート基準として使用できる正規化された列が得られます。 いずれかの係数を降順に並べ替える必要がある場合は、このパラメータを 1 つから引いて、最大の係数と最小の係数を入れ替える必要があります。

この機構を実装するコードは 2 つのメソッドとして示され、最初のメソッドは並べ替えオーダー (昇順または降順) を示し、2 番目のメソッドは並べ替え機構を実装します。 最初のメソッドである SortMethod GetSort メソッド(SortBy sortBy)は簡単なので、2 番目のメソッドに移りましょう。

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);
}

並べ替えを 1 つのパラメータで実行する場合は、系列の正規化を行わずに並べ替えを実行します。 その後、すぐに結果を返します。 ソートが複数のパラメータによって実行される場合、まず、考慮された系列の最大値と最小値からなるディクショナリを生成します。 そうしないと、各繰り返しの間にパラメータをリクエストする必要があるため、計算を高速化できます。 この実装で考えたよりもはるかに多くのループが生成されます。

次に、加重付けされた合計に対して加重付けを形成し、その合計に系列を正規化する操作が実行されます。 ここでは2つのループが再び使用され、上記の操作が内部ループで行われます。 結果の加重合計は、適切な配列要素のSortBy変数に追加されます。 この操作の最後に、ソートに使用する結果の係数が既に形成されている場合は、前述のソート方法を標準List<T>.OrderBy or List<T> 配列メソッドを使用します。 OrderByDescending   — 降順の並べ替えが必要な場合。 加重付けされた合計の個別のメンバの並べ替えメソッドは、関数パラメータの 1 つとして渡されたデリゲートによって設定されます。このデリゲートがデフォルトのパラメータ化された値として残っている場合は、前述のメソッドが使用します。それ以外の場合は、渡されたデリゲートを使用します。
  

結論

今後、アプリケーション内で積極的に使用する仕組みを作り上げます。 実行されたテストに関する構造化情報を格納するカスタム形式の xml ファイルのアンロードと読み取りはもちろん、このメカニズムには、データの並べ替えとフィルタ処理に使用する C# コレクション拡張メソッドがあります。 標準ターミナルテスターでは利用できない多要素分類機構を実装しました。 ソートメソッドの利点の 1 つは、一連の要因を考慮する能力です。 しかし、その欠点は、結果が与えられたシリーズ内でのみ比較することができるということです。 これは、選択した時間間隔の加重合計が他の間隔と比較できないことを意味します。 次の記事では、アルゴリズムのアプリケーションまたは自動化されたオプティマイザを有効にするアルゴリズム変換メソッドと、そのような自動化されたオプティマイザの作成について検討します。   


MetaQuotes Software Corp.によりロシア語から翻訳された
元の記事: https://www.mql5.com/ru/articles/7290

添付されたファイル |
MetaTraderプログラムを簡単かつ迅速に開発するためのライブラリ(第23部): 基本取引クラス - パラメータ有効性の検証 MetaTraderプログラムを簡単かつ迅速に開発するためのライブラリ(第23部): 基本取引クラス - パラメータ有効性の検証

本稿では、取引クラスの不正な取引注文パラメータ値に対する制御と取引イベントの音声通知において開発を続けています。

ピボット平均オシレータの開発:累積移動平均の新規インジケータ ピボット平均オシレータの開発:累積移動平均の新規インジケータ

この記事では、MetaTraderプラットフォームのトレードインジケータとして累積移動平均(CMA)であるピボット平均オシレータ(PMO)を紹介します。 特に、データポイントとCMAの間の分数を計算する時系列の正規化インデックスとしてピボット平均(PM)を導入しました。 次に、2つのPMシグナルに適用される移動平均の差としてPMOを構築します。 提案されたインジケータの有効性をテストするためにEURUSDシンボルで行われた予備的な実験も行いましたが、さらなる検討と改善の余地があります。

MetaTraderプログラムを簡単かつ迅速に開発するためのライブラリ(第24部): 基本取引クラス - 無効なパラメータの自動修正 MetaTraderプログラムを簡単かつ迅速に開発するためのライブラリ(第24部): 基本取引クラス - 無効なパラメータの自動修正

本稿では、無効な取引注文パラメータのハンドラを一瞥して、取引イベントクラスを改善します。これによって、すべての取引イベント(単一のイベントと1ティック内で同時に発生したイベントの両方)がプログラムで正しく定義されるようになります。

Boxplotによる金融時系列のシーズンパターンの探索 Boxplotによる金融時系列のシーズンパターンの探索

この記事では、Boxplotを使用して価格時系列のシーズン特性を表示します。 各Boxplot(あるいは"ボックスアンドウイスキーダイアグラム") は、データセットに沿って値がどのように分布しているかを示す優れたものです。 Boxplotは、視覚的に似ていますが、ローソク足チャートと混同しないでください。