多通貨エキスパートアドバイザーの開発(第8回):新しいバーの負荷テストと処理
はじめに
最初の記事では、2つの取引戦略を備えたEAを開発しました。2回目ではすでに9つのインスタンスを使用しましたが、前回はその数が32に跳ね上がりました。テスト時間に関しては問題はありませんでした。1回のテストパスの時間が短ければ短いほど良いことは明らかです。しかし、全体的な最適化に数時間かかるとしても、数日や数週間を要するよりははるかに良いです。さらに、1つのEAに複数の戦略インスタンスを組み合わせ、その結果を迅速に確認したい場合、1回のパスが数秒や数分で完了するのが理想です。
戦略インスタンスのグループを選択して最適化する場合、いくつかのインスタンスがすでにすべての最適化パスに参加しています。この場合、個々のパスや全体的な最適化にかかる時間が長くなります。そこで、今回の最適化では8つ以下のインスタンスのグループを選定することにしました。
テスターの1回のパスの時間が、異なる期間のテストにおいて取引戦略のインスタンス数によってどのように変化するかを調べてみましょう。また、消費メモリについても確認してみます。もちろん、端末チャート上でEAを起動したときに、異なる数の取引戦略インスタンスでEAがどのように動作するかも確認する必要があります。
テスターでの異なるインスタンス数
このような実験をおこなうには、既存のEAを基本に新しいEAを作成する必要があります。OptGroupExpert.mq5 EAを基本にして、次のように変更してみましょう。
- ファイルから読み込まれたセットの全配列から取り出された8つのパラメータセットのインデックスを指定する入力を削除します。count_パラメータは残しておきます。これで、全セットの配列から読み込むセット数を指定することになります。
- 存在しなくなったインデックスの一意性確認を削除します。新しい戦略を戦略の配列に追加し、パラメータのセットはパラメータのセット配列の最初のcount_要素から取得します。もしこの配列に十分な数のインスタンスがなければ、ループの中で配列の先頭から新しいインスタンスを取り出すことになります。
- この EA はまだ何かの最適化には使用しないため、OnTesterInit()関数とOntesterDeinit()関数は削除します。
コードは以下のようになります。
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "::: Money management" sinput double expectedDrawdown_ = 10; // - Maximum risk (%) sinput double fixedBalance_ = 10000; // - Used deposit (0 - use all) in the account currency sinput double scale_ = 1.00; // - Group scaling multiplier input group "::: Selection for the group" sinput string fileName_ = "Params_SV_EURGBP_H1.csv"; // - File with strategy parameters (*.csv) input int count_ = 8; // - Number of strategies in the group (1 .. 8) input group "::: Other parameters" sinput ulong magic_ = 27183; // - Magic ... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Load strategy parameter sets int totalParams = LoadParams(fileName_, params); // If nothing is loaded, report an error if(totalParams == 0) { PrintFormat(__FUNCTION__" | ERROR: Can't load data from file %s.\n" "Check that it exists in data folder or in common data folder.", fileName_); return(INIT_PARAMETERS_INCORRECT); } // Report an error if if(count_ < 1) { // number of instances is less than 1 return INIT_PARAMETERS_INCORRECT; } ArrayResize(params, count_); // Set parameters in the money management class CMoney::DepoPart(expectedDrawdown_ / 10.0); CMoney::FixedBalance(fixedBalance_); // Create an EA handling virtual positions expert = new CVirtualAdvisor(magic_, "SimpleVolumes_BenchmarkInstances"); // Create and fill the array of all strategy instances CVirtualStrategy *strategies[]; FOREACH(params, APPEND(strategies, new CSimpleVolumesStrategy(params[i % totalParams]))); // Form and add a group of strategies to the EA expert.Add(CVirtualStrategyGroup(strategies, scale_)); return(INIT_SUCCEEDED); }
出来上がったコードを現在のフォルダのBenchmarkInstancesExpert.mq5ファイルに保存します。
それでは、テスターでこのEAを取引戦略のインスタンス数やティックシミュレーションモードを変えて何度か動かしてみましょう。
異なるモードでのテスト結果
これまでの記事で使用した、すでにおなじみの「1分OHLC」ティックシミュレーションモードから始めましょう。次回のローンチではインスタンス数を2倍に増やす予定です。まずは8インスタンスから。テスト時間が長くなりすぎた場合は、テスト期間を短縮します。
図1:1分OHLCモードでの単回実行結果
ご覧のように、512インスタンスまでのテストでは、6年間のテスト期間を使用し、その後1年間のテスト期間に切り替え、最後の 2回のパスでは3か月のみを使用しました。
異なるテスト期間の時間コストを比較できるようにするため、別の値を計算します。1つの戦略インスタンスの1日のシミュレーション時間です。これをおこなうには、総時間を戦略インスタンス数とテスト期間(日)で割ります。小さな数字で苦労しないように、この時間を10^9倍してナノ秒に換算してみましょう。
ログでは、テスターは実行中に使用されたメモリに関する情報を報告し、総容量と履歴データおよびティックデータに費やされた容量を示します。総メモリ量からそれらを差し引くと、EA自体が必要としたメモリ量がわかります。
この結果から、最大数(16,384)であっても、テスターの実行に壊滅的に多くの時間を必要とすることはないと言えます。一般的に、このような数は、例えば15個の銘柄とそれぞれ100個のインスタンスからなる共同作品を準備するのには十分です。すでに多数なのです。同時に、インスタンス数が増えてもメモリ消費量はそれほど増加しません。なぜか、EA自体のメモリ消費量が8192インスタンスでピークに達しましたが、その後、必要なメモリは再び少なくなりました。
より正確な結果を得るためには、同じインスタンス数で異なるパスをおこなった場合でも結果が異なるため、各インスタンス数で複数のパスを繰り返し、平均時間と平均メモリサイズを計算することができます。しかし、これらの差はそれほど大きくなかったため、より大規模なテストをおこなう意味はほとんどありませんでした。比較的少部数でも制限にぶつからないようにしたかっただけです。
それでは、テスターでEAを「全ティック」シミュレーションモードで実行した結果を見てみましょう。
図2:「全ティック」モードでの単回実行結果
1回のパスにかかる時間が約10倍に増加したため、以前のモードと比較して、同じ数のインスタンスでテスト期間を短縮しました。ティックメモリのサイズは当然大きくなり、割り当てられたメモリの総量も増えました。しかし、EAに割り当てられたメモリ量は、使用されたインスタンス数すべてにおいてほぼ同じであることが判明しました。多少の成長はありますが、かなり遅くなります。
512インスタンスと1024インスタンスでは、他のインスタンスサイズのほぼ2倍の速度で、異常に短い実行時間が観察されました。考えられる理由は、CSVデータファイル内の取引戦略インスタンスパラメータセットの順番に関係している可能性が高いです。
最後に検討するシミュレーションモードは、「実ティックに基づく全ティック」です。このモードでは、「全ティック」モードよりも数回多く実行しました。
図3:「実ティックに基づく全ティック」モードでの単回実行結果
以前のモードに比べ、時間は約30%増加し、使用メモリは約20%増加しました。
注目に値するのは、チャートに添付されたEAの1つのコピーが、テスト中に端末で実行されていたことです。8192のインスタンスが使用されました。この場合、端末のメモリ消費量は約200MB、CPUリソースの消費量は0%から4%でした。
全体として、この実験では、1つのEAで一緒に機能する取引戦略のインスタンスの可能な数に対して、かなり大きな予備があることが示されました。もちろん、この金額は取引戦略の具体的な内容に大きく依存します。1つのインスタンスが実行する必要がある計算が多ければ多いほど、組み合わせられる数は少なくなります。
では、テストをスピードアップするために、どのような簡単なステップを踏むことができるかを考えてみましょう。
出力を無効にする
現在の実装では、EAの動作中にかなり多くの情報を表示します。単一インスタンスの最適化に関しては、出力関数が実行されないだけなので、これは問題ではありません。テスターでEAのシングルパスを実行すると、すべてのメッセージがログに送信されます。適用されたVirtualOrder.mqhライブラリでは、各仮想注文のイベント処理に関するメッセージを表示します。仮想注文の数が少ないうちは、テスト時間への影響はほとんどありませんが、その数が数万に及ぶようになると、顕著な影響が出ます。
それを測ってみましょう。EAファイルの先頭に以下の行を追加することで、すべてのメッセージをログに出力しないようにすることができます。
#define PrintFormat StringFormat
これらの関数の関連性により、すべてのPrintFormat()呼び出しはStringFormat()呼び出しに置き換えることができます。文字列は生成されますが、ログには出力されません。
何度か打ち上げを実施した結果、5~10%の時間短縮が見られたものもあれば、時間がわずかに延びたものもあった。将来的には、PrintFormat()を置き換える同様の方法が必要になるかもしれません。
1分OHLCへの移行
単一のテストパスと最適化の両方のプロセスを高速化するもう1つの方法は、「毎ティック」と「実ティックに基づく実ティック」のシミュレーションモードの使用を避けることです。
すべての取引戦略がこのような余裕を持てるわけではないことは明らかです。戦略が非常に頻繁にポジションをオープン/クローズする場合(1分間に1回以上)、すべてのティックでのテストを放棄することは不可能です。高頻度取引といえども、ずっと続くわけではなく、指定された時間帯にのみおこなわれます。しかし、もしその戦略が頻繁なオープン/クローズを必要とせず、ストップロスやテイクプロフィットレベルのトリガーの精度が十分でないために数ポイントの損失にそれほど敏感でないのであれば、このチャンスを利用しない手はないでしょう。
例として取り上げた取引戦略は、全ティックモードの使用から逃れることができる取引戦略の1つです。しかし、ここで別の問題が生じます。単に「1分OHLC」モードで単一インスタンスのパラメータを最適化し、その後組み立てたEAを端末で動作させると、EAはすべてのティックモードで動作する必要があります。1分間に4ティックと決まっているわけではなく、それ以上となります。したがって、OnTick()関数はより頻繁に呼び出され、EAが扱う価格のセットはもう少し多様になります。
この違いは、EAが示す結果のイメージを変えるかもしれません。このシナリオがどれほど現実的かを確認するために、「1分OHLC」と「実ティックに基づく実ティック」モードで同じ入力でEAをテストしたときに得られた取引結果を比較してみましょう。
図4:「実ティックに基づく全ティック」(左)と「1分OHLC」(右)モードでの
単回実行の結果の
比較
モードが異なると、始値、終値、終値が微妙に異なることがわかります。最初はこれが唯一の違いですが、その後、左側に取引が開始され、同時に右側に取引が開始されない瞬間が来ます。取引番号25の行を見てください。したがって、「1分OHLC」モードの結果は、「実ティックに基づく実ティック」モードの結果よりも少ない取引を含んでいます。
どのティックモードでも、利益はわずかに高くなりました。バランス曲線を見ると、両者に大きな違いはありません。
図5:「1分OHLC」(上)と「実ティックに基づく全ティック」(下)でのテスト結果
したがって、このEAを端末で実行する場合、「1分OHLC」モードでのテストよりも悪い結果は得られない可能性が高いです。これは、より高速なティックシミュレーションモードを最適化に使用できることを意味します。戦略の計算が新しいバーの開始時にしか実行できない場合、各ティックでそのような計算を拒否することで、EAの動作をさらに高速化することができます。そのためには、EAで新しいバーを決定する方法が必要です。
全ティックモードでの結果が1分OHLCモードよりも悪い場合、EAがバーの開始時以外の取引を実行することを禁止してみることができます。この場合、すべてのティックモデリングモードで可能な限り近い結果が得られるはずです。これを実現するには、EAに新しいバーを定義する方法が必要です。
新しいバーの定義
まず、私たちの要望を具体化しましょう。指定された銘柄と時間枠で新しいバーが発生した際にtrueを返す関数を作成したいと考えています。このような関数は、取引戦略の単一のインスタンスを実装するEAにおいて、一般的に特定の銘柄と時間枠(または銘柄と複数の時間枠)に対して実装されます。この関数は、最後のバーの時間を記録する変数を使用して、新しいバーが生成されたかどうかを判断します。しばしば、この機能は独立した関数として実装されるのではなく、必要な場所に直接組み込まれることが多いです。
取引戦略の異なるインスタンスに対して新しいバーの発生を複数回確認する必要がある場合、この方法は非常に不便になります。もちろん、このコードを取引戦略インスタンスの実装に直接埋め込むことも可能ですが、ここでは別の方法をとります。
IsNewBar(symbol, timeframe) public関数を作り、銘柄と時間枠によって、現在のティックで新しいバーが発生したことを報告できるようにしましょう。この関数を呼び出すだけで、戦略の取引ロジックコードに変数やアクションを追加する必要がないことが望ましいです。また、現在のティックに新しいバーが到着し、関数が複数回呼び出された場合(たとえば、取引戦略の異なるインスタンスから)、最初の呼び出しだけでなく、各呼び出しでtrueを返す必要があります。
次に、各銘柄と時間枠の最後のバーの時間に関する情報を格納する必要があります。しかし、「各」とは、端末で利用可能な全ての銘柄や時間枠を指すのではなく、取引戦略の特定のインスタンスで実際に必要となる銘柄と時間枠だけを指します。これらの必要な銘柄と時間枠を定義するために、IsNewBar(symbol, timeframe)関数によって実行されるアクションのリストを拡張します。具体的には、まず指定された銘柄と時間枠で現在のバーに関連する時間情報が既に記憶されているかどうかを確認します。記憶されていない場合は、この関数が新たにその時間情報を作成します。すでに存在している場合は、関数は新しいバーの確認結果を返します。
IsNewBar()関数を1ティックで複数回呼び出すには、2つの別々の関数に分ける必要があります。1つ目の関数は、すべての銘柄と時間枠についてティックの開始時に新しいバーをチェックし、その情報を2番目の関数用に保存します。この関数をUpdateNewBar()と名付け、少なくとも1つの銘柄と時間枠に新しいバーがあるかどうかを示す論理値も返すようにします。2つ目の関数は、新しいバーの発生イベントに基づいて目的の結果を見つけて返す役割を担います。
UpdateNewBar()関数は、新しいティックの処理を開始する際に一度だけ呼び出す必要があります。たとえば、この呼び出しをCVirtualAdvisor::Tick()メソッドの冒頭に配置することが考えられます。
void CVirtualAdvisor::Tick(void) { // Define a new bar for all required symbols and timeframes UpdateNewBar(); ... // Start handling in strategies where IsNewBar(...) can already be used CAdvisor::Tick(); ... }
最後のバーの時間を保存できるようにするには、まず静的なCNewBarEventクラスを作成します。つまり、このクラスのオブジェクトは作らず、静的なプロパティとメソッドだけを使用します。これは本質的に、必要なグローバル変数と関数を専用の名前空間に作成することと同じです。
このクラスには、銘柄名の配列(m_symbols) と新しいクラスオブジェクトへのポインタの配列(m_symbolNewBarEvent)の2つの配列があります。最初の配列には、新しいバーイベントを追跡するために使用する銘柄が含まれます。2つ目の配列には新しいCsymbolNewBarEventクラスへのポインタが含まれます。このクラスは異なる時間枠での1つの銘柄のバータイムを格納します。
これら2つのクラスは3つのメソッドを持ちます。
- 新しい監視銘柄または銘柄時間枠を登録するメソッドRegister(...)
- 新しいバーフラグを更新するメソッドUpdate()
- 新しいバーフラグを取得するメソッドIsNewBar(...)
新しい銘柄の新しいバーイベントのトラッキングを登録する必要がある場合、新しいクラスオブジェクトCSymbolNewBarEventが作成されます。そのため、EAが作業を終えたら、これらのオブジェクトが占有しているメモリをクリーンアップする必要があります。このタスクは、CNewBarEvent::Destroy()staticメソッドとDestroyNewBar()グローバル関数によって実行されます。EAのデストラクタに関数呼び出しを追加します。
//+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CVirtualAdvisor::~CVirtualAdvisor() { delete m_receiver; // Remove the recipient delete m_interface; // Remove the interface DestroyNewBar(); // Remove the new bar tracking objects }
これらのクラスの完全な実装は次のようになります。
//+------------------------------------------------------------------+ //| Class for defining a new bar for a specific symbol | //+------------------------------------------------------------------+ class CSymbolNewBarEvent { private: string m_symbol; // Tracked symbol long m_timeFrames[]; // Array of tracked symbol timeframes long m_timeLast[]; // Array of times of the last bars for timeframes bool m_res[]; // Array of flags of a new bar occurrence for timeframes // The method for registering a new tracked timeframe for a symbol int Register(ENUM_TIMEFRAMES p_timeframe) { APPEND(m_timeFrames, p_timeframe); // Add it to the timeframe array APPEND(m_timeLast, 0); // The last time bar for it is still unknown APPEND(m_res, false); // No new bar for it yet Update(); // Update new bar flags return ArraySize(m_timeFrames) - 1; } public: // Constructor CSymbolNewBarEvent(string p_symbol) : m_symbol(p_symbol) // Set a symbol {} // Method for updating new bar flags bool Update() { bool res = (ArraySize(m_res) == 0); FOREACH(m_timeFrames, { // Get the current bar time long time = iTime(m_symbol, (ENUM_TIMEFRAMES) m_timeFrames[i], 0); // If it does not match the saved one, it is a new bar m_res[i] = (time != m_timeLast[i]); res |= m_res[i]; // Save the new time m_timeLast[i] = time; }); return res; } // Method for getting the new bar flag bool IsNewBar(ENUM_TIMEFRAMES p_timeframe) { int index; // Search for the required timeframe index FIND(m_timeFrames, p_timeframe, index); // If not found, then register a new timeframe if(index == -1) { PrintFormat(__FUNCTION__" | Register new event handler for %s %s", m_symbol, EnumToString(p_timeframe)); index = Register(p_timeframe); } // Return the new bar flag for the necessary timeframe return m_res[index]; } }; //+------------------------------------------------------------------+ //| Static class for defining a new bar for all | //| symbols and timeframes | //+------------------------------------------------------------------+ class CNewBarEvent { private: // Array of objects to define a new bar for one symbol static CSymbolNewBarEvent *m_symbolNewBarEvent[]; // Array of required symbols static string m_symbols[]; // Method to register new symbol and timeframe to track a new bar static int Register(string p_symbol) { APPEND(m_symbols, p_symbol); APPEND(m_symbolNewBarEvent, new CSymbolNewBarEvent(p_symbol)); return ArraySize(m_symbols) - 1; } public: // There is no need to create objects of this class - delete the constructor CNewBarEvent() = delete; // Method for updating new bar flags static bool Update() { bool res = (ArraySize(m_symbolNewBarEvent) == 0); FOREACH(m_symbols, res |= m_symbolNewBarEvent[i].Update()); return res; } // Method to free memory for automatically created objects static void Destroy() { FOREACH(m_symbols, delete m_symbolNewBarEvent[i]); ArrayResize(m_symbols, 0); ArrayResize(m_symbolNewBarEvent, 0); } // Method for getting the new bar flag static bool IsNewBar(string p_symbol, ENUM_TIMEFRAMES p_timeframe) { int index; // Search for the required symbol index FIND(m_symbols, p_symbol, index); // If not found, then register a new symbol if(index == -1) index = Register(p_symbol); // Return the new bar flag for the necessary symbol and timeframe return m_symbolNewBarEvent[index].IsNewBar(p_timeframe); } }; // Initialize static members of the CSymbolNewBarEvent class members; CSymbolNewBarEvent* CNewBarEvent::m_symbolNewBarEvent[]; string CNewBarEvent::m_symbols[]; //+------------------------------------------------------------------+ //| Function for checking a new bar occurrence | //+------------------------------------------------------------------+ bool IsNewBar(string p_symbol, ENUM_TIMEFRAMES p_timeframe) { return CNewBarEvent::IsNewBar(p_symbol, p_timeframe); } //+------------------------------------------------------------------+ //| Function for updating information about new bars | //+------------------------------------------------------------------+ bool UpdateNewBar() { return CNewBarEvent::Update(); } //+------------------------------------------------------------------+ //| Function for removing new bar tracking objects | //+------------------------------------------------------------------+ void DestroyNewBar() { CNewBarEvent::Destroy(); } //+------------------------------------------------------------------+
このコードを現在のフォルダのNewBarEvent.mqhに保存します。
では、このライブラリを取引戦略やEAにどのように応用できるか見てみましょう。しかしその前に、新しいバーの処理とは関係のない、取引戦略の微調整をおこなっておきましょう。
取引戦略の改善
残念ながら、この記事を書いている最中に、使用した戦略に2つの誤りがあることが発覚しました。以前の結果には大きな影響はありませんでしたが、発覚された以上修正しましょう。
最初のエラーは、openDistance_パラメータに負の値が設定された場合、現在の銘柄のスプレッドに等しい小さな正の数にリセットされていたことに起因します。つまり、BUY STOPとSELL_STOPを発注する代わりに、成行注文を出していたのです。このため、最適化中に、そのような未決注文を取引することで達成できたはずの結果が見られませんでした。つまり、潜在的に利益を生む可能性のあるパラメータのセットを逃してしまったということです。
このエラーは、SimpleVolumesStrategy.mqhファイルの未決注文を出す関数内の以下のコードで発生しました。
// Let's make sure that the opening distance is not less than the spread int distance = MathMax(m_openDistance, spread);
m_openDistanceが負になった場合、現在値からの始値シフトのdistance値が正に転じたことになります。m_openDistanceと同じ符号の距離を保存するには、単純に次の式を乗算すればよいのです。
// Let's make sure that the opening distance is not less than the spread int distance = MathMax(MathAbs(m_openDistance), spread) * (m_openDistance < 0 ? -1 : 1);
2つ目の誤りは、過去数バーの平均出来高を計算する際に、現在のバーの出来高も含めてしまっていた点です。戦略の説明では、現在のバーの出来高は計算に含めるべきではないとされています。しかし、このエラーの影響はおそらくごくわずかです。出来高平均の期間が長くなるほど、最新のバーが平均に与える影響は相対的に小さくなります。
このエラーを修正するには、渡された配列の最初の要素を除いて平均を計算する関数を少し変更するだけです。
//+------------------------------------------------------------------+ //| Average value of the array of numbers from the second element | //+------------------------------------------------------------------+ double CSimpleVolumesStrategy::ArrayAverage(const double &array[]) { double s = 0; int total = ArraySize(array) - 1; for(int i = 1; i <= total; i++) { s += array[i]; } return s / MathMax(1, total); }
現在のフォルダのSimpleVolumesStrategy.mqhファイルに変更を保存します。
戦略での新しいバーの検討
取引戦略のいくつかのアクションを新しいバーが発生したときにのみ実行するようにするには、このコードブロックを次のような条件演算子の中に置くだけで済みます。
// If a new bar arrived on H1 for the current strategy symbol, then if(IsNewBar(m_symbol, PERIOD_H1)) { // perform the necessary actions ... }
戦略にこのようなコードがあると、自動的にH1の新バーイベントとm_symbol戦略銘柄のトラッキング登録につながります。
他の追加時間枠で新しいバーの発生を簡単に確認できるようにすることができます。例えば、戦略が平均値幅(ATRやADR)の値を利用する場合、その再計算は以下の方法で1日に1回だけ簡単におこなうことができます。
// If a new bar arrived on D1 for the current strategy symbol, then if(IsNewBar(m_symbol, PERIOD_H1)) { CalcATR(); // call our ATR calculation function }
この連載で検討している取引戦略では、新しいバーが到着した瞬間以外のすべてのアクションを完全に除外することができます。
//+------------------------------------------------------------------+ //| "Tick" event handler function | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::Tick() override { // If there is no new bar on M1, if(!IsNewBar(m_symbol, PERIOD_M1)) return; // If their number is less than allowed if(m_ordersTotal < m_maxCountOfOrders) { // Get an open signal int signal = SignalForOpen(); if(signal == 1 /* || m_ordersTotal < 1 */) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } }
また、EAのOnTickイベントハンドラで、使用する銘柄や時間枠のいずれかについて、新しいバーの開始と一致しないティックの時間帯の処理を禁止することもできます。これを実現するために、CVirtualAdvisor::Tick()メソッドに以下の変更を加えることができます。
//+------------------------------------------------------------------+ //| OnTick event handler | //+------------------------------------------------------------------+ void CVirtualAdvisor::Tick(void) { // Define a new bar for all required symbols and timeframes bool isNewBar = UpdateNewBar(); // If there is no new bar anywhere, and we only work on new bars, then exit if(!isNewBar && m_useOnlyNewBar) { return; } // Receiver handles virtual positions m_receiver.Tick(); // Start handling in strategies CAdvisor::Tick(); // Adjusting market volumes m_receiver.Correct(); // Save status Save(); // Render the interface m_interface.Redraw(); }
このコードでは、EAオブジェクトの作成時に設定できる新しいEAプロパティm_useOnlyNewBarを追加しました。
//+------------------------------------------------------------------+ //| Class of the EA handling virtual positions (orders) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: ... bool m_useOnlyNewBar; // Handle only new bar ticks public: CVirtualAdvisor(ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false); // Constructor ... }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(ulong p_magic = 1, string p_name = "", bool p_useOnlyNewBar = false) : // Initialize the receiver with a static receiver m_receiver(CVirtualReceiver::Instance(p_magic)), // Initialize the interface with the static interface m_interface(CVirtualInterface::Instance(p_magic)), m_lastSaveTime(0), m_useOnlyNewBar(p_useOnlyNewBar) { m_name = StringFormat("%s-%d%s.csv", (p_name != "" ? p_name : "Expert"), p_magic, (MQLInfoInteger(MQL_TESTER) ? ".test" : "") ); };
CVirtualAdvisorを継承して新しいEAクラスを作成し、新しいプロパティと新しいバーの有無を検証する機能を追加しました。しかし、デフォルト値がm_useOnlyNewBar = falseであるため、EAクラスにこの機能を追加しなくても、すべてがそのまま動作します。
EAクラスをこのように拡張した場合、取引戦略クラス内ではTick()メソッドで新しい分足バーのイベントを確認する必要がなくなります。新しいバーのイベントのトラッキングを開始するには、現在の銘柄とM1時間枠において、戦略コンストラクタ内でIsNewBar()関数を一度呼び出すだけで十分です。この設定により、m_useOnlyNewBar = trueのEAは、M1時間枠に新しいバーが発生しない限り、戦略インスタンスのティック処理をトリガーしません。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CSimpleVolumesStrategy::CSimpleVolumesStrategy( ...) : // Initialization list ... { CVirtualReceiver::Get(GetPointer(this), m_orders, m_maxCountOfOrders); // Load the indicator to get tick volumes m_iVolumesHandle = iVolumes(m_symbol, m_timeframe, VOLUME_TICK); // Set the size of the tick volume receiving array and the required addressing ArrayResize(m_volumes, m_signalPeriod); ArraySetAsSeries(m_volumes, true); // Register the event handler for a new bar on the minimum timeframe IsNewBar(m_symbol, PERIOD_M1); } //+------------------------------------------------------------------+ //| "Tick" event handler function | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::Tick() override { // If their number is less than allowed if(m_ordersTotal < m_maxCountOfOrders) { // Get an open signal int signal = SignalForOpen(); if(signal == 1 /* || m_ordersTotal < 1 */) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } }
現在のフォルダのSimpleVolumesStrategy.mqhファイルに変更を保存します。
結果
BenchmarkInstancesExpert.mq5 EAは新しい入力useOnlyNewBars_を取得し、新しいバーの開始と一致しないティックを処理するかどうかを設定します。EAを初期化する際に、EAのコンストラクタにパラメータ値を渡します。
//+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ ... input group "::: Other parameters" sinput ulong magic_ = 27183; // - Magic input bool useOnlyNewBars_ = true; // - Work only at bar opening ... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ... // Create an EA handling virtual positions expert = new CVirtualAdvisor(magic_, "SimpleVolumes_BenchmarkInstances", useOnlyNewBars_); ... }
まずuseOnlyNewBars_ = false、次にuseOnlyNewBars_ = trueで、「実ティックに基づく全ティック」モードの取引戦略を小さな期間で256インスタンスでテストしてみましょう。
最初のケースでは、EAは全ティックで機能し、利益は296米ドルで、実行は04:15で完了しました。2つ目のケースでは、EAは新しいバーの開始時以外のすべてのティックをスキップし、利益は434米ドルで、実行は00:25で完了しました。つまり、2番目のケースでは、計算コストを10分の1に減らしただけでなく、若干高い利益を得たのです。
しかし、ここで楽観的になりすぎてはいけません。他の取引戦略の場合、同じような結果を繰り返すことは決して保証されていません。各取引戦略を個別に検討する必要があります。
結論
改めて結果を振り返ってみましょう。多数の取引戦略インスタンスを同時に実行することで、EAのパフォーマンスをテストしました。このアプローチにより、異なる銘柄、時間枠、取引戦略を1つのEAで組み合わせることが可能となり、取引の多様化が実現しました。
さらに、クラスライブラリに新たな機能を追加しました。この機能は現在検討中の戦略には必要ありませんが、他の取引戦略を実行する際には非常に便利です。また、EAの動作を新しいバーの開始に制限する機能を導入することで、計算コストを削減し、異なるティックシミュレーションモードでのテスト結果がより一貫性のあるものとなります。
とはいえ、プロジェクトが意図した方向から少し逸れてしまいました。しかし、これは最終的な目標達成に向けた一助となるでしょう。短い休憩の後、EAのテスト自動化に再び取り組むつもりです。取引戦略のインスタンスを文字列定数で初期化し、最適化結果を保存するシステムの構築に着手する時期が来たようです。
ご清聴ありがとうございました。またすぐにお会いしましょう。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/14574
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索