
トレーディングにおけるOLAPの適用(その4)。テスターレポートの定量的・視覚的分析
今回は、引き続きOLAP(On-Line Analytical Processing)とトレーディングにおける適用性について考察していきます。
以前の記事では,多次元配列を蓄積して解析するクラスを構築するための一般的な手法や,解析結果をグラフィカルなインタフェースで可視化する手法を取り上げました. 応用の観点から、最初の2つの記事では、ストラテジーテスターから、オンライン取引履歴から、HTMLやCSVファイル(MQL5の取引シグナルを含む)から、様々な方法で得られた取引レポートを扱いました。 3つ目の記事でコードを少しリファクタリングした後、OLAPは気配値の分析と取引戦略の開発に使用されました。 新マテリアルを理解できるように、過去記事を読んでみてください(特に注意すべき点は、カッコをチェックしてください)。
- 第1部: 多次元データのオンライン分析の基礎 (セレクタ、アグリゲータ、ハイパーキューブ計算、口座履歴、HTMLファイル、CSVファイルから取引記録を読み込むためのアダプタ)
- 第2部:インタラクティブな多次元データ分析結果の可視化(ラバーウィンドウとコントロール、OLAP GUIの設計と動作原理
- 第3部:取引戦略の開発のための相場の分析 (OLAPエンジンクラス、新しいセレクタとアグリゲータ、アダプターの実装、別のアプリケーション領域(相場)のためのレコードなど、統一されたアプローチを維持しながら、この記事で使用されます)
今回の記事では、MetaTrader 5の最適化結果を分析して、OLAPの範囲を広げていきます。
このプロジェクトを実行できるようにするためには、まず第2部で先に検討したグラフィカルユーザーインターフェースを改善する必要があります。 パート3で行われたすべてのコード改良は、OLAPエンジンに直接関係しています。 しかし、関連する可視化のアップグレードは行われませんでした。 それが今回の記事内のタスクとして、2記事目のOLAPGUIのトレードレポート分析器を使って取り組むことになります。 また、このグラフィカルな部分を統一することで、他の新しい応用分野、特に最適化結果の計画的な解析にも容易に適用できるようにします。
アプリケーショングラフィックスについて
OLAP用GUIの中心となるのは、専用に開発されたビジュアルコンポーネント「CGraphicInPlot」です。 2番目で紹介したその最初の実装には、いくつかの欠点がありました。 これらには、軸上のラベルの表示が含まれていました。 必要に応じてセレクタセルの名前(曜日の名前や通貨の名前など)を横のX軸に表示させることでなんとかなりました。 しかし、それ以外のすべてのケースでは、数字は「そのまま」表示されますが、これは常にユーザーフレンドリーではありません。 通常は集約された値を表示するY軸には、もう一つのカスタマイズが必要です。 設定次第では、セレクタの値を表示することができるので、改善が必要なところです。 メソッドの表示が悪い例として、シンボルの平均ポジション保持時間の要求があります。
シンボル別平均ポジションライフタイム(秒)
Yはセレクタ(セルサイズが立方体になるまで値を丸めたもの)ではなく、秒単位の持続時間の積算値を示しているため、このような大きな数字は知覚しにくいです。 この問題を解決するために、現在のタイムフレームバーの持続時間で秒を割ってみましょう。 この場合、値はバーの数を表します。 これを行うには、CGraphicInPlotクラスに特定のフラグを渡し、さらに軸を扱うクラスCAxisに渡す必要があります。 動作モードを変更するフラグは多数あります。 そのため、Plot.mqhファイル内のAxisCustomizerと名付けられた特別な新しいクラスを予約しておきます。
class AxisCustomizer { public: const CGraphicInPlot *parent; const bool y; // true for Y, false for X const bool periodDivider; const bool hide; AxisCustomizer(const CGraphicInPlot *p, const bool axisY. const bool pd = false, const bool h = false): parent(p), y(axisY), periodDivider(pd), hide(h) {}。 };
可能性としては、様々なラベル表示機能をクラスに追加することができます。 しかし、現時点では、軸タイプの符号(XまたはY)と、periodDividerや'hide'などのいくつかの論理的なオプションを格納しているだけです。 最初のオプションは、値を PeriodSeconds() で分割することを意味します。 第二のオプションは後ほど検討します。
このクラスのオブジェクトは、特別なメソッドを介してCGraphicInPlotに入ります。
class CGraphicInPlot: public CGraphic { ... void InitAxes(CAxis &axe, const AxisCustomizer *custom = NULL); void InitXAxis(const AxisCustomizer *custom = NULL); void InitYAxis(const AxisCustomizer *custom = NULL); }; void CGraphicInPlot::InitAxes(CAxis &axe, const AxisCustomizer *custom = NULL) { if(custom) { axe.Type(AXIS_TYPE_CUSTOM); axe.ValuesFunctionFormat(CustomDoubleToStringFunction); axe.ValuesFunctionFormatCBData((AxisCustomizer *)custom); } else { axe.Type(AXIS_TYPE_DOUBLE); } } void CGraphicInPlot::InitXAxis(const AxisCustomizer *custom = NULL) { InitAxes(m_x, custom); } void CGraphicInPlot::InitYAxis(const AxisCustomizer *custom = NULL) { InitAxes(m_y, custom); }
このようなオブジェクトが作成されず、グラフィッククラスにも渡されていない場合、標準ライブラリは通常の方法で値を数値 AXIS_TYPE_DOUBLE として表示します。
ここでは、軸上のラベルをカスタマイズするために標準ライブラリのアプローチを使用します。軸タイプはAXIS_TYPE_CUSTOMに等しく設定され、AxisCustomizerへのポインタはValuesFunctionFormatCBDataを介して渡されます。 さらに、それはCGraphic基底クラスによってCustomDoubleToStringFunctionラベル描画関数に渡されます(上のコードのValuesFunctionFormat呼び出しによって設定されます)。 もちろん、AxisCustomizerクラスのオブジェクト(CGraphicInPlotチャートはセットアップオブジェクトとして動作していました)を使用せずに、先に簡略化した形で実装したCustomDoubleToStringFunction関数が必要です。
string CustomDoubleToStringFunction(double value, void *ptr) { AxisCustomizer *custom = dynamic_cast<AxisCustomizer *>(ptr); if(custom == NULL) return NULL; // check options if(!custom.y && custom.hide) return NULL; // case of X axis and "no marks" mode // in simple cases return a string if(custom.y) return (string)(float)value; const CGraphicInPlot *self = custom.parent; // obtain actual object with cache if(self != NULL) { ... // retrieve selector mark for value } }
AxisCustomizerのカスタマイズオブジェクトは、GUIコントロール(CWndClientから継承)とCGraphicInPlotのコンテナであるCPlotクラスに格納されています。
class CPlot: public CWndClient { private: CGraphicInPlot *m_graphic; ENUM_CURVE_TYPE type; AxisCustomizer *m_customX; AxisCustomizer *m_customY; ... public: void InitXAxis(const AxisCustomizer *custom = NULL) { if(CheckPointer(m_graphic) != POINTER_INVALID) { if(CheckPointer(m_customX) != POINTER_INVALID) delete m_customX; m_customX = (AxisCustomizer *)custom; m_graphic.InitXAxis(custom); } } ... };
このように、m_customXとm_customYオブジェクトの軸設定は、CustomDoubleToStringFunctionで値を形成する段階だけでなく、データ配列がCurveAddメソッドの1つを使ってCPlotに渡されるだけの場合にも、かなり早い段階で使用することができます。 具体例
CCurve *CPlot::CurveAdd(const PairArray *data, const string name = NULL) { if(CheckPointer(m_customY) != POINTER_INVALID) && m_customY.periodDivider) { for(int i = 0; i < ArraySize(data.array); i++) { data.array[i].value /= PeriodSeconds(); } } return m_graphic.CurveAdd(data, type, name); }
このコードでは、すべての値を PeriodSeconds() で分割する periodDivider オプションを使用しています。 この操作は、標準ライブラリがデータを受信し、それらのグリッドサイズを計算する前に実行されます。 グリッドが既にカウントされた後では、CustomDoubleToStringFunction関数でカスタマイズするには遅すぎるため、このステップは重要です。
ダイアログの呼び出し元コードは、キューブ構築時に AxisCustomizer オブジェクトを作成して初期化する必要があります。 具体例
AGGREGATORS at = ... // get aggregator type from GUI ENUM_FIELDS af = ... // get aggregator field from GUI SORT_BY sb = ... // get sorting mode from GUI int dimension = 0; // calculate cube dimensions from GUI for(int i = 0; i < AXES_NUMBER; i++) { if(Selectors[i] != SELECTOR_NONE) dimension++; } bool hideMarksOnX = (dimension > 1 && SORT_VALUE(sb)); AxisCustomizer *customX = NULL; AxisCustomizer *customY = NULL; customX = new AxisCustomizer(m_plot.getGraphic(), false, Selectors[0] == SELECTOR_DURATION, hideMarksOnX); if(af == FIELD_DURATION) { customY = new AxisCustomizer(m_plot.getGraphic(), true, true); } m_plot.InitXAxis(customX); m_plot.InitYAxis(customY);
ここで、m_plotはCPlotコントロールを格納するダイアログ変数です。 以下の OLAPDialog::process メソッドのフルコードは、実際にどのように実行されるかを示しています。 ここでは、ピリオドディバイダーモードが自動的に有効になっている上記の例を示します。
シンボル別平均ポジションライフタイム(現在の時間足、D1)
AxisCustomizerのもう一つの変数である「hide」は、X軸に沿ってラベルを完全に非表示にする機能です。 このモードは多次元配列の値によるソートを選択する際に必要です。 この場合、各行のラベルにはそれぞれ順番があるので、X軸に沿って表示するものは何もありません。 多次元キューブはソートをサポートしており、他のモード、特にラベルによる使用が可能です。
hide' オプションは CustomDoubleToStringFunction の内部で動作します。 この関数の標準的な動作は、セレクタの存在を意味します。セレクタのラベルは、特殊なCurveSubtitlesクラスでX軸用にキャッシュされ、グリッド分割インデックスによってチャートに返されます。 しかし、設定された 'hide' フラグは、任意の横座標に対してこの処理を最初から終了させ、この関数は NULL (非表示の値) を返します。
グラフィックスで修正が必要な2つ目の問題は、ヒストグラムのレンダリングに関連しています。 チャートに複数の行(デdata vectors)が表示されている場合、ヒストグラム・バーは互いに重なり合っており、最大のものは他のすべてを完全に隠すことができます。
CGraphicの基本クラスには、仮想HistogramPlotメソッドがあります。 列を視覚的に分離するためにオーバーライドする必要があります。 CCurveオブジェクトにカスタムフィールドを設けて、任意のデータを保存しておくと良いでしょう(そのデータは必要に応じてクライアントコードによって解釈されます)。 残念ながら、そのようなフィールドは存在しません。 そのため、今回のプロジェクトでは使用していない標準的なプロパティを1つ使用します。 LinesSmoothStepを選びました。 CCurve::LinesSmoothStepセッターメソッドを使用して、呼び出し元のコードはシーケンス番号をそれに書き込みます。 このコードは、新しいHistogramPlotの実装でCCurve::LinesSmoothStepゲッターメソッドを使用することで簡単に取得することができます。 LinesSmoothStepでの行番号の書き方の例です。
CCurve *CGraphicInPlot::CurveAdd(const double &x[], const double &y[], ENUM_CURVE_TYPE type, const string name = NULL) { CCurve *c = CGraphic::CurveAdd(x, y, type, name); c.LinesSmoothStep((int)CGraphic::CurvesTotal()); // + ... return CacheIt(c); }
行の総数と現在の行の数を知ることで、レンダリング時にそれぞれのポイントを少しだけ左にずらしたり、書き込みをしたりすることができます。 ここでは、HistogramPlotの適応版を紹介します。 更新された行には "*"のコメントが付けられ、新たに追加された行には "+"が付けられています。
void CGraphicInPlot::HistogramPlot(CCurve *curve) override { const int size = curve.Size(); const double offset = curve.LinesSmoothStep() - 1; // + double x[], y[]; int histogram_width = curve.HistogramWidth(); if(histogram_width <= 0) return; curve.GetX(x); curve.GetY(y); if(ArraySize(x) == 0 || ArraySize(y) == 0) return; const int w = m_width / size / 2 / CGraphic::CurvesTotal(); // + const int t = CGraphic::CurvesTotal() / 2; // + const int half = ((CGraphic::CurvesTotal() + 1) % 2) * (w / 2); // + int originalY = m_height - m_down; int yc0 = ScaleY(0.0); uint clr = curve.Color(); for(int i = 0; i < size; i++) { if(!MathIsValidNumber(x[i]) || !MathIsValidNumber(y[i])) continue; int xc = ScaleX(x[i]); int yc = ScaleY(y[i]); int xc1 = xc - histogram_width / 2 + (int)(offset - t) * w + half; // * int xc2 = xc + histogram_width / 2 + (int)(offset - t) * w + half; // * int yc1 = yc; int yc2 = (originalY > yc0 && yc0 > 0) ? yc0 : originalY; if(yc1 > yc2) yc2++; else yc2--; m_canvas.FillRectangle(xc1,yc1,xc2,yc2,clr); } }
早速、この見た目を確認してみましょう。
もう一つの厄介な瞬間は、行の表示の標準的な実装に関連しています。 データに数値以外の値がある場合、CGraphicは改行します。 これは、キューブのセルの一部にはデータが含まれていない場合があり、アグリゲータはそのようなセルにNaNを書き込むため、我々のタスクにとっては悪いことです。 例えば、いくつかのセクションでの累積残高の合計など、いくつかのキューブでは、各ディールの値が1つのセクションでしか変更されないため、表示が悪くなってしまいます。 折れ線の悪影響を見るには、記事2の「シンボルごとのバランス曲線を個別に見る」という図を見てみましょう。
この問題を修正するために、LinePlotメソッドが追加で再定義されました(ソースコード、Plot.mqhファイルを参照してください)。 テスターの標準ファイルの処理に関連する部分で、以下のような操作結果が得られます。
最後に、最後の図形問題は、標準ライブラリのゼロ軸の定義に関連しています。 CGraphic::CreateGridメソッドでは、次のようにゼロが検索されます(Yの場合を示しています;X軸も同じように処理されます)。
if(StringToDouble(m_yvalues[i]) == 0.0) ...
m_y値は文字列のラベルであることに注意してください。 自明ですが、数字を含まないラベルは0を生成します。 これは、チャートにAXIS_TYPE_CUSTOM表示モードを設定していても発生します。 その結果、値によるチャート、曜日、取引の種類、その他のセレクタでは、グリッド全体をループでチェックすると、すべての値がゼロとして扱われます。 しかし、最終的な値は最後のサンプルに依存しており、太線で示されています(ゼロではありませんが)。 さらに、各サンプルが(一時的であっても)0の候補となるため、単純なグリッド線の描画をスキップしてしまい、グリッド全体が消えてしまいます。
CreateGridメソッドもバーチャルなので、よりインテリジェントに0をチェックして再定義します。 このチェックは補助的なisZero関数として実装されています。
bool CGraphicInPlot::isZero(const string &value) { if(value == NULL) return false; double y = StringToDouble(value); if(y != 0.0) return false; string temp = value; StringReplace(temp, "0", ""); ushort c = StringGetCharacter(temp, 0); return c == 0 || c == '.'; } void CGraphicInPlot::CreateGrid(void) override { int xc0 = -1.0; int yc0 = -1.0; for(int i = 1; i < m_ysize - 1; i++) { m_canvas.LineHorizontal(m_left + 1, m_width - m_right, m_yc[i], m_grid.clr_line); // *。 if(isZero(m_yvalues[i])) yc0 = m_yc[i]; // * for(int j = 1; j < m_xsize - 1; j++) { if(i == 1) { m_canvas.LineVertical(m_xc[j], m_height - m_down - 1, m_up + 1, m_grid.clr_line); // *。 if(isZero(m_xvalues[j])) xc0 = m_xc[j]; // * } if(m_grid.has_circle) { m_canvas.FillCircle(m_xc[j], m_yc[i], m_grid.r_circle, m_grid.clr_circle); m_canvas.CircleWu(m_xc[j], m_yc[i], m_grid.r_circle, m_grid.clr_circle); } } } if(yc0 > 0) m_canvas.LineHorizontal(m_left + 1, m_width - m_right, yc0, m_grid.clr_axis_line); if(xc0 > 0) m_canvas.LineVertical(xc0, m_height - m_down - 1, m_up + 1, m_grid.clr_axis_line); }
OLAP GUI
グラフィックスに必要な修正を実装しました。 さて、ウィンドウのインターフェイスを見直してユニバーサルなものにしてみましょう。 2番目の記事のノントレーディングEAのOLAPGUIでは、ダイアログを使った操作をOLAPGUI.mqhヘッダーファイルに実装しています。 それは、前のタスク、トレーディングレポートの分析の適用された機能の多くを格納しました。 任意のデータに対して同じダイアログを使用するつもりなので、ファイルを2つに分割する必要があります。
元OLAPDialogクラスの名前をOLAPDialogBaseに変更します。 実際にダイアログコントロールを記述する、ハードコーディングされた統計的配列「セレクタ」「設定」「デフォルト」は、空の動的テンプレートとなり、派生クラスによって埋められます。 変数:
OLAPWrapper *olapcore; // <-- template <typename S,typename T> class OLAPEngine, since part 3.
OLAPDisplay *olapdisplay;
また、各OLAPエンジンのアプリケーション部で定義されているセレクタやレコードフィールドの種類で標準化する必要があるため、それらも継承されることになります。 古いOLAPWrapperクラスは、三番目のリファクタリングの間にOLAPEngine<S,T>テンプレートクラスに変換されたことを思い出してください。
2つの新しい抽象メソッドがメインロジックに予約されています。
virtual void setup() = 0; virtual int process() = 0;
最初のは、セットアップでインターフェイスを設定し、2番目のものは、プロセスで解析を起動します。 このセットアップは OLAPDialogBase::Create から呼び出されます。
bool OLAPDialogBase::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2) { setup(); // +。 ... }
ユーザーはボタンをクリックすることで分析の起動を開始するため、OLAPDialogBase::OnClickButtonメソッドが最も変更されました。コードのほとんどが削除され、対応する機能(コントロール・プロパティの読み取りとそれに基づくOLAPエンジンの起動)がその'process'メソッドに委譲されました。
void OLAPDialogBase::OnClickButton(void) { if(processing) return; // prevent re-entrancy if(browsing) // 3D-cube browsing support { currentZ = (currentZ + 1) % maxZ; validateZ(); } processing = true; const int n = process(); if(n == 0 && processing) { finalize(); } }
OLAPDialogBase クラスは、コントロールの作成から始まり、コントロールの状態に影響を与えるイベントの処理まで、操作インターフェースのロジック全体を実装していることに注意してください。 しかし、それはコントロールの内容については何も知りません。
OLAPDisplayクラスは、OLAPCommon.mqhからDisplayの仮想インターフェースを実装しています(記事3で説明しました)。 ご存知のように、DisplayインターフェースはOLAPカーネルからのコールバックであり、分析結果を提供することを目的としています(MetaCubeクラスオブジェクトの最初のパラメータで渡されます)。 OLAPDisplay クラスの '親' ウィンドウへのポインタを使用すると、キューブ・データをさらにダイアログに渡すためのチェーンを整理できます (MQL5 は複数の継承を提供しないため、この転送が必要です)。
class OLAPDisplay: public Display { private: OLAPDialogBase *parent; public: OLAPDisplay(OLAPDialogBase *ptr,): parent(ptr) {} virtual void display(MetaCube *metaData, const SORT_BY sortby = SORT_BY_NONE, const bool identity = false) override; };
ここでは、派生アダプタクラスからカスタムフィールドの実名を取得することに関連した具体的な機能について触れておきます。 これまでは、第二部の標準フィールドにカスタムフィールド(MFEやMAEなど)を追加していましたが、第三部では、カスタムフィールドを追加しています。 このように、それらは事前に知られており、コードに組み込まれていました。 しかし、最適化レポートを扱う際には、EAの入力パラメータの観点から分析する必要がありますが、これらのパラメータ(その名前)は分析データからしか得ることができません。
このアダプタは、新しい assignCustomFields メソッドを使用してカスタムフィールドの名前をアグリゲータ (メタキューブ) に渡します。 これは常に "舞台裏" で行われ、つまり Analyst::acquisitionData メソッドで自動的に行われます。 このため、軸の長いセクション指定を取得するために OLAPDisplay::display 内で metaData.getDimensionTitle メソッドが呼び出され、フィールドの序数 n が組み込みのフィールド列挙の容量を超えると、拡張フィールドを扱っていることがわかり、キューブに説明を要求することができます。 OLAPDisplay::displayメソッドの一般的な構造は変更されていません。 以下のソースコードと2番目の記事のコードを比較してみてください。
また、インターフェース要素を埋めるためには、カスタムフィールドの名前をダイアログで事前に知っておく必要があります。 この目的のために、OLAPDialogBase クラスには、カスタム・フィールドを設定するための新しい setCustomFields メソッドが含まれています。
int customFieldCount; string customFields[]; virtual void setCustomFields(const DataAdapter &adapter) { string names[]; if(adapter.getCustomFields(names) > 0) { customFieldCount = ArrayCopy(customFields, names); } }
もちろん、テストEAではこの方法でダイアログとアダプタをバインドする必要があります(後述)。 その後、意味のあるフィールド名('カスタム1'などと番号が付けられたものではなく)がダイアログコントロールに表示されるようになります。 これは一時的な解決策です。 特にこのような面では、さらなるコードの最適化が必要です。 しかし、これらのものはこの記事の中では取るに足らないと考えられています。
修正されたOLAPGUIのインターフェース設定のアプリケーション部分は、OLAPGUI.mqhからOLAPGUI_Trades.mqhヘッダーファイルに「移動」されました。 ダイアログ・クラス名は変更されていません。OLAPDialogです。 しかし、それはテンプレートパラメータに依存しており、OLAPEngineオブジェクトを特殊化するために使用されます。
template<typename S, typename F> class OLAPDialog: public OLAPDialogBase { private: OLAPEngine<S,F> *olapcore; OLAPDisplay *olapdisplay; public: OLAPDialog(OLAPEngine<S,F> &olapimpl); ~OLAPDialog(void); virtual int process() override; virtual void setup() override; }; template<typename S, typename F> OLAPDialog::OLAPDialog(OLAPEngine<S,F> &olapimpl) { curveType = CURVE_POINTS; olapcore = &olapimpl; olapdisplay = new OLAPDisplay(&this); } template<typename S, typename F> OLAPDialog::~OLAPDialog(void) { delete olapdisplay; }
すべての作業は、メソッド「セットアップ」と「プロセス」で行われます。 setup'メソッドは'settings'、'selectors'、'defaults'の配列を同じ値で埋めますが、これは第2回の記事ですでにわかっていることです(インターフェースの外観は変わりません)。 process' メソッドは、指定されたセクションの解析を起動し、前回のハンドラ OnClickButton とほぼ完全に同じです。
template<typename S, typename F> int OLAPDialog::process() override { SELECTORS Selectors[4]; ENUM_FIELDS Fields[4]; AGGREGATORS at = (AGGREGATORS)m_algo[0].Value(); ENUM_FIELDS af = (ENUM_FIELDS)(AGGREGATORS)m_algo[1].Value(); SORT_BY sb = (SORT_BY)m_algo[2].Value(); ArrayInitialize(Selectors, SELECTOR_NONE); ArrayInitialize(Fields, FIELD_NONE); int matches[10] = // selectors in combo-boxes (specific record fields are bound internally) { SELECTOR_NONE, SELECTOR_SERIAL, SELECTOR_SYMBOL, SELECTOR_TYPE, SELECTOR_MAGIC, SELECTOR_WEEKDAY, SELECTOR_WEEKDAY, SELECTOR_DAYHOUR, SELECTOR_DAYHOUR, SELECTOR_DURATION }; int subfields[] = // record fields listed in combo-boxes after selectors and accessible directly { FIELD_LOT, FIELD_PROFIT_AMOUNT, FIELD_PROFIT_PERCENT, FIELD_PROFIT_POINT, FIELD_COMMISSION、FIELD_SWAP、FIELD_CUSTOM_1、FIELD_CUSTOM_2 }; for(int i = 0; i < AXES_NUMBER; i++) // up to 3 orthogonal axes are supported { if(!m_axis[i].IsVisible()) continue; int v = (int)m_axis[i].Value(); if(v < 10) // selectors (every one is specialized for a field already) { Selectors[i] = (SELECTORS)matches[v]; if(v == 5 || v == 7) Fields[i] = FIELD_OPEN_DATETIME; else if(v == 6 || v == 8) Fields[i] = FIELD_CLOSE_DATETIME; } else // pure fields { Selectors[i] = at == AGGREGATOR_IDENTITY ? SELECTOR_SCALAR : SELECTOR_QUANTS; Fields[i] = (TRADE_RECORD_FIELDS)subfields[v - 10]; } } m_plot.CurvesRemoveAll(); AxisCustomizer *customX = NULL; AxisCustomizer *customY = NULL; if(at == AGGREGATOR_IDENTITY || at == AGGREGATOR_COUNT) af = FIELD_NONE; if(at != AGGREGATOR_PROGRESSIVE) { customX = new AxisCustomizer(m_plot.getGraphic(), false, Selectors[0] == SELECTOR_DURATION, (dimension > 1 && SORT_VALUE(sb))); } if((af == FIELD_DURATION) || (at == AGGREGATOR_IDENTITY && Selectors[1] == SELECTOR_DURATION)) { customY = new AxisCustomizer(m_plot.getGraphic(), true, true); } m_plot.InitXAxis(customX); m_plot.InitYAxis(customY); m_button_ok.Text("Processing..."); return olapcore.process(Selectors, Fields, at, af, olapdisplay, sb); }
先ほど説明した軸を設定するためのAxisCustomizerオブジェクトは、メソッドの最後に作成されます。 両軸 (X 軸と Y 軸) の場合、期間フィールド (アグリゲータ・タイプが AGGREGATOR_IDENTITY の場合はアグリゲータ内また はセレクタ内のいずれか - この場合、セレクタはフィールドの内容を指定されたセル間で分配せず、内容はキューブに直接配信されます) を使用しているときは、 PeriodSeconds() による除算が有効になります。 キューブ寸法が 1 より大きい場合、およびソートが選択されている場合、X 軸は無効になります。
それでは、OLAPGUI.mq5のプログラムファイルを見てみましょう。 前バージョンとのその他の違いとしては、ヘッダファイルの接続順序が変更されたことです。 以前は、レポート用のアダプタがコアに含まれていました(他のデータソースがなかったため)。 これで、HTMLcube.mqhとCSVcube.mqhとして明示的に記述されるようになりました。 また、OnInitコードでは、入力データに応じて適切なアダプタタイプを用意し、_defaultEngine.setAdapterを呼び出してエンジンに渡しています。 このコード部分は、記事3のOLAPRPRT.mq5プログラムですでに使用されていたもので、普遍的な部分と応用的な部分に分解して正しいアプローチをテストしました。 しかし、OLAPRPRTは前の部分ではグラフィカルなインターフェースを持っていませんでした。 今すぐこの欠点を修正しましょう。
標準フィールドとカスタムフィールドの厳密な分離を実証するために、MFEとMAEフィールドを計算するCustomTradeRecordクラスはOLAPTrades.mqhからOLAPTradesCustom.mqhに移動されました(そのコードは添付されています)。 このようにして、取引に基づく他のカスタムフィールドの開発が必要な場合には、その開発を簡素化することができます。 OLAPカーネルが変化しない間に、OLAPTradesCustom.mqhのアルゴリズムを変更するだけです。 取引記録フィールド、接続されたセレクタ、TradeRecord 基底クラス、OLAPEngineTrade エンジン、履歴用アダプタなどの標準コンポーネントはすべて OLAPTrades.mqh にあります。 OLAPTradesCustom.mqhにはOLAPTrades.mqhへのリンクがあり、上記のすべてをプロジェクトに含めることができます。
#include <OLAP/OLAPTradesCustom.mqh> // internally includes OLAPTrades.mqh #include <OLAP/HTMLcube.mqh> #include <OLAP/CSVcube.mqh> #include <OLAP/GUI/OLAPGUI_trades.mqh> OLAPDialog<SELECTORS,ENUM_FIELDS> dialog(_defaultEngine); int OnInit() { if(ReportFile == "") { Print("Analyzing account history"); _defaultEngine.setAdapter(&_defaultHistoryAdapter); } else { if(StringFind(ReportFile, ".htm") > 0 && _defaultHTMLReportAdapter.load(ReportFile)) { _defaultEngine.setAdapter(&_defaultHTMLReportAdapter); } else if(StringFind(ReportFile, ".csv") > 0 && _defaultCSVReportAdapter.load(ReportFile)) { _defaultEngine.setAdapter(&_defaultCSVReportAdapter); } else { Print("Unknown file format: ", ReportFile); return INIT_PARAMETERS_INCORRECT; } } ... if(!dialog.Create(0, "OLAPGUI" + (ReportFile != "" ? " : " + ReportFile : ""), 0, 0, 0, 750, 560)) return INIT_FAILED; if(!dialog.Run()) return INIT_FAILED; return INIT_SUCCEEDED; }
更新された OLAPGUI.mq5 を起動し、いくつかのデータセクションをビルドして、適用されたアダプタとレコードタイプに対するカーネルの依存性を動的に有効にするための新しい原則が正しく動作することを確認してください。 変化の視覚効果もチェックしていきます。
記事2のスクリーンショットと下記の結果を比較してみてください。 以下は、各取引の「利益(Profit)」と「期間(Duration)」フィールドの依存性です。 ここで、X軸に沿った持続時間は、秒単位ではなく、現在のタイムフレームバー(ここではD1)で表されます。
継続時間に対する利益の依存性(現在の時間枠のバーでは、D1)
シンボルや曜日別の利益の内訳を見ると、ヒストグラムのバーがバラバラに広がっていて、正しいグリッドが表示されています。
シンボルと曜日別の利益
取引のロットサイズによる利益分析は以下のスクリーンショットの通りです。 記事2とは異なり、ログではなくX軸に直接ロット値が表示されます。
ロットサイズによる利益
最後のオプションは「シンボル・種類別の取引数」ですが、以前のバージョンではヒストグラムが重なっていたため、ラインを使用していましたが、今回は「シンボル・種類別の取引数」に変更しました。 その問題はもう関係ないでしょう。
シンボル・種類別の取引数(ヒストグラム)
取引報告書の分析に関連するすべての要素を考慮しました。 もう一つ特筆すべきことは、MQLプログラマーが利用できるようになった新しいデータソースで、内部テスター形式のtstファイルです。
標準テスターファイルの接続 (*.tst)
MetaTrader 5の開発者は最近、テスターが保存したファイル形式をオープンしました。 特に、HTMLレポートにエクスポートして初めて解析できた1パスのデータが、tstファイルから直接読み込めるようになりました。
ファイルの内部構造については、ここでは深くは触れません。 代わりに、tst ファイルを読み込むためのライブラリ fxsaberのSingleTesterCacheを使ってみましょう。 [ブラックボックス」ベースで利用することで、取引の記録の配列を簡単に手に入れることができます。 ライブラリーではTradeDealクラスで取引が提示されています。 取引一覧を取得するには、ライブラリを接続し、メインクラスのオブジェクト SINGLETESTERCACHE を作成し、'load' メソッドを使用して必要なファイルをロードします。
#include <fxsaber/SingleTesterCache/SingleTesterCache.mqh> ... SINGLETESTERCACHE SingleTesterCache; if(SingleTesterCache.Load(file)) { Print("Tester cache import: ", ArraySize(SingleTesterCache.Deals), " deals"); }
SingleTesterCache.Deals 配列には、すべてのディールが含まれます。 テスターに存在する各取引のデータも適切な分野で利用可能です。
取引に基づいてトレードポジションを生成するアルゴリズムは、HTMLレポートをインポートする場合と全く同じです。 良い OOP スタイルは、共通のコード部分を基底クラスに実装し、そこから HTMLReportAdapter と TesterReportAdapter を継承する必要があります。
レポートの共通の祖先は BaseReportAdapter クラスです (ファイル ReportCubeBase.mqh)。 このファイルを古い HTMLcube.mqh クラスとコンテキストで比較してみると、(新しいクラス名を除いて)ほとんど違いがないことがわかります。 目を引くのは、'load'メソッドのミニマルな内容です。 仮想スタブとして機能するようになりました。
virtual bool load(const string file) { reset(); TradeRecord::reset(); return false; }
子メソッドはこのメソッドをオーバーライドする必要があります。
'generate' メソッドのコードも変更されました。 このメソッドでは、ディールをポジションに変換します。 これで、このメソッドの最初に仮想的な空の "スタブ "であるfillDealsArrayが呼ばれるようになりました。
virtual bool fillDealsArray() = 0; int generate() { ... if(!fillDealsArray()) return 0; ... }
HTML レポートを操作するための既存のコードの一部は、HTMLReportAdapter クラスの新しい仮想メソッドに移動されました。 注意:HTMLReportAdapter クラス全体を以下に示します。 主なコード部分は基底クラスにあるので、ここでは2つの仮想メソッドを定義するだけです。
template<typename T> class HTMLReportAdapter: public BaseReportAdapter<T> { protected: IndexMap *data; virtual bool fillDealsArray() override { for(int i = 0; i < data.getSize(); ++i) { IndexMap *row = data[i]; if(CheckPointer(row) == POINTER_INVALID || row.getSize() != COLUMNS_COUNT) return false; //何かが壊れている。 string s = row[COLUMN_SYMBOL].get<string>(); StringTrimLeft(s); if(StringLen(s) > 0) // there is a symbol -> this is a deal { array << new Deal(row); } else if(row[COLUMN_TYPE].get<string>() == "balance") { string t = row[COLUMN_PROFIT].get<string>(); StringReplace(t, " ", ""); balance += StringToDouble(t); } } return true; } public: ~HTMLReportAdapter() { if(CheckPointer(data) == POINTER_DYNAMIC) delete data; } virtual bool load(const string file) override { BaseReportAdapter<T>::load(file); if(CheckPointer(data) == POINTER_DYNAMIC) delete data; data = NULL; if(StringFind(file, ".htm") > 0) { data = HTMLConverter::convertReport2Map(file, true); if(data != NULL) { size = generate(); Print(data.getSize(), " deals transferred to ", size, " trades"); } } return data != NULL; } };
両メソッドのコードは以前のバージョンから見慣れたもので、何も変更はありません。
それでは、新しい TesterReportAdapter アダプタの実装を見てみましょう。 まず、ReportCubeBase.mqhで定義されているDealクラスから派生したTesterDealクラスを追加する必要がありました(Dealは以前HTMLcube.mqhにあった古いクラスです)。 TesterDeal には TradeDeal パラメータを持つコンストラクタがあり、これは SingleTesterCache ライブラリのディールです。 また、TesterDeal は、type と deal の方向列挙を文字列に変換するためのいくつかのヘルパーメソッドを定義しています。
class TesterDeal. public Deal { public: TesterDeal(const TradeDeal &td) { time = (datetime)td.time_create + TimeShift; price = td.price_open; string t = dealType(td.action); type = t == "buy" ? +1 : (t == "sell" ? -1 : 0); t = dealDir(td.entry); direction = 0; if(StringFind(t, "in") > -1) ++direction; if(StringFind(t, "out") > -1) --direction; volume = (double)td.volume; profit = td.profit; deal = (long)td.deal; order = (long)td.order; comment = td.comment[]; symbol = td.symbol[]; commission = td.commission; swap = td.storage; } static string dealType(const ENUM_DEAL_TYPE type) { return type == DEAL_TYPE_BUY ? "buy" : (type == DEAL_TYPE_SELL ? "sell" : "balance"); } static string dealDir(const ENUM_DEAL_ENTRY entry) { string result = ""; if(entry == DEAL_ENTRY_IN) result += "in"; else if(entry == DEAL_ENTRY_OUT || entry == DEAL_ENTRY_OUT_BY) result += "out"; else if(entry == DEAL_ENTRY_INOUT) result += "in out"; return result; } };
TesterReportAdapter クラスには、'load' および fillDealsArray メソッドと、SingleTesterCache ライブラリのメインクラスである SINGLETESTERCACHE オブジェクトへのポインタが含まれています。 このオブジェクトは、リクエストにより tst ファイルをロードします。 成功した場合、このメソッドは、fillDealsArray 配列の操作に基づいて Deals 配列を埋めます。
template<typename T> class TesterReportAdapter: public BaseReportAdapter<T> { protected: SINGLETESTERCACHE *ptrSingleTesterCache; virtual bool fillDealsArray() override { for(int i = 0; i < ArraySize(ptrSingleTesterCache.Deals); i++) { if(TesterDeal::dealType(ptrSingleTesterCache.Deals[i].action) == "balance") { balance += ptrSingleTesterCache.Deals[i].profit; } else { array << new TesterDeal(ptrSingleTesterCache.Deals[i]); } } return true; } public: ~TesterReportAdapter() { if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) delete ptrSingleTesterCache; } virtual bool load(const string file) override { if(StringFind(file, ".tst") > 0) { // default cleanup BaseReportAdapter<T>::load(file); // specific cleanup if(CheckPointer(ptrSingleTesterCache) == POINTER_DYNAMIC) delete ptrSingleTesterCache; ptrSingleTesterCache = new SINGLETESTERCACHE(); if(!ptrSingleTesterCache.Load(file)) { delete ptrSingleTesterCache; ptrSingleTesterCache = NULL; return false; } size = generate(); Print("Tester cache import: ", size, " trades from ", ArraySize(ptrSingleTesterCache.Deals), " deals"); } return true; } }; TesterReportAdapter<RECORD_CLASS> _defaultTSTReportAdapter;
RECORD_CLASS テンプレート・タイプのデフォルト・アダプタ・インスタンスが最後に作成されます。 私たちのプロジェクトには、CustomTradeRecordカスタムレコードクラスを定義するOLAPTradesCustom.mqhファイルが含まれています。 このファイルでは、RECORD_CLASSマクロとしてプリプロセッサ指令によってクラスが定義されています。 このように、新しいアダプタがプロジェクトに接続され、ユーザが入力で tst ファイルを指定するとすぐに、アダプタは CustomTradeRecord クラスオブジェクトの生成を開始し、そのオブジェクトには MFE と MAE のカスタムフィールドが自動的に生成されます。
新しいアダプタがどのようにタスクを実行するかを見てみましょう。 以下は、tstファイルからのシンボル別のバランスカーブの例です。
シンボルによるバランス曲線
行が途切れることがないことに注意してください。 「プログレッシブ」アグリゲータ(累積)でする場合、最初のセレクタは常にレコードのシリアル番号(またはインデックス)でなければなりません。
OLAP分析アプリケーション領域としてのテスター最適化レポート
単一のテストファイルに加えて、MetaQuotesでは最適化キャッシュを使ってoptファイルにアクセスできるようになりました。 このようなファイルは TesterCache ライブラリ (fxsaber で作成されたもの) を使って読み込むことができます。 このライブラリをベースに、最適化結果のOLAP分析のためのアプリケーション・レイヤーを簡単に作成することができます。 これに必要なもの:各最適化パスのデータを格納するフィールドを持つレコードクラス、アダプタ、セレクタ(オプション)。 他のアプリケーション領域のためのコンポーネントの実装を持っているので、それらをガイド(計画)として使用することができます。 さらに、グラフィカルなインターフェイスを追加します(ほぼすべての準備ができています、我々は設定を変更するだけです)。
OLAPOpts.mqhファイルが作成され、その目的はOLAPTrades.mqhに似ています。 TesterCache.mqhヘッダファイルが追加されます。
#include <fxsaber/TesterCache/TesterCache.mqh>
オプティマイザの全フィールドを含む列挙を定義します。 ExpTradeSummary 構造体のフィールドを使用しました(fxsaber/TesterCache/ExpTradeSummary.mqh にあり、ファイルは自動的にライブラリに接続されます)。
enum OPT_CACHE_RECORD_FIELDS { FIELD_NONE, FIELD_INDEX, FIELD_PASS, FIELD_DEPOSIT, FIELD_WITHDRAWAL, FIELD_PROFIT, FIELD_GROSS_PROFIT, FIELD_GROSS_LOSS, FIELD_MAX_TRADE_PROFIT, FIELD_MAX_TRADE_LOSS, FIELD_LONGEST_SERIAL_PROFIT, FIELD_MAX_SERIAL_PROFIT, FIELD_LONGEST_SERIAL_LOSS, FIELD_MAX_SERIAL_LOSS, FIELD_MIN_BALANCE, FIELD_MAX_DRAWDOWN, FIELD_MAX_DRAWDOWN_PCT, FIELD_REL_DRAWDOWN, FIELD_REL_DRAWDOWN_PCT, FIELD_MIN_EQUITY, FIELD_MAX_DRAWDOWN_EQ, FIELD_MAX_DRAWDOWN_PCT_EQ, FIELD_REL_DRAWDOWN_EQ, FIELD_REL_DRAWDOWN_PCT_EQ, FIELD_EXPECTED_PAYOFF, FIELD_PROFIT_FACTOR, FIELD_RECOVERY_FACTOR, FIELD_SHARPE_RATIO, FIELD_MARGIN_LEVEL, FIELD_CUSTOM_FITNESS, FIELD_DEALS, FIELD_TRADES, FIELD_PROFIT_TRADES, FIELD_LOSS_TRADES, FIELD_LONG_TRADES, FIELD_SHORT_TRADES, FIELD_WIN_LONG_TRADES, FIELD_WIN_SHORT_TRADES, FIELD_LONGEST_WIN_CHAIN, FIELD_MAX_PROFIT_CHAIN, FIELD_LONGEST_LOSS_CHAIN, FIELD_MAX_LOSS_CHAIN, FIELD_AVERAGE_SERIAL_WIN_TRADES, FIELD_AVERAGE_SERIAL_LOSS_TRADES }; #define OPT_CACHE_RECORD_FIELDS_LAST (FIELD_AVERAGE_SERIAL_LOSS_TRADES + 1)
構造には、利益、バランスとドローダウン、売買操作の数、シャープ率などのすべての通常の変数があります。 追加したフィールドは、FIELD_INDEX:レコードインデックスのみです。 構造体のフィールドには、long、double、intという異なる型があります。 これらはすべてRecordから派生したOptCacheRecordレコードクラスに追加され、そのダブルタイプの配列に格納されます。
このライブラリは、特別なOptCacheRecordInternal構造体を介してアクセスされます。
struct OptCacheRecordInternal { ExpTradeSummary summary; MqlParam params[][5]; // [][name, current, low, step, high]。 };
各テスターパスは、性能変数だけでなく、特定の入力パラメータのセットに関連付けられていることも特徴です。 この構造体では、ExpTradeSummaryの後に入力パラメータがMqlParam配列として追加されます。 この構造が手元にあれば、オプティマイザー形式でデータを詰め込んだOptCacheRecordクラスを簡単に書くことができます。
class OptCacheRecord: public Record { protected: static int counter; // number of passes void fillByTesterPass(const OptCacheRecordInternal &internal) { const ExpTradeSummary record = internal.summary; set(FIELD_INDEX, counter++); set(FIELD_PASS, record.Pass); set(FIELD_DEPOSIT, record.initial_deposit); set(FIELD_WITHDRAWAL, record.withdrawal); set(FIELD_PROFIT, record.profit); set(FIELD_GROSS_PROFIT, record.grossprofit); set(FIELD_GROSS_LOSS, record.grossloss); set(FIELD_MAX_TRADE_PROFIT, record.maxprofit); set(FIELD_MAX_TRADE_LOSS, record.minprofit); set(FIELD_LONGEST_SERIAL_PROFIT, record.conprofitmax); set(FIELD_MAX_SERIAL_PROFIT, record.maxconprofit); set(FIELD_LONGEST_SERIAL_LOSS, record.conlossmax); set(FIELD_MAX_SERIAL_LOSS, record.maxconloss); set(FIELD_MIN_BALANCE, record.balance_min); set(FIELD_MAX_DRAWDOWN, record.maxdrawdown); set(FIELD_MAX_DRAWDOWN_PCT, record.drawdownpercent); set(FIELD_REL_DRAWDOWN, record.reldrawdown); set(FIELD_REL_DRAWDOWN_PCT, record.reldrawdownpercent); set(FIELD_MIN_EQUITY, record.equity_min); set(FIELD_MAX_DRAWDOWN_EQ, record.maxdrawdown_e); set(FIELD_MAX_DRAWDOWN_PCT_EQ, record.drawdownpercent_e); set(FIELD_REL_DRAWDOWN_EQ, record.reldrawdown_e); set(FIELD_REL_DRAWDOWN_PCT_EQ, record.reldrawdownpercnt_e); set(FIELD_EXPECTED_PAYOFF, record.expected_payoff); set(FIELD_PROFIT_FACTOR, record.profit_factor); set(FIELD_RECOVERY_FACTOR, record.recovery_factor); set(FIELD_SHARPE_RATIO, record.sharpe_ratio); set(FIELD_MARGIN_LEVEL, record.margin_level); set(FIELD_CUSTOM_FITNESS, record.custom_fitness); set(FIELD_DEALS, record.deals); set(FIELD_TRADES, record.trades); set(FIELD_PROFIT_TRADES, record.profittrades); set(FIELD_LOSS_TRADES, record.losstrades); set(FIELD_LONG_TRADES, record.longtrades); set(FIELD_SHORT_TRADES, record.shorttrades); set(FIELD_WIN_LONG_TRADES, record.winlongtrades); set(FIELD_WIN_SHORT_TRADES, record.winshorttrades); set(FIELD_LONGEST_WIN_CHAIN, record.conprofitmax_trades); set(FIELD_MAX_PROFIT_CHAIN, record.maxconprofit_trades); set(FIELD_LONGEST_LOSS_CHAIN, record.conlossmax_trades); set(FIELD_MAX_LOSS_CHAIN, record.maxconloss_trades); set(FIELD_AVERAGE_SERIAL_WIN_TRADES, record.avgconwinners); set(FIELD_AVERAGE_SERIAL_LOSS_TRADES, record.avgconloosers); const int n = ArrayRange(internal.params, 0); for(int i = 0; i < n; i++) { set(OPT_CACHE_RECORD_FIELDS_LAST + i, internal.params[i][PARAM_VALUE].double_value); } } public: OptCacheRecord(const int customFields = 0): Record(OPT_CACHE_RECORD_FIELDS_LAST + customFields) { } OptCacheRecord(const OptCacheRecordInternal &record, const int customFields = 0): Record(OPT_CACHE_RECORD_FIELDS_LAST + customFields) { fillByTesterPass(record); } static int getRecordCount() { return counter; } static void reset() { counter = 0; } }; static int OptCacheRecord::counter = 0;
fillByTesterPassメソッドは、列挙要素とExpTradeSummaryフィールドの対応を明確に示しています。 このコンストラクタは、パラメータとして生成された OptCacheRecordInternal 構造体を受け取ります。
TesterCache ライブラリと OLAP の間に介在するのは、専用のデータアダプタです。 このアダプタは、OptCacheRecord レコードを生成します。
template<typename T> class OptCacheDataAdapter: public DataAdapter { private: int size; int cursor; int paramCount; string paramNames[]; TESTERCACHE<ExpTradeSummary> Cache;
「size」フィールド - レコードの総数、「cursor」フィールド - キャッシュ内の現在のレコードの数、「paramCount」フィールド - 最適化パラメータの数。 パラメータの名前は paramNames 配列に格納されます。 TESTERCACHE<ExpTradeSummary>型のCache変数は、TesterCacheライブラリのワーキングオブジェクトです。
初期状態では、最適化キャッシュが初期化され、リセット、ロード、カスタマイズのメソッドで読み込まれます。
void customize() { size = (int)Cache.Header.passes_passed; paramCount = (int)Cache.Header.opt_params_total; const int n = ArraySize(Cache.Inputs); ArrayResize(paramNames, n); int k = 0; for(int i = 0; i < n; i++) { if(Cache.Inputs[i].flag) { paramNames[k++] = Cache.Inputs[i].name[]; } } if(k > 0) { ArrayResize(paramNames, k); Print("Optimized Parameters (", paramCount, " of ", n, "):"); ArrayPrint(paramNames); } } public: OptCacheDataAdapter() { reset(); } void load(const string optName) { if(Cache.Load(optName)) { customize(); reset(); } else { cursor = -1; } } virtual void reset() override { cursor = 0; if(Cache.Header.version == 0) return; T::reset(); } virtual int getFieldCount() const override { return OPT_CACHE_RECORD_FIELDS_LAST; }
opt ファイルは、ライブラリの Cache.Load メソッドが呼び出される load メソッドでロードされます。 成功した場合、Expert Advisorのパラメータがヘッダーから選択されます(ヘルパーメソッド'customize'で)。 reset' メソッドは現在のレコード番号をリセットし、次回 getNext が OLAP カーネルのすべてのレコードをイテレートするときにインクリメントされます。 ここでは、OptCacheRecordInternal 構造体に最適化キャッシュからのデータが格納されます。 その上にテンプレート・パラメータ・クラス(T)の新しいレコードが作成されます。
virtual Record *getNext() override { if(cursor < size) { OptCacheRecordInternal internal; internal.summary = Cache[cursor]; Cache.GetInputs(cursor, internal.params); cursor++; return new T(internal, paramCount); } return NULL; } ... };
テンプレートパラメータは前述のOptCacheRecordクラスです。
#ifndef RECORD_CLASS #define RECORD_CLASS OptCacheRecord #endif OptCacheDataAdapter<RECORD_CLASS> _defaultOptCacheAdapter;
また、OLAPカーネルの他の部分で使用されるRECORD_CLASSと同様にマクロとして定義されています。 以下は、すべてのサポートされている以前のデータアダプタと新しいデータアダプタを持つクラスの図です。
データアダプタクラスの図
では、最適化結果の分析に役立つセレクタの種類を決める必要があります。 最初の最小オプションとして、以下の列挙が提案されています。
enum OPT_CACHE_SELECTORS { SELECTOR_NONE, // none. SELECTOR_INDEX, // ordinal number /* all the next require a field as parameter */ SELECTOR_SCALAR, // scalar(field) SELECTOR_QUANTS, // quants(field) SELECTOR_FILTER // filter(field)。 };
すべてのレコードフィールドは、取引統計とEAパラメータの2つのタイプのいずれかに属します。 便利なソリューションは、パラメータをテストされた値に正確に対応するセルに整理することです。 例えば、パラメータに 10 個の値が使用された MA 期間が含まれている場合、OLAP キューブにはこのパラメータ用に 10 個のセルが必要です。 これは、ゼロの「バスケット」サイズを持つ量子化セレクタ(SELECTOR_QUANTS)によって行われます。
可変フィールドの場合は、セルは一定のステップで設定した方が良いでしょう。 例えば、100単位のステップで利益別のパスの分布を見ることができます。 これもまた、量子化セレクタで行うことができます。 「バスケット」のサイズは必要なステップに設定する必要がありますが。 他に追加されたセレクタは、他のサービス機能を実行します。 例えば、累計を計算する際にはSELECTOR_INDEXを使用します。 SELECTOR_SCALARでは、選択範囲全体の特徴として1つの数字を受け取ることができます。
セレクタ・クラスは準備ができており、OLAPCommon.mqhファイルにあります。
これらのセレクタタイプに対して、OLAPEngineクラスのテンプレート特化のcreateSelectorメソッドを書いてみましょう。
class OLAPEngineOptCache: public OLAPEngine<OPT_CACHE_SELECTORS,OPT_CACHE_RECORD_FIELDS> { protected: virtual Selector<OPT_CACHE_RECORD_FIELDS> *createSelector(const OPT_CACHE_SELECTORS selector, const OPT_CACHE_RECORD_FIELDS field) override { const int standard = adapter.getFieldCount(); switch(selector) { case SELECTOR_INDEX: return new SerialNumberSelector<OPT_CACHE_RECORD_FIELDS,OptCacheRecord>(FIELD_INDEX); case SELECTOR_SCALAR: return new OptCacheSelector(field); case SELECTOR_QUANTS: return field != FIELD_NONE ? new QuantizationSelector<OPT_CACHE_RECORD_FIELDS>(field, (int)field < standard ? quantGranularity : 0) : NULL; } return NULL; } public: OLAPEngineOptCache()。OLAPEngine() {}。 OLAPEngineOptCache(DataAdapter *ptr). OLAPEngine(ptr) {} OLAPEngine(ptr) }; OLAPEngineOptCache _defaultEngine;
量子化セレクタを作成する際には、フィールドが「標準」(標準テスター統計情報を格納)かカスタム(Expert Advisorパラメータ)かに応じて、バスケットサイズをquantGranularity変数に設定するか、ゼロに設定します。 quantGranularityフィールドはOLAPEngineの基底クラスで記述されています。 これは、エンジンのコンストラクタで設定するか、後で setQuant メソッドを使用して設定することができます。
OptCacheSelectorは、BaseSelector<OPT_CACHE_RECORD_FIELDS>のシンプルなラッパーです。
テスター最適化レポートを分析するためのグラフィカルなインターフェース
最適化結果の分析は、トレーディングレポートで使用されていたのと同じインターフェイスを使用して可視化されます。 実際にOLAPGUI_Trade.mqhファイルを新しい名前のOLAPGUI_Opts.mqhにコピーして、微調整を行うことができます。 この調整は、仮想メソッドの「セットアップ」と「プロセス」に関するものです。
template<typename S, typename F> void OLAPDialog::setup() override { static const string _settings[ALGO_NUMBER][MAX_ALGO_CHOICES] = { // enum AGGREGATORS 1:1, default - sum。 {"sum", "average", "max", "min", "count", "profit factor", "progressive total", "identity", "variance"}, // enum RECORD_FIELDS 1:1, default - profit amount {""}, // enum SORT_BY, default - none {"none", "value ascending", "value descending", "label ascending", "label descending"}, // enum ENUM_CURVE_TYPE partially, default - points {"points", "lines", "points/lines", "steps", "histogram"} }; static const int _defaults[ALGO_NUMBER] = {0, FIELD_PROFIT, 0, 0}; const int std = EnumSize<F,PackedEnum>(0); const int fields = std + customFieldCount; ArrayResize(settings, fields); ArrayResize(selectors, fields); selectors[0] = "(<selector>/field)"; // none selectors[1] = "<serial number>"; // the only selector, which can be chosen explicitly, it corresponds to the 'index' field for(int i = 0; i < ALGO_NUMBER; i++) { if(i == 1) // pure fields { for(int j = 0; j < fields; j++) { settings[j][i] = j < std ? Record::legendFromEnum((F)j) : customFields[j - std]; } } else { for(int j = 0; j < MAX_ALGO_CHOICES; j++) { settings[j][i] = _settings[i][j]; } } } for(int j = 2; j < fields; j++) // 0-th is none { selectors[j] = j < std ? Record::legendFromEnum((F)j) : customFields[j - std]; } ArrayCopy(defaults, _defaults); }
どのフィールドも同じフィールドの量子化セレクタを意味するので、フィールドとセレクタの違いはほとんどありません。 つまり、量子化セレクタがすべてを担っているのです。 以前のレポートや気配値に関連するプロジェクトでは、個別のフィールド(収益性セレクタ、曜日セレクタ、ローソク足タイプセレクタなど)に特別なセレクタを使用していました。
フィールド(X、Y、Z 軸のセレクタとしても機能する)を持つドロップダウン リストのすべての要素の名前は、OPT_CACHE_RECORD_FIELDS 列挙要素の名前と、EA パラメータの customFields 配列から作成されます。 先ほど、OLAPDialogBase 基底クラスの setCustomFields メソッドを考えましたが、これはアダプタからの名前で customFields 配列を生成します。 これら2つのメソッドは、OLAPGUI_Opts.mq5分析EAのコード内で一緒にリンクすることができます(下記参照)。
標準フィールドは、列挙要素の順に表示されます。 標準フィールドの後には、最適化中のEAのパラメータに関連するカスタムフィールドが続きます。 カ ス タ ム フ ィ ール ド の順序は、 opt フ ァ イ ル内のパ ラ メ タ ーの順序に対応 し てい ます。
制御状態の読み込みと解析プロセスの起動は「プロセス」方式で行われます。
template<typename S, typename F> int OLAPDialog::process() override { SELECTORS Selectors[4]; ENUM_FIELDS Fields[4]; AGGREGATORS at = (AGGREGATORS)m_algo[0].Value(); ENUM_FIELDS af = (ENUM_FIELDS)(AGGREGATORS)m_algo[1].Value(); SORT_BY sb = (SORT_BY)m_algo[2].Value(); if(at == AGGREGATOR_IDENTITY) { Print("Sorting is disabled for Identity"); sb = SORT_BY_NONE; } ArrayInitialize(Selectors, SELECTOR_NONE); ArrayInitialize(Fields, FIELD_NONE); int matches[2] = { SELECTOR_NONE, SELECTOR_INDEX }; for(int i = 0; i < AXES_NUMBER; i++) { if(!m_axis[i].IsVisible()) continue; int v = (int)m_axis[i].Value(); if(v < 2) // selectors (which is specialized for a field already) { Selectors[i] = (SELECTORS)matches[v]; } else // pure fields { Selectors[i] = at == AGGREGATOR_IDENTITY ? SELECTOR_SCALAR : SELECTOR_QUANTS; Fields[i] = (ENUM_FIELDS)(v); } } m_plot.CurvesRemoveAll(); if(at == AGGREGATOR_IDENTITY) af = FIELD_NONE; m_plot.InitXAxis(at != AGGREGATOR_PROGRESSIVE ? new AxisCustomizer(m_plot.getGraphic(), false) : NULL); m_plot.InitYAxis(at == AGGREGATOR_IDENTITY ? new AxisCustomizer(m_plot.getGraphic(), true) : NULL); m_button_ok.Text("Processing..."); return olapcore.process(Selectors, Fields, at, af, olapdisplay, sb); }
最適化レポートのOLAP分析と可視化
メタトレーダー・テスターは、最適化結果をテストするための様々な方法を提供しますが、標準セットに限定されています。 作成されたOLAPエンジンを使用して、利用可能なセットを拡張することができます。 例えば、ビルトインの2D可視化では、常に2つのEAパラメータの組み合わせの最大利益値が表示されますが、通常は2つ以上のパラメータが存在します。 表面上の各ポイントでは、軸上に表示されない他のパラメータの異なる組み合わせの結果が表示されます。 これは、表示されたパラメータの特定の値の収益性を過度に楽観的に評価することにつながる可能性があります。 平均的な利益値とその値の範囲から、よりバランスのとれた評価を得ることができます。 この評価は、他の評価の中でも、OLAPを用いて行うことができる。
最適化レポートのOLAP分析は、新しいノントレーディング・エキスパート・アドバイザーOLAPGUI_Opts.mq5によって実行されます。 その構造はOLAPGUI.mq5と完全に同じです。 さらに、指定されたファイルタイプに応じてアダプタを接続する必要がないので、よりシンプルになります。 これは常に最適化の結果を得るためのオプトファイルとなります。
入力にファイル名を指定し、統計パラメータの量子化ステップを指定します。
input string OptFileName = "Integrity.opt"; input uint QuantGranularity = 0;
なお、量子化ステップはフィールドごとに独立していることが望ましい。 しかし、GUIからは値が変わらないのに、今は1回だけ設定しています。 この欠陥には、さらなる改善の余地があります。 ステップ値は、あるフィールドには適していても、別のフィールドには適していない場合があることを覚えておいてください(大きすぎても小さすぎても構いません)。 したがって、OLAPインターフェースのドロップダウンリストからフィールドを選択する前に、必要に応じてEAプロパティダイアログを呼び出して量子を変更します。
すべてのクラスでヘッダーファイルをインクルードした後、ダイアログのインスタンスを作成し、それをOLAPエンジンにバインドします。
#include <OLAP/OLAPOpts.mqh> #include <OLAP/GUI/OLAPGUI_Opts.mqh> OLAPDialog<SELECTORS,ENUM_FIELDS> dialog(_defaultEngine);
OnInit ハンドラで、新しいアダプタをエンジンに接続し、ファイルからのデータ・ロードを開始します。
int OnInit() { _defaultEngine.setAdapter(&_defaultOptCacheAdapter); _defaultEngine.setShortTitles(true); _defaultEngine.setQuant(QuantGranularity); _defaultOptCacheAdapter.load(OptFileName); dialog.setCustomFields(_defaultOptCacheAdapter); if(!dialog.Create(0, "OLAPGUI" + (OptFileName != "" ? " : " + OptFileName : ""), 0, 0, 0, 750, 560)) return INIT_FAILED; if(!dialog.Run()) return INIT_FAILED; return INIT_SUCCEEDED; }
ここでは,QuantGranularity = 100のIntegrity.optファイルの解析セクションを作成してみましょう。 最適化の際には、以下の3つのパラメータを選択しました。PricePeriod、Momentum、Sigmaです。
下のスクリーンショットは、PricePeriodの値で利益を分解したものです。
EAパラメータ値による平均利益
その結果、分散せずにほとんど情報が得られません。
EAパラメータ値による利益分散
これら2つのヒストグラムを比較することで、どのパラメータ値で分散が平均値を超えていないか、つまり損益分岐点を推定することができます。 より良い解決策は、同じチャート上で自動的に比較を行うことです。 しかし、これはこの記事の範疇を超えています。
あるいは、このパラメータの収益性(全パスの損益率)を見てみましょう。
EAパラメータ値に応じたストラテジー利益率
もう1つの厄介な評価方法は、利益レベル別の平均期間サイズを100単位で評価することです(ステップはQuantGranularityの入力パラメータで設定します)。
様々な範囲での利益発生パラメータの平均値(100台単位)
下の図は、期間による利益の分布を示しています(すべてのパスは「アイデンティティ」アグリゲータを使用して示されています)。
全ポジションの利益対パラメータ値
モメンタムとシグマによる利益の内訳は以下のようになっています。
2つのパラメータによる平均利益
レベル別の利益の一般的な分布を100刻みで表示するには、X軸に沿った統計から「利益」フィールドを選択し、「カウント」アグリゲータを選択します。
100台単位でのレンジ別全利益配分
「アイデンティティ」アグリゲータを使用することで、利益を得るためのトレード数の影響を評価することができます。 一般的に、このアグリゲータは、他の多くの依存関係を視覚的に評価することを可能にします。
利益対取引数
結論
今回は、MQL OLAPの範囲を広げてみました。 今では、シングルパスや最適化からのテスターレポートの分析にも使えるようになりました。 クラスの構造が更新されたことで、OLAP機能のさらなる拡張が可能になりました。 提案された実装は理想的なものではなく、大幅に改善される可能性があります(特に、3D可視化、インタラクティブGUIにおけるフィルタリング設定の実装、異なる軸での量子化など)。 それにもかかわらず、それは最小限のスタートセットとして機能し、OLAPの世界をより簡単に知るのに役立ちます。 OLAP分析により、トレーダーは大量の生データを処理し、さらなる意思決定のための新たな知識を得ることができます。
ファイルを添付しました。
エキスパート
- OLAPRPRT.mq5 - アカウントの履歴を分析するためのエキスパートアドバイザー、HTMLやCSVレポート(第3条から更新されたファイル。
- OLAPQTS.mq5 - クオートを分析するためのエキスパートアドバイザー (第3条からの更新ファイル。GUIなし)
- OLAPGUI.mq5 - アカウントの履歴を分析するためのエキスパートアドバイザー、HTMLやCSV形式のレポート、TST標準のテスターファイル(記事2から更新されたファイル。)
- OLAPGUI_Opts.mq5 - 標準のOPTテスターファイルから最適化結果を解析するためのエキスパート・アドバイザー (新規, GUI)
インクルード
カーネル
- OLAP/OLAPCommon.mqh - OLAPクラスのメインヘッダファイル
- OLAP/OLAPTrades.mqh - 取引履歴のOLAP分析のための標準クラス
- OLAP/OLAPTradesCustom.mqh - 取引履歴のOLAP分析用カスタムクラス
- OLAP/OLAPQuotes.mqh - クオートのOLAP分析のためのクラス
- OLAP/OLAPOpts.mqh - Expert Advisorの最適化結果のOLAP分析用クラス
- OLAP/ReportCubeBase.mqh - 取引履歴のOLAP分析のための基本クラス
- OLAP/HTMLcube.mqh - 売買履歴のOLAP分析をHTML形式で行うためのクラス
- OLAP/CSVcube.mqh - 取引履歴のOLAP分析をCSV形式で行うためのクラス
- OLAP/TSTcube.mqh - TST 形式の取引履歴の OLAP 分析用クラス
- OLAP/PairArray.mqh - すべてのソートタイプをサポートするペアの配列 [value;name] のクラス.
- OLAP/GroupReportInputs.mqh - 取引レポートの分析のための入力パラメータのグループ
- MT4Bridge/MT4Orders.mqh - MetaTrader 4 および MetaTrader 5 のシングルスタイルの注文を扱うための MT4orders ライブラリ
- MT4Bridge/MT4Time.mqh - メタトレーダー4スタイルのデータ処理機能を実装した補助ヘッダーファイル
- Marketeer/IndexMap.mqh - キーとインデックスを組み合わせた配列を実装した補助ヘッダファイル
- Marketeer/Converter.mqh - データ型を変換するための補助ヘッダファイル
- Marketeer/GroupSettings.mqh - 入力パラメータのグループ設定を含む補助ヘッダファイルです。
- Marketeer/WebDataExtractor.mqh - HTML パーサー
- Marketeer/empty_strings.h - 空の HTML タグのリスト
- Marketeer/HTMLcolumns.mqh - HTMLレポートのカラムインデックスの定義
- Marketeer/RubbArray.mqh - "rubber" 配列を持つ補助ヘッダファイル
- Marketeer/CSVReader.mqh - CSVパーサ
- Marketeer/CSVcolumns.mqh - CSVレポートの列インデックスの定義
グラフィカルインターフェース
- OLAP/GUI/OLAPGUI.mqh - インタラクティブなウィンドウインターフェースの一般的な実装
- OLAP/GUI/OLAPGUI_Trades.mqh - トレーディングレポートの分析のためのグラフィカルインターフェースの特殊化
- OLAP/GUI/OLAPGUI_Opts.mqh - 最適化結果を解析するためのグラフィカル・インターフェースの特殊化
- Layouts/Box.mqh - コントロールのコンテナ
- Layouts/ComboBoxResizable.mqh - ダイナミックなサイズ変更が可能なドロップダウンコントロール
- Layouts/MaximizableAppDialog.mqh - ダイナミックなサイズ変更が可能なダイアログウィンドウ
- PairPlot/Plot.mqh - チャートグラフィックスを持つコントロールで、動的なサイズ変更をサポートする
- Layouts/res/expand2.bmp - ウィンドウの最大化ボタン
- Layouts/res/size6.bmp - ボタンのサイズ変更
- Layouts/res/size10.bmp - ボタンのサイズ変更
TypeToBytes
- TypeToBytes.mqh
SingleTesterCache
- fxsaber/SingleTesterCache/SingleTesterCache.mqh
- fxsaber/SingleTesterCache/SingleTestCacheHeader.mqh
- fxsaber/SingleTesterCache/String.mqh
- fxsaber/SingleTesterCache/ExpTradeSummaryExt.mqh
- fxsaber/SingleTesterCache/ExpTradeSummarySingle.mqh
- fxsaber/SingleTesterCache/TradeDeal.mqh
- fxsaber/SingleTesterCache/TradeOrder.mqh
- fxsaber/SingleTesterCache/TesterPositionProfit.mqh
- fxsaber/SingleTesterCache/TesterTradeState.mqh
TesterCache
- fxsaber/TesterCache/TesterCache.mqh
- fxsaber/TesterCache/TestCacheHeader.mqh
- fxsaber/TesterCache/String.mqh
- fxsaber/TesterCache/ExpTradeSummary.mqh
- fxsaber/TesterCache/TestCacheInput.mqh
- fxsaber/TesterCache/TestInputRange.mqh
- fxsaber/TesterCache/Mathematics.mqh
- fxsaber/TesterCache/TestCacheRecord.mqh
- fxsaber/TesterCache/TestCacheSymbolRecord.mqh
標準ライブラリパッチ
- Controls/Dialog.mqh
- Controls/ComboBox.mqh
ファイル
- 518562.history.csv
- Integrity.tst
- Integrity.opt
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/7656





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索