
リプレイシステムの開発(第68回):正しい時間を知る(I)
はじめに
前回の「リプレイシステムの開発(第66回):サービスの再生(VII)」では、現在のバーが終了して次のバーがチャートに表示されるまでの残り時間を、マウスインジケーターで表示する方法を紹介しました。この方法は非常に効率的でうまく機能しますが、1つ問題があります。この問題は多くの人が手法自体にあると思いがちですが、実際には取引銘柄の流動性に起因するものです。記事を注意深く読んだ方はお気づきかもしれませんが、マウスインジケーターが情報を更新するのは、新しいクオートを受信したときだけです。
これは、MetaTrader 5 がOnCalculateイベントをトリガーすることで、インジケーターが秒単位で新しい値を受け取り、現在のバーの残り時間を計算できるようにしているためです。記事でも説明したように、この秒数はスプレッドを通じて設定されています。しかし、サービスが提供するスプレッド値に依存しているため、流動性の低い銘柄や流動性が一時的に低下している期間においては、正確なバーの残り時間を表示することができません。特に銘柄が板寄せの状態に入ると、この問題はさらに深刻になります。市場が数分間停止することもあり、その間は情報の更新がまったくおこなわれません。残念ながら、板寄せ中に正確な時間を知らせる信頼できる回避策は現在のところ存在しません。たとえ流動性の高い銘柄であっても、板寄せの間にはOnCalculateイベントは発生しません。このような状況においてユーザーに正確な情報を提供するためには、リプレイ/シミュレーション機能の改善が不可欠です。まず、流動性が低く、1秒あたりにわずかな取引しか発生しないような場面。そして、銘柄が板寄せの状態に入る場面。この2つの状況に対応する必要があります。これらの問題に対処することで、次の開発段階へと進む準備が整います。
流動性不足に対するソリューションの実装
最初のシナリオである「流動性が低い」ケースについては、少なくとも一般的な意味において、サービス側のコードを修正するだけで比較的容易に対処できます。将来的には他の部分にも変更が必要になるかもしれませんが、現時点ではリプレイ/シミュレーションサービスのソースコードだけを修正することで解決を試みます。
この方法が実際に可能かどうかを効率よく判断するために、元のソースコードは一時的に脇に置き、別途テスト用コードを作成して検証をおこないます。このテストでは、サービスが非アクティブな状態であっても、現在のバーが終了するまでの残り時間をマウスインジケーターに反映させることができるかを確認します。
このために開発するコードは、できる限りシンプルであることが求められます。過度に複雑な構造や精巧な設計は不要です。目的は、チャート上に表示される情報に影響を与えることなく、サーバー側からインジケーターにデータを送信できるかどうかをテストすることにあります。この検証に使用するソースコードは、以下のとおりです。
01. //+------------------------------------------------------------------+ 02. #property service 03. #property copyright "Daniel Jose" 04. #property description "Data synchronization demo service." 05. //+------------------------------------------------------------------+ 06. #include <Market Replay\Defines.mqh> 07. //+------------------------------------------------------------------+ 08. #define def_Loop ((!_StopFlag) && (ChartSymbol(id) != "")) 09. //+------------------------------------------------------------------+ 10. void OnStart() 11. { 12. long id; 13. int handle; 14. MqlRates Rate[1]; 15. 16. Print("Starting Test Service..."); 17. SymbolSelect(def_SymbolReplay, false); 18. CustomSymbolDelete(def_SymbolReplay); 19. CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); 20. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, 0.5); 21. CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, 5); 22. Rate[0].close = 105; 23. Rate[0].open = 100; 24. Rate[0].high = 110; 25. Rate[0].low = 95; 26. Rate[0].tick_volume = 5; 27. Rate[0].spread = 1; 28. Rate[0].real_volume = 10; 29. Rate[0].time = D'10.03.2023 09:00'; 30. CustomRatesUpdate(def_SymbolReplay, Rate, 1); 31. Rate[0].time = D'10.03.2023 09:30'; 32. CustomRatesUpdate(def_SymbolReplay, Rate, 1); 33. SymbolSelect(def_SymbolReplay, true); 34. id = ChartOpen(def_SymbolReplay, PERIOD_M30); 35. Sleep(1000); 36. if ((handle = iCustom(NULL, 0, "\\Indicators\\Mouse Study.ex5")) != INVALID_HANDLE) 37. ChartIndicatorAdd(id, 0, handle); 38. IndicatorRelease(handle); 39. Print(TimeToString(Rate[0].time, TIME_DATE | TIME_SECONDS)); 40. while (def_Loop) 41. { 42. CustomRatesUpdate(def_SymbolReplay, Rate, 1); 43. Sleep(250); 44. Rate[0].spread++; 45. if (Rate[0].spread == 60) 46. { 47. Rate[0].time += 60; 48. Rate[0].spread = 0; 49. Print(TimeToString(Rate[0].time, TIME_DATE | TIME_SECONDS)); 50. EventChartCustom(id, evSetServerTime, (long)(Rate[0].time), 0, ""); 51. } 52. } 53. ChartClose(id); 54. SymbolSelect(def_SymbolReplay, false); 55. CustomSymbolDelete(def_SymbolReplay); 56. Print("Finished Test Service..."); 57. } 58. //+------------------------------------------------------------------+
テストサービスのソースコード
以下に説明する手順を正しく機能させるためには、マウスインジケーターを事前にコンパイルし、後述する特定のディレクトリに配置しておく必要があります。これができていないと、テストは失敗します。そのため、テストが目的通りに有効であると証明された場合に備え、何を実行する必要があるのか、そしてそれをどのように活用できるのかを理解しておきましょう。
最初の4行は、生成される実行ファイルの種別に関する宣言とプロパティの設定です。6行目では、スクリプト内で余計なコンポーネント宣言を避けるために、ヘッダーファイルをインクルードしています。8行目では、すぐに使用するループ用の制御ディレクティブを定義しています。実のところ、この最初の8行の中で本当に不可欠なのは2行目だけです。他の行は省略して、必要なコードを個別に挿入することも可能です。しかし、2行目はMetaTrader 5に対してこの実行ファイルがサービスとして動作することを明示的に伝えるもので、これがないとスクリプトとして扱われてしまい、今回の目的に合致しません。
10行目以降からが本題です。12〜14行目で、必要な変数の宣言をおこないます。次に、17〜21行目ではカスタム銘柄の初期化処理をおこないます。これはよくある標準的な手順なので、次に進めます。そして、22〜28行目では、バーのためのRATE構造体を作成します。ここで注目すべきは29行目です。この行では、最初のバーが作成されるタイムスタンプを指定しています。30行目では、MetaTrader 5がバーを描画する際に使用するデータを設定します。重要なのは次の31行目です。同じデータを使って、2本目のバーを作成しますが、こちらは30分のオフセットが加えられています。これは、チャートを30分足で開くためです。31行目のバー作成時に他の時間足を指定した場合、マウスインジケーターはそのバーの終了時刻に基づいて残り時間を計算します。したがって、この部分の設定は非常に重要です。さらに注意が必要なのは、秒単位の値を含めないことです。分単位でのみ値を指定してください。次に進んで、32行目で実際にバーをチャートに反映させます。
34行目ではチャートを開きます。前回の記事でも触れたとおり、チャートにオブジェクトやインジケーターを追加する前に、MetaTrader 5がチャートを正しく構築・描画するのを待つ必要があります。そのために、35行目で1秒間の待機処理を入れています。
36行目にも注意が必要です。ここでは、マウスインジケーターをチャートに追加するためのハンドルを取得します。ここでiCustom関数を使用し、インジケーターが指定された場所に存在するかどうかを確認します。インジケーターが見つからない場合、無効なハンドルが返され、以降の処理はおこなわれません。したがって、マウスインジケーターは正しい場所に配置されている必要があります。これができていないと、テストは失敗します。正常に見つかった場合は、37行目でインジケーターをチャートに追加し、38行目で不要になったハンドルを解放します。
そして、いよいよ実際のテストです。ここが本当に重要な部分であり、今回のアプローチが有効かどうかを判断します。もし機能すれば、流動性の低い状況に対する一つの解決策が得られることになります。逆にうまくいかない場合は、別の方法を検討しなければなりません。それでは、このテストがどのように実施されるかを理解しましょう。テストは、40行目から52行目までのループの中でおこなわれます。それ以降のコード(53〜56行目)は単にテストを終了するだけの処理であり、今回の内容においては特に重要ではありません。したがって、ここではテストのロジックそのものに注目して解説を続けていきます。
テストの仕組みを理解する
テストは40行目から始まり、ループへと入ります。チャートが開いていて、かつユーザーがサービスを停止していない限り、このループは無限に実行され続けます。ループ内に入ると、まず42行目でバーのデータを強制的に更新します。ただし、実際には新しい情報が渡されるわけではなく、テスト目的で変更されたデータのみが使用されている点に注意してください。続く43行目では短い遅延を挿入します。これは更新があまりにも高速で繰り返されないようにするためです。ここでの遅延時間は1秒ではなく0.25秒であり、これはMetaTrader 5上で結果を観察するときに非常に重要なポイントとなります。
44行目ではスプレッド値を1つ増加させます。これにより、実際の遅延は短いにもかかわらず、マウスインジケーターには1秒が経過したかのように見せかけることができます。このテクニックは、MetaTrader 5がOnCalculateイベントをトリガーする仕組みに深く関係しています。ここまでの予想される挙動は次の通りです。チャートが開かれ、マウスインジケーターが表示されると、MetaTrader 5はOnCalculateイベントを生成し、それに応じて現在のバーの残り時間を減算できるようになります。これは実際にご自身でも確認できますが、私はすでにテストを実施しており、期待どおりに動作することを確認済みです。そのうえで、好奇心からスプレッド値が60を超えたときに何が起こるのかを試してみました。なぜこの値が重要なのかというと、60に達した時点で、たとえ月足などの長期足を使っていても、新しい1分足のバーが表示されるべきだからです。ここでのポイントは、実際のチャートの時間枠は関係がないということです。ここでの目的は、MetaTrader 5に、「新しい1分バーが形成された」と認識させることだけにあります。
したがって、この挙動にコードが正しく対応している必要があります。MetaTrader 5はこの状況を自力で解決することはできません。45行目では、繰り返しの回数が60に達したかどうかを確認しています。そして、それが真であれば、47行目で元のバーの時間に60秒、つまり1分を加算します。これにより、次に42行目の関数が実行された際、MetaTrader 5は新しい1分足バーが存在すると判断できるようになります。ただし、それだけでは十分とは言えません。48行目では反復カウンタをリセットして、新たなサイクルが開始できるようにします。さらに、49行目ではMetaTrader 5に送信される時刻の値を表示し、動作を検証できるようにしています。
ここで重要なのは、現時点では新しいバーが存在することをMetaTrader 5のみが把握しているという点です。しかし、以前の記事でマウスインジケーターに残り時間を表示するシステムを実装した際にも述べたように、OnCalculate関数によって提供される時間の値には依存できません。少なくとも今のところはそうです。この制限を回避するための方法を現在模索しており、それが実現すれば最終的に50行目の処理は不要になるはずです。とはいえ、現段階では50行目の処理が必要です。この行では、MetaTrader 5にカスタムイベントを強制的に生成させています。これにより、マウスインジケーター側に新しいバーが形成されたことが通知されます。結果として、インジケーターは現在の時点を正確に認識できるようになり、現在のバーが終了して次のバーが開始するまでの残り時間を正しく表示することが可能となるのです。
もしこのサービスをMetaTrader 5上でコンパイルしてテストすることを望まない場合でも、まったく問題はありません。以下のビデオでは、このサービスが実際にどのように動作したか、そしてこのテストコードがリプレイ/シミュレーションサービスとして使用可能かどうかを示しています。私の評価では、このテストは非常に良好な結果を残しました。この結果から、テストされた仕組みがリプレイ/シミュレーターアプリケーションに十分統合可能であり、実用面でも有効であることが証明されました。この仕組みにより、流動性の低い時間帯や取引が少ない銘柄であっても、正確な時間の追跡が維持され、ユーザーはチャート上に新しいバーが出現するまでの残り時間を常に把握できるようになります。
テストされたメカニズムをリプレイ/シミュレーターアプリケーションに統合するには、コード内の非常に特定のセクションをわずかに変更する必要があります。このセクションは、ヘッダーファイルC_Replay.mqhにあります。ただし、これらの変更の実装に進む前に、まず理解しておくべき別の概念があります。説明をより整理された形で進めるために、ここで新しいトピックに移ることにしましょう。
時間、時間、時間–どうすれば理解できるのでしょうか?
最初は少々混乱するかもしれません。しかし、これからお見せするのは、一見すると非常に複雑に思えるかもしれない内容です。ただし、これは完全に実現可能であり、論理的にも筋の通った話です。そして、そのすべては、扱っているデータ構造を理解することにかかっています。
まず、OnCalculate関数を見てみましょう。ここで使われているスプレッド値はint型で提供されています。64ビットプロセッサ上でintは通常32ビットのデータになります。これに対して、datetime型を確認すると、これは64ビットのデータを使用していることがわかります。では、こうした技術的な情報が今なぜ重要なのでしょうか。焦らずにお付き合いください。少しだけ計算をおこなう必要がありますが、複雑な数式ではありません。ごく基本的な計算です。
今、私たちが目指しているのは、MetaTrader 5に「マウスインジケーターのタイマーを更新するようなイベント」を強制的に発生させることです。これは必須条件であり、実際に私たちはすでに2つの方法でこれを実現しています。1つはOnCalculate関数を呼び出す方法。もう1つは、一定間隔でトリガーされるカスタムイベントを利用する方法です。これらの処理がどこでおこなわれているかを確認したい場合は、ヘッダーファイルC_Replay.mqhのコードを参照してください。
068. //+------------------------------------------------------------------+ 069. inline void CreateBarInReplay(bool bViewTick) 070. { 071. bool bNew; 072. double dSpread; 073. int iRand = rand(); 074. 075. if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[0], bNew)) 076. { 077. m_Infos.tick[0] = m_MemoryData.Info[m_Infos.CountReplay]; 078. if (m_MemoryData.ModePlot == PRICE_EXCHANGE) 079. { 080. dSpread = m_Infos.PointsPerTick + ((iRand > 29080) && (iRand < 32767) ? ((iRand & 1) == 1 ? m_Infos.PointsPerTick : 0 ) : 0 ); 081. if (m_Infos.tick[0].last > m_Infos.tick[0].ask) 082. { 083. m_Infos.tick[0].ask = m_Infos.tick[0].last; 084. m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; 085. }else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) 086. { 087. m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; 088. m_Infos.tick[0].bid = m_Infos.tick[0].last; 089. } 090. } 091. if (bViewTick) 092. { 093. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); 094. if (bNew) EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)m_Infos.Rate[0].time, 0, ""); 095. } 096. m_Infos.Rate[0].spread = (int)macroGetSec(m_MemoryData.Info[m_Infos.CountReplay].time); 097. CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); 098. } 099. m_Infos.CountReplay++; 100. } 101. //+------------------------------------------------------------------+
C_Replay.mqhファイルからのコード
これはすでに記事「リプレイシステムの開発(第66回):サービスの再生(VII)」で説明されていますが、ここではその説明をより強化していきます。このプロシージャが呼び出され、チャートに現在プロットされているバーに新しいティックが追加されるたびに、96行目でaの値(秒単位)がスプレッドに加算されます。これにより、現在のバーの残り時間を計算できるようになります。さらに、新しい1分足が形成されるたびに94行目の条件が真となり、MetaTrader 5にカスタムイベントを強制的に発生させます。このイベントにより、マウスインジケーターは内部のタイミングを再調整し、新しいバーが生成されたタイミングを正確に検出できるようになります。このようにしている理由は、マウスインジケーター内部で非常に頻繁に実行される追加の呼び出しを避けるためであり、この点については前述の記事で詳しく解説しています。
しかし、この方法では時間もメモリも無駄に使っています。より効率的に、より少ないリソースで多くの情報を伝達することが可能です。ここで計算の話に入りますが、私たちが追跡すべきは1秒単位の経過だけで、それ以上の高速な追跡は必要ありません。つまり、1分は60秒、1時間は60分、1日は24時間で構成されます。合計すると、1日あたり86,400秒(正確には24時間ではないためおおよそですが、本稿では24時間と仮定します)です。さらに、32ビットの符号なし整数は最大で4,294,967,295の値を格納できます。符号なしであること(実際にそうです)を考えると、1つの32ビットフィールドで約49日分の秒数を表現可能です。これは非常に重要なポイントです。32ビットのスプレッド値1つで約49日分の時間をカバーできます。しかし実際には、私たちのリプレイ/シミュレーターアプリケーションは、1日か2日連続で使われることがほとんどなので、スプレッドフィールドだけで必要なデータを完全に同期できます。つまり、新しいバーが形成されたことをマウスインジケーターに通知するために、MetaTrader 5にカスタムイベントを強制発生させる必要はなくなります。
ここからが面白いところです。さらに効率を高めるため、49日間の制限を回避するより洗練されたモデルを導入します。現実的には誰もこの制限に達しないと思いますが、それでも別の方法を実装するつもりです。ビットレベルで制御する戦略を取り入れ、より柔軟な運用を可能にします。この方法はマウスインジケーターにもいくつか調整が必要ですが、その労力に見合う大きなメリットがあります。
しかし、本当にこれが私たちの求める解決策になるのでしょうか。それを確かめるために、同じくC_Replay.mqhヘッダーファイルにある別のコード断片を見てみましょう。以下に示します。
207. //+------------------------------------------------------------------+ 208. bool LoopEventOnTime(void) 209. { 210. int iPos; 211. 212. while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) 213. { 214. UpdateIndicatorControl(); 215. Sleep(200); 216. } 217. m_MemoryData = GetInfoTicks(); 218. AdjustPositionToReplay(); 219. EventChartCustom(m_Infos.IdReplay, evSetServerTime, (long)macroRemoveSec(m_MemoryData.Info[m_Infos.CountReplay].time), 0, ""); 220. iPos = 0; 221. while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) 222. { 223. if (m_IndControl.Mode == C_Controls::ePause) return true; 224. 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); 225. CreateBarInReplay(true); 226. while ((iPos > 200) && (def_CheckLoopService)) 227. { 228. Sleep(195); 229. iPos -= 200; 230. m_IndControl.Position = (ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); 231. UpdateIndicatorControl(); 232. } 233. } 234. 235. return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); 236. } 237. }; 238. //+------------------------------------------------------------------+
C_Replay.mqhファイルからのコード
問題が発生するのは資産の流動性が低い場合です。つまり、ティックが毎秒途切れずに発生するわけではありません。ある時間帯では流動性が十分で、ティックが毎秒生成されることもあり、これは私たちの目的にとって理想的です。しかし、そのような流動性が得られない時もあります。それでも、225行目では前述のコードフラグメントが呼び出されていることが確認できます。また、ティック間の時間が1秒を超えたとしても、225行目の呼び出しは確実に実行されます。したがって、スプレッドを使って時間情報を伝達する方法は確かに有効な解決策です。ただし、必要な仕組みをすべて実装するのはやや複雑になることもあります。そのため、この記事ですべての詳細を解説することはできません。とはいえ、まずは基本から説明を始めましょう。
新たな段階の始まり
最初のステップは、Defines.mqhヘッダーファイルを変更することです。以下に、その変更された部分を示します。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_VERSION_DEBUG 05. //+------------------------------------------------------------------+ 06. #ifdef def_VERSION_DEBUG 07. #define macro_DEBUG_MODE(A) \ 08. Print(__FILE__, " ", __LINE__, " ", __FUNCTION__ + " " + #A + " = " + (string)(A)); 09. #else 10. #define macro_DEBUG_MODE(A) 11. #endif 12. //+------------------------------------------------------------------+ 13. #define def_SymbolReplay "RePlay" 14. #define def_MaxPosSlider 400 15. #define def_MaskTimeService 0xFED00000 16. //+------------------------------------------------------------------+ 17. union uCast_Double 18. { 19. double dValue; 20. long _long; // 1 Information 21. datetime _datetime; // 1 Information 22. uint _32b[sizeof(double) / sizeof(uint)]; // 2 Informations 23. ushort _16b[sizeof(double) / sizeof(ushort)]; // 4 Informations 24. uchar _8b [sizeof(double) / sizeof(uchar)]; // 8 Informations 25. }; 26. //+------------------------------------------------------------------+ 27. enum EnumEvents { 28. evHideMouse, //Hide mouse price line 29. evShowMouse, //Show mouse price line 30. evHideBarTime, //Hide bar time 31. evShowBarTime, //Show bar time 32. evHideDailyVar, //Hide daily variation 33. evShowDailyVar, //Show daily variation 34. evHidePriceVar, //Hide instantaneous variation 35. evShowPriceVar, //Show instantaneous variation 36. evSetServerTime, //Replay/simulation system timer 37. evCtrlReplayInit, //Initialize replay control 38. }; 39. //+------------------------------------------------------------------+
Defines.mqhファイルのソースコード
36行目に取り消し線が引かれていることにご注意ください。これは、ファイルの最終バージョンではこの行が存在しないことを意味します。同様に、15行目には新たな定義が追加されており、これがマスクとして機能し、より扱いやすくなります。タイマーの更新を担当していた36行目のイベントを削除しただけでも、新しいコードを最新のメッセージ処理ポリシーに対応させるために調整が必要になることがわかります。
Defines.mqhファイルに対して変更を加えたのと同様に、今回はMacros.mqhという別のヘッダーファイルにも追加の更新があります。以下に、このファイルの更新版を示します。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define macroRemoveSec(A) (A - (A % 60)) 05. #define macroGetDate(A) (A - (A % 86400)) 06. #define macroGetSec(A) (A - (A - (A % 60))) 07. #define macroGetTime(A) (A % 86400) 08. //+------------------------------------------------------------------+ 09. #define macroColorRGBA(A, B) ((uint)((B << 24) | (A & 0x00FF00) | ((A & 0xFF0000) >> 16) | ((A & 0x0000FF) << 16))) 10. #define macroTransparency(A) (((A > 100 ? 100 : (100 - A)) * 2.55) / 255.0) 11. //+------------------------------------------------------------------+
Macros.mqhファイルのソースコード
ここでは7行目のみを追加しています。これはdatetime型の変数から時間の値を抽出するためのマクロです。簡単な追加なので、次に別のヘッダーファイルを見ていきましょう。今回はC_Stuty.mqhというファイルで、以下に全文を示します。このファイルはマウスインジケーターの一部です。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\C_Mouse.mqh" 005. //+------------------------------------------------------------------+ 006. #define def_ExpansionPrefix def_MousePrefixName + "Expansion_" 007. //+------------------------------------------------------------------+ 008. class C_Study : public C_Mouse 009. { 010. private : 011. //+------------------------------------------------------------------+ 012. struct st00 013. { 014. eStatusMarket Status; 015. MqlRates Rate; 016. string szInfo, 017. szBtn1, 018. szBtn2, 019. szBtn3; 020. color corP, 021. corN; 022. int HeightText; 023. bool bvT, bvD, bvP; 024. datetime TimeDevice; 025. }m_Info; 026. //+------------------------------------------------------------------+ 027. void Draw(void) 028. { 029. double v1; 030. 031. if (m_Info.bvT) 032. { 033. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 18); 034. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo); 035. } 036. if (m_Info.bvD) 037. { 038. v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 039. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 040. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 041. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 042. } 043. if (m_Info.bvP) 044. { 045. v1 = NormalizeDouble((((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * 100.0), 2); 046. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - 1); 047. ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < 0 ? m_Info.corN : m_Info.corP)); 048. ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); 049. } 050. } 051. //+------------------------------------------------------------------+ 052. inline void CreateObjInfo(EnumEvents arg) 053. { 054. switch (arg) 055. { 056. case evShowBarTime: 057. C_Mouse::CreateObjToStudy(2, 110, m_Info.szBtn1 = (def_ExpansionPrefix + (string)ObjectsTotal(0)), clrPaleTurquoise); 058. m_Info.bvT = true; 059. break; 060. case evShowDailyVar: 061. C_Mouse::CreateObjToStudy(2, 53, m_Info.szBtn2 = (def_ExpansionPrefix + (string)ObjectsTotal(0))); 062. m_Info.bvD = true; 063. break; 064. case evShowPriceVar: 065. C_Mouse::CreateObjToStudy(58, 53, m_Info.szBtn3 = (def_ExpansionPrefix + (string)ObjectsTotal(0))); 066. m_Info.bvP = true; 067. break; 068. } 069. } 070. //+------------------------------------------------------------------+ 071. inline void RemoveObjInfo(EnumEvents arg) 072. { 073. string sz; 074. 075. switch (arg) 076. { 077. case evHideBarTime: 078. sz = m_Info.szBtn1; 079. m_Info.bvT = false; 080. break; 081. case evHideDailyVar: 082. sz = m_Info.szBtn2; 083. m_Info.bvD = false; 084. break; 085. case evHidePriceVar: 086. sz = m_Info.szBtn3; 087. m_Info.bvP = false; 088. break; 089. } 090. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 091. ObjectDelete(GetInfoTerminal().ID, sz); 092. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 093. } 094. //+------------------------------------------------------------------+ 095. public : 096. //+------------------------------------------------------------------+ 097. C_Study(long IdParam, string szShortName, color corH, color corP, color corN) 098. :C_Mouse(IdParam, szShortName, corH, corP, corN) 099. { 100. if (_LastError != ERR_SUCCESS) return; 101. ZeroMemory(m_Info); 102. m_Info.Status = eCloseMarket; 103. m_Info.Rate.close = iClose(GetInfoTerminal().szSymbol, PERIOD_D1, ((GetInfoTerminal().szSymbol == def_SymbolReplay) || (macroGetDate(TimeCurrent()) != macroGetDate(iTime(GetInfoTerminal().szSymbol, PERIOD_D1, 0))) ? 0 : 1)); 104. m_Info.corP = corP; 105. m_Info.corN = corN; 106. CreateObjInfo(evShowBarTime); 107. CreateObjInfo(evShowDailyVar); 108. CreateObjInfo(evShowPriceVar); 109. } 110. //+------------------------------------------------------------------+ 111. void Update(const eStatusMarket arg) 112. { 113. int i0; 114. datetime dt; 115. 116. switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) 117. { 118. case eCloseMarket : 119. m_Info.szInfo = "Closed Market"; 120. break; 121. case eInReplay : 122. case eInTrading : 123. i0 = PeriodSeconds(); 124. dt = (m_Info.Status == eInReplay ? (datetime) m_Info.TimeDevice + GL_TimeAdjust : TimeCurrent()); 125. dt = (m_Info.Status == eInReplay ? (datetime) GL_TimeAdjust : TimeCurrent()); 126. m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); 127. if (dt > 0) m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time - dt, TIME_SECONDS); 128. break; 129. case eAuction : 130. m_Info.szInfo = "Auction"; 131. break; 132. default : 133. m_Info.szInfo = "ERROR"; 134. } 135. Draw(); 136. } 137. //+------------------------------------------------------------------+ 138. virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 139. { 140. C_Mouse::DispatchMessage(id, lparam, dparam, sparam); 141. switch (id) 142. { 143. case CHARTEVENT_CUSTOM + evHideBarTime: 144. RemoveObjInfo(evHideBarTime); 145. break; 146. case CHARTEVENT_CUSTOM + evShowBarTime: 147. CreateObjInfo(evShowBarTime); 148. break; 149. case CHARTEVENT_CUSTOM + evHideDailyVar: 150. RemoveObjInfo(evHideDailyVar); 151. break; 152. case CHARTEVENT_CUSTOM + evShowDailyVar: 153. CreateObjInfo(evShowDailyVar); 154. break; 155. case CHARTEVENT_CUSTOM + evHidePriceVar: 156. RemoveObjInfo(evHidePriceVar); 157. break; 158. case CHARTEVENT_CUSTOM + evShowPriceVar: 159. CreateObjInfo(evShowPriceVar); 160. break; 161. case (CHARTEVENT_CUSTOM + evSetServerTime): 162. m_Info.TimeDevice = (datetime)lparam; 163. break; 164. case CHARTEVENT_MOUSE_MOVE: 165. Draw(); 166. break; 167. } 168. ChartRedraw(GetInfoTerminal().ID); 169. } 170. //+------------------------------------------------------------------+ 171. }; 172. //+------------------------------------------------------------------+ 173. #undef def_ExpansionPrefix 174. #undef def_MousePrefixName 175. //+------------------------------------------------------------------+
C_Study.mqhファイルのソースコード
このファイル内のすべての取り消し線の付いた行は、元のファイルから削除する必要があります。これらの行は以前、カスタムイベントがインジケーターのタイマーを同期させるための古いコードの一部でしたが、現在は別の方法を実装しているため不要です。ただし、このヘッダーファイルのある重要な点に注意してください。124行目と125行目を比較してください。行全体が置き換えられたように見えますが、実際にはm_Info.TimeDeviceが削除されただけです。この変数は以前、タイマーの同期に使用されていましたが、現在は不要となっています。これが、いわばマウスインジケーターのコード自体に新たな課題をもたらしています。以下にインジケーターの全コードを示します。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property description "This is an indicator for graphical studies using the mouse." 04. #property description "This is an integral part of the Replay / Simulator system." 05. #property description "However it can be used in the real market." 06. #property version "1.68" 07. #property icon "/Images/Market Replay/Icons/Indicators.ico" 08. #property link "https://www.mql5.com/pt/articles/" 09. #property indicator_chart_window 10. #property indicator_plots 0 11. #property indicator_buffers 1 12. #property indicator_applied_price PRICE_CLOSE 13. //+------------------------------------------------------------------+ 14. double GL_PriceClose; 15. datetime GL_TimeAdjust; 16. //+------------------------------------------------------------------+ 17. #include <Market Replay\Auxiliar\Study\C_Study.mqh> 18. //+------------------------------------------------------------------+ 19. C_Study *Study = NULL; 20. //+------------------------------------------------------------------+ 21. input color user02 = clrBlack; //Price Line 22. input color user03 = clrPaleGreen; //Positive Study 23. input color user04 = clrLightCoral; //Negative Study 24. //+------------------------------------------------------------------+ 25. C_Study::eStatusMarket m_Status; 26. int m_posBuff = 0; 27. double m_Buff[]; 28. //+------------------------------------------------------------------+ 29. int OnInit() 30. { 31. ResetLastError(); 32. Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); 33. if (_LastError != ERR_SUCCESS) return INIT_FAILED; 34. if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay) 35. { 36. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); 37. OnBookEvent((*Study).GetInfoTerminal().szSymbol); 38. m_Status = C_Study::eCloseMarket; 39. }else 40. m_Status = C_Study::eInReplay; 41. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 42. ArrayInitialize(m_Buff, EMPTY_VALUE); 43. 44. return INIT_SUCCEEDED; 45. } 46. //+------------------------------------------------------------------+ 47. int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], 48. const double& high[], const double& low[], const double& close[], const long& tick_volume[], 49. const long& volume[], const int& spread[]) 50. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double& price[]) 51. { 52. GL_PriceClose = close[rates_total - 1]; 53. GL_PriceClose = price[rates_total - 1]; 54. GL_TimeAdjust = (spread[rates_total - 1] < 60 ? spread[rates_total - 1] : 0); 55. if (_Symbol == def_SymbolReplay) 56. GL_TimeAdjust = iSpread(NULL, PERIOD_M1, 0) & (~def_MaskTimeService); 57. m_posBuff = rates_total; 58. (*Study).Update(m_Status); 59. 60. return rates_total; 61. } 62. //+------------------------------------------------------------------+ 63. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 64. { 65. (*Study).DispatchMessage(id, lparam, dparam, sparam); 66. (*Study).SetBuffer(m_posBuff, m_Buff); 67. 68. ChartRedraw((*Study).GetInfoTerminal().ID); 69. } 70. //+------------------------------------------------------------------+ 71. void OnBookEvent(const string &symbol) 72. { 73. MqlBookInfo book[]; 74. C_Study::eStatusMarket loc = m_Status; 75. 76. if (symbol != (*Study).GetInfoTerminal().szSymbol) return; 77. MarketBookGet((*Study).GetInfoTerminal().szSymbol, book); 78. m_Status = (ArraySize(book) == 0 ? C_Study::eCloseMarket : C_Study::eInTrading); 79. for (int c0 = 0; (c0 < ArraySize(book)) && (m_Status != C_Study::eAuction); c0++) 80. if ((book[c0].type == BOOK_TYPE_BUY_MARKET) || (book[c0].type == BOOK_TYPE_SELL_MARKET)) m_Status = C_Study::eAuction; 81. if (loc != m_Status) (*Study).Update(m_Status); 82. } 83. //+------------------------------------------------------------------+ 84. void OnDeinit(const int reason) 85. { 86. if (reason != REASON_INITFAILED) 87. { 88. if ((*Study).GetInfoTerminal().szSymbol != def_SymbolReplay) 89. MarketBookRelease((*Study).GetInfoTerminal().szSymbol); 90. } 91. delete Study; 92. } 93. //+------------------------------------------------------------------+
マウスポインタのソースコード
このコードは、テスト中に時々苛立たしい挙動を示します。また、後ほど詳しく触れる別の要素もあり、こちらは意味がわからず非常に腹立たしいものです。これらは本来動作するはずですが、なぜか奇妙な理由で動作しません。これらのトピックについては、別の記事で落ち着いて説明することにします。ここでは、読者の皆さんに、いくつかの重要な変更点に注目していただきたいと思います。
始まりを理解するために終わりから見ていきましょう。47行目から49行目が取り消し線で消され、50行目に置き換えられています。つまり、OnCalculateイベントハンドラはこれらのパラメータを受け取らなくなりました。なぜこの変更をしたのか疑問に思うかもしれません。特に、以前はこうした変更はよくないと述べていたからです。その理由についてはサービスコードの説明時にお話ししますが、ここでは割愛します。次に52行目が53行目に、54行目が55・56行目に置き換えられています。また、もう一つの変更として12行目の追加がありますが、そこに行く前に55行目と56行目の内容を詳しく見てみましょう。
サービスがRATEを更新すると(更新方法は後ほど説明します)、MetaTrader 5はCalculateイベントを発生させ、OnCalculate関数で処理されます。ただし、チャートの時間軸が1分でない場合、新しいバーが形成されるまでrates_totalの値は更新されません。リプレイ/シミュレーション時にスプレッド値だけを使うので、まず銘柄が期待通りかどうかを確認します。該当すれば、iSpread関数でスプレッド値を取得します。なぜOnCalculateのパラメーターを使わずにこれをおこない、56行目で計算するのかは次回説明します。
こうして正しい値をiSpreadから取得するため、以前のようにOnCalculateをすべてのパラメータ付きで呼び出す必要がなくなりました。結果として53行目で価格が正しく取得されます。さて、ここで疑問です。どの価格を使うでしょうか。いくつか候補があります。そこで12行目の出番です。インジケーターのプロパティとして使う価格を定義しています。今回は各バーの終値を使うことにしました。これによりマウスインジケーターは、チャートに以前表示されていた値との互換性を保つことができます。
最後に
この記事では、まだ解決していないいくつかの問題について触れました。これらについては、今後の記事でさらに詳しく解説する予定です。本記事だけを読んだだけでは理解しづらい部分もあるかもしれません。たとえば、MetaTrader 5がOnCalculateを通じて既に渡しているスプレッド値を単純に使わずに、なぜiSpread関数を使っているのか、といったことです。また、まだ触れていない別の重要なトピックとして、資産が板寄せの状態に入った際に何が起こるのかという問題もあります。これは、カスタム銘柄を扱う際に特に特殊なケースです。マウスインジケーターが板寄せ中の資産を表示できないわけではなく、問題は別の部分にあります。
この機能はすでに実装されており、動作も確認済みです。マウスインジケーターのOnBookEvent関数を見れば、自分で確かめることができます。マウスインジケーターに資産が板寄せ中であることを示すには、BOOK_TYPE_BUY_MARKETかBOOK_TYPE_SELL_MARKETの値を報告すれば十分です。ただし、その具体的な方法については別の記事で詳しく説明します。今回は以上です。次回の記事でお会いしましょう。
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12309





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索