MetaTrader 5における取引戦略最適化の可視化

Anatoli Kazharski | 8 5月, 2018

内容

はじめに

取引アルゴリズムを開発する際に、パラメータを最適化しながらテスト結果を表示すると便利です。しかし、最適化グラフタブにある単一のグラフは取り引き戦略の効果を評価するには十分でありません。複数のテストのバランス曲線を同時に見て、最適化後でもそれらを解析出来らならばずっと良いでしょう。MetaTrader 5テスター戦略のビジュアル化稿では、このようなアプリケーションを既に検討しました。しかし、それ以来多くの新しい機会が登場しました。したがって、類似してはるかに強力なアプリケーションを実装することが可能になりました。

本稿では、最適化プロセスの可視化を拡張するためのグラフィカルインターフェイスを備えたMQLアプリケーションが実装されます。グラフィカルインターフェイスには、tEasyAndFastライブラリの最新バージョンが適用されます。多くのMQLコミュニティユーザは、なぜMQLアプリケーションでグラフィカルインターフェイスが必要かを尋ねるかもしれません。本稿では、その潜在的な用途を示します。これは、ライブラリを作業に適用する人にとっても役に立つかもしれません。

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

ここでは、グラフィカルインターフェイスの開発について簡単に説明します。既にEasyAndFast ライブラリをマスターしていれば、使い方を素早く理解し、MQLアプリケーション用のグラフィカルインターフェイスを開発するのがどれだけ簡単かを評価することができるでしょう。

まず、開発されたアプリケーションの一般的な構造について説明します。Program.mqhファイルにはCProgramアプリケーションクラスが含まれます。この基本クラスはグラフィカルライブラリエンジンに接続されるべきです。

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
//--- グラフィカルインターフェイスを作成するためのライブラリクラス
#include <EasyAndFastGUI\WndEvents.mqh>
//+------------------------------------------------------------------+
//| アプリケーション開発クラス                                          |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
  };

EasyAndFastライブラリは、画像を乱雑にしないために単一のブロック(ライブラリGUI)に表示されます。これはライブラリページで完全に見ることができます。 

 図1 GUI作成ライブラリのインクルード

図1 GUI作成ライブラリのインクルード

MQLプログラムのメイン関数と接続するにはCProgramクラスには同じようなメソッドが作成されるべきです。フレームの作業にはOnTesterXXX()カテゴリーからのメソッドが必要です。

class CProgram : public CWndEvents
  {
public:
   //--- 初期化/初期化会場
   bool              OnInitEvent(void);
   void              OnDeinitEvent(const int reason);
   //--- 「新ティック」イベントハンドラ
   void              OnTickEvent(void);
   //--- 取引イベントハンドラ
   void              OnTradeEvent(void);
   //--- タイマー
   void              OnTimerEvent(void);
   //--- テスター
   double            OnTesterEvent(void);
   void              OnTesterPassEvent(void);
   void              OnTesterInitEvent(void);
   void              OnTesterDeinitEvent(void);
  };

子の場合メソッドはアプリケーションのメインファイルで次のように呼び出されるべきです

//--- アプリケーションクラスをインクルードする
#include "Program.mqh"
CProgram program;
//+------------------------------------------------------------------+
//| エキスパート初期化関数                                              |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- プログラムを初期化する
   if(!program.OnInitEvent())
     {
      ::Print(__FUNCTION__," > Failed to initialize!");
      return(INIT_FAILED);
     }  
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| エキスパート初期化解除関数                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) { program.OnDeinitEvent(reason); }
//+------------------------------------------------------------------+
//| エキスパートティック関数                                            |
//+------------------------------------------------------------------+
void OnTick(void) { program.OnTickEvent(); }
//+------------------------------------------------------------------+
//| タイマー関数                                                       |
//+------------------------------------------------------------------+
void OnTimer(void) { program.OnTimerEvent(); }
//+------------------------------------------------------------------+
//| ChartEvent関数                                                    |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
   { program.ChartEvent(id,lparam,dparam,sparam); }
//+------------------------------------------------------------------+
//| テスター関数                                                       |
//+------------------------------------------------------------------+
double OnTester(void) { return(program.OnTesterEvent()); }
//+------------------------------------------------------------------+
//| TesterInit関数                                                    |
//+------------------------------------------------------------------+
void OnTesterInit(void) { program.OnTesterInitEvent(); }
//+------------------------------------------------------------------+
//| TesterPass関数                                                    |
//+------------------------------------------------------------------+
void OnTesterPass(void) { program.OnTesterPassEvent(); }
//+------------------------------------------------------------------+
//| TesterDeinit関数                                                  |
//+------------------------------------------------------------------+
void OnTesterDeinit(void) { program.OnTesterDeinitEvent(); }
//+------------------------------------------------------------------+

これにより、アプリケーションワークピースのグラフィカルインターフェイスを開発する準備が整います。主な作業はCProgramクラスで行われます。必要なファイルはすべてProgram.mqh.にインクルードされています。

ここでグラフィカルインターフェイスのコンテンツを定義しましょう。作成するすべての要素を一覧表示します。

以下に、コントロール要素クラスのインスタンスとその作成メソッドの宣言を示します(下記のコードを参照してください)。メソッドのコードは、 CProgramクラスファイルに関連付けられた別のファイル( CreateFrameModeGUI.mqh )に入れられます。開発されたアプリケーションのコードが大きくなるにつれ、個々のファイルによる配布方法の関連性が高まり、プロジェクトのナビゲートが容易になります。

class CProgram : public CWndEvents
  {
private:
   //--- ウィンドウ
   CWindow           m_window1;
   //--- ステータスバー
   CStatusBar        m_status_bar;
   //--- 入力フィールド
   CTextEdit         m_curves_total;
   CTextEdit         m_sleep_ms;
   //--- ボタン
   CButton           m_reply_frames;
   //--- 表
   CTable            m_table_stat;
   CTable            m_table_param;
   //--- グラフ
   CGraph            m_graph1;
   CGraph            m_graph2;
   //--- プログレスバー
   CProgressBar      m_progress_bar;
   //---
public:
   //--- 最適化モードでフレームを操作するためのグラフィカルインターフェイスを作成する
   bool              CreateFrameModeGUI(void);
   //---
private:
   //--- フォーム
   bool              CreateWindow(const string text);
   //--- ステータスバー
   bool              CreateStatusBar(const int x_gap,const int y_gap);
   //--- 表
   bool              CreateTableStat(const int x_gap,const int y_gap);
   bool              CreateTableParam(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              CreateProgressBar(const int x_gap,const int y_gap,const string text);
  };
//+------------------------------------------------------------------+
//| コントロール要素作成メソッド                                         |
//+------------------------------------------------------------------+
#include "CreateFrameModeGUI.mqh"
//+------------------------------------------------------------------+

CreateFrameModeGUI.mqh で接続されるファイルのインクルードも有効にしましょう。ここでは例としてアプリのグラフィカルインターフェイス作成のメインメソッドだけを示します。

//+------------------------------------------------------------------+
//|                                           CreateFrameModeGUI.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include "Program.mqh"
//+------------------------------------------------------------------+
//| 最適化結果の分析とフレームの操作のための                              |
//| グラフィカルインターフェイスを作成する                                |
//+------------------------------------------------------------------+
bool CProgram::CreateFrameModeGUI(void)
  {
//--- フレームを最適化するためのモードでのみインターフェイスを作成する
   if(!::MQLInfoInteger(MQL_FRAME_MODE))
      return(false);
//--- コントロール要素のフォームを作成する
   if(!CreateWindow("Frame mode"))
      return(false);
//--- コントロール要素を作成する
   if(!CreateStatusBar(1,23))
      return(false);
   if(!CreateCurvesTotal(7,25,"Curves total:"))
      return(false);
   if(!CreateSleep(145,25,"Sleep:"))
      return(false);
   if(!CreateReplyFrames(255,25,"Replay frames"))
      return(false);
   if(!CreateTableStat(2,50))
      return(false);
   if(!CreateTableParam(2,212))
      return(false);
   if(!CreateGraph1(200,50))
      return(false);
   if(!CreateGraph2(200,159))
      return(false);
//--- プログレスバー
   if(!CreateProgressBar(2,3,"Processing..."))
      return(false);
//--- GUI 作成を完成する
   CWndEvents::CompletedGUI();
   return(true);
  }
...

1つのクラスに属するファイル間の接続は、両面の黄色の矢印で示されます。

 図2 プロジェクトのいくつかのファイルへの分割

図2 プロジェクトのいくつかのファイルへの分割



フレームデータ処理クラスの開発

フレームを操作するために別のCFrameGeneratorクラスを書きましょう。クラスはFrameGenerator.mqhに含まれ、これはProgram.mqhにインクルードされるべきです。例として、グラフィカルインターフェイス要素で表示するためにこれらのフレームを受信する2つのオプションを示します。 

これらのオプションのどれを主なものとして残すかはあなた次第です。

EasyAndFastライブラリはデータの可視化に、標準ライブラリのCGraphicクラスを適用します。 そのメソッドにアクセスするためにFrameGenerator.mqhにインクルードしましょう。

//+------------------------------------------------------------------+
//|                                               FrameGenerator.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include <Graphics\Graphic.mqh>
//+------------------------------------------------------------------+
//| 最適化の結果を受信するためのクラス                                   |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
  };

プログラムの構成は次のようになります。

 図3 作業のためのクラスプロジェクトへの接続

図3 作業のためのクラスプロジェクトへの接続

ここでCFrameGeneratorクラスがどのように構成されているかを見ていきましょう。また、ストラテジーテスターイベントの処理方法も必要です(下記のコードリストを参照)。それらは、我々が開発するプログラムの類似のクラスメソッド( CProgram )で呼び出されます。現在の最適化プロセスが表示されているグラフオブジェクトへのポインタはCFrameGenerator::OnTesterInitEvent()メソッドに渡されます。 

class CFrameGenerator
  {
private:
   //--- データの可視化のためのグラフポインタ
   CGraphic         *m_graph_balance;
   CGraphic         *m_graph_results;
   //---
public:
   //--- ストラテジーテスターイベントハンドラ
   void              OnTesterEvent(const double on_tester_value);
   void              OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_result);
   void              OnTesterDeinitEvent(void);
   bool              OnTesterPassEvent(void);
  };
//+------------------------------------------------------------------+
//| OnTesterInit()ハンドラで呼び出されるべき                            |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterInitEvent(CGraphic *graph_balance,CGraphic *graph_results)
  {
   m_graph_balance =graph_balance;
   m_graph_results =graph_results;
  }

両方のグラフで、肯定的な結果は緑色で表示され、否定的な結果は赤色で表示されます。

CFrameGenerator::OnTesterEvent()メソッドでは、テスト結果のバランスと統計パラメータを受け取ります。これらのデータはCFrameGenerator::GetBalanceData()メソッドとCFrameGenerator::GetStatData()メソッドを使ってフレームに渡されます。CFrameGenerator::GetBalanceData() メソッドはテスト履歴全体を受け取り、in-/inoutのすべての取引をまとめます。得られた結果はステップごとに m_balance[]配列に保存されます。次に、この配列はCFrameGeneratorクラスのメンバーです。

フレームに送信される動的配列はCFrameGenerator::GetStatData()メソッドに渡されます。そのサイズは、以前に受信した結果のバランスの配列のサイズと一致するものです。さらに、統計パラメータを受け取るいくつかの要素が追加されています。

//--- 統計パラメータの数
#define STAT_TOTAL 7
//+------------------------------------------------------------------+
//| 最適化結果を処理するためのクラス                                     |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- 結果バランス
   double            m_balance[];
   //---
private:
   //--- バランスデータを受け取る
   int               GetBalanceData(void);
   //--- 統計データを受け取る
   void              GetStatData(double &dst_array[],double on_tester_value);
  };
//+------------------------------------------------------------------+
//| バランスデータを取得する                                            |
//+------------------------------------------------------------------+
int CFrameGenerator::GetBalanceData(void)
  {
   int    data_count      =0;
   double balance_current =0;
//--- すべての取引履歴をリクエストする
   ::HistorySelect(0,LONG_MAX);
   uint deals_total=::HistoryDealsTotal();
//--- 取引のデータを集める
   for(uint i=0; i<deals_total; i++)
     {
      //--- ティケットを受け取る
      ulong ticket=::HistoryDealGetTicket(i);
      if(ticket<1)
         continue;
      //--- 初期バランスまたはout-/inout取引の場合
      long entry=::HistoryDealGetInteger(ticket,DEAL_ENTRY);
      if(i==0 || entry==DEAL_ENTRY_OUT || entry==DEAL_ENTRY_INOUT)
        {
         double swap      =::HistoryDealGetDouble(ticket,DEAL_SWAP);
         double profit    =::HistoryDealGetDouble(ticket,DEAL_PROFIT);
         double commision =::HistoryDealGetDouble(ticket,DEAL_COMMISSION);
         //--- バランスを計算する
         balance_current+=(profit+swap+commision);
         //--- 配列に保存する
         data_count++;
         ::ArrayResize(m_balance,data_count,100000);
         m_balance[data_count-1]=balance_current;
        }
     }
//--- データの量を取得する
   return(data_count);
  }
//+------------------------------------------------------------------+
//| 統計データを受け取る                                               |
//+------------------------------------------------------------------+
void CFrameGenerator::GetStatData(double &dst_array[],double on_tester_value)
  {
   ::ArrayResize(dst_array,::ArraySize(m_balance)+STAT_TOTAL);
   ::ArrayCopy(dst_array,m_balance,STAT_TOTAL,0);
//--- 最初の配列値(STAT_TOTAL)をテスト結果で入力する
   dst_array[0] =::TesterStatistics(STAT_PROFIT);               // 純利益
   dst_array[1] =::TesterStatistics(STAT_PROFIT_FACTOR);        // 利益率
   dst_array[2] =::TesterStatistics(STAT_RECOVERY_FACTOR);      // 回復率
   dst_array[3] =::TesterStatistics(STAT_TRADES);               // 取引数
   dst_array[4] =::TesterStatistics(STAT_DEALS);                // 約定数
   dst_array[5] =::TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // 最大資金ドローダウン%
   dst_array[6] =on_tester_value;                               // カスタム最適化基準値
  }

CFrameGenerator::GetBalanceData()及びCFrameGenerator::GetStatData()メソッドはテスト完了イベントハンドラ(CFrameGenerator::OnTesterEvent())で呼ばれます。データが受信されます。それらをフレームでターミナルに送信します。 

//+------------------------------------------------------------------+
//| バランス値の配列を準備し、フレームに送る                              |
//| 関数はEAのOnTester()ハンドラで呼ばれるべきである                     |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterEvent(const double on_tester_value)
  {
//--- バランスデータを取得する
   int data_count=GetBalanceData();
//--- フレームにデータを送信するための配列
   double stat_data[];
   GetStatData(stat_data,on_tester_value);
//--- データを含むフレームを作成し、ターミナルに送信する
   if(!::FrameAdd(::MQLInfoString(MQL_PROGRAM_NAME),1,data_count,stat_data))
      ::Print(__FUNCTION__," > Frame add error: ",::GetLastError());
   else
      ::Print(__FUNCTION__," > Frame added, Ok");
  }

N次に、最適化中にフレーム到着イベントハンドラで使用されるメソッド(CFrameGenerator::OnTesterPassEvent())を考えてみましょう。名前、ID、パス番号、受け入れられた値、受け入れられたデータ配列など、フレームを扱うための変数が必要です。これらのデータはすべて、上記のFrameAdd()関数を使用してフレームに送信されます。

class CFrameGenerator
  {
private:
   //--- フレーム操作のための変数
   string            m_name;
   ulong             m_pass;
   long              m_id;
   double            m_value;
   double            m_data[];
  };

フレームで受け入れた配列の CFrameGenerator :: SaveStatData ()メソッドは、統計パラメータを取得し、それらを別の文字列配列に保存するために使用されます。データには、指標名とその値が含まれている必要があります。'=' 記号は区切りとして使用されます。

class CFrameGenerator
  {
private:
   //--- 統計パラメータの配列
   string            m_stat_data[];
   //---
private:
   //--- 統計データを保存する 
   void              SaveStatData(void);
  };
//+------------------------------------------------------------------+
//| 結果統計パラメータを配列に保存する                                   |
//+------------------------------------------------------------------+
void CFrameGenerator::SaveStatData(void)
  {
//--- フレーム統計パラメータを受け入れるための配列
   double stat[];
   ::ArrayCopy(stat,m_data,0,0,STAT_TOTAL);
   ::ArrayResize(m_stat_data,STAT_TOTAL);
//--- 配列にテスト結果を書き入れる
   m_stat_data[0] ="Net profit="+::StringFormat("%.2f",stat[0]);
   m_stat_data[1] ="Profit Factor="+::StringFormat("%.2f",stat[1]);
   m_stat_data[2] ="Factor Recovery="+::StringFormat("%.2f",stat[2]);
   m_stat_data[3] ="Trades="+::StringFormat("%G",stat[3]);
   m_stat_data[4] ="Deals="+::StringFormat("%G",stat[4]);
   m_stat_data[5] ="Equity DD="+::StringFormat("%.2f%%",stat[5]);
   m_stat_data[6] ="OnTester()="+::StringFormat("%G",stat[6]);
  }

統計データは別の配列に保存する必要があります。そのため、テーブルに書き込むためのアプリケーション(CProgram))クラスで検索できます。CFrameGenerator::CopyStatData() メソッドは、コピーのために配列を渡した後に受け取るために呼び出されます。

class CFrameGenerator
  {
public:
   //--- 渡された配列に統計的パラメータを取得する
   int               CopyStatData(string &dst_array[]) { return(::ArrayCopy(dst_array,m_stat_data)); }
  };

最適化中に結果グラフを更新するには、配列に正の結果と負の結果を追加する補助的なメソッドが必要です。結果は現在のフレームカウンタ値にX軸で加算されることにご注意ください。その結果、形成された空隙はゼロ値としてグラフに反映されません。

//--- 配列のスタンドバイサイズ
#define RESERVE_FRAMES 1000000
//+------------------------------------------------------------------+
//| 最適化結果を処理するためのクラス                                     |
//+------------------------------------------------------------------+
class CFrameGenerator
  {
private:
   //--- フレームカウンタ
   ulong             m_frames_counter;
   //--- 正と負の結果のデータ
   double            m_loss_x[];
   double            m_loss_y[];
   double            m_profit_x[];
   double            m_profit_y[];
   //---
private:
   //--- (1) 負と (2) 正の結果を配列に追加する
   void              AddLoss(const double loss);
   void              AddProfit(const double profit);
  };
//+------------------------------------------------------------------+
//| 負の結果を配列に追加する                                            |
//+------------------------------------------------------------------+
void CFrameGenerator::AddLoss(const double loss)
  {
   int size=::ArraySize(m_loss_y);
   ::ArrayResize(m_loss_y,size+1,RESERVE_FRAMES);
   ::ArrayResize(m_loss_x,size+1,RESERVE_FRAMES);
   m_loss_y[size] =loss;
   m_loss_x[size] =(double)m_frames_counter;
  }
//+------------------------------------------------------------------+
//| 正の結果を配列に追加する                                            |
//+------------------------------------------------------------------+
void CFrameGenerator::AddProfit(const double profit)
  {
   int size=::ArraySize(m_profit_y);
   ::ArrayResize(m_profit_y,size+1,RESERVE_FRAMES);
   ::ArrayResize(m_profit_x,size+1,RESERVE_FRAMES);
   m_profit_y[size] =profit;
   m_profit_x[size] =(double)m_frames_counter;
  }

ここでグラフを更新する主なメソッドはCFrameGenerator::UpdateResultsGraph()とCFrameGenerator::UpdateBalanceGraph()です。

class CFrameGenerator
  {
private:
   //--- 結果グラフを更新する
   void              UpdateResultsGraph(void);
   //--- 残高グラフを更新する
   void              UpdateBalanceGraph(void);
  };

CFrameGenerator::UpdateResultsGraph()メソッドでは、テスト結果(正/負利益)は配列に追加されます。そして、 これらのデータは適切なグラフに表示されます。グラフシリーズの名前は、現在の正/負の結果を表示します。 

//+------------------------------------------------------------------+
//| 結果グラフを更新する                                                |
//+------------------------------------------------------------------+
void CFrameGenerator::UpdateResultsGraph(void)
  {
//--- 負の結果
   if(m_data[0]<0)
      AddLoss(m_data[0]);
//--- 正の結果
   else
      AddProfit(m_data[0]);
//--- 最適化結果グラフでシリーズを更新する
   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);
//--- x軸プロパティ
   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();
  }

CFrameGenerator::UpdateBalanceGraph()メソッドの開始時に、フレームに渡されたデータの配列からバランスに関連するデータが取得されます。グラフには複数のシリーズを表示できるので、シリーズの更新を一貫させる必要があります。これを達成するために、別のシリーズカウンタを使用します。グラフに表示されるバランスシリーズの数を同時に設定するには、CFrameGenerator::SetCurvesTotal()パブリックメソッドが必要です。シリーズカウンタが設定された制限に達するとすぐにカウントは最初から開始されます。フレームカウンタはシリーズ名として機能します。シリーズの色は結果にも依存します。緑は正の結果を表し、赤は負の結果を表します。

各結果の取引数が異なるため、最大のシリーズを定義して x座標で最大値を設定しグラフに必要なすべてのシリーズを収めます。

class CFrameGenerator
  {
private:
   //--- シリーズ数
   uint              m_curves_total;
   //--- グラフでの現在のシリーズのインデックス
   uint              m_last_serie_index;
   //--- 最大シリーズの定義
   double            m_curve_max[];
   //---
public:
   //--- グラフに表示するシリーズの数を設定する
   void              SetCurvesTotal(const uint total);
  };
//+------------------------------------------------------------------+
//| グラフに表示するシリーズの数を設定する                                |
//+------------------------------------------------------------------+
void CFrameGenerator::SetCurvesTotal(const uint total)
  {
   m_curves_total=total;
   ::ArrayResize(m_curve_max,total);
   ::ArrayInitialize(m_curve_max,0);
  }
//+------------------------------------------------------------------+
//| 残高グラフを更新する                                                |
//+------------------------------------------------------------------+
void CFrameGenerator::UpdateBalanceGraph(void)
  {
//--- 現在のフレームのバランス値を受け入れる配列
   double serie[];
   ::ArrayCopy(serie,m_data,0,STAT_TOTAL,::ArraySize(m_data)-STAT_TOTAL);
//--- バランスグラフに表示する配列を送る
   CCurve *curve=m_graph_balance.CurveGetByIndex(m_last_serie_index);
   curve.Name((string)m_frames_counter);
   curve.Color((m_data[0]>=0)?::ColorToARGB(clrLimeGreen) : ::ColorToARGB(clrRed));
   curve.Update(serie);
//--- シリーズのサイズを取得する
   int serie_size=::ArraySize(serie);
   m_curve_max[m_last_serie_index]=serie_size;
//--- 最大要素数を持つ系列を定義する
   double x_max=0;
   for(uint i=0; i<m_curves_total; i++)
      x_max=::fmax(x_max,m_curve_max[i]);
//--- x軸プロパティ
   CAxis *x_axis=m_graph_balance.XAxis();
   x_axis.Min(0);
   x_axis.Max(x_max);
   x_axis.DefaultStep((int)(x_max/8.0));
//--- グラフを更新する
   m_graph_balance.CalculateMaxMinValues();
   m_graph_balance.CurvePlotAll();
   m_graph_balance.Update();
//--- シリーズカウンタを増加する
   m_last_serie_index++;
//--- 上限に達したらシリーズカウンタをゼロに設定する
   if(m_last_serie_index>=m_curves_total)
      m_last_serie_index=0;
  }

フレームハンドラーで作業を整理するために必要なメソッドを検討しました。次に、CFrameGenerator::OnTesterPassEvent() メソッドハンドラ自体を詳しく見ていきましょう。最適化が進行中はtrueが返され、FrameNext()関数がフレームデータを取得します。最適化が完了すると、このメソッドはfalseを返します。

FrameInputs()関数を使用して取得できるパラメータのEAリストでは、最適化のために設定されたパラメータの後に、最適化に参加しないパラメータが続きます。 

フレームデータが取得された場合FrameInputs() 関数によって、現在の最適化パスの間にEAパラメータを取得することができます。次に、統計を保存し、グラフを更新し、フレームカウンタを増やします。この後、CFrameGenerator::OnTesterPassEvent()メソッドは次の呼び出しまでtrue を返します。

class CFrameGenerator
  {
private:
   //--- EAパラメータ
   string            m_param_data[];
   uint              m_par_count;
  };
//+------------------------------------------------------------------+
//| 最適化中にデータとともにフレームを受信し、グラフを表示する              |
//+------------------------------------------------------------------+
bool CFrameGenerator::OnTesterPassEvent(void)
  {
//--- 新しいフレームを取得したら、そこからデータ取得を試みる
   if(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- フレームが形成されたEAの入力パラメータを取得する
      ::FrameInputs(m_pass,m_param_data,m_par_count);
      //--- 結果統計パラメータを配列に保存する
      SaveStatData();
      //--- 結果と残高グラフを更新する
      UpdateResultsGraph();
      UpdateBalanceGraph();
      //--- 処理済みフレームカウンタを増加する
      m_frames_counter++;
      return(true);
     }
//---
   return(false);
  }

最適化が完了すると、TesterDeinitイベントが生成され、フレーム処理モードで CFrameGenerator::OnTesterDeinitEvent()メソッドが呼び出されます。現時点では、最適化中にすべてのフレームを処理できるわけではないため、結果の可視化グラフは不完全です。完全な画像を見るには、CFrameGenerator::FinalRecalculateFrames()メソッドを使用してすべてのフレームを循環させ、最適化の直後にグラフを再ロードする必要があります。

これを行うには、ポインタをフレームリストの先頭に移動し、結果配列とフレームカウンタをゼロに設定します。次に、フレームの完全なリストを反復処理し、正および負の結果によって配列を記入し、最終的にグラフを更新します。

class CFrameGenerator
  {
private:
   //--- 配列を解放する
   void              ArraysFree(void);
   //--- 最適化後の全フレームからの最終データ再計算
   void              FinalRecalculateFrames(void);
  };
//+------------------------------------------------------------------+
//| 配列を解放する                                                     |
//+------------------------------------------------------------------+
void CFrameGenerator::ArraysFree(void)
  {
   ::ArrayFree(m_loss_y);
   ::ArrayFree(m_loss_x);
   ::ArrayFree(m_profit_y);
   ::ArrayFree(m_profit_x);
  }
//+------------------------------------------------------------------+
//| 最適化後の全フレームからの最終データ再計算                            |
//+------------------------------------------------------------------+
void CFrameGenerator::FinalRecalculateFrames(void)
  {
//--- フレームポインタを先頭に設定する
   ::FrameFirst();
//--- カウンタと配列をリセットする
   ArraysFree();
   m_frames_counter=0;
//--- フレームの反復処理を始める
   while(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- 負の結果
      if(m_data[0]<0)
         AddLoss(m_data[0]);
      //--- 正の結果
      else
         AddProfit(m_data[0]);
      //--- 処理済みのフレームのカウンタを増加する
      m_frames_counter++;
     }
//--- グラフのシリーズを更新する
   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);
//--- x軸プロパティ
   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();
  }

子の場合CFrameGenerator::OnTesterDeinitEvent()メソッドコードは下記のようになります。ここでは、フレームの総数を覚えて、カウンタをゼロに設定します。

//+------------------------------------------------------------------+
//| OnTesterDeinit()ハンドラで呼び出されるべき                          |
//+------------------------------------------------------------------+
void CFrameGenerator::OnTesterDeinitEvent(void)
  {
//--- 最適化後の全フレームからのデータの最終的な再計算
   FinalRecalculateFrames();
//--- フレームの総数を覚えておき、カウンターをゼロに設定する
   m_frames_total     =m_frames_counter;
   m_frames_counter   =0;
   m_last_serie_index =0;
  }

次にアプリケーションクラスのCFrameGeneratorクラスメソッドの使用についてみてみましょう。 


アプリケーションクラス内の最適化データの処理

グラフィカルインターフェイスはCProgram::OnTesterInitEvent()テスト初期化メソッドで作成されます。その後、グラフィカルインターフェイスはアクセス不能にする必要があります。これを行うには、他のCProgramクラスメソッドで使用される別のCProgram::IsAvailableGUI()及びCProgram::IsLockedGUI()メソッドが必要です。

フレームジェネレータを初期化します。最適化結果を可視化するために使用するグラフへのポインタを渡します。

class CProgram : public CWndEvents
  {
private:
   //--- インターフェイスの可用性
   void              IsAvailableGUI(const bool state);
   void              IsLockedGUI(const bool state);
  }
//+------------------------------------------------------------------+
//| 最適化開始イベント                                                 |
//+------------------------------------------------------------------+
void CProgram::OnTesterInitEvent(void)
  {
//--- グラフィカルインターフェイスを作成する
   if(!CreateFrameModeGUI())
     {
      ::Print(__FUNCTION__," > Could not create the GUI!");
      return;
     }
//--- インターフェイスをアクセス不能にする
   IsLockedGUI(false);
//--- フレームジェネレータを初期化する
   m_frame_gen.OnTesterInitEvent(m_graph1.GetGraphicPointer(),m_graph2.GetGraphicPointer());
  }
//+------------------------------------------------------------------+
//| インターフェイスの可用性                                            |
//+------------------------------------------------------------------+
void CProgram::IsAvailableGUI(const bool state)
  {
   m_window1.IsAvailable(state);
   m_sleep_ms.IsAvailable(state);
   m_curves_total.IsAvailable(state);
   m_reply_frames.IsAvailable(state);
  }
//+------------------------------------------------------------------+
//| インターフェイスをブロックする                                       |
//+------------------------------------------------------------------+
void CProgram::IsLockedGUI(const bool state)
  {
   m_window1.IsAvailable(state);
   m_sleep_ms.IsLocked(!state);
   m_curves_total.IsLocked(!state);
   m_reply_frames.IsLocked(!state);
  }

CProgram::UpdateStatTable()及びCProgram::UpdateParamTable()メソッドを使用して、テーブルのデータをアプリケーションクラス内で更新することは既に述べました。両方のテーブルのコードは同じですので、それらのうちの1つのみの例を示します。同じ行のパラメータ名と値は、セパレータとして '='を使用して表示されます。したがって、ループで反復処理し、別々の配列に分割して2つの要素に分割します。その後、値をテーブルセルに入力します。

class CProgram : public CWndEvents
  {
private:
   //--- 統計表を更新する
   void              UpdateStatTable(void);
   //--- パラメータ表を更新する
   void              UpdateParamTable(void);
  }
//+------------------------------------------------------------------+
//| 統計表を更新する                                                   |
//+------------------------------------------------------------------+
void CProgram::UpdateStatTable(void)
  {
//--- 統計表のデータ配列を取得する
   string stat_data[];
   int total=m_frame_gen.CopyStatData(stat_data);
   for(int i=0; i<total; i++)
     {
      //--- 2つの線に分けて表に入力する
      string array[];
      if(::StringSplit(stat_data[i],'=',array)==2)
        {
         if(m_frame_gen.CurrentFrame()>1)
            m_table_stat.SetValue(1,i,array[1],0,true);
         else
           {
            m_table_stat.SetValue(0,i,array[0],0,true);
            m_table_stat.SetValue(1,i,array[1],0,true);
           }
        }
     }
//--- 表を更新する
   m_table_stat.Update();
  }

テーブル内のデータを更新する両方のメソッドCProgram::OnTesterPassEvent()メソッドで、同名のCFrameGenerator::OnTesterPassEvent()メソッドからの肯定的答えによって呼ばれます。

//+------------------------------------------------------------------+
//| 最適化パス処理イベント                                              |
//+------------------------------------------------------------------+
void CProgram::OnTesterPassEvent(void)
  {
//--- テスト結果を処理してグラフを表示する
   if(m_frame_gen.OnTesterPassEvent())
     {
      UpdateStatTable();
      UpdateParamTable();
     }
  }

最適化が完了すると、CProgram::CalculateProfitsAndLosses() メソッドは正と負の結果のパーセンテージ比を計算し、ステータスバーにデータを表示します。

class CProgram : public CWndEvents
  {
private:
   //--- 正と負の結果のパーセンテージ比を計算する
   void              CalculateProfitsAndLosses(void);
  }
//+------------------------------------------------------------------+
//| 正と負の結果の比を計算する                                          |
//+------------------------------------------------------------------+
void CProgram::CalculateProfitsAndLosses(void)
  {
//--- フレームがない場合は終了する
   if(m_frame_gen.FramesTotal()<1)
      return;
//--- 負と正の結果の数
   int losses  =m_frame_gen.LossesTotal();
   int profits =m_frame_gen.ProfitsTotal();
//--- パーセント比
   string pl =::DoubleToString(((double)losses/(double)m_frame_gen.FramesTotal())*100,2);
   string pp =::DoubleToString(((double)profits/(double)m_frame_gen.FramesTotal())*100,2);;
//--- ステータスバーで表示する
   m_status_bar.SetValue(1,"Profits: "+(string)profits+" ("+pp+"%)"+" / Losses: "+(string)losses+" ("+pl+"%)");
   m_status_bar.GetItemPointer(1).Update(true);
  }

TesterDeinitイベントを処理するコードは下に見られます。グラフィックコアの初期化とはマウスカーソルの動きを追跡し、タイマーをオンにすることを意味します。残念ながら、現在のMetaTrader 5 バージョンでは、最適化が完了してもタイマーはオンになりません。将来的にこれが可能になることを祈りましょう。

//+------------------------------------------------------------------+
//| 最適化完了イベント                                                 |
//+------------------------------------------------------------------+
void CProgram::OnTesterDeinitEvent(void)
  {
//--- 最適化の完了
   m_frame_gen.OnTesterDeinitEvent();
//--- インターフェイスをアクセス可能にする
   IsLockedGUI(true);
//--- 正と負の結果のパーセンテージ比を計算する
   CalculateProfitsAndLosses();
//--- GUIコアを初期化する
   CWndEvents::InitializeCore();
  }

これで、最適化が完了した後にもフレームデータでの作業ができます。EAはターミナルチャートに配置され、フレームにアクセスして結果を分析することができます。グラフィカルインターフェイスによってこれはすべて直感的になります。CProgram::OnEvent()イベントハンドラメソッドでは、下記を追跡します。

CProgram::UpdateBalanceGraph()メソッドは、シリーズの数を変更した後にグラフを更新するために使用されます。ここでは、フレームジェネレータで作業するためのシリーズの数を設定し、グラフ上にこの数を予約します。

class CProgram : public CWndEvents
  {
private:
   //--- グラフを更新する
   void              UpdateBalanceGraph(void);
  };
//+------------------------------------------------------------------+
//| グラフを更新する                                                   |
//+------------------------------------------------------------------+
void CProgram::UpdateBalanceGraph(void)
  {
//--- 作業のシリーズの数を設定する
   int curves_total=(int)m_curves_total.GetValue();
   m_frame_gen.SetCurvesTotal(curves_total);
//--- シリーズを削除する
   CGraphic *graph=m_graph1.GetGraphicPointer();
   int total=graph.CurvesTotal();
   for(int i=total-1; i>=0; i--)
      graph.CurveRemoveByIndex(i);
//--- シリーズを追加する
   double data[];
   for(int i=0; i<curves_total; i++)
      graph.CurveAdd(data,CURVE_LINES,"");
//--- グラフを更新する
   graph.CurvePlotAll();
   graph.Update();
  }

イベントハンドラでは、CProgram::UpdateBalanceGraph()メソッドは入力フィールドのボタンを取るぐするtoggling the buttons in the input field (ON_CLICK_BUTTON) 時やキーボードから値が入力された (ON_END_EDIT)時に呼ばれます。

//+------------------------------------------------------------------+
//| イベントハンドラ                                                    |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- ボタン押下イベント
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- グラフでのシリーズの数を変える
      if(lparam==m_curves_total.Id())
        {
         UpdateBalanceGraph();
         return;
        }
      return;
     }
//--- 入力フィールドに値を入力するイベント
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT)
     {
      //--- グラフでのシリーズの数を変える
      if(lparam==m_curves_total.Id())
        {
         UpdateBalanceGraph();
         return;
        }
      return;
     }
  }

CFrameGeneratorクラスで最適化後の結果を表示するには、CFrameGenerator::ReplayFrames() publicメソッドが実装されています。最初の段階では、フレームカウンタで次のように定義します。プロセスが開始されたばかりの場合、配列はゼロに設定され、フレームポインタはリストの先頭に移動されます。その後、フレームが循環され、前述のCFrameGenerator::OnTesterPassEvent()メソッドと同じアクションが実行されます。フレームが受信された場合、このメソッドはtrueを返します。完了すると、フレームカウンタとシリーズカウンタはゼロに設定され、このメソッドは falseを返します。 

class CFrameGenerator
  {
public:
   //--- フレームを反復処理する
   bool              ReplayFrames(void);
  };
//+------------------------------------------------------------------+
//| 最適化の完了後でフレームを再生する                                   |
//+------------------------------------------------------------------+
bool CFrameGenerator::ReplayFrames(void)
  {
//--- フレームポインタを先頭に設定する
   if(m_frames_counter<1)
     {
      ArraysFree();
      ::FrameFirst();
     }
//--- フレームの反復処理を始める
   if(::FrameNext(m_pass,m_name,m_id,m_value,m_data))
     {
      //--- フレームが形成したEA入力を取得する
      ::FrameInputs(m_pass,m_param_data,m_par_count);
      //--- 統計結果パラメータを配列に保存する
      SaveStatData();
      //--- 結果と残高グラフを更新する
      UpdateResultsGraph();
      UpdateBalanceGraph();
      //--- 処理済みのフレームのカウンタを増加する
      m_frames_counter++;
      return(true);
     }
//--- 反復処理を完了する
   m_frames_counter   =0;
   m_last_serie_index =0;
   return(false);
  }

CFrameGenerator::ReplayFrames() method is called in CProgram class from ViewOptimizationResults() method. フレームを起動する前には、グラフィカルインターフェイスは利用できなくなります。スクロール速度はSleep入力フィールドで一時停止を指定することで調整できます。一方、ステータスバーには、処理が終了するまでの時間を示す進行状況バーが表示されます。

class CFrameGenerator
  {
private:
   //--- 最適化結果を表示する
   void              ViewOptimizationResults(void);
  };
//+------------------------------------------------------------------+
//| 最適化結果を表示する                                               |
//+------------------------------------------------------------------+
void CProgram::ViewOptimizationResults(void)
  {
//--- インターフェイスをアクセス不能にする
   IsAvailableGUI(false);
//--- 一時停止
   int pause=(int)m_sleep_ms.GetValue();
//--- フレームを再生する
   while(m_frame_gen.ReplayFrames() && !::IsStopped())
     {
      //--- 表を更新する
      UpdateStatTable();
      UpdateParamTable();
      //--- プログレスバーを更新する
      m_progress_bar.Show();
      m_progress_bar.LabelText("Replay frames: "+string(m_frame_gen.CurrentFrame())+"/"+string(m_frame_gen.FramesTotal()));
      m_progress_bar.Update((int)m_frame_gen.CurrentFrame(),(int)m_frame_gen.FramesTotal());
      //--- 一時停止
      ::Sleep(pause);
     }
//--- 正と負の結果のパーセンテージ比を計算する
   CalculateProfitsAndLosses();
//--- プログレスバーを非表示にする
   m_progress_bar.Hide();
//--- インターフェイスをアクセス可能にする
   IsAvailableGUI(true);
   m_reply_frames.MouseFocus(false);
   m_reply_frames.Update(true);
  }

CProgram::ViewOptimizationResults()メソッドは、アプリケーションのグラフィカルインターフェイスでフレーム再生ボタンが押されると呼ばれます。ON_CLICK_BUTTONイベントが生成されます。

//+------------------------------------------------------------------+
//| イベントハンドラ                                                   |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- ボタン押下イベント
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- 最適化結果を表示する 
      if(lparam==m_reply_frames.Id())
        {
         ViewOptimizationResults();
         return;
        }
      //--- 
      ...
      return;
     }
  }

ここで、結果を見て、フレームで作業するときに最適化中にグラフ上に実際に表示されるものを定義します。


結果の表示

テストの場合、標準パッケージの取引アルゴリズム(移動平均)を使用します。ここではクラスを追加したり訂正したりすることなく、「現状のまま」実装します。開発されたアプリケーションのすべてのファイルは、同じフォルダーに配置されます。戦略ファイルはProgram.mqhファイルにインクルードされます。

FormatString.mqh は、線の書式設定のための関数としての追加としてここに含まれています。これらはまだどのクラスにも属していないので、矢印を黒色にします。結果的に、アプリケーション構造は次のようになります。

図4 取引戦略クラスと追加機能を含むファイルのインクルード 

図4 取引戦略クラスと追加機能を含むファイルのインクルード

パラメータを最適化して、ターミナルチャートでどのように見えるかを見てみましょう。テスター設定: EURUSD H1、時間範囲2017.01.01 – 2018.01.01.

図5 標準パッケージの移動平均EAの結果

図5 標準パッケージの移動平均EAの結果

ここで見る通り、それはかなり有益です。この取引アルゴリズムのほとんどすべての結果は負(95.23%)です。時間範囲を広げると、それはさらに悪化します。しかし、取引システムを開発する際には、ほとんどの結果が正であることを確認する必要があります。それ以外の場合は、アルゴリズムは損失を引き起こすので、使用されてはなりません。より多くのデータのパラメータを最適化し、できるだけ多くの取引が存在することを確認する必要があります。  

標準パッケージから別の取引アルゴリズムを試してみましょう。 MACD Sample.mq5です。これはすでにクラスとして実装されています。わずかな改良を加えれば、以前のようにアプリケーションに接続するだけで済みます。テストは同じ銘柄と時間枠で行うべきです。テストではより多くの取引があるように時間範囲を広げる必要があります (2010.01.01 – 2018.01.01)。以下は、取引EAの最適化結果です。

 図6 標準パッケージの MACDサンプルの結果

図6  MACDサンプル最適化の結果

ここでは、肯定的結果が90.89%という非常に異なる結果が得られます。

使用されるデータの量によって、パラメータの最適化には非常に時間がかかることがあります。プロセスの最初から最後までPCの前に座っている必要はありません。最適化後にフレーム再生を押すと、加速モードで結果の繰り返し表示を開始できます。表示が25シリーズに制限されたフレームを再生しましょう。それはこのように見えます。

図7 最適化の後のMACDサンプルEA結果

図7 最適化の後のMACDサンプルEA結果


終わりに

本稿では、最適化フレームを受け取って分析するための最新版のプログラムを紹介しました。データはEasyAndFastライブラリに基づいて開発されたグラフィカルインターフェイス環境で可視化されます。 

この解決策の欠点は、フレーム処理モードで最適化を完了すると、タイマーを起動できないことです。これは、同じグラフィカルインターフェイスで作業する場合にいくつかの制限を課します。2番目の問題は、チャートからEAを削除するときにOnDeinit()関数での初期化解除がトリガされないことです。これにより、正しいイベント処理が妨げられます。おそらく、これらの問題は将来のMetaTrader 5 ビルドのどれかで解決されるでしょう。