English Русский 中文 Español Deutsch Português
preview
リプレイシステムの開発(第71回):正しい時間を知る(IV)

リプレイシステムの開発(第71回):正しい時間を知る(IV)

MetaTrader 5 |
98 0
Daniel Jose
Daniel Jose

はじめに

前回の「リプレイシステムの開発(第70回):正しい時間を知る(III)」では、マウスインジケーターに必要な変更点について説明しました。これらの変更は、マウスインジケーターがオーダーブックのイベントを受け取れるようにするためのものでした。特に、リプレイ/シミュレーションアプリケーションと併用する場合に関係しています。読者の皆さんの中には、これらの変更に非常に困惑し、いら立ちを覚えた方もいたかもしれません。その内容が最初は全く意味不明だったと感じた方もいるでしょう。私としても、それらの変更がもっとシンプルに見えるようにしたかったのですが、実際にはかなり複雑でした。それでもなお、読者の皆さんには、たとえ最初は混乱したように感じたとしても、前回の内容を完全に理解することが極めて重要であるということをご理解いただきたいと思います。前回の記事で私が伝えようとしたことを理解するのに苦労された方も多いかもしれません。しかし、前回の内容(よりシンプルなサービスを使って全体の仕組みがどのように機能するのかを説明した部分)を理解していなければ、これから説明する内容を理解するのははるかに困難になります。 

ですので、この記事の内容に入る前に、前回の記事の内容を理解しておいてください。特に、オーダーブックイベントをカスタム銘柄に追加することによって、それまで不可能だった方法でOnCalculate関数を使用できるようになった部分についてです。これには、MetaTrader 5が提供するデータを取得するためにiSpreadの呼び出しを使用する必要がありました。

本記事では、テストサービスで使っていたコードの一部を、実際にリプレイ/シミュレーションサービスに移植(より正確には転記)していきます。ここで重要なのは、どうやってこれをおこなうかではなく、どのようにおこなうべきかという点です。

読者の皆さんに思い出していただきたいのは、前回までリプレイ/シミュレーションサービスで作業していた際、マウスインジケーターはテンプレートを通じて読み込まれていたということです。しかし、私は今後この方法を使いません。もちろん、テンプレートを使ってマウスインジケーターを読み込みたいのであれば、続けても問題ありません。しかし、実用的な理由から、これからはマウスインジケーターをリプレイ/シミュレーション銘柄のチャートに手動で追加していきます。したがって、いくつかの動画の中で私がそのように実演していても驚かないでください。私なりの理由がありますが、ここでは詳しく説明しません。では、テストサービスで使われていたコードをリプレイ/シミュレーションサービスに転記していきましょう。


転写の開始

まず最初におこなうのは、ヘッダファイルC_Replay.mqh内のコードの一部を修正することです。以下のスニペットをご覧ください。

197. //+------------------------------------------------------------------+
198.       bool InitBaseControl(const ushort wait = 1000)
199.          {
200.             Print("Waiting for Mouse Indicator...");
201.             Sleep(wait);
202.             while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200);
203.             if (def_CheckLoopService)
204.             {
205.                AdjustViewDetails();
206.                Print("Waiting for Control Indicator...");
207.                if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false;
208.                ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle);
209.                UpdateIndicatorControl();
210.             }
211.             
212.             return def_CheckLoopService;
213.          }
214. //+------------------------------------------------------------------+

C_Terminal.mqhファイルのコード

このフラグメントは、元のコードです。さて、ここで注目していただきたいポイントがあります。このコードは、マウスインジケーターがテンプレート経由で読み込まれることを前提に設計されていました。しかし、今後はマウスインジケーターを手動でチャートに配置するようになるため、もはや適切ではありません。技術的には、このままでも動作はします。ただし、実行フローや表示されるメッセージの観点から、より適切な構成に改善することが可能です。そこで、以下に示すのが、今後使用する新しいコードです。

197. //+------------------------------------------------------------------+
198.       bool InitBaseControl(const ushort wait = 1000)
199.          {
200.             Sleep(wait);
201.             AdjustViewDetails();
202.             Print("Loading Control Indicator...");
203.             if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false;
204.             ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle);
205.             Print("Waiting for Mouse Indicator...");
206.             while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200);
207.             UpdateIndicatorControl();
208.             
209.             return def_CheckLoopService;
210.          }
211. //+------------------------------------------------------------------+

C_Replay.mqhファイルからのコード

基本的に、コード自体に大きな変更はありません。しかし、表示されるメッセージがより明確になり、現在何が起こっているのかが把握しやすくなっています。また、実行順序も逆転しています。現在では、まず最初にコントロールインジケーターの読み込みを試みます。これは、リプレイ/シミュレーションアプリケーションの一部であり、リプレイ/シミュレーターの実行ファイル内に組み込まれているためです。その後になってから、マウスインジケーターを読み込みます。ここで注意していただきたいのは、コントロールインジケーターの読み込みに失敗した場合、それは重大な問題を示しているという点です。一方、読み込みに成功した場合は、その時点でユーザーにマウスインジケーターの読み込みも必要であることを通知できます。このような流れの方が、より適切なワークフローであると私は考えています。ただし、読み込みの順序については、好みに応じて変更していただいて構いません。とはいえ、マウスインジケーターがチャート上に存在しない限り、コントロールインジケーターは実際には機能しません。

この変更は、実は美的な性質のものです。では、次に進みましょう。ここからは、オーダーブックメッセージをサポートするための実際の変更に入ります。前回の記事の内容を理解していない場合は、必ずそちらに戻って、以前のコードを使って復習してください。これから示すコードを使って全体の仕組みを理解しようとしても、完全に混乱してしまうだけです。

次に、クラスコンストラクタに新しい行を追加する必要があります。以下のコードに、その新しい行が示されています。

149. //+------------------------------------------------------------------+
150.       C_Replay()
151.          :C_ConfigService()
152.          {
153.             Print("************** Market Replay Service **************");
154.             srand(GetTickCount());
155.             SymbolSelect(def_SymbolReplay, false);
156.             CustomSymbolDelete(def_SymbolReplay);
157.             CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay));
158.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0);
159.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0);
160.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0);
161.             CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
162.             CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8);
163.             CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1);
164.             SymbolSelect(def_SymbolReplay, true);
165.             m_Infos.CountReplay = 0;
166.             m_IndControl.Handle = INVALID_HANDLE;
167.             m_IndControl.Mode = C_Controls::ePause;
168.             m_IndControl.Position = 0;
169.             m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState;
170.          }
171. //+------------------------------------------------------------------+

C_Replay.mqhファイルからのコード

新しい行は正確に163行目です。これが完了すれば、オーダーブックのメッセージを使用できるようになります。ここで一つ注意してください。重要なポイントはC_Replay.mqhヘッダファイルにはありません。重要なのはマウスインジケーターの方です。したがって、その仕組みをよりよく理解するために、インジケーターのコードから一部を抜粋して見てみましょう。これは重要です。

27. //+------------------------------------------------------------------+
28. int OnInit()
29. {
30.    Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04);
31.    if (_LastError >= ERR_USER_ERROR_FIRST) return INIT_FAILED;
32.    MarketBookAdd((*Study).GetInfoTerminal().szSymbol);
33.    OnBookEvent((*Study).GetInfoTerminal().szSymbol);
34.    m_Status = C_Study::eCloseMarket;
35.    SetIndexBuffer(0, m_Buff, INDICATOR_DATA);
36.    ArrayInitialize(m_Buff, EMPTY_VALUE);
37.    
38.    return INIT_SUCCEEDED;
39. }
40. //+------------------------------------------------------------------+

マウスポインタファイルのフラグメント

34行目に注目してください。ここでは、マウスインジケーターに初期ステータスを設定しています。このステータスは、市場が閉じていることを示しています。しかし、ここで扱っているのは実際の市場ではありません。私たちが作業しているのは、市場の動きをリプレイまたはシミュレーションするためのアプリケーションです。つまり、マウスインジケーターが表示しているメッセージは、リプレイ/シミュレーション用のカスタム銘柄では正しくありません。幸いなことに、これは非常に簡単に修正できます。ただし、その前に理解しておくべき重要な点があります。チャートにコントロールインジケーターが配置された瞬間、リプレイ/シミュレーションは一時停止状態にあるということです。この状態は、アプリケーションが最初から初期化されたときに適用されます。ここで、今後の動作に大きく影響する決断を下さなければなりません。

これをよく考えてみましょう。一時停止モードのとき、マウスインジケーターは「オークション中」というメッセージを表示すべきでしょうか。それとも、現在のバーの残り時間を表示すべきでしょうか。仮に残り時間を表示するとして、それは最初の再生がトリガーされる前に出すべきでしょうか、それとも後に出すべきでしょうか。少しわかりにくいかもしれないので、説明しましょう。市場が正式に開く前には、オークションフェーズが存在します。このフェーズでは、参加者が「本当に売買したい価格」で注文を出すことができます。つまり、リプレイ/シミュレーションアプリがMetaTrader 5にチャートを読み込ませ、マウスインジケーターが表示された瞬間に表示されるべきメッセージは、「オークション中」であるべきなのです。これは事実です。では、再生を開始してから再度一時停止した場合、メッセージはどうあるべきでしょう。「オークション中」のままであるべきでしょうか。それとも、現在のバーの残り時間」に変わるべきでしょうか。ここが、私たちが考えるべきポイントです。とはいえ、まず最初に確実にしなければならないのは、アプリケーションが開始されたとき、オークションフェーズにいることが明確に表示されているという点です。

これは実は非常に簡単です。以下のコードスニペットを参照してください。

212. //+------------------------------------------------------------------+
213.       bool LoopEventOnTime(void)
214.          {         
215.             int iPos, iCycles;
216.             MqlBookInfo book[1];
217.             
218.             book[0].price = 1.0;
219.             book[0].volume = 1;
220.             book[0].type = BOOK_TYPE_BUY_MARKET;
221.             CustomBookAdd(def_SymbolReplay, book, 1);            
222.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
223.             {
224.                UpdateIndicatorControl();
225.                Sleep(200);
226.             }
227.             m_MemoryData = GetInfoTicks();
228.             AdjustPositionToReplay();
229.             iPos = iCycles = 0;
230.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
231.             {
232.                if (m_IndControl.Mode == C_Controls::ePause) return true;
233.                iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0);
234.                CreateBarInReplay(true);
235.                while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause))
236.                {
237.                   Sleep(195);
238.                   iPos -= 200;
239.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
240.                   UpdateIndicatorControl();
241.                   iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1);
242.                }
243.             }
244. 
245.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
246.          }
247. };
248. //+------------------------------------------------------------------+

C_Replay.mqhファイルからのコード

ここからは、実際に実装する簡単な部分について説明していきます。今後、少しずつ実装内容を紹介していきますので、処理の流れをしっかり追えるようになります。まずは216行目を見てください。ここでは、新しい変数が登場しています。これは要素数1の配列です。そして、ここからが面白いところです。

おそらく、ブックイベントに投入される値には何らかの意味が必要だと考えているかもしれません。しかし実際には、特別な意味は一切必要ありません。重要なのは、内部的なロジックに沿って値が設定されているということだけで、その値に意味がある必要はないのです(本物のオーダーブックをシミュレーションしたい場合は別ですが)。ただし、今回はそれが目的ではありません。将来的には考えるかもしれませんが、現段階ではそうではありません。

いずれにせよ、218行目と219行目は何をしているのかというと、MetaTrader 5がブック情報を初期化できるように何らかの値を提供しているだけです。ここで使われている値にも特に意味はありません。本当に重要なのはその次、220行目です。ここで、オークション状態であることを示すポジション情報をブックに伝えています。この挙動の意味がピンとこない場合は、前回の記事をもう一度見直すことをおすすめします。そして、221行目でMetaTrader 5に対してカスタムブックイベントを発生させるように指示しています。このイベントは、マウスインジケーター内に存在するOnEventBook関数によって受け取られます。結果として、LoopEventOnTime関数が実行されるたびに、マウスインジケーターがオークション中だと表示してくれるようになるのです。このLoopEventOnTimeが最初から実行されるタイミングは2つあります。最初は、アプリケーションが初期化された直後です。2番目は、ユーザーがコントロールインジケーターの一時停止ボタンを押したときです。後者の場合、232行目が実行され、その直後に再びLoopEventOnTimeが呼び出されることになります。どんなに簡単かにお気づきでしょうか。これで「オークション中」メッセージが表示されるようになりました。では次に、バーの残り時間を表示するにはどうしたらよいでしょうか。これは実は非常に簡単です。そのためには、コードを次のように変更する必要があります。

212. //+------------------------------------------------------------------+
213.       bool LoopEventOnTime(void)
214.          {         
215.             int iPos, iCycles;
216.             MqlBookInfo book[1];
217.             
218.             book[0].price = 1.0;
219.             book[0].volume = 1;
220.             book[0].type = BOOK_TYPE_BUY_MARKET;
221.             CustomBookAdd(def_SymbolReplay, book, 1);
222.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
223.             {
224.                UpdateIndicatorControl();
225.                Sleep(200);
226.             }
227.             m_MemoryData = GetInfoTicks();
228.             AdjustPositionToReplay();
229.             iPos = iCycles = 0;
230.             book[0].type = BOOK_TYPE_BUY;
231.             CustomBookAdd(def_SymbolReplay, book, 1);
232.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
233.             {
234.                if (m_IndControl.Mode == C_Controls::ePause) return true;
235.                iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0);
236.                CreateBarInReplay(true);
237.                while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause))
238.                {
239.                   Sleep(195);
240.                   iPos -= 200;
241.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
242.                   UpdateIndicatorControl();
243.                   iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1);
244.                }
245.             }
246. 
247.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
248.          }
249. };
250. //+------------------------------------------------------------------+

C_Replay.mqhファイルからのコード

違いに気づきましたか。もし気づかなかったなら、おそらく何かに気を取られていたのでしょう。というのも、違いはまさに230行目と231行目の追加にあるからです。この2行によって、ユーザーがリプレイ/シミュレーションサービスで再生ボタンを押した瞬間に、マウスインジケーターがカスタムのオーダーブックイベントを受け取るようになります。このイベントは、オークション状態を終了し、通常の取引が始まったことをマウスインジケーターに通知する役割を果たします。その結果、現在のバーの残り時間がマウスインジケーター上に表示されるようになるのです。ご覧のとおり、ここではすべてが簡単です。しかしここから、もう少し複雑な条件に対処しなければならなくなります。

実際の市場(つまり、取引サーバーに接続してリアルなデータを扱っている場合)には、銘柄が一時的に停止されたり、オークションフェーズに入ったりすることがあります。これは、特定の規制条件によって発生するもので、以前の記事でも触れました。とはいえ、今の段階ではもっと単純化されたルールを実装し、あるティックと次のティックの間に60秒以上の時間差があれば、その資産はオークション状態に入ったと見なします。これは実は非常に簡単です。難しいのはそのあとです。つまり、オークション状態のあとに、再びバーの残り時間を表示させるにはどうすればいいのでしょうか。

たとえば、資産がオークションモードに入ったときはBOOK_TYPE_BUY_MARKETを送って、オークションを抜けたときはBOOK_TYPE_BUYを送ればいいと考えるかもしれません。まさに、それが私たちがやるべきことなのです。では、どうすれば正しく実現できるのでしょうか。これをよく考えてみましょう。私たちはLoopEventOnTime関数を最初から再実行させたくない、あくまで、232行目から245行目までのループ内で継続して処理をおこないたいということです。さて、もしループの中でBOOK_TYPE_BUY_MARKETとBOOK_TYPE_BUYの両方を送信してしまった場合、問題が発生します。というのも、CustomBookAddをこれら異なる定数とともに繰り返し呼び出すと、マウスインジケーターの表示が不快な視覚効果を生んでしまうのです。表示が点滅したり、バーの残り時間と「AUCTION」という文字列が高速で交互に切り替わったりするのです。

そこで、ここからが工夫のしどころです。私たちは、このちらつきの効果を避けながら、なおかつこの問題をきちんと解決する方法を実装しなければなりません。私が提案する解決策は以下の通りです。

212. //+------------------------------------------------------------------+
213.       bool LoopEventOnTime(void)
214.          {         
215.             int iPos, iCycles;
216.             MqlBookInfo book[1];
217.             ENUM_BOOK_TYPE typeMsg;
218.             
219.             book[0].price = 1.0;
220.             book[0].volume = 1;
221.             book[0].type = BOOK_TYPE_BUY_MARKET;
222.             CustomBookAdd(def_SymbolReplay, book, 1);
223.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
224.             {
225.                UpdateIndicatorControl();
226.                Sleep(200);
227.             }
228.             m_MemoryData = GetInfoTicks();
229.             AdjustPositionToReplay();
230.             iPos = iCycles = 0;
231.             book[0].type = BOOK_TYPE_BUY;
232.             CustomBookAdd(def_SymbolReplay, book, 1);
233.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
234.             {
235.                if (m_IndControl.Mode == C_Controls::ePause) return true;
236.                iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0);
237.                if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type)
238.                {
239.                   book[0].type = typeMsg;
240.                   CustomBookAdd(def_SymbolReplay, book, 1);                  
241.                }
242.                CreateBarInReplay(true);
243.                while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause))
244.                {
245.                   Sleep(195);
246.                   iPos -= 200;
247.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
248.                   UpdateIndicatorControl();
249.                   iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1);
250.                }
251.             }
252. 
253.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
254.          }
255. };
256. //+------------------------------------------------------------------+

C_Replay.mqhファイルからのコード

たしかに、このコードは決してエレガントとは言えませんが、少なくともきちんと動作します。そして、もし望むなら、タイミングの閾値を増減させて異なる条件下でテストすることも可能です。それは完全にあなた次第です。ただし、何かを変更する前に、このコードスニペットで何が起きているのかを一度きちんと理解してみましょう。

まず注目すべき点があります。217行目では、新しい変数が宣言されています。この変数は、オーダーブックが受け入れる定数のいずれかを格納するためのものです。そして237行目では、三項演算子を使用してロジックを簡潔にしています。ここでの狙いは、ある条件を評価し、それに応じてtypeMsg変数に値を代入するというものです。ここからが重要です。もっとコードを凝縮して書くこともできました。ですが、それでは説明が不要に複雑になってしまうため、今回は避けました。仕組みは以下のとおりです。まず、ある定数がtypeMsgに代入されます。その後、その値が最後に送信されたカスタムブックイベントの値と異なるかどうかを確認します。異なっていた場合には、239行目でその新しい定数を次に使うものとして代入し、240行目でCustomBookAddを呼び出します。ここで特に注目してほしいのは、237行目でiPos変数と比較されている値です。この値は60000です。閾値として使用しているのは1分なのに何故この数値なのかと思うかもしれませんが、ここで一つシンプルな点を思い出す必要があります。1分は60秒であり、1秒は1,000ミリ秒です。一方で、236行目においてiPosに代入されている値はミリ秒単位であるため、比較対象として60000を用いるのは理にかなっていると言えます。これは、ミリ秒単位で表された60秒間隔を反映します。したがって、この閾値をより適切と思われる別の値に変更する場合は、必ず適切にミリ秒単位に変換してください。そうしないと、特に資産にチャートにティックが追加されずにしばらく経つと、マウスインジケーターが予期しない動作を示す可能性があります。

これで、C_Replay.mqhコードファイルの最終バージョンを最終的に表示できるようになります。この段階では、タイミングに関連するすべての側面が完全に実装されており、少なくとも最終結果は以下のビデオに示されているとおりに動作します。


C_Replay.mqhヘッダファイルの完全なソースコードを以下に示します。

001. //+------------------------------------------------------------------+
002. #property copyright "Daniel Jose"
003. //+------------------------------------------------------------------+
004. #include "C_ConfigService.mqh"
005. #include "C_Controls.mqh"
006. //+------------------------------------------------------------------+
007. #define def_IndicatorControl   "Indicators\\Market Replay.ex5"
008. #resource "\\" + def_IndicatorControl
009. //+------------------------------------------------------------------+
010. #define def_CheckLoopService ((!_StopFlag) && (ChartSymbol(m_Infos.IdReplay) != ""))
011. //+------------------------------------------------------------------+
012. #define def_ShortNameIndControl   "Market Replay Control"
013. #define def_MaxSlider             (def_MaxPosSlider + 1)
014. //+------------------------------------------------------------------+
015. class C_Replay : public C_ConfigService
016. {
017.    private   :
018.       struct st00
019.       {
020.          C_Controls::eObjectControl Mode;
021.            uCast_Double             Memory;
022.          ushort                     Position;
023.          int                        Handle;
024.       }m_IndControl;
025.       struct st01
026.       {
027.          long     IdReplay;
028.          int      CountReplay;
029.          double   PointsPerTick;
030.          MqlTick  tick[1];
031.          MqlRates Rate[1];
032.       }m_Infos;
033.       stInfoTicks m_MemoryData;
034. //+------------------------------------------------------------------+
035. inline bool MsgError(string sz0) { Print(sz0); return false; }
036. //+------------------------------------------------------------------+
037. inline void UpdateIndicatorControl(void)
038.          {
039.             double Buff[];
040.                                  
041.             if (m_IndControl.Handle == INVALID_HANDLE) return;
042.             if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position)
043.             {
044.                if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1)
045.                   m_IndControl.Memory.dValue = Buff[0];
046.                if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay)
047.                   m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition];
048.             }else
049.             {
050.                m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position;
051.                m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode;
052.                m_IndControl.Memory._8b[7] = 'D';
053.                m_IndControl.Memory._8b[6] = 'M';
054.                EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, "");
055.             }
056.          }
057. //+------------------------------------------------------------------+
058.       void SweepAndCloseChart(void)
059.          {
060.             long id;
061.             
062.             if ((id = ChartFirst()) > 0) do
063.             {
064.                if (ChartSymbol(id) == def_SymbolReplay)
065.                   ChartClose(id);
066.             }while ((id = ChartNext(id)) > 0);
067.          }
068. //+------------------------------------------------------------------+
069. inline int RateUpdate(bool bCheck)
070.          {
071.             static int st_Spread = 0;
072. 
073.             st_Spread = (bCheck ? (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time) : st_Spread + 1);
074.             m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread);
075.             CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate);
076.             
077.             return 0;
078.          }
079. //+------------------------------------------------------------------+
080. inline void CreateBarInReplay(bool bViewTick)
081.          {
082.             bool   bNew;
083.             double dSpread;
084.             int    iRand = rand();
085. 
086.             if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew))
087.             {
088.                m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay];
089.                if (m_MemoryData.ModePlot == PRICE_EXCHANGE)
090.                {                  
091.                   dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 );
092.                   if (m_Infos.tick[0].last > m_Infos.tick[0].ask)
093.                   {
094.                      m_Infos.tick[0].ask = m_Infos.tick[0].last;
095.                      m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread;
096.                   }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid)
097.                   {
098.                      m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread;
099.                      m_Infos.tick[0].bid = m_Infos.tick[0].last;
100.                   }
101.                }
102.                if (bViewTick)
103.                   CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
104.                RateUpdate(true);
105.             }
106.             m_Infos.CountReplay++;
107.          }
108. //+------------------------------------------------------------------+
109.       void AdjustViewDetails(void)
110.          {
111.             MqlRates rate[1];
112. 
113.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
114.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX);
115.             ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE);
116.             m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE);
117.             CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate);
118.             if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE))
119.                for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++);
120.             if (rate[0].close > 0)
121.             {
122.                if (GetInfoTicks().ModePlot == PRICE_EXCHANGE)
123.                   m_Infos.tick[0].last = rate[0].close;
124.                else
125.                {
126.                   m_Infos.tick[0].bid = rate[0].close;
127.                   m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick);
128.                }               
129.                m_Infos.tick[0].time = rate[0].time;
130.                m_Infos.tick[0].time_msc = rate[0].time * 1000;
131.             }else
132.                m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay];
133.             CustomTicksAdd(def_SymbolReplay, m_Infos.tick);
134.          }
135. //+------------------------------------------------------------------+
136.       void AdjustPositionToReplay(void)
137.          {
138.             int nPos, nCount;
139.             
140.             if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return;
141.             nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider);
142.             for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread);
143.             if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1);
144.             while ((nPos > m_Infos.CountReplay) && def_CheckLoopService)
145.                CreateBarInReplay(false);
146.          }
147. //+------------------------------------------------------------------+
148.    public   :
149. //+------------------------------------------------------------------+
150.       C_Replay()
151.          :C_ConfigService()
152.          {
153.             Print("************** Market Replay Service **************");
154.             srand(GetTickCount());
155.             SymbolSelect(def_SymbolReplay, false);
156.             CustomSymbolDelete(def_SymbolReplay);
157.             CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay));
158.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0);
159.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0);
160.             CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0);
161.             CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation");
162.             CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8);
163.             CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1);
164.             SymbolSelect(def_SymbolReplay, true);
165.             m_Infos.CountReplay = 0;
166.             m_IndControl.Handle = INVALID_HANDLE;
167.             m_IndControl.Mode = C_Controls::ePause;
168.             m_IndControl.Position = 0;
169.             m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState;
170.          }
171. //+------------------------------------------------------------------+
172.       ~C_Replay()
173.          {
174.             SweepAndCloseChart();
175.             IndicatorRelease(m_IndControl.Handle);
176.             SymbolSelect(def_SymbolReplay, false);
177.             CustomSymbolDelete(def_SymbolReplay);
178.             Print("Finished replay service...");
179.          }
180. //+------------------------------------------------------------------+
181.       bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate)
182.          {
183.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0)
184.                return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket.");
185.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0)
186.                return MsgError("Asset configuration is not complete, need to declare the ticket value.");
187.             if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0)
188.                return MsgError("Asset configuration not complete, need to declare the minimum volume.");
189.             SweepAndCloseChart();
190.             m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1);
191.             if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl"))
192.                Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl");
193.             else
194.                Print("Apply template: ", szNameTemplate, ".tpl");
195. 
196.             return true;
197.          }
198. //+------------------------------------------------------------------+
199.       bool InitBaseControl(const ushort wait = 1000)
200.          {
201.             Sleep(wait);
202.             AdjustViewDetails();
203.             Print("Loading Control Indicator...");
204.             if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false;
205.             ChartIndicatorAdd(m_Infos.IdReplay, 0, m_IndControl.Handle);
206.             Print("Waiting for Mouse Indicator...");
207.             while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(200);
208.             UpdateIndicatorControl();
209.             
210.             return def_CheckLoopService;
211.          }
212. //+------------------------------------------------------------------+
213.       bool LoopEventOnTime(void)
214.          {         
215.             int iPos, iCycles;
216.             MqlBookInfo book[1];
217.             ENUM_BOOK_TYPE typeMsg;
218.             
219.             book[0].price = 1.0;
220.             book[0].volume = 1;
221.             book[0].type = BOOK_TYPE_BUY_MARKET;
222.             CustomBookAdd(def_SymbolReplay, book, 1);
223.             while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay))
224.             {
225.                UpdateIndicatorControl();
226.                Sleep(200);
227.             }
228.             m_MemoryData = GetInfoTicks();
229.             AdjustPositionToReplay();
230.             iPos = iCycles = 0;
231.             book[0].type = BOOK_TYPE_BUY;
232.             CustomBookAdd(def_SymbolReplay, book, 1);
233.             while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService))
234.             {
235.                if (m_IndControl.Mode == C_Controls::ePause) return true;
236.                iPos += (int)(m_Infos.CountReplay < (m_MemoryData.nTicks - 1) ? m_MemoryData.Info[m_Infos.CountReplay + 1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : 0);
237.                if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type)
238.                {
239.                   book[0].type = typeMsg;
240.                   CustomBookAdd(def_SymbolReplay, book, 1);                  
241.                }
242.                CreateBarInReplay(true);
243.                while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause))
244.                {
245.                   Sleep(195);
246.                   iPos -= 200;
247.                   m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks);
248.                   UpdateIndicatorControl();
249.                   iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1);
250.                }
251.             }
252. 
253.             return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService));
254.          }
255. };
256. //+------------------------------------------------------------------+
257. #undef def_SymbolReplay
258. #undef def_CheckLoopService
259. #undef def_MaxSlider
260. //+------------------------------------------------------------------+

C_Replay.mqhファイルのソースコード


これは、なかなか興味深いものの、無害なバグです。

さて、ここまで構築し動作を確認してきたシステムは、非常にうまく機能しています。とはいえ、まだ1つ問題が残っています。この問題は動画内では示していませんが、冷静に仕組みを考えれば、現在の実装にある欠陥に気づくはずです。ですがその問題を説明する前に、まず一度立ち止まって、MetaTrader 5の動作原理をどれだけ理解しているかを確認してみましょう。MetaTrader 5がさまざまな条件下でどのように動作するかをきちんと理解していなければ、おそらくこの問題にはまだ気づいていないかもしれません。なぜなら、この不具合は非常に特定の挙動に起因しているからです。それは「リプレイ/シミュレーションシステムを使用中に、チャートの時間枠を変更することはできないという点です。

おそらく、チャートの時間枠を変更できないというのはどういう意味なのか。試してみるとどうなるのかと思われたのではないでしょうか。ご説明しましょう。これは100%の確率で起きる問題です。時間枠を変更した瞬間、今回の実装における欠陥が発動します。ただし、この不具合が表面化するのは、あくまでも特定の条件が揃った場合に限ります。リプレイ/シミュレーションシステムが再生モードで実行されている場合にのみトリガーされます。一時停止モードの場合、または取引回数に基づいて銘柄がオークション中であることをシステムが検出した場合、エラーは発生しません。繰り返しますが、これは再生モード中にティックがチャートに追加されているときだけ起こります。

MQL5でのリプレイ/シミュレータシステム構築に関する本連載をフォローしている人に対してs質問します。この欠陥が何なのか、わかりますか。もし「はい」と答えたなら、それはあなたがMQL5とMetaTrader 5の仕組みをしっかりと学んでいる証拠です。答えが「いいえ」であっても、心配する必要はありません。まだ学習の初期段階にあるというだけのことです。これをモチベーションにして、さらに学びを進めていきましょう。

それでは、このバグが実際には何なのかを見てみましょう。これは小さく、無害で、再生モード中にのみ現れるバグです。ティックが活発に処理されているときに時間足を変更すると、面白い現象が発生します。上述のすべての条件が満たされていて、時間足を変更した場合、マウスインジケーターが突然市場が閉じていると表示するようになります。また、現在のバーの残り時間も表示されなくなります。この問題を解決するには、シミュレーションを一時停止し、再開する必要があります。


さて、こう思っているかもしれません。「時間枠を変更すると、インジケーターが市場が閉まっていると表示するのはなぜですか。意味が分かりません。」それに関しては、私も同感です。でも、MetaTrader 5を長く使っている人なら、おそらくその理由をすでにご存知でしょう。MetaTrader 5は、チャートからすべてのインジケーターやその他の要素をアンロードし、その後、新しい時間足に基づいてチャートデータを更新し、インジケーターなどを一から再読み込みします。これがMetaTrader 5の仕組みです。つまり、マウスインジケーターが再読み込みされると、デフォルトの状態からスタートするのです。これを実際に確認したければ、以下のマウスインジケーターのコードスニペットを見てください。

27. //+------------------------------------------------------------------+
28. int OnInit()
29. {
30.    Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04);
31.    if (_LastError >= ERR_USER_ERROR_FIRST) return INIT_FAILED;
32.    MarketBookAdd((*Study).GetInfoTerminal().szSymbol);
33.    OnBookEvent((*Study).GetInfoTerminal().szSymbol);
34.    m_Status = C_Study::eCloseMarket;
35.    SetIndexBuffer(0, m_Buff, INDICATOR_DATA);
36.    ArrayInitialize(m_Buff, EMPTY_VALUE);
37.    
38.    return INIT_SUCCEEDED;
39. }
40. //+------------------------------------------------------------------+

マウスポインタファイルのフラグメント

さて、次の点に注目してください。スニペットの34行目では、status変数の値を初期化しています。この値は、実質的に市場が閉じていることを示します。しかし、リプレイ/シミュレーションサービスからは引き続きティックが到着しています。そして、まさにこの点に今回の欠陥が存在しています。ご覧の通り、このバグは無害で深刻な問題を引き起こすわけではありません。ただの不便さであり、サービスを一時停止して再開すれば簡単に解決できます。それだけで、すべては正常に戻ります。

この問題を解決する方法は、何千通りも思いつくかもしれません。しかし、私が紹介する解決策は、あなたの予想を遥かに超える意外な方法です。その前に、ひとつ質問しましょう。なぜ、シミュレーションを一時停止してから再開するだけで問題が解消されるのかわかりますか。もしわからない場合は、その理由を一緒に見ていきましょう。この動作を理解するには、C_Replay.mqhヘッダファイル内のLoopEventOnTime関数に戻る必要があります。バグが発生すると、マウスインジケーターは市場が閉じていると表示します。そして、一時停止モードに切り替えると、233行目から始まるループが、235行目で中断されます。これにより関数の実行は中断され、メインコードの処理に戻ります。戻り値がtrueであるため、サービスのメインルーチンはすぐにLoopEventOnTimeを再び呼び出します。この時点で、222行目が221行目で定義された値を使ってカスタムのオーダーブックイベントをトリガーします。その結果、マウスインジケーターのステータスが「市場が閉じている」から「オークションモード」に更新されます。そして、ユーザーが再度再生ボタンを押してシミュレーションを再開すると、232行目が別のカスタムイベントを発生させますが、今度は231行目で定義された定数を使用します。この定数によって、マウスインジケーターは再び「現在のバーの残り時間」を表示できるようになります。

つまり、チャートの時間足を変更することでこのバグが発生した場合でも、一時停止して再開するだけで正常な状態に戻るというわけです。

ただし、この問題はマウスインジケーターには大きな影響を与えませんが、コントロールインジケーターには別の問題を引き起こします。この場合、簡単な回避策は存在しません。C_Replay.mqhヘッダファイルの42行目で評価されている条件が失敗するのを待つ必要があるのです。なぜでしょうか。なぜこの条件が失敗しないと、コントロールインジケーターはチャート上に再表示されないのでしょうか。それは、その条件が失敗したときだけ、54行目のコードが実行されるからです。そして、私たちはこの54行目の実行を必要としているのです。なぜなら、それによってインジケーターが更新されるからです。一度更新されれば、以前に何らかの理由で非表示になっていたとしても、再びチャートに表示されます。

さて、ここまで読んで、状況がどんどん複雑になっていることに気づいたでしょう。マウスインジケーターの不具合であれば、「一時停止 → 再生」で簡単に直りますが、コントロールインジケーターが表示されていない状態ではそれすらできないのです。では、どうすれば良いのでしょうか。コントロールインジケーターがなければ、アプリケーションを一時停止できません。そして、サービスはチャートにティックを送り続けます。ティックの追加が止まらない限り、42行目の条件が失敗する状態には到達しません。その条件が失敗するのは、ある程度のティック数が追加されたあとに限られます。これは深刻な問題です。

サービス側でチャートの時間足を監視すればいい。変更があれば、マウスとコントロール両方のインジケーターを再初期化するようにすればいい、と考えるかもしれません。いいでしょう。でも、サービス側からチャートの時間足の変更を検知する方法を本当に知っていますか。もしかすると可能かもしれません。でも、それは私がこれから紹介する方法より遥かに複雑になるでしょう。その解決策は、この記事ではなく次回の記事で紹介します。なぜなら、読者の皆さんに、まずは自分でこの問題を解決してみる機会を与えたいからです。


結論

この記事では、前回の記事で使用したコードを再利用する方法を紹介しました。前回はカスタムオーダーブックメッセージの機能をテストする目的で使われていたコードを、今回開発中のサービスへ統合しました。また、一見無害に見える小さなバグが、実は大きな頭痛の種になることもあるという例もお見せしました。 

とはいえ、チャートの時間足変更に関する問題の解決方法について、きっと興味が湧いてきたのではないでしょうか。特に、「どうすればサービス側でチャートの時間足変更を検出できるのか」という点です。その解決策については、次回の記事で詳しく解説します。

プログラマーであるということは、他の人が「もう手詰まりだ」と思うところに、道を見出すことです。問題は常に存在します。もし問題がなければ、プログラミングはこんなにも楽しく、やりがいのあるものではなかったはずです。だからこそ、問題が多いほど良いのです。そして、問題が複雑であればあるほど、それを解決したときの達成感も大きくなります。そうした課題こそが、私たちを型にはまった考え方から解放し、新たな発想へと導いてくれるのです。 

次の記事でまたお会いしましょう。そして、読む前にぜひ、チャートの時間足変更を検知する方法を自分なりに考えてみてください。

MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12335

添付されたファイル |
Anexo.zip (420.65 KB)
多通貨エキスパートアドバイザーの開発(第19回):Pythonで実装されたステージの作成 多通貨エキスパートアドバイザーの開発(第19回):Pythonで実装されたステージの作成
これまでは、標準のストラテジーテスター内で最適化タスクを順に自動実行することだけを考えてきました。しかし、もしそれらの実行の合間に、別の手段で得られたデータを処理したいとしたらどうなるでしょうか。ここでは、Pythonで記述されたプログラムによって新たな最適化ステージを作成する機能の追加を試みます。
初級から中級まで:配列(III) 初級から中級まで:配列(III)
この記事では、MQL5における配列の扱い方について解説します。具体的には、関数や手続き(プロシージャ)間で配列を使って情報を受け渡す方法に焦点を当てます。本連載の今後の資料で説明・実演される内容の準備段階として、今回の記事は非常に重要です。そのため、ここで紹介される内容を注意深く学ぶことを強くお勧めします。
ALGLIBライブラリの最適化手法(第1回): ALGLIBライブラリの最適化手法(第1回):
この記事では、MQL5におけるALGLIBライブラリの最適化手法について紹介します。記事には、最適化問題を解決するためにALGLIBを使用するシンプルで分かりやすい例が含まれており、これらの手法をできるだけ身近に感じられるように構成されています。BLEIC、L-BFGS、NSといったアルゴリズムのつながりを詳しく見ていき、それらを使って簡単なテスト問題を解いてみます。
取引におけるニューラルネットワーク:対照パターンTransformer 取引におけるニューラルネットワーク:対照パターンTransformer
Contrastive Transformerは、個々のローソク足のレベルと、全体のパターンに基づいて市場を分析するよう設計されています。これにより、市場トレンドのモデリングの質が向上します。さらに、ローソク足とパターンの表現を整合させるために対照学習を用いることで、自己調整が促され、予測の精度が高まります。