高速数学的計算に基づくカスタムストラテジーテスター

Vasiliy Sokolov | 9 3月, 2018



コンテンツ



イントロダクション

MetaTrader5 で提供されるストラテジーテスターは、さまざまなタスクを解決するための強力な関数を備えています。 ツールのバスケットを交換するための複雑な戦略の両方をテストするために使用することができます。 しかし、このような膨大な関数は、常に有用であるとは限りません。 多くの場合、簡単なトレードのアイデアをチェックしたり、おおよその計算これを行うには、その速度によって補償される精度が必要です。 MetaTrader5の標準的なテスターは、興味深いがめったに使用されていない関数があります。つまり、計算モードで演算を実行することができます。 にもかかわらず、これは本格的な最適化のすべての利点があるストラテジーテスターを実行するための制限モードです。クラウドコンピューティングは、遺伝的最適化を使用することができますし、カスタムデータ型を収集することが可能です。

カスタムストラテジーテスターは、絶対速度を必要とする人だけでなく、必要な場合があります。 数学計算モードでのテストでは、研究者のための方法も開きます。 標準的なストラテジーテスターは、可能な限り現実に近いトレードオペレーションをシミュレートすることができます。 この要件は、常に研究に有用とは限りません。 例えば、時には、トレードシステムの正味効率のクオートを取得する必要があります。 この記事で開発された数学計算のテスターは、そのような機能があります。

当然、円を正方形にすることはできません。 この記事も例外ではありません。 カスタムストラテジーテスターを作成するには、時間のかかるタスクが必要です。 今回の目標は控えめです。正しいライブラリがあれば、カスタムテスターの作成はそれほど難しくないことを示すことが目的です。

このトピックでは、私の同僚が興味をしめせば、この記事はアイデアの継続的な開発があるかもしれません。


数学計算モードに関する一般的な情報

ストラテジーテスタ ウィンドウで計算モードが起動します。 これを行うには、ドロップダウンリストで同じ名前のメニュー項目を選択します。

図1. ストラテジーテスタでの数学計算モードの選択

このモードでは、制限された一連の関数のみが呼び出され、トレード環境 (シンボル、アカウント情報、トレードサーバーのプロパティ) は使用できません。 OnTester () は、ユーザーが特別なカスタム最適化基準を設定するために利用できるメインの call 関数になります。 他の標準の最適化基準と一緒に使用され、標準のユーザー戦略レポートに表示することができます。 以下のスクリーンショットで赤で概説されています:

 

図2. OnTester 関数で計算されたカスタム最適化基準

OnTester 関数によって返される値は選択可能で最適化ます。 簡単なEAでを実証しましょう:

//+------------------------------------------------------------------+
//|                                                OnTesterCheck.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
input double x = 0.01;
//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
{
   double ret = MathSin(x);
   return(ret);
}
//+------------------------------------------------------------------+


このコードには、インプットパラメータ x と、渡された引数からの正弦値を計算する OnTester 関数が含まれていません。 この場合は、 xです。 今度はこの関数を最適化してみてください。 これを行うには、ストラテジーテスタで「Slow complete algorithm」最適化モード、および前のシミュレーションモードを選択します: 「Math calculations」。

最適化パラメータの x の範囲を設定します: スタート-0.01、ステップ-0.01、ストップ-10。 すべての準備ができたら、ストラテジーテスターを実行します。 ほぼ即座に終えるでしょう。 その後、最適化チャートを開き、コンテキストメニューの「1D チャート」を選択します。 グラフィカルな解釈で正弦関数が表示されます:

図3. 正弦関数のグラフィック表現

このモードの特徴は、リソースの消費量が少ないことです。 ハードドライブ上の読み取り/書き込み操作が最小化され、テスターエージェントは、リクエストされたシンボルのクオートをダウンロードしていません。すべての計算は、OnTester 関数に焦点を当てています。

OnTester の高速ポテンシャルを考えると、簡単なシミュレーションを行うことができる十分な計算モジュールを作成することが可能です。 このモジュールの要素を次に示します。

  • テストのシンボルのヒストリー
  • 仮想ポジションシステム
  • 仮想ポジションを管理するためのトレーディングシステム
  • 結果分析システム

このモジュールは、1つのEAが使用するテストおよびテストシステム自体のすべての必要なデータを含んでいることを意味します。 クラウドの最適化が必要な場合には、このエキスパートを分散コンピューティングネットワークに渡すことができます。

システムの最初の部分の説明に、すなわち、テストのヒストリーを格納する方法に移りましょう。


数学的計算に基づいてテスターのシンボルヒストリーデータを保存

算術計算モードでは、トレーディングツールへのアクセスを意味しません。 CopyRates (シンボル (),...) のような関数の呼び出しは、ここでは無意味です。 ただし、シミュレーションにはヒストリカルデータが必要です。 この目的のために、必要なシンボルのクオートの履歴は、uchar [] 型の混入配列に格納できます。

uchar symbol[128394] = {0x98,0x32,0xa6,0xf7,0x64,0xbc...};

任意の種類のデータ (サウンド、イメージ、数字、および文字列) は、シンプルなバイトセットとして表すことができます。 バイトは8ビットから成る短いブロックです。 すべての情報は、バイトから成るシーケンスに "バッチ" として格納されます。 MQL5 には特殊なデータ型 (uchar) があり、値が1バイトだけを表すことができます。 したがって、100要素を持つ uchar 型の配列は100バイトを格納できます。

シンボルの引用は多くの足から成っています。 各足には、足の開始時刻、価格 (高値、安値、オープン、クローズ)、およびボリュームに関する情報があります。 各値は、適切な長さの変数に格納されます。 テーブルです:

データ型 バイト単位のサイズ
オープンタイム datetime 8
Open double 8
High double 8
Low double 8
Close double 8
ティック・ボリューム long  8
Spread   int 4
実ボリューム   long 8

1つの足を格納すると、60バイト、または60要素で構成される uchar 配列が必要になるので、計算が簡単にできます。 24時間外国為替相場は、1つのトレード日が1440分の足から成っています。 その結果、1年の1分間のヒストリーは約391680の足から成ります。 この数値を60バイトで乗算すると、圧縮されていない形式の1年分の履歴が約 23 MB もかかることがわかります。 これは多いでしょうか、少ないでしょうか。 現代の標準にそえば多くはないが、10年間データのEAをテストすることになった場合何が起きるでしょうか。 230 MB のデータを格納する必要があり、ネットワーク経由で配布することもあります。 これは、現代の基準では非常に膨大です。

したがって、何とかこの情報を圧縮する必要があります。 幸いにも、Zip アーカイブを扱うための特別なライブラリがあります。 このライブラリでは、さまざまな関数に加えて、圧縮結果をバイト配列に変換できるため、タスクが大幅に容易になります。

したがって、このアルゴリズムは、足の MqlRates 配列をロードし、バイト表現に変換し、Zip アーカイバで圧縮し、mqh ファイルで定義された uchar 配列として圧縮データを保存します。

クオートをバイト配列に変換するには、 union 型を介した変換システムが使用されます。 このシステムでは、1つのストレージフィールドに複数のデータ型を配置できます。 したがって、別のアドレスによって1つの型のデータにアクセスすることができます。 このような共用体は、MqlRates 構造と uchar 配列の2つの型を格納し、要素の数は MqlRates のサイズと等しくなります。 このシステムのしくみを理解するには、シンボル履歴を uchar バイト配列に変換する SaveRates.mq5 スクリプトの最初のバージョンを参照してください。

//+------------------------------------------------------------------+
//                                                   SaveRates mq5 |
//|                                 Copyright 2016, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2016, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include<Zip\Zip.mqh>
#include<ResourceCreator.mqh>
input ENUM_TIMEFRAMES MainPeriod;

union URateToByte
{
   MqlRates bar;
   uchar    bar_array[sizeof(MqlRates)];
}RateToByte;
//+------------------------------------------------------------------+
//スクリプトプログラム開始関数                                           |
//+------------------------------------------------------------------+
void OnStart()
{
   //--ダウンロードのクオート
   MqlRates rates[];
   int total = CopyRates(Symbol(), Period(), 0, 20000, rates);
   uchar symbol_array[];
   //--バイト表現に変換する
   ArrayResize(symbol_array, sizeof(MqlRates)*total);
   for(int i = 0, dst = 0; i < total; i++, dst +=sizeof(MqlRates))
   {
      RateToByte.bar = rates[i];
      ArrayCopy(symbol_array, RateToByte.bar_array, dst, 0, WHOLE_ARRAY);
   }
   //--zip アーカイブに圧縮する
   CZip Zip;
   CZipFile* file = new CZipFile(Symbol(), symbol_array);
   Zip.AddFile(file);
   uchar zip_symbol[];
   //--圧縮されたアーカイブのバイト表現を取得する
   Zip.ToCharArray(zip_symbol);
   //--mqh インクルードファイルとして書く
   CCreator creator;
   creator.ByteArrayToMqhArray(zip_symbol, "rates.mqh", "rates");
}
//+------------------------------------------------------------------+

このコードが実行されると、zip_symbol 配列には MqlRates 構造体の圧縮された配列 (クオートの圧縮履歴) が含まれます。 次に、圧縮された配列は、コンピュータのハードドライブ上の mqh ファイルとして格納されます。 詳細は以下に提供されます。

クオートのバイト表現を取得し、それを圧縮するだけでは不十分です。 この表現を uchar 配列として記録する必要があります。 この場合、配列はリソースの形式でロードする必要があり、つまりプログラムでコンパイルする必要があります。 この目的のために、この配列を含む特殊な mqh ヘッダーファイルを ASCII 文字のセットとして作成します。 これを行うには、特別なCResourceCreatorクラスを使用します。

//+------------------------------------------------------------------+
//                                               ResourceCreator mqh |
//|                                 Copyright 2017, Vasiliy Sokolov. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, Vasiliy Sokolov."
#property link      "http://www.mql5.com"
#include<Arrays\ArrayObj.mqh>
//+------------------------------------------------------------------+
//作成されたリソース配列の文字列識別子を格納。                            |
//+------------------------------------------------------------------+
class CResInfo : public CObject
{
public:
   string FileName;
   string MqhFileName;
   string ArrayName;
};
//+------------------------------------------------------------------+
//MQL リソースをバイト配列として作成                                     |
//+------------------------------------------------------------------+
class CCreator
{
private:
   int      m_common;
   bool     m_ch[256];
   string   ToMqhName(string name);
   void     CreateInclude(CArrayObj* list_info, string file_name);
public:
            CCreator(void);
   void     SetCommonDirectory(bool common);
   bool     FileToByteArray(string file_name, uchar& byte_array[]);
   bool     ByteArrayToMqhArray(uchar& byte_array[], string file_name, string array_name);
   void     DirectoryToMqhArray(string src_dir, string dst_dir, bool create_include = false);
};
//+------------------------------------------------------------------+
//デフォルトのコンストラクタ                                            |
//+------------------------------------------------------------------+
CCreator::CCreator(void) : m_common(FILE_COMMON)
{
   ArrayInitialize(m_ch, false);
   for(uchar i = '0'; i < '9'; i++)
      m_ch[i] = true;
   for(uchar i = 'A'; i < 'Z'; i++)
      m_ch[i] = true;
}
//+------------------------------------------------------------------+
//FILE_COMMON フラグを設定するか、または削除します。                      |
//+------------------------------------------------------------------+
CCreator::SetCommonDirectory(bool common)
{
   m_common = common ? FILE_COMMON : 0;   
}

//+------------------------------------------------------------------+
//src_dir ディレクトリ内のすべてのファイルを mqh ファイルに変換            |
//ファイルのバイト表現を含む                                            |
//+------------------------------------------------------------------+
void CCreator::DirectoryToMqhArray(string src_dir,string dst_dir, bool create_include = false)
{
   string file_name;
   string file_mqh;
   CArrayObj list_info;
   long h = FileFindFirst(src_dir+"\\*", file_name, m_common);
   if(h == INVALID_HANDLE)
   {
      printf("Directory" + src_dir + " is not found, or it does not contain files");
      return;
   }
   do
   {
      uchar array[];
      if(FileToByteArray(src_dir+file_name, array))
      {
         string norm_name = ToMqhName(file_name);
         file_mqh = dst_dir + norm_name + ".mqh";
         ByteArrayToMqhArray(array, file_mqh, "m_"+norm_name);
         printf("Create resource: " + file_mqh);
         //作成したリソースに関する情報を追加する
         CResInfo* info = new CResInfo();
         list_info.Add(info);
         info.FileName = file_name;
         info.MqhFileName = norm_name + ".mqh";
         info.ArrayName = "m_"+norm_name;
      }
   }while(FileFindNext(h, file_name));
   if(create_include)
      CreateInclude(&list_info, dst_dir+"include.mqh");
}
//+------------------------------------------------------------------+
/|生成されたすべてのファイルのインクルードを含む mqh ファイルを作成          |
//+------------------------------------------------------------------+
void CCreator::CreateInclude(CArrayObj *list_info, string file_name)
{
   int handle = FileOpen(file_name, FILE_WRITE|FILE_TXT|m_common);
   if(handle == INVALID_HANDLE)
   {
      printf("Failed to create the include file " + file_name);
      return;
   }
   //インクルードヘッダーを作成する
   for(int i = 0; i < list_info.Total(); i++)
   {
      CResInfo* info = list_info.At(i);
      string line = "#include \"" + info.MqhFileName + "\"\n";
      FileWriteString(handle, line);
   }
   //リソース配列を呼び出し元のコードにコピーするための関数を作成する
   FileWriteString(handle, "\n");
   FileWriteString(handle, "void CopyResource(string file_name, uchar &array[])\n");
   FileWriteString(handle, "{\n");
   for(int i = 0; i < list_info.Total(); i++)
   {
      CResInfo* info = list_info.At(i);
      if(i == 0)
         FileWriteString(handle, "   if(file_name == \"" + info.FileName + "\")\n");
      else
         FileWriteString(handle, "   else if(file_name == \"" + info.FileName + "\")\n");
      FileWriteString(handle,    "      ArrayCopy(array, " + info.ArrayName + ");\n");
   }
   FileWriteString(handle, "}\n");
   FileClose(handle);
}
//+------------------------------------------------------------------+
//渡された名前を MQL 変数の正しい名前に変換します                         |
//+------------------------------------------------------------------+
string CCreator::ToMqhName(string name)
{
   uchar in_array[];
   uchar out_array[];
   int total = StringToCharArray(name, in_array);
   ArrayResize(out_array, total);
   int t = 0;
   for(int i = 0; i < total; i++)
   {
      uchar ch = in_array[i];
      if(m_ch[ch])
         out_array[t++] = ch;
      else if(ch == ' ')
         out_array[t++] = '_';
      uchar d = out_array[t-1];
      int dbg = 4;
   }
   string line = CharArrayToString(out_array, 0, t);
   return line;
}
//+------------------------------------------------------------------+
//渡されたファイルのバイト表現                                          |
//byte_array 配列                                                    |
//+------------------------------------------------------------------+
bool CCreator::FileToByteArray(string file_name, uchar& byte_array[])
{
   int handle = FileOpen(file_name, FILE_READ|FILE_BIN|m_common);
   if(handle == -1)
   {
      printf("Failed to open file " + file_name + ". Reason: " + (string)GetLastError());
      return false;
   }
   FileReadArray(handle, byte_array, WHOLE_ARRAY);
   FileClose(handle);
   return true;
}
//+------------------------------------------------------------------+
//渡された byte_array バイト配列を名前付きの mqh ファイルに変換します      |
//file_name という名前の配列の説明を含む                                 |
//array_name                                                         |
//+------------------------------------------------------------------+
bool CCreator::ByteArrayToMqhArray(uchar& byte_array[], string file_name, string array_name)
{
   int size = ArraySize(byte_array);
   if(size == 0)
      return false;
   int handle = FileOpen(file_name, FILE_WRITE|FILE_TXT|m_common, "");
   if(handle == -1)
      return false;
   string strSize = (string)size;
   string strArray = "uchar " +array_name + "[" + strSize + "] = \n{\n";
   FileWriteString(handle, strArray);
   string line = "   ";
   int chaptersLine = 32;
   for(int i = 0; i < size; i++)
   {
      ushort ch = byte_array[i];
      line += (string)ch;
      if(i == size - 1)
         line += "\n";
      if(i>0 && i%chaptersLine == 0)
      {
         if(i < size-1)
            line += ",\n";
         FileWriteString(handle, line);
         line = "   ";
      }
      else if(i < size - 1)
         line += ",";
   }
   if(line != "")
      FileWriteString(handle, line);
   FileWriteString(handle, "};");
   FileClose(handle);
   return true;
}

操作にこだわり過ぎないようにし、関数を説明します。

  • ハードドライブ上の任意のファイルを読み取り、そのバイト表現を uchar 配列として mqh ファイルに格納します。
  • ドライブ上の任意のディレクトリを読み取り、このディレクトリにあるすべてのファイルのバイト表現を格納します。 各ファイルのバイト表現は、uchar 配列を含む別の mqh ファイルに格納されます。
  • エントリーとしてバイトの uchar 配列を受け取り、別の mqh ファイルに文字の配列として格納します。
  • 生成プロセス中に作成されたすべての mqh ファイルへのリンクを含む特別なヘッダーファイルを作成します。 また、配列名をエントリーとして受け取り、バイト表現を返す特別な関数が作成されます。 このアルゴリズムは、動的なコード生成を使用します。 

説明したクラスは、MQL プログラムの通常のリソース割り当てシステムに代わり強力です。

デフォルトでは、すべてのファイル操作は共有ファイルディレクトリ (FILE_COMMON) で行われます。 前の一覧からスクリプトを実行した場合、フォルダには新しい mqh ファイルがあります (ファイル名は ByteArrayToMqhArray メソッドの2番目のパラメータで定義されます)。 巨大なrates[] 配列 (配列名は、このメソッドの3番目のパラメータによって定義されます) が含まれます。 これは、ファイルの内容のスニペットです:


図4. MqlRates は、レートという名前の圧縮バイト配列として引用します

データ圧縮は正常に動作します。 EURUSD 通貨ペアの非圧縮1分足の履歴は、圧縮後に約 20 mb かかります (わずか 5 mb)。 ただし、MetaEditorでrates.mqlは開かない方がいいです。そのサイズは、この図よりもはるかに大きく、エディタがフリーズすることがあります。 しかし、心配しないでください。 コンパイル後、テキストはバイトに変換され、プログラムの実際のサイズは、格納された情報の実際の値 (この場合は 5 MB) だけ増加します。

ところで、この手法は、ex5 プログラムでは、クオートの履歴だけでなく、任意の型の必要な情報を格納するために使用することができます。


圧縮バイト配列からの MqlRates の読み込み

履歴が保存されるようになったので、最初に include ディレクティブを追加するだけで、MQL プログラムに含めることができます。

...
#include "rates.mqh"
...

mqh ファイルは、プログラム自体のソースディレクトリに移動する必要があることに注意してください。

データを含めるだけでは不十分です。 また、通常の MqlRates 配列にデータを逆にするための手順のブロックを記述する必要があります。 これを行うには、関数 LoadRates を実装してみましょう。 空の MqlRates 配列への参照をエントリーとして取得します。 実行されると、この配列には、圧縮された配列から読み込まれた従来の MqlRates クオートが含まれます。 この関数のコードを次に示します。

//+------------------------------------------------------------------+
//   Mtester mqh                                                     |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#include<Zip\Zip.mqh>
#include "rates.mqh"
//+------------------------------------------------------------------+
//| Casting MqlRates to uchar[]                                      |
//+------------------------------------------------------------------+
union URateToByte
{
   MqlRates bar;
   uchar    bar_array[sizeof(MqlRates)];
};
//+------------------------------------------------------------------+
//圧縮されたデータをクオートの MqlRates 配列に変換する                    |
//受信した足の数を返す。                                                |
//失敗                                                                |
//+------------------------------------------------------------------+
int LoadRates(string symbol_name, MqlRates &mql_rates[])
{
   CZip Zip;
   Zip.CreateFromCharArray(rates);
   CZipFile* file = dynamic_cast<CZipFile*>(Zip.ElementByName(symbol_name));
   if(file == NULL)
      return -1;
   uchar array_rates[];
   file.GetUnpackFile(array_rates);
   URateToByte RateToBar;
   ArrayResize(mql_rates, ArraySize(array_rates)/sizeof(MqlRates));
   for(int start = 0, i = 0; start < ArraySize(array_rates); start += sizeof(MqlRates), i++)
   {
      ArrayCopy(RateToBar.bar_array, array_rates, 0, start, sizeof(MqlRates));
      mql_rates[i] = RateToBar.bar;
   }
   return ArraySize(mql_rates);
}
//+------------------------------------------------------------------+


この関数は、 Mtester mqhファイルにあります。 数学計算モードでするための最初の関数になります。 新しい関数が最終的に Mtester の mqh ファイルに追加され、本格的な数学的戦略テストエンジンになることがあります。

数学の計算モードの戦略を書いてみましょう。 2つの関数のみを実行します: OnTester 関数内のすべての終値の平均値を計算します。 計算結果は、MetaTrader に返されます。

//+------------------------------------------------------------------+
//|                                                      MExpert.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include「Mtester mqh」
//+------------------------------------------------------------------+
//テストで使用するクオート                                              |
//+------------------------------------------------------------------+
MqlRates Rates[];
//+------------------------------------------------------------------+
//エキスパート初期化関数                                                |
//+------------------------------------------------------------------+
int()
{
   //--指定したシンボルのクオートを読み込み。
   if(LoadRates(Symbol(), Rates)==-1)
   {
      printf("Quotes for symbol " + Symbol() + " not found. Create the appropriate quotes resource.");
      return INIT_PARAMETERS_INCORRECT;
   }
   printf("Loaded " + (string)ArraySize(Rates) + " bars for symbol " + Symbol());
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Tester function                                                  |
//+------------------------------------------------------------------+
double OnTester()
{
   double mean = 0.0;
   for(int i = 0; i < ArraySize(Rates); i++)
      mean += Rates[i].close;
   mean /= ArraySize(Rates);
   return mean;
}
//+------------------------------------------------------------------+

エキスパートがコンパイルされた後、ストラテジーテスターにロードし、「数学計算」モードを選択します。 テストを実行し、ジャーナルを有効にします。

2017.12.13 15:12:25.127 Core 2  math calculations test of Experts\MTester\MExpert.ex5 started
2017.12.13 15:12:25.127 Core 2  Loaded 354159 bars for symbol EURUSD
2017.12.13 15:12:25.127 Core 2  OnTester result 1.126596405653942
2017.12.13 15:12:25.127 Core 2  EURUSD,M15: mathematical test passed in 0:00:00.733 (total tester working time 0:00:01.342)
2017.12.13 15:12:25.127 Core 2  217 Mb memory used

ご覧の通り、EA は期待どおりに働きました。 読み込まれた足の数に関するレコードで示されているように、すべてのクオートが正しく読み込まれました。 また、すべての足を正しく繰り返し処理して、呼び出し元のスレッドに返された平均値を計算します。 最後の年のすべての EURUSD の引用の平均価格は1.12660 に等しいことが判明した。


移動平均に基づくプロトタイプ戦略

結果が印象的です: データを受信し、圧縮され、その後、EAに読み込まれた静的な uchar 配列として格納し、データが解凍され、クオートの配列に変換されます。 さて、最初の有用な戦略を記述しましょう。 2つの移動平均のクロスオーバーに基づいて古典的なバージョンを使用してみましょう。 この戦略は簡単に実装できます。 算術計算モードでのトレード環境は利用できないため、iMA のようなインジケータを直接呼び出すことはできません。 代わりに、移動平均の値を手動で計算する必要があります。 このテストモードの主なタスクは、最大加速度です。 したがって、使用されるアルゴリズムはすべて高速に動作する必要があります。 移動平均の計算は、 O (1)の計算上の複雑さを伴うシンプルな問題のクラスを指すことが知られています。 これは、平均値の計算速度が移動平均期間に依存しないことを意味します。 この目的のために、リングバッファ用のライブラリが使用されます。 このアルゴリズムの詳細については、別の記事で説明します。

最初のエキスパートのテンプレートを作成します。

//+------------------------------------------------------------------+
//|                                                      MExpert.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include「Mtester mqh」
#include<RingBuffer\RiSma.mqh>

input int PeriodFastMa = 9;
input int PeriodSlowMa = 16;

CRiSMA FastMA;    //高速移動平均を計算するためのリングバッファ
CRiSMA SlowMA;    //低速移動平均を計算するためのリングバッファ
//+------------------------------------------------------------------+
//テストで使用するクオート                                              |
//+------------------------------------------------------------------+
MqlRates Rates[];
//+------------------------------------------------------------------+
//エキスパート初期化関数                                                |
//+------------------------------------------------------------------+
int()
{
   //--パラメータの組み合わせの正当性を確認する
   //-- --高速移動平均は低速移動平均より小さい場合はない
   if(PeriodFastMa >= PeriodSlowMa)
      return INIT_PARAMETERS_INCORRECT;
   //--リングバッファの期間を初期化します。
   FastMA.SetMaxTotal(PeriodFastMa);
   SlowMA.SetMaxTotal(PeriodSlowMa);
   //--指定したシンボルのクオートを読み込み。
   if(LoadRates(Symbol(), Rates)==-1)
   {
      printf("Quotes for symbol " + Symbol() + " not found. Create the appropriate quotes resource.");
      return INIT_FAILED;
   }
   printf("Loaded " + (string)ArraySize(Rates) + " bars for symbol " + Symbol());
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//戦略説明                                                            |
//+------------------------------------------------------------------+
double OnTester()
{
   for(int i = 1; i < ArraySize(Rates); i++)
   {
      FastMA.AddValue(Rates[i].close);
      SlowMA.AddValue(Rates[i].close);
      //EA のロジックはここに配置されます
   }
   return 0.0;
}
//+------------------------------------------------------------------+


高速移動平均、低速移動平均の平均期間を持つ2つのパラメータを定義します。 次に、平均値を計算するために2つのリングバッファが宣言されます。 初期化ブロックは、エントリーされたパラメータの正当性を検証します。 パラメータはユーザによって設定されるのではなく、最適化モードでストラテジーテスタによって自動的に選択されるため、多くの場合、パラメータは正しく結合されません。 この場合、高速MA は低速MA よりも小さいことが判明することがあります。 この混乱を回避し、最適化の時間を節約するために、そのようなパスは、開始する前であっても除去します。 INIT_PARAMETERS_INCORRECT 定数を返すことによって行われます。

バッファが初期化されると、パラメータのチェックとクオートのロードは、テスト自体を実行するための時間になります: OnTester 関数が開始されます。 その中で、主なテストは ' for ' ブロック内になります。 このコードは、FastMA リングバッファの平均値が SlowMA の平均値より大きい場合、ロングポジションを開く必要があることを示しており、その逆も同様です。 しかし、このような長いとショートポジションを開くためのトレーディングモジュールはまだありません。 まだ書かれていません。 


仮想ポジションクラス

前に述べたように、数学の計算モードは、任意の戦略の計算に適合していません。 したがって、トレード関数がありません。 また、MetaTrader 環境も使用できません。 「ポジション」という用語は全く無意味であり、存在しません。 したがって、メタトレーダーのポジションの簡略化されたアナログを作成する必要があります。 必要な情報のみが含まれます。 これを行うには、次のフィールドを持つクラスを作成します。 

  • position opening time;
  • position Open price;
  • position closing time;
  • position Close price;
  • position volume;
  • ポジションの開始の時に広がる;
  • ポジションの方向。

おそらく、今後追加情報を補足しますが、今のところ十分です。

//+------------------------------------------------------------------+
//Mtester mqh |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#include<Object.mqh>
#include "rates.mqh"
#include"Type2Char mqh"
//+------------------------------------------------------------------+
//数学計算に基づくテスターの仮想ポジションクラス                           |
//+------------------------------------------------------------------+
class CMposition : public CObject
{
private:
   datetime    m_time_open;
   datetime    m_time_close;
   double      m_price_open;
   double      m_price_close;
   double      m_volume;
   int         m_spread;
   ENUM_POSITION_TYPE m_type;
public:
               CMposition(void);
   static int  Sizeof(void);
   bool        IsActive(void);
   datetime    TimeOpen(void);
   datetime    TimeClose(void);
   double      PriceOpen(void);
   double      PriceClose(void);
   double      Volume(void);
   double      Profit(void);
   ENUM_POSITION_TYPE PositionType(void);
   static CMposition*  CreateOnBarOpen(MqlRates& bar, ENUM_POSITION_TYPE pos_type, double vol);
   void        CloseOnBarOpen(MqlRates& bar);
};
//+------------------------------------------------------------------+
//1つの CMposition のポジションは、データの45バイトを取る                 |
//+------------------------------------------------------------------+
intCMposition:: Sizeof (void)
{
   return 48;
}
CMposition::CMposition(void):m_time_open(0),
                             m_time_close(0),
                             m_price_open(0.0),
                             m_price_close(0.0),
                             m_volume(0.0)
{
}
//+------------------------------------------------------------------+
//ポジションが閉じている場合は True                                    、|
//+------------------------------------------------------------------+
boolCMposition:: IsActive ()
{
   return m_time_close == 0;
}
//+------------------------------------------------------------------+
//ポジションの開始時間                                                  |
//+------------------------------------------------------------------+
datetimeCMposition:: TimeOpen (void)
{
   return m_time_open;
}
//+------------------------------------------------------------------+
//ポジション決済時間                                                   |
//+------------------------------------------------------------------+
datetimeCMposition:: TimeClose (void)
{
   return m_time_close;
}
//+------------------------------------------------------------------+
//| Position Open price                                              |
//+------------------------------------------------------------------+
double CMposition::PriceOpen(void)
{
   return m_price_open;
}
//+------------------------------------------------------------------+
//| Position Close price                                             |
//+------------------------------------------------------------------+
double CMposition::PriceClose(void)
{
   return m_price_close;
}
//+------------------------------------------------------------------+
//ポジションボリューム                                                  |
//+------------------------------------------------------------------+
double CMposition::Volume(void)
{
   return m_volume;
}
//+------------------------------------------------------------------+
//トレードポジションのタイプを返します。                                  |
//+------------------------------------------------------------------+
ENUM_POSITION_TYPE CMposition::PositionType(void)
{
   return m_type;
}
//+------------------------------------------------------------------+
//ポジション利益                                                       |
//+------------------------------------------------------------------+
double CMposition::Profit(void)
{
   if(IsActive())
      return 0.0;
   int sign = m_type == POSITION_TYPE_BUY ? 1 : -1;
   double pips = (m_price_close - m_price_open)*sign;
   double profit = pips*m_volume;
   return profit;
}
//+------------------------------------------------------------------+
//渡されたパラメータに基づいてポジションを作成する                         |
//+------------------------------------------------------------------+
static CMposition* CMposition::CreateOnBarOpen(MqlRates &bar, ENUM_POSITION_TYPE pos_type, double volume)
{
   CMposition* position = new CMposition();
   position.m_time_open = bar.time;
   position.m_price_open = bar.open;
   position.m_volume = volume;
   position.m_type = pos_type;
   return position;
}
//+------------------------------------------------------------------+
//渡された足の始値でポジションを閉じます                                  |
//+------------------------------------------------------------------+
void CMposition::CloseOnBarOpen(MqlRates &bar)
{
   m_price_close = bar.open;
   m_time_close = bar.time;
}
//+------------------------------------------------------------------+


この実装では、ポジションの作成が最も興味深い点です。 そのフィールドは外部の変更から保護されますが、静的 CreateOnBarOpen メソッドは、適切に set パラメータを使用してクラスオブジェクトを返します。 このメソッドを参照せずに、このクラスのオブジェクトを作成する方法はありません。 意図しない変更からデータが保護されます。


トレーディングブロッククラス

ポジションを管理するためのクラスを作成する必要があります。 メタトレーダーの関数のアナログになります。 明らかに、ポジション自体はこのモジュールにも格納する必要があります。 2つの CArrayObj コレクションは、この目的に意図されています。: アクティブ-戦略のアクティブなポジションを格納するために必要、ヒストリー-ヒストリーの中でのポジションが含まれます。

このクラスには、ポジションを開始および決済するための特別なメソッドもあります。

  • EntryAtOpenBar —必要な方向とボリュームを持つポジションを開きます。
  • CloseAtOpenBar —指定したインデックスポジションを閉じます。

このポジションは、渡された足の価格で開かれ、閉じられます。 残念ながら、このアプローチは、"未来へのぞき" を防ぐことはありませんが、非常に高速で簡単です。

CMtrade クラスは (そう呼ぶことにします) 非常に簡単であることが判明しました:

//+------------------------------------------------------------------+
//Mtester mqh |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#include<Object.mqh>
#include<Arrays\ArrayObj.mqh>
#include"Mposition mqh"
//+------------------------------------------------------------------+
//仮想ポジションを開くためのトレーディングモジュール                       |
//+------------------------------------------------------------------+
class CMtrade
{
public:
               CMtrade(void);
               ~CMtrade();
   CArrayObj   Active;
   CArrayObj   History;
   void        EntryAtOpenBar(MqlRates &bar, ENUM_POSITION_TYPE pos_type, double volume);
   void        CloseAtOpenBar(MqlRates &bar, int pos_index);
};
//+------------------------------------------------------------------+
//デフォルトのコンストラクタ                                            |
//+------------------------------------------------------------------+
CMtrade::CMtrade(void)
{
   Active.FreeMode(false);
}
//+------------------------------------------------------------------+
//残りのすべてのポジションを削除する                                     |
//+------------------------------------------------------------------+
CMtrade::~CMtrade()
{
   Active.FreeMode(true);
   Active.Clear();
}
//+------------------------------------------------------------------+
//新しいポジションを作成し、アクティブなリストに追加します                  |
//ポジション.                                                         |
//+------------------------------------------------------------------+
void CMtrade::EntryAtOpenBar(MqlRates &bar, ENUM_POSITION_TYPE pos_type, double volume)
{
   CMposition* pos = CMposition::CreateOnBarOpen(bar, pos_type, volume);
   Active.Add(pos);
}
//+------------------------------------------------------------------+
//オープン時に pos_index インデックスで仮想ポジションを閉じます            |
//渡された足の価格                                                     |
//+------------------------------------------------------------------+
void CMtrade::CloseAtOpenBar(MqlRates &bar, int pos_index)
{
   CMposition* pos = Active.At(pos_index);
   pos.CloseOnBarOpen(bar);
   Active.Delete(pos_index);
   History.Add(pos);
}
//+------------------------------------------------------------------+


 実際には、すべての関数が2つの関数に削減されます。

  1. static CMposition:: CreateOnBarOpen メソッドから新しいポジションを取得し、アクティブリスト (EntryOnOpenBar メソッド) に追加します。
  2. 選択したポジションをアクティブなポジションのリストから履歴ポジションのリストに移動し、移動したポジションはstatic CMposition:: CLoseOnBarOpen メソッドによって閉じられます。

トレーディングクラスが作成され、エキスパートをテストするためのすべてのコンポーネントが利用可能になりました。


EAの最初のテスト オプティマイザでのタスク

一緒にすべてのコンポーネントを入れてみましょう。 ここでは、数学的なオプティマイザの2つの移動平均に基づいた戦略のソースコードです。 

//+------------------------------------------------------------------+
//|                                                      MExpert.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
#include<RingBuffer\RiSma.mqh>
#include「Mtester mqh」

input int PeriodFastMa = 9;
input int PeriodSlowMa = 16;

CRiSMA FastMA;    //高速移動平均を計算するためのリングバッファ
CRiSMA SlowMA;    //低速移動平均を計算するためのリングバッファ
CMtrade Trade;    //仮想計算用トレーディングモジュール

//+------------------------------------------------------------------+
//テストで使用するクオート                                              |
//+------------------------------------------------------------------+
MqlRates Rates[];
//+------------------------------------------------------------------+
//エキスパート初期化関数                                                |
//+------------------------------------------------------------------+
int()
{
   //--パラメータの組み合わせの正当性を確認する
   //-- --高速移動平均は低速移動平均より小さい場合はない
   //f (PeriodFastMa > = PeriodSlowMa)
   //NIT_PARAMETERS_INCORRECT を返します。
   //--リングバッファの期間を初期化します。
   FastMA.SetMaxTotal(PeriodFastMa);
   SlowMA.SetMaxTotal(PeriodSlowMa);
   //--指定したシンボルのクオートを読み込み。
   if(LoadRates(Symbol(), Rates)==-1)
   {
      printf("Quotes for symbol " + Symbol() + " not found. Create the appropriate quotes resource.");
      return INIT_FAILED;
   }
   printf("Loaded " + (string)ArraySize(Rates) + " bars for symbol " + Symbol());
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//戦略説明                                                            |
//+------------------------------------------------------------------+
double OnTester()
{
   for(int i = 1; i < ArraySize(Rates)-1; i++)
   {
      MqlRates bar = Rates[i];
      FastMA.AddValue(Rates[i].close);
      SlowMA.AddValue(Rates[i].close);
      ENUM_POSITION_TYPE pos_type = FastMA.SMA() > SlowMA.SMA() ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
      //--現在のシグナルとは反対のポジションを閉じる
      for(int k = Trade.Active.Total()-1; k >= 0 ; k--)
      {
         CMposition* pos = Trade.Active.At(k);
         if(pos.PositionType() != pos_type)
            Trade.CloseAtOpenBar(Rates[i+1], k);   
      }
      //--ポジションがない場合は、指定された方向に新しく開きます。
      if(Trade.Active.Total() == 0)
         Trade.EntryAtOpenBar(Rates[i+1], pos_type, 1.0);
   }
   double profit = 0.0;
   for(int i = 0; i < Trade.History.Total(); i++)
   {
      CMposition* pos = Trade.History.At(i);
      profit += pos.Profit();
   }
   return profit;
}
//+------------------------------------------------------------------+


OnTester 関数が完了しました。 コードは非常に簡単です。 その操作を一歩一歩検討してみましょう。

  1. for ' サイクルはすべてのクオートを繰り返し処理します。
  2. 取引の現在の方向性は、サイクルの中で決定されます: 高速SMAは、低速SMAの上にある場合に買い、それ以外の場合は売りです。
  3. アクティブなトレードはすべて繰り返し処理され、方向が現在の方向と一致しない場合は閉じられます。
  4. ポジションがない場合は、指定した方向に新しいポジションが開かれます。
  5. 検索の終了時に、すべてのクローズポジションが繰り返され、その合計利益が計算され、後でストラテジーテスタに戻されます。

このエキスパートは、オプティマイザでテストできるようになりました。 数学の計算モードでを実行します。 最適化が機能することを確認するには、次の図に示すように、移動平均パラメータの完全な検索を行います。 

図5. パラメータの最適化フィールドの選択

指定された例では、1分間の履歴を処理する1000の最適化パスを示しています。 にもかかわらず、計算はこのモードで多くの時間がかかることはありません。 7 プロセッサを搭載したコンピュータでは、全体の最適化は、チャートが構築された後、約1分かかりました。

図6. 「スローコンプリートアルゴリズム」モードでの1000パスのチャート。

しかし、これまでのところ、得られた結果を分析するためのツールは非常に少ない。 実際には、現在持っているすべての仮想利益を反映した1つの番号です。 このような状況を解決するには、カスタム最適化データ形式を開発し、生成およびロードするためのメカニズムを考案する必要があります。 こだわってみましょう。

フレーム機構を使用したカスタム最適化結果の保存

MetaTrader5は、カスタムデータを扱うための非常に高度なテクニックを実装しています。 いわゆるフレームの生成と取得のメカニズムに基づいています。 本質的に通常のバイナリデータであり、別々の値として、または値の配列として配置されます。 たとえば、最適化中に、任意のサイズのデータの配列を生成し、MetaTrader5ストラテジーテスターに送信することができます。 この配列に含まれるデータは、FrameNext 関数を使用して読み取ることができ、さらに、たとえば、画面に表示される処理されます。 フレームを使用するタスクは、最適化モードでのみ可能で、OnTesterInit ()、OnTesterDeinit ()、および OnTesterPass () の3つの関数内でのみ使用できます。 パラメータはなく、戻り値もありません。 しかし、すべては見た目以上に簡単です。 これを説明するために、フレームを操作するための一般的なアルゴリズムを示すシンプルスクリプトを作成しましょう。

//+------------------------------------------------------------------+
//                                              OnTesterSample mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2017, MetaQuotes Software Corp."
#property link      "http://www.mql5.com"
#property version   "1.00"
input int Param = 1;
//+------------------------------------------------------------------+
//| OnTesterInit function                                            |
//+------------------------------------------------------------------+
void OnTesterInit()
{
   printf("Optimization started");      
}
//+------------------------------------------------------------------+
//作戦のパスはここに起こる                                              |
//+------------------------------------------------------------------+
double OnTester()
{
   uchar data[5] = {1,2,3,4,5};        //フレームのデータを生成
   FrameAdd("sample", 1, Param, data); //指定されたデータを使用して新しいフレームを作成
   return 3.0;
}
//+------------------------------------------------------------------+
//最後に追加された最適化フレームはここで取得することができます               |
//+------------------------------------------------------------------+
void OnTesterPass()
{
   ulong pass = 0;
   string name = "";
   ulong id = 0;
   double value = 0.0;
   uchar data[];
   FrameNext(pass, name, id, value, data);
   //--パスファイルを作成し、zip アーカイブに追加します。
   printf("Received new frame # " + (string)pass + ". Name: " + (string)name + " ID: " + (string)id + " Value of Param: " + DoubleToString(value, 0));
}
//+------------------------------------------------------------------+
//| OnTesterDeinit function                                          |
//+------------------------------------------------------------------+
void OnTesterDeinit()
{
   printf("Optimization complete");
}
//+------------------------------------------------------------------+

ストラテジーテスターでこのコードを実行するには、選択した math 計算モードを使用します。 最適化モードとして "低速完全アルゴリズム" を設定します。 パラメータ Param は、10 ~ 90 の範囲で変更されます。

新しいフレームの受信に関するメッセージは、最適化の開始直後に表示されます。 最適化の開始と終了は、特別なイベントを通じても追跡されます。 アプリケーションログ:

2017.12.19 16:58:08.101 OnTesterSample (EURUSD,M15)     Optimization started
2017.12.19 16:58:08.389 OnTesterSample (EURUSD,M15)     Received new frame # 1. Name: sample ID: 1 Value of Param: 20
2017.12.19 16:58:08.396 OnTesterSample (EURUSD,M15)     Received new frame # 0. Name: sample ID: 1 Value of Param: 10
2017.12.19 16:58:08.408 OnTesterSample (EURUSD,M15)     Received new frame # 4. Name: sample ID: 1 Value of Param: 50
2017.12.19 16:58:08.426 OnTesterSample (EURUSD,M15)     Received new frame # 5. Name: sample ID: 1 Value of Param: 60
2017.12.19 16:58:08.426 OnTesterSample (EURUSD,M15)     Received new frame # 2. Name: sample ID: 1 Value of Param: 30
2017.12.19 16:58:08.432 OnTesterSample (EURUSD,M15)     Received new frame # 3. Name: sample ID: 1 Value of Param: 40
2017.12.19 16:58:08.443 OnTesterSample (EURUSD,M15)     Received new frame # 6. Name: sample ID: 1 Value of Param: 70
2017.12.19 16:58:08.444 OnTesterSample (EURUSD,M15)     Received new frame # 7. Name: sample ID: 1 Value of Param: 80
2017.12.19 16:58:08.450 OnTesterSample (EURUSD,M15)     Received new frame # 8. Name: sample ID: 1 Value of Param: 90
2017.12.19 16:58:08.794 OnTesterSample (EURUSD,M15)     Optimization complete

最も興味深いのは、フレーム番号、その識別子、および Param パラメータの値に関する情報を表示するメッセージです。 すべてのこの情報は、FrameNext 関数を使用して取得することができます。

このモードの興味深い関数は、 エキスパートのダブルスタートです。 コード内のイベントハンドラを持つエキスパートは、戦略オプティマイザーで最初に2回開始され、その後、チャートのリアルタイムになります。 オプティマイザのエキスパートは新しいデータを生成しますが、チャート上で実行されているエキスパートはこれを受け取ります。 したがって、エキスパートのソースコードは、同じ場所に配置されている場合でも、エキスパートの異なるインスタンスによって処理されます。

OnTesterPass 関数でデータを受信すると、どのようなメソッドでも処理できます。 テストサンプルでは、データは printf 関数を使用してコンソールに出力されます。 しかし、実装されるべきデータ処理は、はるかに複雑であることが判明するかもしれません。 これは次のセクションで考慮されます。


ポジション履歴のバイト表現を取得 データをフレームに保存

フレーム機構は、情報を保存、処理、および配布するための便利なメソッドを提供します。 ただし、この情報自体を生成する必要があります。 この例の配列では、値1、2、3、4、5を持つシンプルなstatic uchar 配列でした。 このようなデータはあまり使用されていません。 しかし、バイト配列は任意の長さを持ち、任意のデータを格納できます。 これを行うには、カスタムデータ型を uchar 型のバイト配列に変換する必要があります。 似たようなものがすでに MqlRates に、クオートがバイト配列に保存された場所で行われています。 カスタムデータに対しても同じことが行われます。

カスタムストラテジーテスターは、2つの部分で構成されています。 最初の部分はデータを生成し、2つ目はデータを分析し、ユーザーフレンドリな形式で表示します。 また、戦略分析の主要な情報は、ヒストリーのすべてのトレードを分析することによって得ることができることは明らかです。 したがって、各実行の終了時に、すべてのトレードは、後で新しいフレームに追加されるバイト配列に変換されます。 OnTesterPass () 関数でこのようなフレームを受信した後、以前に受け取ったものを追加することが可能であり、フレームの全体のコレクションを作成します。 

ポジションに関するデータをバイト配列に変換するだけでなく、データを抽出することも必要になります。 これは、データ型ごとに2 つのプロシージャが必要です。 

  • カスタム型をバイト配列に変換する手順
  • バイト配列をカスタム型に変換する手順。

すでにポジションの2つのコレクションと CMtrade トレードモジュールを持っています。-アクティブとヒストリー。 ヒストリー的なポジションに焦点を当てることができます。 仮想ポジションを変換する手順は、対応するメソッドとして書き込まれます。

ポジションをバイト配列に変換するためのメソッド:

//+------------------------------------------------------------------+
//配列の形式でポジションをバイト表現に変換する                            |
//+------------------------------------------------------------------+
int CMposition::ToCharArray(int dst_start, uchar &array[])
{
   int offset = dst_start;
   //-- Copy time open position
   type2char.time_value = m_time_open;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(datetime));
   offset += sizeof(datetime);
   //-- Copy time close position
   type2char.time_value = m_time_close;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(datetime));
   offset += sizeof(datetime);
   //--コピー価格のオープンポジション
   type2char.double_value = m_price_open;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double));
   offset += sizeof(double);
   //--価格の終値をコピー
   type2char.double_value = m_price_close;  
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double));
   offset += sizeof(double);
   //--コピーボリュームのポジション
   type2char.double_value = m_volume; 
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(double));
   offset += sizeof(double);
   //--スプレッドシンボルのコピー
   type2char.int_value = m_spread;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(int));
   offset += sizeof(int);
   //--ポジションのコピーのタイプ
   type2char.int_value = m_type;
   ArrayCopy(array, type2char.char_array, offset, 0, sizeof(char));
   offset += sizeof(int);
   //--最後のオフセットを返す
   return offset;
}

 逆の手順:

//+------------------------------------------------------------------+
//バイト配列からポジションを読み込む                                     |
//+------------------------------------------------------------------+
int CMposition::FromCharArray(int dst_start, uchar &array[])
{
   int offset = dst_start;
   //-- Copy time open position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(datetime));
   m_time_open = type2char.time_value;
   offset += sizeof(datetime);
   //-- Copy time close position
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(datetime));
   m_time_close = type2char.time_value;
   offset += sizeof(datetime);
   //--コピー価格のオープンポジション
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(double));
   m_price_open = type2char.double_value;
   offset += sizeof(double);
   //--価格の終値をコピー
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(double));
   m_price_close = type2char.double_value;
   offset += sizeof(double);
   //--コピーボリュームのポジション
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(double));
   m_volume = type2char.double_value;
   offset += sizeof(double);
   //--スプレッドシンボルのコピー
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(int));
   m_spread = type2char.int_value;
   offset += sizeof(int);
   //--ポジションのコピーのタイプ
   ArrayCopy(type2char.char_array, array, 0, offset, sizeof(int));
   m_type = (ENUM_POSITION_TYPE)type2char.int_value;
   offset += sizeof(int);
   //--最後のオフセットを返す
   return offset;
}

TypeToChar ユニオン (そのインスタンス type2char を使用して) は、両方の変換プロシージャの中核となります。

//+------------------------------------------------------------------+
//シンプル型をバイト配列に変換する                                       |
//+------------------------------------------------------------------+
union TypeToChar
{
   uchar    char_array[128];
   int      int_value;
   double   double_value;
   float    float_value;
   long     long_value;
   short    short_value;
   bool     bool_value;
   datetime time_value;
   char     char_value;
};

すべては引用の転換のセクションで論議される RateToByte 連合に類似しています。 

すべてのプロシージャは、すべての閉じた仮想ポジションのデータを含むグローバル配列からのデータの読み込みを許可するように設計されています。 メモリの追加コピーを必要としない、非常に効率的な網羅的アルゴリズムを編成できます。

CMTrade クラスは、履歴内のすべてのポジションをループします。 ヒストリー的なポジションのコレクションを格納するものであることを考慮して、論理的です。 CMposition に似たクラスは、次の2つの方向で動作します: 履歴のポジションのコレクションを uchar 配列に変換し、また逆のプロシージャを実行します: バイト配列から履歴ポジションのリストをロードします。

コレクションをバイト配列に変換する手順は次のとおりです。

//+------------------------------------------------------------------+
//履歴内のポジションのリストを圧縮 zip アーカイブに変換します               |
//バイト配列の形式。 成功した場合は true を返します                       |
//それ以外の場合は false。                                             |
//+------------------------------------------------------------------+
bool CMtrade::ToCharArray(uchar &array[])
{
   int total_size = CMposition::Sizeof()*History.Total();
   if(total_size == 0)
   {
      printf(__FUNCTION__ +  ": Received  array is empty");
      return false;
   }
   if(ArraySize(array) != total_size && ArrayResize(array, total_size) != total_size)
   {
      printf(__FUNCTION__ +  ": failed resized received array");
      return false;
   }
   //--バイトストリームにポジションを格納する
   for(int offset = 0, i = 0; offset < total_size; i++)
   {
      CMposition* pos = History.At(i);
      offset = pos.ToCharArray(offset, array);
   }
   return true;
}

逆の手順:

//+------------------------------------------------------------------+
//圧縮された zip アーカイブからのヒストリーポジションのリストをロードします   |
//バイト配列の形式で渡されます。 成功した場合は true を返します             |
//それ以外の場合は false。                                             |
//+------------------------------------------------------------------+
bool CMtrade::FromCharArray(uchar &array[], bool erase_prev_pos = true)
{
   if(ArraySize(array) == 0)
   {
      printf(__FUNCTION__ +  ": Received  array is empty");
      return false;
   }
   //--バイトストリームのサイズは、ポジションのバイト表現と正確に一致する必要があります
   int pos_total = ArraySize(array)/CMposition::Sizeof();
   if(ArraySize(array)%CMposition::Sizeof() != 0)
   {
      printf(__FUNCTION__ +  ": Wrong size of received  array");
      return false;
   }
   if(erase_prev_pos)
      History.Clear();
   //--バイトストリームからすべてのポジションを復元する
   for(int offset = 0; offset < ArraySize(array);)
   {
      CMposition* pos = new CMposition();
      offset = pos.FromCharArray(offset, array);
      History.Add(pos);
   }
   return History.Total() > 0;
}

すべての要素をまとめるには、パスの履歴ポジションのバイト表現を取得し、フレームに保存するだけで十分です。

//+------------------------------------------------------------------+
//戦略説明                                                            |
//+------------------------------------------------------------------+
double OnTester()
{
   for(int i = 1; i < ArraySize(Rates)-1; i++)
   {
      MqlRates bar = Rates[i];
      FastMA.AddValue(Rates[i].close);
      SlowMA.AddValue(Rates[i].close);
      ENUM_POSITION_TYPE pos_type = FastMA.SMA() > SlowMA.SMA() ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
      //--現在のシグナルとは反対のポジションを閉じる
      for(int k = Trade.Active.Total()-1; k >= 0 ; k--)
      {
         CMposition* pos = Trade.Active.At(k);
         if(pos.PositionType() != pos_type)
            Trade.CloseAtOpenBar(Rates[i+1], k);   
      }
      //--ポジションがない場合は、指定された方向に新しく開きます。
      if(Trade.Active.Total() == 0)
         Trade.EntryAtOpenBar(Rates[i+1], pos_type, 1.0);
   }
   uchar array[];
   //--ヒストリー的なポジションのバイト表現を得る
   Trade.ToCharArray(array); 
   //--フレームにバイト表現をロードし、以降の処理にを送る
   FrameAdd(MTESTER_STR, MTESTER_ID, 0.0, array);  
   return Trade.History.Total();
}

フレームが形成され、処理の OnTesterPass () プロシージャに送られたら、次にフレームをどうするか把握することは必要です。 前に述べたように、このストラテジーテスターは、データ生成ブロックと収集された情報分析ブロックの2つの部分で構成されています。 このような解析では、生成されたすべてのフレームを便利な形式で保存する必要があるため、この形式を後で分析することもできます。 zip アーカイブを使用して行われます。 最初に、効果的にデータを圧縮し、取引の情報が多くのスペースを取らないことを意味します。 第2に、便利なファイルシステムを提供します。 各パスは、1つの zip アーカイブ内に別個のファイルとして格納できます。

そこで、フレームのバイト内容を zip アーカイブに変換する手順を作成してみましょう。

//+------------------------------------------------------------------+
//各新しいパスを zip アーカイブに追加する                                |
//+------------------------------------------------------------------+
void OnTesterPass()
{
   ulong pass = 0;
   string name = "";
   ulong id = 0;
   double value = 0.0;
   uchar data[];
   FrameNext(pass, name, id, value, data);
   //--パスファイルを作成し、zip アーカイブに追加します。
   printf("Received new frame of size " + (string)ArraySize(data));
   string file_name = name + "_" + (string)id + "_" + (string)pass + "_" + DoubleToString(value, 5)+".hps";
   CZipFile* zip_file = new CZipFile(file_name, data);
   Zip.AddFile(zip_file);
}

zip アーカイブでタスクするためのクラスは非常に強力であり、普遍的なメソッドを備えているという事実に、別のファイルとしてアーカイブに新しいパスを追加することは非常に簡単です。 本質的に、OnTesterPass は、グローバルスコープで宣言されている zip アーカイブに新しい zip ファイルを追加します。

CZip     Zip;     //Zip アーカイブは、最適化パスでいっぱいになる

このプロシージャは、各最適化パスの最後に並行して呼び出され、リソースを集中的に処理することはできません。

最適化の終わりに、生成された zip アーカイブは、対応する zip ファイルとして保存する必要があります。 これも非常に簡単です。 OnTesterDeinit () プロシージャで実行されます。

//+------------------------------------------------------------------+
//すべてのパスの zip アーカイブをコンピュータのハードドライブに保存する      |
//+------------------------------------------------------------------+
void OnTesterDeinit()
{
   Zip.SaveZipToFile(OptimizationFile, FILE_COMMON);
   string f_totals = (string)Zip.TotalElements();
   printf("Optimization complete. Total optimization passes saved: " + f_totals);
}

ここで、OptimizationFile は、最適化名を設定するカスタム文字列パラメータです。 デフォルトでは、"Optimization.zip" です。 したがって、更新された SmaSample 戦略の最適化が完了した後、対応する zip アーカイブが作成されます。 ファイルフォルダにあり、標準の手段で開くことができます:

図7. 最適化ファイルの内部コンテンツ

ご覧のように、保存されたパスはすべて完全に保存され、3 ~ 5 倍の高圧縮率を示します。 

データをハードドライブに収集して保存した場合は、別のプログラムに読み込んで分析する必要があります。 これは次のセクションで考慮されます。


ストラテジーアナライザの作成

前のセクションでは、すべてのパスの情報を含む zip アーカイブが作成されています。 この情報は現在処理する必要があります。 この目的のために、 M-Tester Analyzerという名前の特別なプログラムを作成してみましょう。 生成されたアーカイブをロードし、便利なバランスチャートとして各パスを表示します。 また、M テスターアナライザは、選択したパスのサマリー統計を計算します。

テスト・コンプレックス全体の主要な関数の1つは、すべてのパスに関する情報を同時に保存する関数です。 これは最適化を1回だけ実行するのに十分であることを意味します。 すべてのパスは、単一のアーカイブに保存され、ユーザーに転送します。 任意のパスは、後でこの最適化からロードすることができ、その統計情報は、ストラテジーテスターを再度実行する上で時間を費やすことなく見ることができます。

アナライザのアクションのシーケンス:

  1. 選択した最適化アーカイブをロードする
  2. このアーカイブの最適化パスの1つを選択してください
  3. 既存のトレードに基づいて仮想バランスのダイナミクスチャートをプロットする
  4. トレード数、総利益、総損失、利益率、期待値などのパラメータを含む、パスの基本統計を計算します。
  5. メインプログラムウィンドウの計算された統計情報を表形式で出力します。

アーカイブから任意のパスを選択するためのツールをユーザーに装備する必要があります: 現在のパスから次または前のいずれかに簡単な移行を提供するだけでなく、カスタムパス番号を設定しましょう。

このプログラムは、 CPanelグラフィックスエンジンに基づいています。 現在、このライブラリ専用の記事はありませんが、さまざまなプロジェクトや記事で繰り返し使用されています。

アナライザーのメインコードは、CElChart から派生した CAnalizePanel クラスにあります。 アナライザ自体は、エキスパートの形で実装されています。 主なエキスパートファイルは、アナライザのグラフィカルウィンドウを起動します。 ここでは、主なEAのファイルです:

//+------------------------------------------------------------------+
//|                                                    mAnalizer.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include"mAnalizerPanel mqh"
CAnalyzePanel Panel;
input string FilePasses = "Optimization.zip";
//+------------------------------------------------------------------+
//エキスパート初期化関数                                                |
//+------------------------------------------------------------------+
int()
  {
//---
   Panel.Width(800);
   Panel.Height(630);
   Panel.XCoord(10);
   Panel.YCoord(20);
   Panel.LoadFilePasses(FilePasses);
   Panel.Show();
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   Panel.Hide();
}

//+------------------------------------------------------------------+
//ChartEvent 関数                                                     |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   switch(id)
   {
      case CHARTEVENT_OBJECT_ENDEDIT:
      {
         CEventChartEndEdit event(sparam);
         Panel.Event(&event);
         break;
      }
      case CHARTEVENT_OBJECT_CLICK:
      {
         CEventChartObjClick event(sparam);
         Panel.Event(&event);
         break;
      }
   }
   ChartRedraw();
}
//+------------------------------------------------------------------+


ご覧のように、このコードは非常に簡単です。 CAnalyzePanel 型のオブジェクトが作成されます。 次に、そのサイズは、現在のチャート (Show メソッド) に表示された後、OnInitメソッドで設定されます。 チャートから来るすべてのイベントのうち、2つに興味があります。つまり、テキストエントリーの終わりとグラフィカルなオブジェクトをクリックします。 このイベントは、CEvent オブジェクトに変換され、パネルに渡されます(Panel.Event(...)). アナライザパネルは、イベントを受信して処理します。

アナライザパネル自体を説明しましょう。 大きな CAnalyzePanel クラスで構成されているため、コンテンツ全体が公開されることはありません。 その完全なコードは、興味のある人のためにこの記事の最後に添付されています。 次のクラスプロトタイプを使用して、その操作の簡単な説明のみがここで提供されます。

//+------------------------------------------------------------------+
//|                                                    mAnalizer.mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include<Panel\ElChart.mqh>
#include<Panel\ElButton.mqh>
#include<Graphics\Graphic.mqh>
#include"ListPass mqh"
#include"TradeAnalyze mqh"
//+------------------------------------------------------------------+
//数理解析装置のパスを解析するためのパネル                                |
//+------------------------------------------------------------------+
class CAnalizePanel : public CElChart
{
private:
   //--要素の配列とその統計
   CArrayObj      m_stat_descr;     //統計の説明
   CArrayObj      m_stat_all;       //すべての取引の統計量の値
   CArrayObj      m_stat_long;      //長期トレードの統計値
   CArrayObj      m_stat_short;     //短期トレードの統計値
   CTradeAnalize  m_analize;        //統計計算モジュール
   //--グラフィカルコントロール
   CElChart       m_name_analyze;   //メインウィンドウの名前
   CElChart       m_np;             //"パス #" テキスト
   CElChart       m_of_pass;        //"# # # パス" のテキスト
   CElChart       m_pass_index;     //パス番号エントリーボックス
   CElButton      m_btn_next;       //「次のパス」ボタン
   CElButton      m_btn_prev;       //「前のパス」ボタン
   CGraphic       m_graphic;        //バランスダイナミクスチャート
   //--インフラストラクチャ
   CListPass      m_passes;         //パスの一覧
   int            m_curr_pass;      //現在のパスのインデックス
   CCurve*        m_balance_hist;   //チャート上のバランスダイナミクスライン
   bool           IsEndEditPass(CEvent* event);
   bool           IsClick(CEvent* event, CElChart* el);
   void           NextPass(void);
   void           PrevPass(void);
   int            GetCorrectPass(string text);
   void           RedrawGraphic(void);
   void           RedrawCurrPass(void);
   void           PlotStatistic(void);
   string         TypeStatToString(ENUM_MSTAT_TYPE type);
   void           CreateStatElements(void);
   string         ValueToString(double value, ENUM_MSTAT_TYPE type);
public:
                  CAnalizePanel(void);
   bool           LoadFilePasses(string file_name, int file_common = FILE_COMMON);
   virtual void   OnShow();
   virtual void   OnHide();
   virtual void   Event(CEvent *event);
};

ご覧の通り、このクラスの主なタスクは、内部に隠されています。 パブリックメソッドの中で、主なものは、最適化パスを含む zip ファイルを読み込んでいます。 クラスのすべてのタスクは、次の3つの部分に分けることができます。

  1. チャートを作成し、バランスチャートを追加します。
  2. CElChart コントロールの形式でテキストラベルを作成すると、テストの統計情報が表示されます。
  3. パス統計の実際の計算。

各セクションについて説明しましょう。 

各パスの収集された統計をすべて表示するには、多くのコントロールを作成する必要があります。 このアナライザには、10個の基本統計パラメータが表示されます。 さらに、各パラメータは、すべてのトレードについて個別に計算されます。 インジケーターの名前を表示するには、10個の追加ラベルが必要です。 したがって、40テキストラベルを作成する必要があります。 各コントロールを手動で作成しないようにするには、オートメーションプロシージャを作成します。 これを行うには、各計算された統計パラメータは、特殊な列挙体に独自の識別子が割り当てられます。

//+------------------------------------------------------------------+
//統計値タイプの識別子                                                  |
//+------------------------------------------------------------------+
enum ENUM_MSTAT_TYPE
{
   MSTAT_PROFIT,
   MSTAT_ALL_WINS_MONEY,
   MSTAT_ALL_LOSS_MONEY,
   MSTAT_TRADERS_TOTAL,
   MSTAT_WIN_TRADERS,
   MSTAT_LOSS_TRADERS,   
   MSTAT_MAX_PROFIT,
   MSTAT_MAX_LOSS,
   MSTAT_PROFIT_FACTOR,
   MSTAT_MATH_EXP,   
};
#defineMSTAT_ELEMENTS_TOTAL 10

また、計算方向の識別子を定義します。

//+------------------------------------------------------------------+
//| Statistics can be calculated for one of three directions         |
//+------------------------------------------------------------------+
enum ENUM_MSTATE_DIRECT
{
   MSTATE_DIRECT_ALL,      //すべての情報
   MSTATE_DIRECT_LONG,     //買いの取引
   MSTATE_DIRECT_SHORT,    //売りの取引
};

このパネルには、それぞれが独自の配列に配置されているコントロールの4つのグループがあります。

  • 統計名を表示するコントロール (m_stat_descr 配列)
  • すべてのトレードの統計情報の値を表示するコントロール (m_stat_all 配列)
  • ロングディールの統計情報の値を表示するコントロール (m_stat_long 配列)
  • 短いトレードの統計情報の値を表示するコントロール (m_stat_short 配列)

コントロールはすべて、CAnalyzePanel:: CreateStatElements (void) メソッドの最初の開始時に作成されます。

すべてのコントロールが作成されたら、正しい値をエントリーする必要があります。 値の計算は、外部 CTradeAnalize クラスに委任されます。

#include<Arrays\ArrayObj.mqh>
#include<Dictionary.mqh>
#include"・・ ・\MTester\Mposition.mqh」
//+------------------------------------------------------------------+
//必要なフィールドを含む補助制御                                         |
//+------------------------------------------------------------------+
class CDiffValues : public CObject
{
public:
   double all;
   double sell;
   double buy;
   CDiffValues(void) : all(0), buy(0), sell(0)
   {
   }
};
//+------------------------------------------------------------------+
//統計解析クラス                                                       |
//+------------------------------------------------------------------+
class CTradeAnalize
{
private:
   CDictionary m_values;
   
public:
   void     CalculateValues(CArrayObj* history);
   double   GetStatistic(ENUM_MSTAT_TYPE type, ENUM_MSTATE_DIRECT direct);
};
//+------------------------------------------------------------------+
//統計の値を計算します                                                  |
//+------------------------------------------------------------------+
double CTradeAnalize::GetStatistic(ENUM_MSTAT_TYPE type, ENUM_MSTATE_DIRECT direct)
{
   CDiffValues* value = m_values.GetObjectByKey(type);
   switch(direct)
   {
      case MSTATE_DIRECT_ALL:
         return value.all;
      case MSTATE_DIRECT_LONG:
         return value.buy;
      case MSTATE_DIRECT_SHORT:
         return value.sell;
   }
   return EMPTY_VALUE;
}
//+------------------------------------------------------------------+
//各方向のトレード数を計算します                                         |
//+------------------------------------------------------------------+
void CTradeAnalize::CalculateValues(CArrayObj *history)
{
   m_values.Clear();
   for(int i = 0; i < MSTAT_ELEMENTS_TOTAL; i++)
      m_values.AddObject(i, new CDiffValues());
   CDiffValues* profit = m_values.GetObjectByKey(MSTAT_PROFIT);
   CDiffValues* wins_money = m_values.GetObjectByKey(MSTAT_ALL_WINS_MONEY);
   CDiffValues* loss_money = m_values.GetObjectByKey(MSTAT_ALL_LOSS_MONEY);
   CDiffValues* total_traders = m_values.GetObjectByKey(MSTAT_TRADERS_TOTAL);
   CDiffValues* win_traders = m_values.GetObjectByKey(MSTAT_WIN_TRADERS);
   CDiffValues* loss_traders = m_values.GetObjectByKey(MSTAT_LOSS_TRADERS);
   CDiffValues* max_profit = m_values.GetObjectByKey(MSTAT_MAX_PROFIT);
   CDiffValues* max_loss = m_values.GetObjectByKey(MSTAT_MAX_LOSS);
   CDiffValues* pf = m_values.GetObjectByKey(MSTAT_PROFIT_FACTOR);
   CDiffValues* mexp = m_values.GetObjectByKey(MSTAT_MATH_EXP);
   total_traders.all = history.Total();
   for(int i = 0; i < history.Total(); i++)
   {
      CMposition* pos = history.At(i);
      profit.all += pos.Profit();
      if(pos.PositionType() == POSITION_TYPE_BUY)
      {
         if(pos.Profit() > 0)
         {
            win_traders.buy++;
            wins_money.buy += pos.Profit();
         }
         else
         {
            loss_traders.buy++;
            loss_money.buy += pos.Profit();
         }
         total_traders.buy++;
         profit.buy += pos.Profit();
      }
      else
      {
         if(pos.Profit() > 0)
         {
            win_traders.sell++;
            wins_money.sell += pos.Profit();
         }
         else
         {
            loss_traders.sell++;
            loss_money.sell += pos.Profit();
         }
         total_traders.sell++;
         profit.sell += pos.Profit();
      }
      if(pos.Profit() > 0)
      {
         win_traders.all++;
         wins_money.all += pos.Profit();
      }
      else
      {
         loss_traders.all++;
         loss_money.all += pos.Profit();
      }
      if(pos.Profit() > 0 && max_profit.all < pos.Profit())
         max_profit.all = pos.Profit();
      if(pos.Profit() < 0 && max_loss.all > pos.Profit())
         max_loss.all = pos.Profit();
   }
   mexp.all = profit.all/total_traders.all;
   mexp.buy = profit.buy/total_traders.buy;
   mexp.sell = profit.sell/total_traders.sell;
   pf.all = wins_money.all/loss_money.all;
   pf.buy = wins_money.buy/loss_money.buy;
   pf.sell = wins_money.sell/loss_money.sell;
}

計算自体は、CalculateValues メソッドによって実行されます。 CMposition コントロールを含む CArrayObj 配列を渡す必要があります。 しかし、仮想ポジションのこの配列はどこから来るのでしょうか。

実際には、CAnalyzePanel クラスには別のクラス ( CListPass) があります。 zip アーカイブをロードし、パスのコレクションを作成するものです。 このクラスは非常に簡単です。

//+------------------------------------------------------------------+
//                                                   Optimazer mq5 |
//|                        Copyright 2017, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#include<Zip\Zip.mqh>
#include<Dictionary.mqh>
#include"・・ ・\MTester\MTrade.mqh」
//+------------------------------------------------------------------+
//最適化パスの一覧を格納します                                          |
//+------------------------------------------------------------------+
class CListPass
{
private:
   CZip        m_zip_passes;  //すべての最適化パスのアーカイブ
   CDictionary m_passes;      //ロードされた履歴のポジション
public:
   bool        LoadOptimazeFile(string file_name, int file_common = FILE_COMMON);
   int         PassTotal(void);
   CArrayObj*  PassAt(int index);
};
//+------------------------------------------------------------------+
//zip アーカイブから最適化パスの一覧を読み込みます                         |
//+------------------------------------------------------------------+
bool CListPass::LoadOptimazeFile(string file_name,int file_common=FILE_COMMON)
{
   m_zip_passes.Clear();
   if(!m_zip_passes.LoadZipFromFile(file_name, file_common))
   {     
      printf("Failed load optimization file. Last Error");
      return false;
   }
   return true;
}
//+------------------------------------------------------------------+
//パスの数                                                            |
//+------------------------------------------------------------------+
int CListPass::PassTotal(void)
{
   return m_zip_passes.TotalElements();
}
//+------------------------------------------------------------------+
//指定されたインデックスを持つパスの情報の一覧を返します                    |
//+------------------------------------------------------------------+
CArrayObj* CListPass::PassAt(int index)
{
   if(!m_passes.ContainsKey(index))
   {
      CZipFile* zip_file = m_zip_passes.ElementAt(index);
      uchar array[];
      zip_file.GetUnpackFile(array);
      CMtrade* trade = new CMtrade();
      trade.FromCharArray(array);
      m_passes.AddObject(index, trade);
   }
   CMtrade* trade = m_passes.GetObjectByKey(index);
   //printf("Total Traders: " + (string)trade.History.Total());
   return &trade.History;
}

このように、 CListPass クラスは最適化アーカイブを読み込みますが、解凍しません。 つまり、すべてのデータはコンピュータのメモリでも圧縮された形式で保存されるため、コンピュータの RAM を節約できます。リクエストされたパスはアンパックされ、CMtrade オブジェクトに変換した後、アンパックされた形式で内部ストレージに保存します。 次にこのコントロールが呼び出されたときに、アンパックは必要ありません。

ここでも、CAnalyzePanel クラスを参照してください。 ここで、ポジションの読み込み先 (CListPass クラス) とその統計の計算メソッド (CTradeAnalyze クラス) の情報があります。 グラフィカルコントロールを作成した後は、正しい値で塗りつぶされたままになります。 これは、CAnalyzePanel::P lotstatistic (void) メソッドによって行われます。

//+------------------------------------------------------------------+
//統計情報を表示します                                                 |
//+------------------------------------------------------------------+
void CAnalyzePanel::PlotStatistic(void)
{
   if(m_stat_descr.Total() == 0)
      CreateStatElements();
   CArrayObj* history = m_passes.PassAt(m_curr_pass-1);
   m_analize.CalculateValues(history);
   for(int i = 0; i < MSTAT_ELEMENTS_TOTAL; i++)
   {
      ENUM_MSTAT_TYPE stat_type = (ENUM_MSTAT_TYPE)i;
      //--すべてのトレード
      CElChart* el = m_stat_all.At(i);
      string v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_ALL), stat_type);
      el.Text(v);
      //--ロングトレード
      el = m_stat_long.At(i);
      v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_LONG), stat_type);
      el.Text(v);
      //--ショートトレード
      el = m_stat_short.At(i);
      v = ValueToString(m_analize.GetStatistic(stat_type, MSTATE_DIRECT_SHORT), stat_type);
      el.Text(v);
   }
}

アナライザパネルの操作に必要なすべての基本コントロールが考慮されています。 この説明は矛盾していることが判明したが、そのようなプログラミングの本質は すべての要素が相互接続され、時にはすべてを同時に記述する必要があります。

よって、チャート上でアナライザーを開始します。 これを行う前に、最適化の結果を持つ zip アーカイブが FILE_COMMON ディレクトリに存在していることを確認してください。 デフォルトでは、アナライザは "最適化 .zip" ファイルを読み込み、共通ディレクトリに配置する必要があります。

実装された関数の最大の効果は、パスを切り替えるときに見ることができます。 チャートと統計が自動的に更新されます。 以下のスクリーンショットは、この瞬間を示しています:


図8. 数学的計算のアナライザでのスイッチングパス

パネル操作についての理解を深めるために、ここではツールヒントを使用したグラフィカルなスキームを示します。メインコントロールは、各コントロールグループのクラスとメソッドを示すフレームで説明されています。


図9. インターフェイスの主なコントロール

結論として、結果のプロジェクトの構造を説明しましょう。 すべてのソースコードは MTester.zip アーカイブにあります。 プロジェクト自体は MQL5\Experts\MTester フォルダにあります。 ただし、他の複雑なプログラムと同様に、プロジェクトには追加のライブラリを含める必要があります。 標準MetaTrader5パッケージで使用できないものは、このアーカイブに MQL5\Include フォルダに存在します。 まず第一に、 CPanel のグラフィカルなライブラリ (場所: MQL5\Include\Panel) です。 また、zip アーカイブ (MQL5\Include\Zip) を使用するためのライブラリと連想配列を編成するためのクラスが含まれています (MQL5\Include\Dictionary)。 ユーザーの利便性のため、2つの MQL5 プロジェクトが作成されています。 これは、最近実装されたMetaTrader5の新関数です。 最初のプロジェクトは MTester と呼ばれ、ストラテジーテスターと戦略自体は、移動平均 (SmaSample mq5) の交点に基づいて含まれています。 2番目のプロジェクトは MAnalyzer と呼ばれ、アナライザパネルのソースコードがあります。

ソースコードとは別に、アーカイブには最適化の結果を含む最適化 .zip ファイルが含まれており、テストデータに関する戦略の約160パスが使用されます。 新しい最適化を実行する必要なく、パスアナライザの関数をすばやく確認できます。 このファイルは MQL5\Files. にあります。


結論

結論として、ここではこの記事に記載されている簡単なまとめをします。

  • 数学計算テスターは、トレード環境シミュレーションの欠如により、高速です。 これより、簡単なトレード戦略をテストするためのカスタムの高性能アルゴリズムを作成することができます。 但し、実行されたトレード操作の制御の欠乏のため、「未来をのぞき見することは可能である」。 このような "Grails" のエラーは識別するのは非常に困難ですが、これは高パフォーマンスのための対価です。
  • 数学計算テスターの中からトレード環境にアクセスすることはできません。 したがって、目的のツールの引用を取得することは不可能です。 したがって、このモードでは、必要なデータを事前に手動でダウンロードするだけでなく、カスタムライブラリを使用してインジケーターを計算する必要があります。 この記事では、データの準備メソッド、効率的に圧縮するメソッド、およびプログラム実行モジュールに統合するメソッドを示しました。 この手法は、その操作に必要なプログラムで追加データを配布したいすべての人にとっても便利です。
  • 数学計算モードでは、標準インジケータにアクセスすることもできません。 したがって、必要なインジケーターを手動で計算する必要があります。 しかし、速度も非常に重要です。 EA内のインジケーターのマニュアル計算だけでなく、速度の面でも最高のソリューションになります。 幸いにも、リングバッファライブラリは、一定の時間内にすべての必要な指標の効率的な計算をすることができます。
  • MetaTrader5 のフレーム生成モードは、複雑なメカニズムとはいえ、ユーザーにカスタム分析アルゴリズムを記述する絶好の機会を提供します。 たとえば、この記事に示されているカスタムストラテジーテスターを作成することができます。 フレーム生成モードの可能性を最大限に活用するには、バイナリデータを最大限に使用できるようにする必要があります。 このデータ型を使用すると、複雑なデータ (たとえば、ポジションのリストなど) を生成できるようになります。 この記事では、複雑なカスタムデータ型 (CMPosition クラスのポジション) を作成するメソッドを示しました。
  • ストラテジーテスターの最も重要な部分の1つは、データストレージシステムです。 明らかに、テスト中に取得されるデータの量は膨大です: 各テストでは、数百または数千のパスを含めることができます。 そして、各パスは、数千人の数十まで達する情報が含まれています。 プロジェクト全体の成功は、この情報がどの程度効果的に格納および配布されるかによって異なります。 そのため、zip アーカイブが選択されました。 MQL5 はこのタイプのファイルを扱うためのパワフルで高速なライブラリを備えているため、最適化パス用のカスタムファイルストレージを整理することができます。 各最適化は、すべてのパスを含む単一の zip ファイルです。 各パスは、1つの圧縮ファイルで表されます。 アーカイブは、大スケールな最適化にも適度なサイズを持たせる高データ圧縮につながります。
  • カスタムストラテジーテスターを作成するだけでは不十分です。 最適化結果を分析するには、個別のサブシステムが必要です。 このようなサブシステムは、M テスターアナライザプログラムの形式で実装されています。 これは、zip アーカイブとして最適化の結果をロードし、チャートに出力し、各パスの基本的な統計情報を表示する別のプログラムモジュールです。 M-テスターアナライザは、クラスと CPanel のグラフィカルライブラリに基づいています。 簡単で便利なライブラリであり、これを使用することで、迅速かつ強力なグラフィカルインターフェイスを構築できます。 システムライブラリ CGraphic を使用して、バランスダイナミクスの有益なチャートが表示されます。

印象的な結果が得られたという事実にもかかわらず、そして実際に利益を得た数学的計算に基づいたテスターは、多くの必要なものが欠落しています。 次のバージョンに、追加する必要がある優先コンポーネントの一部を以下に示します。

  • シンボル (名前、ティック値、シンボル、スプレッドなど) に関する情報。 この情報は、可能な手数料、スプレッドとスワップを計算するために必要です。 また、資産通貨での利益の計算に必要となります (現在、利益はポイントで計算されます)。
  • 各パスの戦略とそのパラメータに関する情報。 戦略の結果だけでなく、すべてのパラメータ値も知る必要があります。 これを行うには、生成されたレポートにも追加のデータ型が組み込まれている必要があります。
  • 実行されたアクションの正確性を制御します。 この段階では、現実とは何の関係もない "聖杯" になります、"未来をのぞき見" することは非常に簡単です。 少なくとも、将来のバージョンでは最小限の制御メカニズムが必要です。 しかし、まだどのように見えるかを判断することは困難です。
  • 実際のストラテジーテスターとのレポート生成メカニズムの統合。 何も開発されたレポート形式に標準的なMetaTrader5ストラテジーテスターで得られた結果を変換することを防ぎます。 M-トレードアナライザを使用して信頼性の高いテスト結果を分析する関数を提供します。 したがって、テストシステムと1つの分析システムがあります。
  • M トレード分析装置のさらなる発展 プログラムは現在、基本的な関数のみがあります。 データを完全に処理するには、明らかに十分ではありません。 売り取引および買い取引の付加的な統計量および別のバランスのチャートを加える必要があります。 また、テキストファイル内の情報の履歴を保存する方法を学習し、Excel でロードするのが妥当でしょう。
M-テスターのすべての主要な側面だけでなく、その開発のさらなる展望と考えられています。 提案されたトピックが十分に興味深い証明すれば、この記事は続くでしょう。 いろいろやってきましたが、まだまだやることも残っています。 M-テスターの新しいバージョンがすぐに来ることを願いましょう!