連続歩行順最適化(パート1):最適化レポートの使用
イントロダクション
前回の記事(最適化管理(パート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 つのパラメータを保持する必要があります。
- エキスパートフォルダに対するロボットパス
- 資産通貨と資産
- アカウントレバレッジ
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
- 90 - quantile 90
- 95 - quantile 95
- 99 - quantile 99
- Mx - math expectation
- Std - standard deviation
- Max_PL_DD
- Profit - total profit
- DD - total drawdown
- Total Profit Trades - total number of profitable trades
- Total Lose Trades - total number of losing trades
- Consecutive Wins - winning trades in a row
- Consecutive Lose - losing trades in a row
- Trading_Days - trading reports by days
- Profit - average profit per day
- DD - average losses per day
- Number Of Profit Trades - number of profitable trades
- 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 Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/7290
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索