リプレイシステムの開発(第73回):異例のコミュニケーション(II)
はじめに
前回の「リプレイシステムの開発(第72回):異例のコミュニケーション(I)」では、通常では取得不可能な特定の情報をインジケーターを通じて伝達する方法を紹介し始めました。その記事ではいくつかの点について説明を加えましたが、実際にリプレイ/シミュレーターアプリケーションでコードを実装するのは、決して簡単なことではありません。一見すると、こうした機能の実装は直感的で明確に思えるかもしれません。また、このテーマに関して私があえて複雑さを強調しているように思われるかもしれません。
そうであったらよいのですが、実際に、このトピックは思った以上に複雑で、慎重に取り組む必要があります。本連載では、MQL5を真剣に学びたいと考える読者にも理解しやすいように、できる限り丁寧に内容を解説してきました。今回取り上げるテーマは、少なくとも執筆時点において、他ではあまり例を見ない内容です。考えつかないからではなく、非常に特殊かつニッチで、取り組む人が少ない領域だからです。そのため、前例のない課題に挑戦する際の進め方について、できる限り詳細に取り上げることが重要であると考えています。
実装の続き
本連載で最も複雑と思われるサービス自体の実装に入る前に、まずはコントロール用インジケーターのコードを確認しておきましょう。というのも、前回の記事ではC_Controls.mqhヘッダファイルの紹介のみに留まり、内容が非常に多かったため、コントロールインジケーターについての説明をこの回で完結させることにしました。それでは、ソースコードの確認から始めていきましょう。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property icon "/Images/Market Replay/Icons/Replay - Device.ico" 04. #property description "Control indicator for the Replay-Simulator service." 05. #property description "This one doesn't work without the service loaded." 06. #property version "1.73" 07. #property link "https://www.mql5.com/pt/articles/12363" 08. #property indicator_chart_window 09. #property indicator_plots 0 10. #property indicator_buffers 1 11. //+------------------------------------------------------------------+ 12. #include <Market Replay\Service Graphics\C_Controls.mqh> 13. //+------------------------------------------------------------------+ 14. C_Controls *control = NULL; 15. //+------------------------------------------------------------------+ 16. input long user00 = 0; //ID 17. //+------------------------------------------------------------------+ 18. double m_Buff[]; 19. int m_RatesTotal = 0; 20. //+------------------------------------------------------------------+ 21. int OnInit() 22. { 23. if (CheckPointer(control = new C_Controls(user00, "Market Replay Control", new C_Mouse(user00, "Indicator Mouse Study"))) == POINTER_INVALID) 24. SetUserError(C_Terminal::ERR_PointerInvalid); 25. if ((_LastError >= ERR_USER_ERROR_FIRST) || (user00 == 0)) 26. { 27. Print("Control indicator failed on initialization."); 28. return INIT_FAILED; 29. } 30. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 31. ArrayInitialize(m_Buff, EMPTY_VALUE); 32. 33. return INIT_SUCCEEDED; 34. } 35. //+------------------------------------------------------------------+ 36. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 37. { 38. (*control).SetBuffer(m_RatesTotal = rates_total, m_Buff); 39. 40. return rates_total; 41. } 42. //+------------------------------------------------------------------+ 43. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 44. { 45. (*control).DispatchMessage(id, lparam, dparam, sparam); 46. (*control).SetBuffer(m_RatesTotal, m_Buff); 47. } 48. //+------------------------------------------------------------------+ 49. void OnDeinit(const int reason) 50. { 51. switch (reason) 52. { 53. case REASON_TEMPLATE: 54. Print("Modified template. Replay // simulation system shutting down."); 55. case REASON_INITFAILED: 56. case REASON_PARAMETERS: 57. case REASON_REMOVE: 58. case REASON_CHARTCLOSE: 59. ChartClose(user00); 60. break; 61. } 62. delete control; 63. } 64. //+------------------------------------------------------------------+
コントロールインジケーターのソースコード
このコードを以前のものと比較すると、25行目に違いがあることに気づかれると思います。これまでは、_LastError変数のチェックにERR_SUCCESS定数を使っていました。しかし、実際にはそれがいくつかの問題を引き起こしていたのです。というのも、インジケーターがチャートに表示された時点で、_LastErrorにERR_SUCCESSとは異なる値が入っているケースがあったからです。
初期化が時々失敗する理由を理解するのに、少し時間がかかりました。ResetLastError関数を明示的に呼び出してエラーをクリアしたり、デバッガで問題を確認しても、C_Controlsクラスのコンストラクタが_LastErrorに何らかの値を持ったまま終了してしまうことがあったのです。
さらに奇妙なのは、そのエラーが別の銘柄やチャートに関係しているような内容であることも多かったという点です。つまり、他のチャート間で何らかの情報がリークしている可能性があります。ただし、これはあくまで推測に過ぎず、断定はできません。ただし、カスタム銘柄のチャートしか開いていない状況でも、アプリケーションとは無関係なエラーが返されることがありました。そのため、エラーの分離処理をおこなうことにしました。SetUserError関数によって明示的に設定されたエラーのみが、インジケーターをチャートから削除する条件になるようにしたのです。
しかし、ここで本当に注目すべきなのはOnCalculate関数です。なぜこの関数が重要なのでしょうか。それは、OnChartEvent関数がマウスの動きなどで頻繁にトリガーされるのに対し、OnCalculateは新しいティック(レートの更新)を受信するたびに実行されるからです。このアプリケーションではマウスを使っていることを思い出してください。もちろん、キーボードや何らかのUIイベントによってOnChartEventがトリガーされることもありますが、OnCalculateを使うことでより迅速な情報更新、あるいはバッファの最新状態をより確実に保つことができるのです。だからこそ、ここではOnCalculateを利用しているのです。
たとえば、38行目のコードを見てください。これは46行目と同じ処理をしていますが、38行目の方はインジケーターが新しいティックを受け取るたびにバッファを更新します。たとえマウスが動いていなくても、マウスイベントが発生していなくても処理がおこなわれるのです。OnChartEventが何かの変化の後にしか呼び出されれない一方で、OnCalculateはほぼ常時トリガーされているというわけです。
これで、コントロール用インジケーターの役割について理解できたかと思います。ただし、時間足を切り替えるとOnDeinitが呼ばれ、インジケーターがチャートから削除された後、すぐにOnInitが呼び出されます。そして、チャートに何かが表示される前にOnCalculateが実行され、その後でようやくOnChartEventが動きます。だからこそ、バッファ内のデータを常に最新に保つ必要があるのです。
さて、これでC_Replay.mqhヘッダファイルの改修に入っていいのではと感じる方もいるかもしれません。技術的には、もう始めても問題ない段階です。ただし、すべての読者がこの通信方式の仕組みを本当に理解できているかどうか、私は少し懸念しています。すでに理解されている方も、ほんの少しだけお付き合いください。まだ理解が追いついていない方のために、もう少し解説を続けさせてください。ということで、次のトピックに移ります。
高速情報交換の理解
多くの初心者や、MQL5での経験が浅い開発者の方は、インジケーターのバッファからサービス側で情報を読み取ることについて、それほど難しくないと考えるかもしれません。ハンドルさえ取得できればいいと思うのです。そして実際、それはかなりうまく機能します。しかしながら、注意点があります。より正確に言えば、この特定の状況においては、ハンドルを使用する方法に欠陥があります。
この段階で、「ハンドルの使用に欠陥があるとはどういうことか。今までハンドルをずっと使っていて、問題なく動いていたはずだ」と思われるかもしれません。ここで論理や経験について議論するつもりはありません。意味のない議論をしても、前には進みません。言葉で証明しようとする代わりに、実際のテストをおこないます。そうすることで、誰も反論できなくなります。なぜなら、一度検証され、証明されたことは、否定のしようがない事実になるからです。
つまり、サービス経由でインジケーターのバッファにハンドルを使ってアクセスする方法が信頼できないことを証明するために、実験をおこないます。ここではっきりさせておきたいのは、問題が、チャートの時間足を変更したときに不安定になるという点です。これを理解することが大切です。問題の本質は、時間足を切り替える操作にあります。そこで、以下のように進めます。動作検証のために、2つのシンプルなコード例を用意します。心配はいりません。どちらも簡単で、すぐに理解できる内容です。まずは、インジケーターのコードから確認していきます。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property version "1.00" 04. #property indicator_chart_window 05. #property indicator_plots 0 06. #property indicator_buffers 1 07. //+------------------------------------------------------------------+ 08. #include <Market Replay\Defines.mqh> 09. //+------------------------------------------------------------------+ 10. double m_Buff[]; 11. //+------------------------------------------------------------------+ 12. int OnInit() 13. { 14. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 15. ArrayInitialize(m_Buff, EMPTY_VALUE); 16. IndicatorSetString(INDICATOR_SHORTNAME, "TEST"); 17. 18. return INIT_SUCCEEDED; 19. } 20. //+------------------------------------------------------------------+ 21. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 22. { 23. m_Buff[rates_total - 1] = 1.0 * def_IndicatorTimeFrame; 24. 25. return rates_total; 26. } 27. //+------------------------------------------------------------------+
テストインジケーターのソースコード
5行目では、実際には何もプロットしないことをコンパイラに指示しています。これにより、再コンパイルのたびに警告が表示されるのを防ぐことができます。6行目では、バッファを使用することを宣言しています。そして8行目では、リプレイ/シミュレーションアプリケーションで使用されるヘッダファイルをインクルードしています。これは非常に重要な部分です。なぜなら、ここでの目的は、リプレイ/シミュレーションシステムが実際にどのようにデータへアクセスするのかを理解することにあるからです。10行目では、バッファを宣言しています。OnInit関数内では、バッファの初期化と、インジケーター名の定義をおこないます。この部分には注意が必要です。なぜなら、この情報は後で必要になるからです。
次に、OnCalculate関数の本体部分を詳しく見ていきましょう。23行目では、チャートの時間足の値をバッファに書き込んでいます。この値は、前回の記事でも扱ったものと同じです。そのため、前回の内容を理解していることが前提となります。さて、ここまででインジケーターが何をしているのか、基本的なレベルでは理解していただけたと思います。それでは、次に2つ目のコード例を見ていきましょう。
01. //+------------------------------------------------------------------+ 02. #property service 03. #property copyright "Daniel Jose" 04. #property description "Data synchronization demo service." 05. //+------------------------------------------------------------------+ 06. input string user01 = "IBOV"; //Accompanying Symbol 07. //+------------------------------------------------------------------+ 08. void OnStart() 09. { 10. int ret, handle; 11. long id; 12. double Buff[1]; 13. 14. if ((id = ChartFirst()) > 0) do 15. { 16. if (ChartSymbol(id) == user01) break; 17. }while ((id = ChartNext(id)) > 0); 18. handle = ChartIndicatorGet(id, 0, "TEST"); 19. do 20. { 21. ret = CopyBuffer(handle, 0, 0, 1, Buff); 22. PrintFormat("CopyBuffer: [ %d ] Value: [ %f ]", ret, Buff[0]); 23. Sleep(250); 24. } while ((!_StopFlag) && (ret > 0)); 25. IndicatorRelease(handle); 26. } 27. //+------------------------------------------------------------------+
テストサービスのソースコード
2行目では、このスクリプトがサービスであることをコンパイラに通知しています。次に、6行目では入力パラメータを追加しており、ユーザーがこのサービスが監視する銘柄を定義できるようにしています。ここまでは特に変わった点はありません。10〜12行目では、いくつかの変数を宣言しています。さて、ここからが重要な部分です。サービスを機能させるためには、少なくとも1つのチャートが開かれていなければなりません。そうでない場合、問題が発生してしまいます。しかし、少なくとも1つのチャートが開かれていれば、14行目でそのチャートIDを正しく取得することができます。取得したIDを使って、開かれているチャートの中から6行目で定義された銘柄と一致するものを探します。一致するチャートが見つかれば、そのチャートの正しいIDを取得します。
そのチャートIDが手に入ったら、MetaTrader 5に対してインジケーターのハンドルを要求します。ここで使用するインジケーター名に注目してください。この名前は、インジケーターのソースコード16行目で定義された名前と一致している必要があります。そこから、ループ処理に入ります。ここではすでに有効なハンドルを持っているため、それを使ってインジケーターバッファを読み取ることができます。これを21行目で実行しています。そして22行目では、読み取ったバッファのデータと、その読み取り操作の結果を端末に出力しています。このループは、24行目の条件が満たされている限り続きます。言い換えれば、サービスが停止されるか、バッファの読み取りに失敗するまで続きます。これらの条件が満たされなくなると、25行目でハンドルを解放します。
このコードがインジケーターバッファを正しく読み取っていることは、皆さんにもご理解いただけると思います。表示される値は、前回の記事で説明したテスト内容に基づいて、想定された値と一致します。それでは、実際にこのコードをMetaTrader 5上で実行してみましょう。なお、結果については以下のビデオでもご確認いただけます。
何が起こったのか、困惑されたかもしれません。もしかすると、私が読者を騙そうとしていると思われたかもしれません。それでも構いません。何を信じるかは、読者自身の自由です。実際、私の言葉をそのまま信じる必要はありません。ぜひご自身でテストをおこなってみてください。ただし、ここでの本質的な「サービスからインジケーターバッファを読み取る際にハンドルを使う」というアイデアは忘れないでください。ビデオで確認できた現象は、まさにこの仕組みによって発生します。しかし、なぜでしょうか。
ポイントとなるのは、ハンドルの仕組みです。MetaTrader 5では、チャートの時間足を変更すると、ハンドルIDも変更されます。しかし、コードは依然として古いハンドルを参照しています。ところがそのハンドルはすでに無効です。なぜなら、MetaTrader 5が内部的にインジケーターを再生成し、新しいバッファは新たなハンドルに紐づけられているからです。エキスパートアドバイザー(EA)や別のインジケーターを使用している場合、それらは時間足の変更後に自動的にチャートへ再アタッチされます。この再アタッチによって、すべての関数呼び出しが再実行され、ハンドルも更新されます。なぜなら、EAやインジケーターは再度チャート上に読み込まれるからです。その際、ハンドルが更新され、バッファを正しく読み取ることが可能になります。
一方で、サービスはチャートのコンテキスト外で動作します。そのため、ハンドルは更新されません。これが、読み取ったバッファの値が変わらない理由です。読者の中には、「自分のサービスは違う動作をしているから関係ない」と思われる方もいるかもしれません。それも構いません。それでは、その仮説を検証してみましょう。今回は、サービスコードのみを変更した新しいバージョンを使って確認します。コードは以下のとおりです。
01. //+------------------------------------------------------------------+ 02. #property service 03. #property copyright "Daniel Jose" 04. #property description "Data synchronization demo service." 05. //+------------------------------------------------------------------+ 06. input string user01 = "IBOV"; //Accompanying Symbol 07. //+------------------------------------------------------------------+ 08. void OnStart() 09. { 10. int ret; 11. long id; 12. double Buff[1]; 13. 14. if ((id = ChartFirst()) > 0) do 15. { 16. if (ChartSymbol(id) == user01) break; 17. }while ((id = ChartNext(id)) > 0); 18. do 19. { 20. ret = CopyBuffer(ChartIndicatorGet(id, 0, "TEST"), 0, 0, 1, Buff); 21. PrintFormat("CopyBuffer: [ %d ] Value: [ %f ]", ret, Buff[0]); 22. Sleep(250); 23. } while ((!_StopFlag) && (ret > 0)); 24. } 25. //+------------------------------------------------------------------+
テストサービスのソースコード
このコードにはわずかな変更のみが加えられています。ぱっと見ただけでは気づきにくいほどの小さな変更です。しかし、重要なポイントは20行目にあります。ここに今回の本質的な変更が含まれています。これまで静的だったハンドルが、動的に変化するようになったのです。つまり、サービス側がチャートの現在の時間足を知らなくても、あるいはその時間足が途中で変更されていても、適切なインジケーターのインスタンスを見つけて正しくバッファを読み取ることができるようになりました。この動作の違いについては、下記のビデオを見ていただければ一目で理解できると思います。
ここまで読むと、私が読者を混乱させようとしているのではないかと思われたかもしれません。どうしてこのようなことが可能なのでしょうか。落ち着いてください、読者の皆さん。先ほども述べたように、C_Replay.mqhファイルの変更内容をお見せする前に、まずこの概念を理解していただきたかったのです。
一見シンプルで、しかも理にかなっているように見えるものが、コードの挙動を大きく変えることがあるのです。このことが、どれほど重要かを知っていただきたいと思います。肝心なのは、常に最も単純な環境でテストをおこなうことです。私がよく見かけるのは、しっかりした基盤がないまま、複雑なコードを書こうとして挫折してしまうケースです。結局、どこに微妙な問題が潜んでいるのかが分からず、諦めてしまうのです。
しかし現実には、計算コストと実装の複雑さを常に天秤にかける必要があります。正しく動作するコードがあっても、それが遅ければ意味がありません。逆に、いくら高速でも、動かないコードも同様に無意味です。
今回の第2版のサービスコードでおこなったように、ハンドルを動的に扱うことで、わずかなパフォーマンスコストが発生します。なぜなら、毎回の実行でハンドルの検索が必要になるからです。一方で、最初のバージョンではハンドルをあらかじめ保持していたため、変数を参照するだけで高速に動作していました。
このようなトレードオフは、常に意識して評価すべき点です。だからこそ、私は最小限かつシンプルなコードで構築・検証をおこなうことを強く推奨しています。では、このロジックは実際にどのようにC_Replay.mqhヘッダファイルへと統合・実装されるのでしょうか。このヘッダは、リプレイ/シミュレーションサービスを制御する重要な要素です。その答えを探るために、次のセクションへと進みましょう。
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 SendEventCustom(const ENUM_BOOK_TYPE Arg1 = BOOK_TYPE_BUY_MARKET) 038. { 039. MqlBookInfo book[1]; 040. 041. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 042. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 043. m_IndControl.Memory._8b[7] = 'D'; 044. m_IndControl.Memory._8b[6] = 'M'; 045. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 046. book[0].price = 1.0; 047. book[0].volume = 1; 048. book[0].type = Arg1; 049. CustomBookAdd(def_SymbolReplay, book, 1); 050. } 051. //+------------------------------------------------------------------+ 052. inline void CheckIndicatorControl(void) 053. { 054. static uchar memTimeFrame = 0; 055. static C_Controls::eObjectControl memMode = m_IndControl.Mode; 056. double Buff[]; 057. 058. if (CopyBuffer(ChartIndicatorGet(m_Infos.IdReplay, 0, "Market Replay Control"), 0, 0, 1, Buff) < 0) ChartClose(m_Infos.IdReplay); 059. m_IndControl.Memory.dValue = Buff[0]; 060. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] >= m_IndControl.Position) 061. { 062. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 063. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 064. if (m_IndControl.Memory._8b[def_IndexTimeFrame] == memTimeFrame) 065. { 066. memMode = m_IndControl.Mode; 067. return; 068. } 069. memTimeFrame = m_IndControl.Memory._8b[def_IndexTimeFrame]; 070. m_IndControl.Mode = memMode; 071. } 072. SendEventCustom(m_IndControl.Mode != C_Controls::ePlay ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY); 073. } 074. //+------------------------------------------------------------------+ 075. inline void UpdateIndicatorControl(void) 076. { 077. double Buff[]; 078. 079. if (m_IndControl.Handle == INVALID_HANDLE) return; 080. if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) 081. { 082. if (CopyBuffer(m_IndControl.Handle, 0, 0, 1, Buff) == 1) 083. m_IndControl.Memory.dValue = Buff[0]; 084. if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) 085. m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; 086. { 087. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; 088. m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (ushort)m_IndControl.Mode; 089. m_IndControl.Memory._8b[7] = 'D'; 090. m_IndControl.Memory._8b[6] = 'M'; 091. EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, 0, m_IndControl.Memory.dValue, ""); 092. } 093. } 094. //+------------------------------------------------------------------+ 095. void SweepAndCloseChart(void) 096. { 097. long id; 098. 099. if ((id = ChartFirst()) > 0) do 100. { 101. if (ChartSymbol(id) == def_SymbolReplay) 102. ChartClose(id); 103. }while ((id = ChartNext(id)) > 0); 104. } 105. //+------------------------------------------------------------------+ 106. inline int RateUpdate(bool bCheck) 107. { 108. static int st_Spread = 0; 109. 110. st_Spread = (bCheck ? (int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time) : st_Spread + 1); 111. m_Infos.Rate[0].spread = (int)(def_MaskTimeService | st_Spread); 112. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 113. 114. return 0; 115. } 116. //+------------------------------------------------------------------+ 117. inline void CreateBarInReplay(bool bViewTick) 118. { 119. bool bNew; 120. double dSpread; 121. int iRand = rand(); 122. 123. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 124. { 125. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 126. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 127. { 128. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 129. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 130. { 131. m_Infos.tick[0].ask = m_Infos.tick[0].last; 132. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 133. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 134. { 135. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 136. m_Infos.tick[0].bid = m_Infos.tick[0].last; 137. } 138. } 139. if (bViewTick) 140. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 141. RateUpdate(true); 142. } 143. m_Infos.CountReplay++; 144. } 145. //+------------------------------------------------------------------+ 146. void AdjustViewDetails(void) 147. { 148. MqlRates rate[1]; 149. 150. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_ASK_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 151. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_BID_LINE, GetInfoTicks().ModePlot == PRICE_FOREX); 152. ChartSetInteger(m_Infos.IdReplay, CHART_SHOW_LAST_LINE, GetInfoTicks().ModePlot == PRICE_EXCHANGE); 153. m_Infos.PointsPerTick = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); 154. CopyRates(def_SymbolReplay, PERIOD_M1, 0, 1, rate); 155. if ((m_Infos.CountReplay == 0) && (GetInfoTicks().ModePlot == PRICE_EXCHANGE)) 156. for (; GetInfoTicks().Info[m_Infos.CountReplay].volume_real == 0; m_Infos.CountReplay++); 157. if (rate[0].close > 0) 158. { 159. if (GetInfoTicks().ModePlot == PRICE_EXCHANGE) 160. m_Infos.tick[0].last = rate[0].close; 161. else 162. { 163. m_Infos.tick[0].bid = rate[0].close; 164. m_Infos.tick[0].ask = rate[0].close + (rate[0].spread * m_Infos.PointsPerTick); 165. } 166. m_Infos.tick[0].time = rate[0].time; 167. m_Infos.tick[0].time_msc = rate[0].time * 1000; 168. }else 169. m_Infos.tick[0] = GetInfoTicks().Info[m_Infos.CountReplay]; 170. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 171. } 172. //+------------------------------------------------------------------+ 173. void AdjustPositionToReplay(void) 174. { 175. int nPos, nCount; 176. 177. if (m_IndControl.Position == (int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) return; 178. nPos = (int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); 179. for (nCount = 0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); 180. if (nCount > 0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - 1); 181. while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) 182. CreateBarInReplay(false); 183. } 184. //+------------------------------------------------------------------+ 185. void WaitIndicatorLoad(const string szArg, const bool ViewCtrl = true) 186. { 187. Print("Waiting for ", szArg); 188. while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, 0, szArg) == INVALID_HANDLE)) 189. { 190. if (ViewCtrl) CheckIndicatorControl(); 191. Sleep(100); 192. } 193. } 194. //+------------------------------------------------------------------+ 195. public : 196. //+------------------------------------------------------------------+ 197. C_Replay() 198. :C_ConfigService() 199. { 200. Print("************** Market Replay Service **************"); 201. srand(GetTickCount()); 202. SymbolSelect(def_SymbolReplay, false); 203. CustomSymbolDelete(def_SymbolReplay); 204. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 205. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0); 206. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 0); 207. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, 0); 208. CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); 209. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, 8); 210. CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, 1); 211. SymbolSelect(def_SymbolReplay, true); 212. m_Infos.CountReplay = 0; 213. m_IndControl.Handle = INVALID_HANDLE; 214. m_IndControl.Mode = C_Controls::ePause; 215. m_IndControl.Position = 0; 216. m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; 217. } 218. //+------------------------------------------------------------------+ 219. ~C_Replay() 220. { 221. SweepAndCloseChart(); 222. IndicatorRelease(m_IndControl.Handle); 223. SymbolSelect(def_SymbolReplay, false); 224. CustomSymbolDelete(def_SymbolReplay); 225. Print("Finished replay service..."); 226. } 227. //+------------------------------------------------------------------+ 228. bool OpenChartReplay(const ENUM_TIMEFRAMES arg1, const string szNameTemplate) 229. { 230. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == 0) 231. return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); 232. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == 0) 233. return MsgError("Asset configuration is not complete, need to declare the ticket value."); 234. if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == 0) 235. return MsgError("Asset configuration not complete, need to declare the minimum volume."); 236. SweepAndCloseChart(); 237. m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); 238. if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) 239. Print("Failed apply template: ", szNameTemplate, ".tpl Using template default.tpl"); 240. else 241. Print("Apply template: ", szNameTemplate, ".tpl"); 242. 243. return true; 244. } 245. //+------------------------------------------------------------------+ 246. bool InitBaseControl(const ushort wait = 1000) 247. { 248. int handle; 249. 250. Sleep(wait); 251. AdjustViewDetails(); 252. Print("Loading Control Indicator..."); 253. if ((handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) return false; 254. ChartIndicatorAdd(m_Infos.IdReplay, 0, handle); 255. IndicatorRelease(handle); 256. WaitIndicatorLoad("Market Replay Control", false); 257. SendEventCustom(); 258. WaitIndicatorLoad("Indicator Mouse Study"); 259. UpdateIndicatorControl(); 260. SendEventCustom(); 261. 262. return def_CheckLoopService; 263. } 264. //+------------------------------------------------------------------+ 265. bool LoopEventOnTime(void) 266. { 267. int iPos, iCycles; 268. MqlBookInfo book[1]; 269. ENUM_BOOK_TYPE typeMsg, memBook; 270. 271. book[0].price = 1.0; 272. book[0].volume = 1; 273. book[0].type = BOOK_TYPE_BUY_MARKET; 274. CustomBookAdd(def_SymbolReplay, book, 1); 275. SendEventCustom(); 276. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 277. { 278. UpdateIndicatorControl(); 279. CheckIndicatorControl(); 280. Sleep(200); 281. } 282. m_MemoryData = GetInfoTicks(); 283. AdjustPositionToReplay(); 284. iPos = iCycles = 0; 285. SendEventCustom(memBook = BOOK_TYPE_BUY); 286. book[0].type = BOOK_TYPE_BUY; 287. CustomBookAdd(def_SymbolReplay, book, 1); 288. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 289. { 290. if (m_IndControl.Mode == C_Controls::ePause) return true; 291. 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); 292. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != book[0].type) 293. if ((typeMsg = (iPos >= 60000 ? BOOK_TYPE_BUY_MARKET : BOOK_TYPE_BUY)) != memBook) 294. SendEventCustom(memBook = typeMsg); 295. { 296. book[0].type = typeMsg; 297. CustomBookAdd(def_SymbolReplay, book, 1); 298. } 299. CreateBarInReplay(true); 300. while ((iPos > 200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) 301. { 302. Sleep(195); 303. iPos -= 200; 304. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 305. UpdateIndicatorControl(); 306. CheckIndicatorControl(); 307. iCycles = (iCycles == 4 ? RateUpdate(false) : iCycles + 1); 308. } 309. } 310. 311. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 312. } 313. }; 314. //+------------------------------------------------------------------+ 315. #undef def_SymbolReplay 316. #undef def_CheckLoopService 317. #undef def_MaxSlider 318. //+------------------------------------------------------------------+
C_Replay.mqhファイルのソースコード
コード中で取り消し線が引かれているすべての行は削除する必要があります。ただし、なぜこれほど多くの行が削除されたのかについて、詳しく見ていきます。
まず最初の重要な点は、23行目です。前のセクションで示したように、静的なハンドルは使用すべきではありません。このため、handle変数は不要となり、これに関連するすべてのコードが削除されました。また、1つの手続き全体も削除されました。75行目から93行目まで存在していたUpdateIndicatorControlです。それに伴い、この関数を呼び出していたすべての箇所も削除されました。これにより、UpdateIndicatorControlが担っていた機能を新たな方法で再実装する必要があります。
ただし、それについては後ほど詳しく説明します。ここでは、まず2つの関数に注目します。1つ目は243行目から始まるInitBaseControlです。この関数には小規模ながらも意味のある変更が加えられています。これらの調整は、ユーザー体験の向上と、インジケーターの初期化の標準化を目的としたものです。何が起こっているのか見てみましょう。
253行目から255行目では、コントロールインジケーターの読み込みを試みています。ここで重要なのは、インジケーターの読み込みが即時に完了するわけではないという点です。処理には若干の遅延があるため、257行目を実行する前に、コントロールインジケーターがチャート上で正しく読み込まれていることを確認しなければなりません。この確認は253行目でおこなわれています。256行目および258行目のインジケーター名に注意してください。ここで指定されているのが、読み込むべきインジケーターです。これらの呼び出しは、185行目から開始される処理内でおこなわれます。このあたりから、より注意深く見る必要があります。
188行目のループは、指定されたインジケーターがチャートに読み込まれるまで待機します。256行目がコントロールインジケーターの読み込みを要求するとき、187行目のチェックが早すぎる確認を防いでいます。一方、255行目がマウスインジケーターの読み込みを要求する際には、190行目がコントロールインジケーターのバッファをチェックします。なぜこのような動作になるのかというと、ユーザーがチャートの時間足を変更する可能性があるからです。それを理解するために、今度は52行目に注目します。
52行目には、コントロールインジケーターのバッファを読み取る処理が記述されています。ここで最も興味深いのは、54行目と55行目に宣言された2つのstatic変数です。この時点で関係するのは、54行目の変数で、初期値として0が設定されています。58行目でバッファの読み取りを試み、その戻り値が負の値だった場合、それはコントロール・インジケーターがチャートから削除されたことを意味します。ユーザーは手動でこのインジケーターを再設定できず、また本アプリケーションにはその存在が必須です。したがって、該当のチャートは閉じられ、アプリケーションも終了します。このため、インジケーターの読み込み中にその状態をチェックしてはいけません。
60行目では、コントロールインジケーターの値が、サービスで処理している位置よりも大きいかどうかを確認します。もしそうであれば、ユーザーが[再生]を押した際に実行を先送りする必要があることを意味します。しかし、最も重要な条件は64行目にあります。このチェックこそが、前節で取り上げた「時間足ロック機構」のアキレス腱です。ユーザーがチャートの時間足を変更していない場合、この条件はtrueを返し、それ以上の処理は不要となります。
falseが返された場合、69行目で新たな時間足を保存し、70行目でインジケーターの直近ステータスを取得します。これは72行目で新しい手続きが呼ばれるために必要な処理です。ここで、37行目にジャンプします。ここが非常に興味深い箇所です。この部分こそが、MetaTrader 5に対してチャート上でイベントを発生させるよう指示している処理です。これをバッファの検証処理と分離している理由は、単にイベントだけを発生させたい場合と、コントロール・インジケーターの値に意味のある変化があったかどうかを確認したい場合があるからです。
なお、SendEventCustomのロジック自体は、以前のコードバージョンにも存在していたため、改めて説明する必要はありません。構造は明確で理解しやすいものとなっています。以上で、構造上の主要な変更点の説明は完了しました。しかし、LoopEventOnTime関数にも変更が加えられているため、そちらも簡単に確認しておきます。構造そのものが変わったわけではありませんが、UpdateIndicatorControlが2つの新しい手続きへと分割された理由が見えてきます。これを簡単に見ていきましょう。
275行目では、MetaTrader 5に対してカスタムイベントの送信を要求しています。これにより、マウスおよびコントロールの各インジケーターが、シミュレーションやリプレイの開始前に正しく設定されることが保証されます。275行目の処理は最初の実行時にはスキップ可能ですが、最初の「再生」コマンドの後には必ず実行される必要があります。システムが一時停止状態に入ると、この行が再び実行されます。
279行目ではイベントはトリガーせず単にコントロールインジケーターの監視をおこなっています。ユーザーが[再生]を押すと、276行目のループが終了し、シミュレーションが開始されます。
285行目では、再びカスタムイベントの送信を要求しています。今回は、マウスインジケーターに残りのバー時間を表示させるためです。
293行目と294行目には小さな変更が加えられています。これは、マウスインジケーターの状態を更新するためのロジックであり、アセットがオークションモードに入ったか、あるいは退出したかを検出する目的で使用されます。
最後の変更点は306行目です。ここでは、時間足の変更があったかどうかを確認しています。変更が検出された場合、セクション冒頭で述べたように、インジケーターが再読み込みされます。
以上で、必要なコード修正はすべて完了しました。チャートの時間足が変更された際に、システムがどのように動作するかについては、以下の動画でご確認いただけます。
結論
ここまでの2つの記事において、プログラミング言語を実験的に使い、限界まで試してみることの重要性を示してきました。一部の読者にとっては、直接的な知識の習得が少なかったように感じられたかもしれませんが、実際にはそうではありません。ここで説明した内容は、「実現不可能である」あるいは「現実的に実装・保守が難しい」と考えていた方も多かったはずです。しかし、メインコードを変更する前に、まず仮説を立て、それを検証するためのシンプルなプロトタイプを構築する重要性を説明しました。
最も大切な教訓は、最初の試みで諦めてはいけないということです。もし何かがうまく動作しなかったとしても、アプローチを変えて、常に仮説を検証することに集中し続けてください。私自身もまさにそのように取り組みました。私の目的は、「インジケーターバッファにデータを書き込み、それを用いて時間足の変化を明確かつ確実に検出すること」でした。チャート上で直接おこなうことは簡単でした。そこで次に浮かんだ疑問は、サービス側でもこの変化を検出できるかということです。最初の実装では、サービスは確かに時間足の値を読み取ることができました。しかし、それはインジケーターがチャートに初めてアタッチされた時点の値であり、リアルタイムではありませんでした。
ここで、真の着想が生まれました。もし私がこの時点で諦めていたなら、呼び出しごとにハンドルを動的に取得するという設計へと思考を進めることはなかったでしょう。そして、リアルタイムで更新されたインジケーターデータにアクセスするチャンスを逃していたはずです。設計を見直すことで、新たな可能性が開けました。それは、多くの人が「不可能だ」と考えていた領域を、実際に超えていけることを証明するものでした。
本物のシステムとは、このようにして構築されていきます。仮説から始めて、それを検証し、たとえ最初のアプローチが部分的にしか機能しなかったとしても、同じ核心となるアイデアに導かれながら改善を重ねていくのです。
添付ファイルには、リプレイ/シミュレーター機能を使用するために必要な実行ファイルが含まれています。次回の記事では、このリプレイ/シミュレーターアプリケーションに組み込むべき追加機能の検討を始めていきます。そこでお会いしましょう。
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12363
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
Pythonによる農業国通貨への天候影響分析
初級から中級まで:共用体(I)
レーベンバーグ・マルカートアルゴリズムを用いた多層パーセプトロンのトレーニング
取引におけるニューラルネットワーク:方向性拡散モデル(DDM)
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索