グラフィカルインタフェースを通して最適化の結果を処理する

Anatoli Kazharski | 21 5月, 2018


目次

概論

最適化結果の分析と処理についての話を展開していきます。前の記事で、MQL5アプリケーションのグラフィカルインタフェースを通じて、最適化の結果を視覚化する方法について書きました。今回は課題を難しくして、100の最良の最適化結果を選択して、グラフィカルインタフェースの表に表示します。 

また、別の記事でご紹介したマルチシンボルの残高グラフについてのテーマをさらに広げていきます。これらの2つの記事のアイデアを組み合わせて、ユーザーが最適化結果の表で列を選択しつつ、残高とドローダウンのマルチシンボルのグラフを別々に入手できるようにします。こうすることで、EAのパラメータを最適化した後、トレーダーが興味のある結果を迅速に選択して分析することができるようになります。

グラフィカルインタフェースの開発

テストEAのグラフィカルインタフェースは、以下の要素で構成されます。

  • コントロール要素のフォーム
  • 追加の要約情報を表示するためのステータスバー
  • グループ別にアイテムを分けるためのタブ
    • Frames
      • 最適化後の結果を再スクロールする時に、表示する残高結果をコントロールする為の入力フィールド
      • 結果をスクロールする時の遅延時間(ミリ秒単位)
      • 結果の再スクロールを開始するボタン
      • 指定した数の残高結果を表示する為のグラフ
      • すべての結果を表示するグラフ
    • Results
      • 最良の結果の表
    • Balance
      • 表で選択したマルチシンボルの残高結果を表示するためのグラフ
      • 表で選択したドローダウンの結果を表示するためのグラフ
  • フレームを再生するプロセスを実行するインジケータ

上記のリストにリストアップされている要素を作成するメソッドのコードは、別のファイルに入れられており、 MQLプログラムのクラスのファイルに接続します

//+------------------------------------------------------------------+
// |アプリケーションを作成するクラス                                     |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   // --- ウィンドウ
   CWindow           m_window1;
   // --- ステータスバー
   CStatusBar        m_status_bar;
   // --- タブ
   CTabs             m_tabs1;
   // --- 入力フィールド
   CTextEdit         m_curves_total;
   CTextEdit         m_sleep_ms;
   // --- ボタン
   CButton           m_reply_frames;
   // --- グラフ
   CGraph            m_graph1;
   CGraph            m_graph2;
   CGraph            m_graph3;
   CGraph            m_graph4;
   // --- 表
   CTable            m_table_param;
   // --- 実行インジケータ
   CProgressBar      m_progress_bar;
   //---
public:
   // --- グラフィカルインタフェースを作成する
   bool              CreateGUI(void);
   //---
private:
   // --- フォーム
   bool              CreateWindow(const string text);
   // --- ステータスバー
   bool              CreateStatusBar(const int x_gap,const int y_gap);
   // --- タブ
   bool              CreateTabs1(const int x_gap,const int y_gap);
   // --- 入力フィールド
   bool              CreateCurvesTotal(const int x_gap,const int y_gap,const string text);
   bool              CreateSleep(const int x_gap,const int y_gap,const string text);
   // --- ボタン
   bool              CreateReplyFrames(const int x_gap,const int y_gap,const string text);
   // --- グラフ
   bool              CreateGraph1(const int x_gap,const int y_gap);
   bool              CreateGraph2(const int x_gap,const int y_gap);
   bool              CreateGraph3(const int x_gap,const int y_gap);
   bool              CreateGraph4(const int x_gap,const int y_gap);
   // --- ボタン
   bool              CreateUpdateGraph(const int x_gap,const int y_gap,const string text);
   // --- 表
   bool              CreateMainTable(const int x_gap,const int y_gap);
   // --- 実行インジケータ
   bool              CreateProgressBar(const int x_gap,const int y_gap,const string text);
  };
//+------------------------------------------------------------------+
// |コントロール要素を作成するメソッド                                   |
//+------------------------------------------------------------------+
#include "CreateGUI.mqh"
//+------------------------------------------------------------------+

上記のように、表には100の最良の最適化の結果が表示されます(最大の利益合計)。最適化が始まる前にグラフィカルインタフェースが作成されるので、最初は表は空の状態です。ヘッダーの列とテキストの数は、最適化フレームを処理するクラスで決定されます。

表の作成は、次の一連の関数を使用して行います。

  • ヘッダーを表示する
  • ソート機能
  • 列の選択
  • 選択した列を固定する(選択を解除することはできません)
  • 列幅の手動変更
  • "ゼブラ"スタイルに書式を設定する

表を作成するコードは以下に記述します。表を2つ目のタブに固定するには、タブのインデックスを指定して、表のオブジェクトをタブのオブジェクトに移す必要があります。この場合、表にとっての主要なクラスは「タブ」要素となります。このように、タブ領域のサイズの変更時に、『表』要素のプロパティで指定された場合に、表のサイズは主要素に合わせて変化します。

//+------------------------------------------------------------------+
// |メインの表を作成します。                                 |
//+------------------------------------------------------------------+
bool CProgram::CreateMainTable(const int x_gap,const int y_gap)
  {
// ---ポインタをメインの要素に保存します
   m_table_param.MainPointer(m_tabs1);
// ---タブに固定します
   m_tabs1.AddToElementsArray(1,m_table_param);
// ---プロパティ
   m_table_param.TableSize(1,1);
   m_table_param.ShowHeaders(true);
   m_table_param.IsSortMode(true);
   m_table_param.SelectableRow(true);
   m_table_param.IsWithoutDeselect(true);
   m_table_param.ColumnResizeMode(true);
   m_table_param.IsZebraFormatRows(clrWhiteSmoke);
   m_table_param.AutoXResizeMode(true);
   m_table_param.AutoYResizeMode(true);
   m_table_param.AutoXResizeRightOffset(2);
   m_table_param.AutoYResizeBottomOffset(2);
// --- コントロール要素を作成します
   if(!m_table_param.CreateTable(x_gap,y_gap))
      return(false);
// --- オブジェクトグループの一般配列にオブジェクトを追加します
   CWndContainer::AddToElementsArray(0,m_table_param);
   return(true);
  }

最適化結果の保存

最適化結果を処理するためのCFrameGeneratorクラスを実装しました。METATRADER 5における取引戦略最適化の可視化の記事のバージョンを使用し、これを手直しして、必要なメソッドを追加します。フレームでは、総残高と総括統計だけでなく、各シンボルの残高とデポジットのドローダウンを別個に保存する必要があります。残高を保存するために、別の配列構造CSymbolBalanceを使用します。この配列構造には2つの目的があります。この配列には、後で汎用配列のフレームに引き渡されるデータが格納されます。そして最適化をした後で、データはフレームの配列から抽出され、マルチシンボルのグラフに表示する為に、この構造体の配列に戻されます。

// ---全シンボルの残高の配列
struct CSymbolBalance
  {
   double            m_data[];
  };
//+------------------------------------------------------------------+
// | 最適化結果を扱うクラス                                             |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   // --- 残高の構造体
   CSymbolBalance    m_symbols_balance[];
  };

文字列パラメータとして、セパレータ '、'を介して、シンボルのリストがフレームに渡されます。当初より文字列配列のフルレポートとして、データをフレームに保存する予定でしたが、この時点では、文字列配列をフレームに渡すことはできません。FrameAdd()関数への引き渡しを試みると、文字列型の配列はコンパイル時に、『文字列配列とオブジェクトを含む構造体は使用できません』というエラーメッセージを出します。

string arrays and structures containing objects are not allowed

もう1つの方法は、レポートをファイルに保存し、それをフレームに引き渡すことですが、この方法も私たちには合いません。あまりにも頻繁にハードディスクに結果を記録する必要が出てきてしまいます。

そこで私は、必要なすべてのデータを1つの配列に集めて、それからフレームパラメータに含まれるキーに基づいて抽出することにしました。この配列の冒頭には、統計的指標が入ります。次に、総残高のデータ、その次に各シンボルの残高が別々に入ります。最後に、2つの軸のドローダウンのデータが別々に入ります。 

下の図は、配列内のデータが格納される順序を示しています。簡潔にするために、2つのシンボルの場合を紹介しています。

 


図 1. 1 - 配列内のデータ配列の順序。

上記のように、この配列内で各範囲のインデックスを調べるには、キーが必要になります。統計指標の数は一定であり、事前に決定されています。この場合、最適化後にこの結果データにアクセスできるようにする為に、表に5つのインジケータとパス番号を表示します。

// ---統計指標の数
#define STAT_TOTAL 6

残高データの数と、シンボルごとの合計とは同じ数字になります。この値はFrameAdd()関数にダブルパラメータとして送られます。テストに参加したシンボルを特定するために、OnTester()関数のパスごとに取引の履歴の中からそのシンボルを特定をします。この情報はFrameAdd()関数に文字列パラメータとして送られます。

::FrameAdd(m_report_symbols,1,data_count,stat_data);

文字列パラメーターで指定された文字のシーケンスは、配列内のデータのシーケンスと同じです。このように、これらのすべてのパラメータを持つと、何も混乱を起こすことなく、配列に格納されたすべてのデータを抽出できます。 

取引履歴からシンボルを特定する為のCFrameGenerator :: GetHistorySymbols()メソッドは、下のコードのリストで紹介します。

#include <Trade\DealInfo.mqh>
//+------------------------------------------------------------------+
// | 最適化結果を扱うクラス                                             |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   // --- 取引の処理
   CDealInfo         m_deal_info;
   // --- レポートからのシンボル
   string            m_report_symbols;
   //---
private:
   // --- アカウント履歴からシンボルを取得し、その数を返します
   int               GetHistorySymbols(void);
  };
//+------------------------------------------------------------------+
// | アカウント履歴からシンボルを取得し、その数を返します                   |
//+------------------------------------------------------------------+
int CFrameGenerator::GetHistorySymbols(void)
  {
// --- 初めてのサイクルを行い、取引されたシンボルを取得します
   int deals_total=::HistoryDealsTotal();
   for(int i=0; i<deals_total; i++)
     {
      // --- 取引のチケットを取得する
      if(!m_deal_info.SelectByIndex(i))
         continue;
      // --- シンボル名がある場合
      if(m_deal_info.Symbol()=="")
         continue;
      // --- そのような行がまだない場合は、それを追加します
      if(::StringFind(m_report_symbols,m_deal_info.Symbol(),0)==-1)
         ::StringAdd(m_report_symbols,(m_report_symbols=="")? m_deal_info.Symbol() : ","+m_deal_info.Symbol());
     }
// --- セパレータで文字列の要素を取得する
   ushort u_sep=::StringGetCharacter(",",0);
   int symbols_total=::StringSplit(m_report_symbols,u_sep,m_symbols_name);
// --- シンボル数を返します
   return(symbols_total);
  }

取引履歴に複数のシンボルがあることが判明した場合は、もう1つの要素が配列のサイズに設定されます。最初の要素は総残高のために保存されています。 

// --- 残高の配列のサイズを、総残高用にシンボルの数+1で設定します
   ::ArrayResize(m_symbols_balance,(m_symbols_total>1)? m_symbols_total+1 : 1);

取引履歴からのデータを別々の配列に格納した後、このデータを1つの共通の配列に配置する必要があります。この処理の為にCFrameGenerator::CopyDataToMainArray()メソッドを使用します。ここで、順に追加されるデータの数で配列全体を増加させ、最後の反復ではドローダウンのデータをコピーします。

class CFrameGenerator
  {
private:
   //--- 結果残高
   double            m_balances[];
   //---
private:
   // --- 残高データをメイン配列にコピーする
   void              CopyDataToMainArray(void);
  };
//+------------------------------------------------------------------+
// | 残高データをメイン配列にコピーする
//+------------------------------------------------------------------+
void CFrameGenerator::CopyDataToMainArray(void)
  {
// --- 残高曲線の数
   int balances_total=::ArraySize(m_symbols_balance);
// --- 残高配列のサイズ
   int data_total=::ArraySize(m_symbols_balance[0].m_data);
// --- 共有配列にデータを書き込む
   for(int i=0; i<=balances_total; i++)
     {
      // --- 現在の残高サイズ
      int array_size=::ArraySize(m_balances);
      // --- 残高を配列にコピーする
      if(i<balances_total)
        {
         // --- 残高を配列にコピーする
         ::ArrayResize(m_balances,array_size+data_total);
         ::ArrayCopy(m_balances,m_symbols_balance[i].m_data,array_size);
        }
      // --- ドローダウンを配列にコピーする
      else
        {
         data_total=::ArraySize(m_dd_x);
         ::ArrayResize(m_balances,array_size+(data_total*2));
         ::ArrayCopy(m_balances,m_dd_x,array_size);
         ::ArrayCopy(m_balances,m_dd_y,array_size+data_total);
        }
     }
  }

統計指標は、CFrameGenerator :: GetStatData()メソッドの共通配列の先頭に追加されます。配列はこのメソッドに参照渡しされ、最終的にフレームに格納されます。ここに残高データの配列のサイズと統計指標の数が設定されます。残高データは、統計指標の範囲内の最後のインデックスから配置されます。 

class CFrameGenerator
  {
private:
   // --- 統計データを取得する
   void              GetStatData(double &dst_array[],double on_tester_value);
  };
//+------------------------------------------------------------------+
// | 統計データを取得する                                |
//+------------------------------------------------------------------+
void CFrameGenerator::GetStatData(double &dst_array[],double on_tester_value)
  {
// ---配列をコピーする
   ::ArrayResize(dst_array,::ArraySize(m_balances)+STAT_TOTAL);
   ::ArrayCopy(dst_array,m_balances,STAT_TOTAL,0);
// --- 配列の最初の値(STAT_TOTAL)にテスト結果を入力します
   dst_array [0] =0; // パス番号
   dst_array [1] = on_tester_value; // カスタム最適化基準の値
   dst_array [2] = ::TesterStatisticsSTAT_PROFIT); // 純利益
   dst_array [3] = ::TesterStatisticsSTAT_TRADES); // 取引数
   dst_array [4] = ::TesterStatisticsSTAT_EQUITY_DDREL_PERCENT); // 最大ドローダウン率
   dst_array [5] = ::TesterStatisticsSTAT_RECOVERY_FACTOR);// 回復係数
  }

上記の動作は、メインプログラムファイルのOnTester()関数で呼び出されるCFrameGenerator::OnTesterEvent()メソッドで実行されます。 

//+------------------------------------------------------------------+
// | 残高値の配列を準備し、これをフレームに送信します                      |
// | この関数は、OnTester()のEAで呼び出す必要があります                   |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterEvent(const double on_tester_value)
  {
// --- 残高データを取得する
   int data_count=GetBalanceData();
// --- フレームにデータを送信する配列
   double stat_data[];
   GetStatData(stat_data,on_tester_value);
// --- データを含むフレームを作成し、それを端末に送信する
   if(!::FrameAdd(m_report_symbols,1,data_count,stat_data))
      ::Print(__FUNCTION__," > Frame add error: ",::GetLastError());
   else
      ::Print(__FUNCTION__," > Frame added, OK");
  }

表の配列は最適化の最後にCFrameGenerator :: OnTesterDeinitEvent()メソッドで呼び出される、FinalRecalculateFrames()に入力されます。ここで最適化結果の最終的な再計算を実行し、最適化されるパラメータの数を決定し、表のヘッダの配列が埋められ、データが表の配列に集めらます。その後、データは指定された基準に従ってソートされます。 

フレーム処理の最後のサイクルで呼び出されるいくつかの補助メソッドを見てみましょう。最適化に関与するEAのパラメータの数を決定するCFrameGenerator :: GetParametersTotal()から始めましょう。

フレームからEAのパラメータを取得するには、FrameInputs()関数を呼び出す必要があります。パス番号をこの関数に渡すと、パラメータの配列とその数が取得されます。そのリストには、最適化に参加したものが最初に、その次に全てのその他のものが入ります。最適化されるパラメータのみが表に表示されるため、表に入るべきではないグループを除くために、最初に最適化されていないインデックスを決定する必要があります。私たちのケースでは、プログラムを導くEAの最初の最適化されていない外部パラメータを事前に指定することができます。ここでは、これはSymbolsとなります。インデックスを決定したら、EAの最適化されたパラメータの数を計算することができます。

class CFrameGenerator
  {
private:
   // --- 最初に最適化されないパラメータ
   string            m_first_not_opt_param;
   //---
private:
   // --- 最適化されたパラメータの数を取得する
   void              GetParametersTotal(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CFrameGenerator::CFrameGenerator(void) : m_first_not_opt_param("Symbols")
  {
  }
//+------------------------------------------------------------------+
// | 最適化されたパラメータの数を取得します                               |
//+------------------------------------------------------------------+
void CFrameGenerator::GetParametersTotal(void)
  {
// --- 最初のフレームでは、最適化されたパラメータの数を定義します
   if(m_frames_counter<1)
     {
      // --- フレームを形成する目的であるEAの入力パラメータを取得する
      ::FrameInputs(m_pass,m_param_data,m_par_count);
      // --- 最適化されていない最初のパラメータのインデックスを見つける
      int limit_index=0;
      int params_total=::ArraySize(m_param_data);
      for(int i=0; i<params_total; i++)
        {
         if(::StringFind(m_param_data[i],m_first_not_opt_param)>-1)
           {
            limit_index=i;
            break;
           }
        }
      // --- 最適化されたパラメータの数
      m_param_total=(m_par_count-(m_par_count-limit_index));
     }
  }

表のデータは配列構造体CReportTableに格納されます。EAの最適化されたパラメータの数がはっきりしたら、表の列数を決定して設定することができるようになります。これはCFrameGenerator :: SetColumnsTotal()メソッド内で行われます。最初は、行数はゼロです。 

// ---表の配列
struct CReportTable
  {
   string            m_rows[];
  };
//+------------------------------------------------------------------+
// | 最適化結果を扱うクラス                                             | 
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   // --- レポート用の表
   CReportTable      m_columns[];
   //---
private:
   // --- 表の列数を設定する
   void              SetColumnsTotal(void);
  };
//+------------------------------------------------------------------+
// | 表内の列数の設定                                 |
//+------------------------------------------------------------------+
void CFrameGenerator::SetColumnsTotal(void)
  {
// --- 結果を示す表の為の列数を定義する
   if(m_frames_counter<1)
     {
      int columns_total=int(STAT_TOTAL+m_param_total);
      ::ArrayResize(m_columns,columns_total);
      for(int i=0; i<columns_total; i++)
         ::ArrayFree(m_columns[i].m_rows);
     }
  }

列はCFrameGenerator :: AddRow()メソッドに追加されます。すべてのフレームを列挙するプロセスでは、取引がある結果のみが表に入ります。パス番号で始まる表の最初の列には、統計指標が表示され、次に最適化されたEAのパラメータが表示されます。フレームからパラメータを取得すると、「parameterN = valueN」[パラメータ名] [区切り文字] [パラメータ値]の形式で出力されます。表に入るべきパラメータの値だけが必要になる為、文字列をセパレータ '='で分割し、配列の2番目の要素から値を保存します

class CFrameGenerator
  {
private:
   // --- 列のデータを追加する
   void              AddRow(void);
  };
//+------------------------------------------------------------------+
//| 列のデータを追加する                                             |
//+------------------------------------------------------------------+
void CFrameGenerator::AddRow(void)
  {
// --- 表の列数を設定する
   SetColumnsTotal();
// --- 取引がない場合は終了する
   if(m_data[3]<1)
      return;
// --- 表を埋める
   int columns_total=::ArraySize(m_columns);
   for(int i=0; i<columns_total; i++)
     {
      // --- 行を追加する
      int prev_rows_total=::ArraySize(m_columns[i].m_rows);
      ::ArrayResize(m_columns[i].m_rows,prev_rows_total+1,RESERVE);
      // --- パス番号
      if(i==0)
        {
         m_columns[i].m_rows[prev_rows_total]=string(m_pass);
         continue;
        }
      // --- 統計指標
      if(i<STAT_TOTAL)
         m_columns[i].m_rows[prev_rows_total]=string(m_data[i]);
      // --- 最適化されたEAのパラメータ
      else
        {
         string array[];
         if(::StringSplit(m_param_data[i-STAT_TOTAL],'=',array)==2)
            m_columns[i].m_rows[prev_rows_total]=array[1];
        }
     }
  }

表の見出しは別のCFrameGenerator :: GetHeaders()メソッドで取得しますが、分割線の配列要素の最初の要素のみとなります。

class CFrameGenerator
  {
private:
   // --- 表のヘッダを取得する
   void              GetHeaders(void);
  };
//+------------------------------------------------------------------+
// | 表のヘッダーを取得する                                             |
//+------------------------------------------------------------------+
void CFrameGenerator::GetHeaders(void)
  {
   int columns_total =::ArraySize(m_columns);
// --- ヘッダー
   ::ArrayResize(m_headers,STAT_TOTAL+m_param_total);
   for(int c=STAT_TOTAL; c<columns_total; c++)
     {
      string array[];
      if(::StringSplit(m_param_data[c-STAT_TOTAL],'=',array)==2)
         m_headers[c]=array[0];
     }
  }

プログラムにどんな基準で表に追加する100の結果を選択するかを指定するために、簡単なCFrameGenerator :: ColumnSortIndex()メソッドを使用します。こちらには表のインデックスが渡されます。最適化の終了後、結果表はこのインデックスによってソートされ、上位100の結果がグラフィカルインタフェースに表示される表に入ります。デフォルトでは、3番目の列(インデックス2)が設定されます。つまり、ソートは最大利益に基づいて行われます。

class CFrameGenerator
  {
private:
   //--- ソートされた列のインデックス
   uint              m_column_sort_index;
   //---
public:
   // --- 表のソートを実行する列のインデックスを設定する
   void              ColumnSortIndex(const uint index) { m_column_sort_index=index; }
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CFrameGenerator::CFrameGenerator(void) : m_column_sort_index(2)
  {
  }

結果を別の基準で選択したい場合は、CFrameGenerator :: ColumnSortIndex()メソッドでCProgram :: OnTesterInitEvent()を最適化の冒頭で呼び出す必要があります。

//+------------------------------------------------------------------+
// | 最適化プロセスを開始するイベント                                    |
//+------------------------------------------------------------------+
void CProgram::OnTesterInitEvent(void)
  {
...
   m_frame_gen.ColumnSortIndex(3);
...
  }

フレームの最終計算の為のCFrameGenerator :: FinalRecalculateFrames()は、これで次のアルゴリズムに従って動作するようになりました。

  • フレームのポインタをリストの先頭に移します。フレームカウンタをリセットし、配列をリセットします。 
  • さらにサイクルですべてのフレームを通過し、
    • 最適化されたパラメータの数を取得し、 
    • マイナスの結果とプラスの結果を配列に配布し、 
    • 一連のデータを表に追加します。
  • フレームの選別のサイクルの後で、表のヘッダーを取得します。
  • 次に、 設定で指定された列に従って表をソートします。
  • メソッドはグラフを最適化の結果で更新します。

CFrameGenerator::FinalRecalculateFrames()メソッドのコード。

class CFrameGenerator
  {
private:
   // --- 最適化後のすべてのフレームから取得したデータの最終的な計算
   void              FinalRecalculateFrames(void);
  };
//+------------------------------------------------------------------+
// | 最適化後のすべてのフレームから取得したデータの最終的な計算             |
//+------------------------------------------------------------------+
void CFrameGenerator::FinalRecalculateFrames(void)
  {
// --- フレームポインタを先頭に移動する
   ::FrameFirst();
// --- カウンタと配列をリセットする
   ArraysFree();
   m_frames_counter=0;
// --- フレームの選択を実行する
   while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      // --- 最適化されたパラメータの数を取得する
      GetParametersTotal();
      // --- マイナスの結果
      if(m_data[m_profit_index]<0)
         AddLoss(m_data[m_profit_index]);
      // --- プラスの結果
      else
         AddProfit(m_data[m_profit_index]);
      // --- 列のデータを追加する
      AddRow();
      // --- 処理されたフレームのカウンタを増やす
      m_frames_counter++;
     }
// --- 表のヘッダを取得する
   GetHeaders();
// --- 列と行の数
   int rows_total =::ArraySize(m_columns[0].m_rows);
// --- 指定された列で表をソートする
   QuickSort(0,rows_total-1,m_column_sort_index);
//--- グラフ上の系列を更新する
   CCurve *curve=m_graph_results.CurveGetByIndex(0);
   curve.Name("P: "+(string)ProfitsTotal());
   curve.Update(m_profit_x,m_profit_y);
//---
   curve=m_graph_results.CurveGetByIndex(1);
   curve.Name("L: "+(string)LossesTotal());
   curve.Update(m_loss_x,m_loss_y);
// --- 水平軸のプロパティ
   CAxis *x_axis=m_graph_results.XAxis();
   x_axis.Min(0);
   x_axis.Max(m_frames_counter);
   x_axis.DefaultStep((int)(m_frames_counter/8.0));
// --- グラフを更新する
   m_graph_results.CalculateMaxMinValues();
   m_graph_results.CurvePlotAll();
   m_graph_results.Update();
  }

以下では、ユーザーの要求に応じた、フレームからデータを取得する方法について説明します。

フレームからのデータの抽出

上記では、異なるカテゴリの一連のデータを持つ、共通配列の構造を紹介しました。ここでは、この配列のデータをどのように抽出するのかを理解する必要があります。上の部分で、シンボルの列挙と残高の配列のサイズが、フレームにキーとしてに含まれていることについて説明しました。残高のサイズがドローダウンの配列のサイズと等しい場合、格納されたデータのすべての範囲のインデックスは、以下の図で示すように、サイクル内の同じ式で決定できます。しかし、配列のサイズは異なります。したがって、サイクルの最後の反復では、ドローダウンの配列のサイズが等しいため、デポジットのドローダウンに関連するデータの範囲内に残されたアイテムの数を決定し、2つに分割する必要があります。 

 


図2. 次のカテゴリから配列のインデックスを計算するためのパラメータを持つスキーム。

フレームからデータを取得する為に、CFrameGenerator :: GetFrameDataz()パブリックメソッドを実装しました。より詳細に見ていきましょう。

メソッドの始めに、フレームポインタをリストの先頭に移動する必要があります。それから最適化結果を含むすべてのフレームを選択するプロセスが始まります。引数としてメソッドに引き渡されたパス番号を検出する必要があります。これが見つかった場合、プログラムは次のアルゴリズムに従って動作します。

  • フレームのデータを持つ共通配列のサイズを取得する。 
  • 文字列パラメータの文字列の要素とその数を取得する。複数のシンボルがあると判明した場合、配列内の残高の数は一つ多くなる。つまり、最初の範囲は総残高であり、残りはシンボルの残高に対応する。
  • 次に、データを残高の配列に移す必要がある。一般的な配列からデータを抽出するためにサイクルを開始する(反復回数は残高の数に等しい)。データをコピーしたいインデックスを特定するには、統計指標の数によって相殺し(STAT_TOTAL)、反復インデックスを(i)残高の配列のサイズ(m_value)分増やす。各反復で、すべての残高のデータを別々の配列に取得する。
  • 最後の反復では、別個の配列にドローダウンのデータを取得する。これは配列内の最後のデータなので、残りの要素数を知り、それを2に分割するだけで済みます。次に一連の2つのステップで、ドローダウンのデータを取得します。 
  • 最後のステップでは、グラフを新しいデータで更新し、フレームの選択サイクルを停止することです。
class CFrameGenerator
  {
public:
   // --- 指定したフレーム番号のデータを取得する
   void              GetFrameData(const ulong pass_number);
  };
//+------------------------------------------------------------------+
// | 指定したフレーム番号のデータを取得する                               |
//+------------------------------------------------------------------+
void CFrameGenerator::GetFrameData(const ulong pass_number)
  {
// --- フレームポインタを先頭に移動する
   ::FrameFirst();
//--- データの抽出
   while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- パス番号が一致しない場合、次へ進む
      if(m_pass!=pass_number)
         continue;
      // --- データを持つ配列のサイズ
      int data_total=::ArraySize(m_data);
      // --- セパレータで文字列の要素を取得する
      ushort u_sep          =::StringGetCharacter(",",0);
      int    symbols_total  =::StringSplit(m_name,u_sep,m_symbols_name);
      int    balances_total =(symbols_total>1)? symbols_total+1 : symbols_total;
      // --- 残高の数の配列にサイズをに設定する
      ::ArrayResize(m_symbols_balance,balances_total);
      // --- 配列ごとにデータを配布する
      for(int i=0; i<balances_total; i++)
        {
         // --- データ配列を解放する
         ::ArrayFree(m_symbols_balance[i].m_data);
         // --- ソースデータをコピーするインデックスを定義する
         int src_index=STAT_TOTAL+int(i*m_value);
         // --- 残高の構造体の配列にデータをコピーする
         ::ArrayCopy(m_symbols_balance[i].m_data,m_data,0,src_index,(int)m_value);
         // --- これが最後の反復である場合、ドローダウンのデータを取得する
         if(i+1==balances_total)
           {
            // --- 残りのデータの数と2つの軸に沿った配列のサイズを取得する
            double dd_total   =data_total-(src_index+(int)m_value);
            double array_size =dd_total/2.0;
            // --- コピーを開始するインデックス
            src_index=int(data_total-dd_total);
            // --- ドローダウンの配列のサイズを設定する
            ::ArrayResize(m_dd_x,(int)array_size);
            ::ArrayResize(m_dd_y,(int)array_size);
            // --- データを連続してコピーする
            ::ArrayCopy(m_dd_x,m_data,0,src_index,(int)array_size);
            ::ArrayCopy(m_dd_y,m_data,0,src_index+(int)array_size,(int)array_size);
           }
        }
      // --- グラフを更新してサイクルを停止する
      UpdateMSBalanceGraph();
      UpdateDrawdownGraph();
      break;
     }
  }

表の配列のセルからデータを取得するには、引数の中に表の列と行のインデックスを指定してCFrameGenerator :: GetValue()を呼び出します。 

class CFrameGenerator
  {
public:
   // --- 指定されたセルから値を返す
   string            GetValue(const uint column_index,const uint row_index);
  };
//+------------------------------------------------------------------+
// | 指定されたセルから値を返す                                         |
//+------------------------------------------------------------------+
string CFrameGenerator::GetValue(const uint column_index,const uint row_index)
  {
// --- 列の範囲をチェックする
   uint csize=::ArraySize(m_columns);
   if(csize<1 || column_index>=csize)
      return("");
// --- 行の範囲をチェックする
   uint rsize=::ArraySize(m_columns[column_index].m_rows);
   if(rsize<1 || row_index>=rsize)
      return("");
//---
   return(m_columns[column_index].m_rows[row_index]);
  }

データの視覚化とグラフィカルインタフェースとの相互作用

残高データとドローダウンのグラフを更新する為に、CFrameGeneratorクラスにCGraphicタイプの2つのオブジェクトが宣言されました。CFrameGeneratorクラスでの他のこのタイプのオブジェクトの場合と同様に、最適化の冒頭でCFrameGenerator::OnTesterInitEvent()メソッドにグラフィカルインタフェースの要素にポインタを移す必要があります。 

#include <Graphics\Graphic.mqh>
//+------------------------------------------------------------------+
// | 最適化結果を扱うクラス                                             |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   // --- データの視覚化のためのグラフへのポインタ
   CGraphic         *m_graph_ms_balance;
   CGraphic         *m_graph_drawdown;
   //---
public:
   // --- ストラテジーテスターのイベントハンドラ
   void              OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results,CGraphic *graph_ms_balance,CGraphic *graph_drawdown);
  };
//+------------------------------------------------------------------+
// | OnTesterInit()で呼び出す必要がある                                 |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results,
                                        CGraphic *graph_ms_balance,CGraphic *graph_drawdown)
  {
   m_graph_balance    =graph_balance;
   m_graph_results    =graph_results;
   m_graph_ms_balance =graph_ms_balance;
   m_graph_drawdown   =graph_drawdown;
  }

グラフィカルインタフェースの表内のデータは、CProgram :: GetFrameDataToTable()メソッドを使用して表示されます。CFrameGeneratorオブジェクトから、表のヘッダーの配列へ受け取る列の数を決定します。その後グラフィカルインタフェースで表のサイズを設定します(100行)。次に、ヘッダーとデータ型を設定します。

ここで、最適化結果で表を初期化する必要があります。CTable :: SetValue()メソッドでここに値を設定します。データ表のセルから値を取得する為に、CFrameGenerator :: GetValue()メソッドを使用します。変更を表示するには、表を更新する必要があります。

class CProgram
  {
private:
   // --- 最適化結果の表にフレームデータを取得する
   void              GetFrameDataToTable(void);
  };
//+------------------------------------------------------------------+
// | 最適化結果の表のデータを取得する                                    |
//+------------------------------------------------------------------+
void CProgram::GetFrameDataToTable(void)
  {
//--- ヘッダーを取得する
   string headers[];
   m_frame_gen.CopyHeaders(headers);
//--- 表のサイズを設定する
   uint columns_total=::ArraySize(headers);
   m_table_param.Rebuilding(columns_total,100,true);
//--- ヘッダーとデータ型を設定する
   for(uint c=0; c<columns_total; c++)
     {
      m_table_param.DataType(c,TYPE_DOUBLE);
      m_table_param.SetHeaderText(c,headers[c]);
     }
// --- フレームのデータで表を埋める
   for(uint c=0; c<columns_total; c++)
     {
      for(uint r=0; r<m_table_param.RowsTotal(); r++)
        {
         if(c==1 || c==2 || c==4 || c==5)
            m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),2);
         else
            m_table_param.SetValue(c,r,m_frame_gen.GetValue(c,r),0);
        }
     }
// --- 表を更新する
   m_table_param.Update(true);
   m_table_param.GetScrollHPointer().Update(true);
   m_table_param.GetScrollVPointer().Update(true);
  }

CProgram :: GetFrameDataToTable()メソッドは、EAのパラメータの最適化の終了でOnTesterDeinit()メソッドで呼び出されます。その後、グラフィカルインタフェースをユーザーが使用できるようになります。Resultsタブに移動すると、指定された基準によって選択された最適化結果を見ることができます。この例では、2番目の列の指標によって選択が実行されました(Profit)。

 図3  - グラフィカルインタフェースにおける最適化結果の表。

図3.グラフィカルインタフェースでの最適化結果の表。

ここで、どうすればユーザーがこの表からマルチシンボルの残高結果を見ることができるかを紹介します。テーブル内の特定の行を選択すると、表の識別子を持つカスタムイベントON_CLICK_LIST_ITEMが生成されます。これによってどの表からメッセージが来たかを特定することができます(もしメッセージが複数ある場合)。データ表の第1列にはパス番号が含まれているため、CFrameGenerator::GetFrameData()メソッドにこの番号を引き渡すことで、この結果のデータを取得することが可能です。

//+------------------------------------------------------------------+
// |イベントハンドラ                                     |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
// --- 表の行を押すイベント
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM)
     {
      if(lparam==m_table_param.Id())
        {
         // --- 表からパス番号を取得する
         ulong pass=(ulong)m_table_param.GetValue(0,m_table_param.SelectedItem());
         // --- パス番号のデータを取得する
         m_frame_gen.GetFrameData(pass);
        }
      //---
      return;
     }
...
  }

ユーザーが表内の行を選択するたびに、マルチシンボル残高のグラフがBalanceタブ内で更新されます。

 図4  - 取得結果のデモンストレーション。

図4.取得結果のデモンストレーション。

マルチシンボルのテスト結果をすばやく見るためには、かなり便利なツールが出来上がりました。 

まとめ

私は、処理の完了後に最適化の結果をどのように扱うことができるかをもう1つ紹介しました。このテーマはこれで終わりではなく、更に掘り下げることができるし、それが必要でもあります。グラフィカルインタフェースを作成するためのライブラリを使用すると、多くの興味深い便利なソリューションを作成できます。コメント欄にぜひこの記事に対するあなたのアイデアを書いてください。あなたにとって必要な最適化結果を扱うツールがこれからの記事に出てくるかもしれません。 

テストと記事で紹介したコードを詳しく知る為のファイルは、下のリンクからご自分のパソコンにダウンロードすることができます。

ファイル名 コメント
MacdSampleMSFrames.mq5 標準配布から変更されたEA - MACD Sample
Program.mqh プログラムクラスファイル
CreateGUI.mqh Program.mqhファイルのプログラムクラスのメソッドが実装されたファイル
Strategy.mqh MACD Sampleのストラテジーが変更されたクラスのファイル(マルチシンボルバージョン)
FormatString.mqh 文字列を整えるための補助関数ファイル
FrameGenerator.mqh 最適化結果を処理するクラスのファイル