
リプレイシステムの開発(第72回):異例のコミュニケーション(I)
はじめに
前回の「リプレイシステムの開発(第71回):正しい時間を知る(IV)」では、別の記事で紹介した内容をリプレイ/シミュレーターサービスに組み込む方法を解説しました。
具体的には、「リプレイシステムの開発(第70回):正しい時間を知る(III)」で紹介したテスト用サービスを使って、オーダーブックイベントをどのように扱えばよいかを、より深く理解する手法について述べました。これは、カスタム銘柄を対象とする場合に必要となる作業です。
ここ2回の記事で扱ったプロセス全体は非常に興味深いものでした。というのも、目的とする結果を得るために、特別なアプローチを取る必要があったからです。多くの方が、MetaTrader 5でオーダーブックを正しく扱う方法を学び、理解できたのではないかと思います。繰り返しになりますが、ここで扱っているのはカスタム銘柄です。この点を忘れないようにしてください。
興味深い点として、オーダーブックを追加するだけで、マウスインジケーターが OnCalculate関数を利用できるようになったことが挙げられます。この関数では、MetaTrader 5によってデータが配列へ格納されます。つまり、バーのスプレッドを取得するだけのためにiSpread関数を使う必要がなくなり、処理が大幅に簡素化されたのです。
この2つの記事で取り上げた知識がなければ、時間足が1分を超える場合、OnCalculate関数の引数からデータを取得することはできませんでした。それ以外のケースでは、OnCalculateの引数を通じたデータ取得に特別な問題はありません。しかし、カスタムのブックイベントを使用すると、それだけでスプレッドをOnCalculateの引数から直接取得できるようになります。
ただし、前回の記事の最後で、解決しなければならない重要な問題について説明しました。この問題を放置したままでは、アプリケーションを正しく動作させることができません。そこで、今回の記事から読み始めた方のために、その問題が何であったのかを簡単に振り返っておきましょう。
問題の再確認
ここで取り上げる問題は、時間足を変更したときに発生する不具合です。致命的なエラーではありませんが、非常に不便です。具体的には次のような現象です。リプレイ/シミュレーターサービスを開始すると、希望する時間足を選択できます。そしてチャートが開くと、その選択された時間足で動作します。
ところが、MetaTrader 5上で時間足を変更すると、マウスインジケーターが「オークション中」から「市場は閉じています」へと状態を変え、さらにコントロールインジケーターがチャートから完全に消えてしまい、操作できなくなってしまいます。
これは不便ではありますが、MT5側の不具合ではありません。というのも、MetaTrader 5は、チャート上のインジケーターをリロードし、すぐにOnInit関数を呼び出すという、設計通りの動作をしているだけだからです。問題は、私たちがそれをまったく異なる方法で使おうとしていることにあります。MetaTrader 5を通常とは異なる方法で動作させる必要があるのです。
では、このような状況で何が起こるのでしょうか。この不具合が発生すると、サービスがコントロールインジケーターへカスタムイベントを発行してくれるのを待たなければなりません。このイベントによって、コントロールインジケーターは新しい値で再初期化されるわけですが、それが送られてくるまでに時間がかかる可能性があります。さらに問題なのは、このカスタムイベントが送られるためには、サービスが再生モードで動作していなければならないという点です。
つまり、サービスが再生モードでない場合、コントロールインジケーターは初期化に必要なカスタムイベントを一切受け取れず、操作不能になってしまうということです。このような場合、ユーザーはサービスを停止して再起動しなければならず、非常に面倒です。そして仮にサービスが再生モードであったとしても、カスタムイベントが発行されるまで待たなければなりません。イベントが送られると、コントロールインジケーターのコントロールが再び使えるようになります。ここでユーザーはリプレイ/シミュレーターサービスを一時停止し、もう一度[再生]ボタンを押さなければなりません。すると、マウスインジケーターに表示される情報(たとえばバーの残り時間など)が再び正しく表示されるようになります。しかし、時間足を再度変更すれば、この問題が再び発生することになります。
前回の記事では、この問題をより詳細に説明しました。ここでは単に復習として取り上げているだけであり、特に今回から本連載を読み始めた読者のための補足です。なお、前回の記事の最後には、小さなチャレンジを提示しておきました。それは、「この問題をどのように解決できるかを想像してみてください」というものでした。多くの人はプログラミングを、コンピュータが実行できるコードを書く技術だと考えていますが、実際にはそれ以上のものです。コードを書くこと自体は簡単で、本当の課題は問題の解決方法を見つけることにあります。プログラミングとは、コードを通じて問題を解決する技術なのです。
正直に言って、読者の皆さんがこの問題を解決できたかどうかは重要ではありません。大切なのは、少なくとも解決しようと試みたかどうかです。問題を解決し、解決策を考えることこそが本質です。コードを入力するのは単なる形式にすぎません。それでは、この問題をどのように解決していくか見ていきましょう。
最初の解決策の試み
もしこの問題を本当に理解しているなら、最初に思いつくのは次のような解決策かもしれません。つまり、チャートの時間足が変更されたことを検出できるようにして、それが起きた瞬間に、インジケーターがチャートから削除される前の状態を復元できるようにすることです。こうすれば、MetaTrader 5がインジケーターをチャートに戻した際に、コントロールインジケーターとマウスインジケーターの両方が直前の状態を認識し、そこから動作を再開できるはずです。素晴らしいアイデアです。しかし、これには欠点もあります。インジケーターの最後の状態をどのように保存したとしても、その情報をインジケーター自体に直接結びつけてはいけません。その情報は、OnInit関数が呼び出された瞬間にインジケーターが参照できるようにしておく必要があります。そうでなければ問題が生じます。
これを実現する方法はいくつかありますが、どれも追加の実装と、保存された状態が安全に使えるかどうかを確認するためのテストが必要になります。個人的には、この解決策は非常に煩雑すぎると感じます。なぜなら、保存された情報が正しく使えるかどうかを検証するだけのために、膨大なテストが必要になるからです。
では、実際にどのように実装するかを考えてみましょう。まず、どんな情報を保存する必要があるかを考えます。コントロールインジケーターの場合、保存すべき情報は「一時停止中か再生中か」という状態だけで十分です。それだけで良さそうです。マウスインジケーターに関しては、「その資産がオークションモードだった」という情報だけを復元すればよいでしょう。確かに、このデータを保存するのは簡単そうです。ファイルに書き込むか、グローバル端末変数に保存すればいいのです。ここまでは問題なく実装できます。しかし、ここから問題が発生します。
コントロールインジケーターについては、それほど問題にはなりません。これはリプレイ/シミュレーターサービスが稼働中にしかチャートに追加されないからです。したがって、ファイルを使っても、グローバル変数を使っても構いません。ファイルや変数が存在するかどうかを確認して、存在すればその値を使用します。しかし、問題は次の点です。時間が経つにつれて、そのデータがサービスの現在の状態とずれてしまう可能性があります。サービスが終了する際には、必ずグローバル変数やファイルを削除するようにしなければなりません。
つまり、管理すべきプログラム的な負担が増えるということです。次にマウスインジケーターに戻ります。「オークションモード」という値で初期化してしまうと、実際の資産のチャートに追加されたときに問題が起こるかもしれません。なぜなら、そのとき市場が実際には閉まっていても、マウスインジケーターは誤って「オークションモード」と表示してしまうからです。そして私は将来的にマウスインジケーターをさらに改良する予定なので、起動時に無理やり「オークションモード」とさせるのは、余計な問題の火種を抱えることになります。これは絶対に避けたいのです。
結論として言えるのは、グローバル端末変数を使う方法は完全に却下です。これを使うと、インジケーターに2種類の初期化経路が生まれてしまいます。同様に、ファイルを使う方法でも同じ問題が発生します。この解決策は私たちのニーズには合いません。私たちには別の解決策が必要です。それは、インジケーターに紐づいたままで、なおかつチャート上で何が起きているのかをテストできる方法です。
第二の解決策の試み
サービス側でカスタム銘柄の現在の時間足を確認するようにすることを提案する方がいるかもしれません。実際、これは非常に賢い解決策です。なぜなら、インジケーターの状態情報を保存する必要が一切なくなるからです。思い出してください。リプレイ/シミュレーターサービスは、実行中の任意のタイミングで、インジケーターの状態を常に把握しています。ただし、この解決策にも小さな問題があります。それは、サービス側から現在のチャートの時間足を直接取得する方法が存在しないという点です。
しかし、この代替案をすぐに捨ててしまう前に、少し考えてみましょう。もし時間足が変更されたことを何らかの方法で検出できるのであれば、あとはサービス側からC_Replay.mqhヘッダファイル内のUpdateIndicatorControlメソッドを呼び出せば、問題は解決します。これはほぼ完全に正しいアプローチです。この場合、UpdateIndicatorControl手続きに少しだけ修正を加える必要はありますが、それでもインジケーターにまったく新しい初期化処理を追加するよりははるかに簡単です。つまり、私たちに必要なのは、チャートの時間足が変更されたことをサービス側で検出できる手段です。それさえできれば、既に実装されている仕組みを利用してインジケーターを正しく再初期化することが可能になります。たとえサービス側に多少の変更が必要だとしても、UpdateIndicatorControlを利用するための調整であれば、インジケーター自体にこの問題への対処を追加するよりも、ずっと小さく、扱いやすい変更です。要するに、この代替案は検討する価値が十分にあります。
第三の解決策の試み
では、少し変わった方法を試してみましょう。前の2つのアプローチを組み合わせつつ、グローバル端末変数やファイルを使用しない形にします。代わりに、データバッファを使って必要な情報(この場合は時間足)を渡す方法を採用します。こうすることで、サービス側は状況を監視でき、時間足が変更された瞬間にそれを即座に検出できます。そのタイミングで、サービスがインジケーターに更新が必要であることを通知するのです。
つまり、本当の課題は、データバッファに字観測の情報を書き込み、サービス側がそれを読み取れるようにすることです。ユーザーが時間足を変更すると、アプリケーション全体がサービスからのカスタムイベントを受け取るようになります。このイベントにより、マウスインジケーターにはユーザーに表示すべき正しい値が送られ、コントロールインジケーター側も適切に更新されます。こうすれば、再生・一時停止の制御ができなくなる問題を防ぐことができます。
実装テストの開始
一部の人が考えることとは反対に、私たちプログラマーは、実際に機能を実装する前に必ずテストを行います。開発が進んだ段階のプログラムやアプリケーションをいきなり変更したりはしません。新しい機能を実装する必要がある場合、まずはテスト用のバージョンを作成します。このテストバージョンはできる限りシンプルにしておき、その中で実装の調整をおこない、最終的な処理がどのように機能すべきかを検討します。これにより、最終版に適さないことが後から判明したコードの削除や書き直しを避けることができます。
それでは、時間足変更の問題をテストするための非常にシンプルなインジケーターを作成しましょう。以下に、このインジケーターの完全なソースコードを示します。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property version "1.00" 04. #property indicator_chart_window 05. #property indicator_plots 0 06. //+------------------------------------------------------------------+ 07. int OnInit() 08. { 09. Print(_Period); 10. 11. return INIT_SUCCEEDED; 12. } 13. //+------------------------------------------------------------------+ 14. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 15. { 16. return rates_total; 17. } 18. //+------------------------------------------------------------------+
テストインジケーターのソースコード
この小さなコードは何をするのでしょうか。簡潔に言えば、すべての処理は9行目でおこなわれています。つまり、内部変数「_Period」に格納されている値を端末に出力しているだけです。以下の画像では、その実行結果の一例が確認できます。
実行結果
ここで注目すべき点があります。上の画像で、Source列にはインジケーターの名前が表示されていますが、本当に重要なのは括弧内の値、正確にはカンマの後ろの値です。ご覧のとおり、私は時間足を何度か変更しましたが、そのたびにMessage列には異なる値が出力されました。これは一見すると当然のように思えるかもしれません。しかし、今のところは値が異なるという事実を無視してください。そして、それらの値から何らかのロジックを導き出そうともしないでください。私たちが本当に関心を持つべきなのは、_Periodが実際に保持し得る最大値は何かという点です。MetaTrader 5では、月足まで使用することができます。画像では、この値が下から2行目に表示されており、最大値は49153です。では、なぜこの値を知っておくことが重要なのでしょうか。それは、サービス側がこの値の変化を検出できるようにする必要があるからです。ただし、そのために任意のビット長を使うわけにはいきません。使用するビット数はできるだけ少なく抑える必要があります。その理由は、まもなく説明します。
上記のコードを少し変更してみましょう。今あるのは次のコードです。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property version "1.00" 04. #property indicator_chart_window 05. #property indicator_plots 0 06. //+------------------------------------------------------------------+ 07. int OnInit() 08. { 09. Print(_Period < 60 ? _Period : (_Period < PERIOD_D1 ? _Period - 16325 : (_Period == PERIOD_D1 ? 84 : (_Period == PERIOD_W1 ? 91 : 96)))); 10. 11. return INIT_SUCCEEDED; 12. } 13. //+------------------------------------------------------------------+ 14. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 15. { 16. return rates_total; 17. } 18. //+------------------------------------------------------------------+
テストインジケーターのソースコード
9行目が現在何をしているかを分析する前に、下の画像に示す実行結果を見てみましょう。
実行結果
さて、次の点に注目してください。以前は、時間足が変更されたかどうかを検出するために16ビットが必要でした。しかし今、最大値は96であることがわかりました。すごいですね。しかし、これが本当に可能なのでしょうか。はい、可能ですし、そうします。なぜなら、どうやらMetaTrader 5はこれらの時間足の値を既に統合しているからです。したがって、この簡略化された値を使って動作するように安全に修正することができます。
コードに戻ると、この縮小処理は非常に簡単で、定数だけを使って1行で実行できます。これは私たちの目的には十分すぎるほどで、現在は7ビットだけで値を伝達すればよいのです。ですから、実装方法について少し考える必要があります。ここで覚えておいてほしいのは、私たちが知りたいのは時間足そのものではなく、変更されたかどうかだけだということです。
サービスへの情報の転送を開始する
時間足の情報をサービスに転送し、変化を検出できるようにするためには、適切な仕組みが他にないのでインディケータのバッファを使う必要があります。実際には他の方法もありますが、このタスクにはあえてインディケーターバッファを使いたいと思います。この方法はユーザーには見えないまま、転送中の情報のカプセル化もある程度保てるためです。
しかし、その前に、先ほどテストした最新のコードをシステムに統合しましょう。これは定義として追加されます。したがって、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. #define def_IndicatorTimeFrame (_Period < 60 ? _Period : (_Period < PERIOD_D1 ? _Period - 16325 : (_Period == PERIOD_D1 ? 84 : (_Period == PERIOD_W1 ? 91 : 96)))) 17. //+------------------------------------------------------------------+ 18. union uCast_Double 19. { 20. double dValue; 21. long _long; // 1 Information 22. datetime _datetime; // 1 Information 23. uint _32b[sizeof(double) / sizeof(uint)]; // 2 Informations 24. ushort _16b[sizeof(double) / sizeof(ushort)]; // 4 Informations 25. uchar _8b [sizeof(double) / sizeof(uchar)]; // 8 Informations 26. }; 27. //+------------------------------------------------------------------+ 28. enum EnumEvents { 29. evHideMouse, //Hide mouse price line 30. evShowMouse, //Show mouse price line 31. evHideBarTime, //Hide bar time 32. evShowBarTime, //Show bar time 33. evHideDailyVar, //Hide daily variation 34. evShowDailyVar, //Show daily variation 35. evHidePriceVar, //Hide instantaneous variation 36. evShowPriceVar, //Show instantaneous variation 37. evCtrlReplayInit //Initialize replay control 38. }; 39. //+------------------------------------------------------------------+
Defines.mqhファイルのソースコード
確かに、次に16行目を見てください。これは前節で説明したテストで使われたコードです。素晴らしいです。これにより、すでにテスト済みで正しく機能しているコードを使っていることが保証されます。さて、ここからが少し複雑な部分です。少なくとも、本連載を最初から追っていなかったり、今回初めて読む方にとってはそうでしょう。
インディケータのデータバッファは常にdouble型で、メモリ上では8バイトを占めます。本連載の過去の記事で説明した理由から、インディケータからサービスへ転送する情報はこの8バイトに収まらなければなりません。情報交換に8バイトを超えて使用することはできません。すべてこの8バイトに詰め込む必要があります。少し考えてみましょう。マウスインディケータは実取引でも使えます。つまり、実際の取引サーバーからデータを受け取るシンボルで動作可能です。しかし、今回扱っている問題はリプレイ/シミュレーションサービスを使った場合にのみ発生します。今のところ、リプレイ/シミュレーションモード専用で厳密に必要なのはコントロールインディケーターだけです。したがって、コントロールインディケーターのデータバッファが現在どのように構成されているかに注目するのが合理的です。以下で確認できます。
QWORDはアセンブリ言語に由来する用語で、64ビットの値を扱っていることを示します。最も右側のバイトがバイト0で、最も左側のバイトがバイト7です。コントロールインディケータの場合、現在はこのうちの4バイトを使用しています。この点をよりよく理解するために、ヘッダーファイルC_Controls.mqhからのコード抜粋を以下に示します。
168. //+------------------------------------------------------------------+ 169. void SetBuffer(const int rates_total, double &Buff[]) 170. { 171. uCast_Double info; 172. 173. info._16b[eCtrlPosition] = m_Slider.Minimal; 174. info._16b[eCtrlStatus] = (ushort)(m_Slider.Minimal > def_MaxPosSlider ? m_Slider.Minimal : (m_Section[ePlay].state ? ePlay : ePause)); 175. if (rates_total > 0) 176. Buff[rates_total - 1] = info.dValue; 177. } 178. //+------------------------------------------------------------------+
ファイルC_Controls.mqhからの抜粋
171行目では、先ほどDefines.mqhファイル内で定義したunionを宣言しています。ここでは、2つの配列値を使用しており、それぞれが16ビット(=2バイト)を使用しています。つまり、合計で32ビット(=4バイト)になります。実際には全てのビットが使用されているわけではありませんが、ここでは単純化のために使用されていると仮定します。したがって、使用可能な8バイトのうち、すでに4バイトが使われていることになります。しかし、私たちのモデリング方式では、時間足を1バイトでエンコードできるようになっているため、新たに使用するのは1バイトだけです。つまり、使用される合計バイト数は5バイトとなります。ここで最も重要なのは、新しいデータにどのインデックスを使用するかを決定することです。この種の細かな実装は、プログラミング初心者をよく混乱させます。というのも、unionを使用する際には、すでに一部のバイトが有効なデータとして占有されていることを忘れてしまうからです。誤ったインデックスを使ってしまうと、既存のデータを上書きして破損させてしまう可能性があります。そこで、再びQWORDの図を見てみましょう。そこでは、インデックス0から始まる4バイト(インデックス0〜3)がすでに割り当てられているのがわかります。つまり、これらのインデックスは他の用途には使えません。よって、新しいデータに使用できる最初のインデックスはインデックス4となります。
このため、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. #define def_IndicatorTimeFrame (_Period < 60 ? _Period : (_Period < PERIOD_D1 ? _Period - 16325 : (_Period == PERIOD_D1 ? 84 : (_Period == PERIOD_W1 ? 91 : 96)))) 17. #define def_IndexTimeFrame 4 18. //+------------------------------------------------------------------+ 19. union uCast_Double 20. { 21. double dValue; 22. long _long; // 1 Information 23. datetime _datetime; // 1 Information 24. uint _32b[sizeof(double) / sizeof(uint)]; // 2 Informations 25. ushort _16b[sizeof(double) / sizeof(ushort)]; // 4 Informations 26. uchar _8b [sizeof(double) / sizeof(uchar)]; // 8 Informations 27. }; 28. //+------------------------------------------------------------------+ 29. enum EnumEvents { 30. evHideMouse, //Hide mouse price line 31. evShowMouse, //Show mouse price line 32. evHideBarTime, //Hide bar time 33. evShowBarTime, //Show bar time 34. evHideDailyVar, //Hide daily variation 35. evShowDailyVar, //Show daily variation 36. evHidePriceVar, //Hide instantaneous variation 37. evShowPriceVar, //Show instantaneous variation 38. evCtrlReplayInit //Initialize replay control 39. }; 40. //+------------------------------------------------------------------+
Defines.mqhファイルのソースコード
17行目には、新しい定義が追加されており、これにより正しいインデックスを安全に参照することができます。これで、必要な基盤が整いました。とはいえ、このタスクは一見したほど単純ではありません。そこでまずは、C_Controls.mqhヘッダーファイルが実際にどのような構造になるのかを確認してみましょう。コード全体は下にあります。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #include "..\Auxiliar\C_DrawImage.mqh" 005. #include "..\Defines.mqh" 006. //+------------------------------------------------------------------+ 007. #define def_PathBMP "Images\\Market Replay\\Control\\" 008. #define def_ButtonPlay def_PathBMP + "Play.bmp" 009. #define def_ButtonPause def_PathBMP + "Pause.bmp" 010. #define def_ButtonCtrl def_PathBMP + "Ctrl.bmp" 011. #define def_ButtonCtrlBlock def_PathBMP + "Ctrl_Block.bmp" 012. #define def_ButtonPin def_PathBMP + "Pin.bmp" 013. #resource "\\" + def_ButtonPlay 014. #resource "\\" + def_ButtonPause 015. #resource "\\" + def_ButtonCtrl 016. #resource "\\" + def_ButtonCtrlBlock 017. #resource "\\" + def_ButtonPin 018. //+------------------------------------------------------------------+ 019. #define def_ObjectCtrlName(A) "MarketReplayCTRL_" + (typename(A) == "enum eObjectControl" ? EnumToString((C_Controls::eObjectControl)(A)) : (string)(A)) 020. #define def_PosXObjects 120 021. //+------------------------------------------------------------------+ 022. #define def_SizeButtons 32 023. #define def_ColorFilter 0xFF00FF 024. //+------------------------------------------------------------------+ 025. #include "..\Auxiliar\C_Mouse.mqh" 026. //+------------------------------------------------------------------+ 027. class C_Controls : private C_Terminal 028. { 029. protected: 030. private : 031. //+------------------------------------------------------------------+ 032. enum eMatrixControl {eCtrlPosition, eCtrlStatus}; 033. enum eObjectControl {ePause, ePlay, eLeft, eRight, ePin, eNull, eTriState = (def_MaxPosSlider + 1)}; 034. //+------------------------------------------------------------------+ 035. struct st_00 036. { 037. string szBarSlider, 038. szBarSliderBlock; 039. ushort Minimal; 040. }m_Slider; 041. struct st_01 042. { 043. C_DrawImage *Btn; 044. bool state; 045. short x, y, w, h; 046. }m_Section[eObjectControl::eNull]; 047. C_Mouse *m_MousePtr; 048. //+------------------------------------------------------------------+ 049. inline void CreteBarSlider(short x, short size) 050. { 051. ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSlider = def_ObjectCtrlName("B1"), OBJ_RECTANGLE_LABEL, 0, 0, 0); 052. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_XDISTANCE, def_PosXObjects + x); 053. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Section[ePin].y + 11); 054. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_XSIZE, size); 055. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_YSIZE, 9); 056. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue); 057. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack); 058. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_WIDTH, 3); 059. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT); 060. ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSliderBlock = def_ObjectCtrlName("B2"), OBJ_RECTANGLE_LABEL, 0, 0, 0); 061. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XDISTANCE, def_PosXObjects + x); 062. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YDISTANCE, m_Section[ePin].y + 6); 063. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YSIZE, 19); 064. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BGCOLOR, clrRosyBrown); 065. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BORDER_TYPE, BORDER_RAISED); 066. } 067. //+------------------------------------------------------------------+ 068. void SetPlay(bool state) 069. { 070. if (m_Section[ePlay].Btn == NULL) 071. m_Section[ePlay].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(ePlay), def_ColorFilter, "::" + def_ButtonPause, "::" + def_ButtonPlay); 072. m_Section[ePlay].Btn.Paint(m_Section[ePlay].x, m_Section[ePlay].y, m_Section[ePlay].w, m_Section[ePlay].h, 20, (m_Section[ePlay].state = state) ? 1 : 0, state ? "Press to Pause" : "Press to Start"); 073. if (!state) CreateCtrlSlider(); 074. } 075. //+------------------------------------------------------------------+ 076. void CreateCtrlSlider(void) 077. { 078. if (m_Section[ePin].Btn != NULL) return; 079. CreteBarSlider(77, 436); 080. m_Section[eLeft].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(eLeft), def_ColorFilter, "::" + def_ButtonCtrl, "::" + def_ButtonCtrlBlock); 081. m_Section[eRight].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(eRight), def_ColorFilter, "::" + def_ButtonCtrl, "::" + def_ButtonCtrlBlock, true); 082. m_Section[ePin].Btn = new C_DrawImage(GetPointer(this), def_ObjectCtrlName(ePin), def_ColorFilter, "::" + def_ButtonPin, NULL); 083. PositionPinSlider(m_Slider.Minimal); 084. } 085. //+------------------------------------------------------------------+ 086. inline void RemoveCtrlSlider(void) 087. { 088. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 089. for (eObjectControl c0 = ePlay + 1; c0 < eNull; c0++) 090. { 091. delete m_Section[c0].Btn; 092. m_Section[c0].Btn = NULL; 093. } 094. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("B")); 095. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 096. } 097. //+------------------------------------------------------------------+ 098. inline void PositionPinSlider(ushort p) 099. { 100. int iL, iR; 101. string szMsg; 102. 103. m_Section[ePin].x = (short)(p < m_Slider.Minimal ? m_Slider.Minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); 104. iL = (m_Section[ePin].x != m_Slider.Minimal ? 0 : 1); 105. iR = (m_Section[ePin].x < def_MaxPosSlider ? 0 : 1); 106. m_Section[ePin].x += def_PosXObjects; 107. m_Section[ePin].x += 95 - (def_SizeButtons / 2); 108. for (eObjectControl c0 = ePlay + 1; c0 < eNull; c0++) if (m_Section[c0].Btn != NULL) 109. { 110. switch (c0) 111. { 112. case eLeft : szMsg = "Previous Position"; break; 113. case eRight : szMsg = "Next Position"; break; 114. case ePin : szMsg = "Go To: " + IntegerToString(p); break; 115. default : szMsg = "\n"; 116. } 117. m_Section[c0].Btn.Paint(m_Section[c0].x, m_Section[c0].y, m_Section[c0].w, m_Section[c0].h, 20, (c0 == eLeft ? iL : (c0 == eRight ? iR : 0)), szMsg); 118. } 119. 120. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, m_Slider.Minimal + 2); 121. } 122. //+------------------------------------------------------------------+ 123. inline eObjectControl CheckPositionMouseClick(short &x, short &y) 124. { 125. C_Mouse::st_Mouse InfoMouse; 126. 127. InfoMouse = (*m_MousePtr).GetInfoMouse(); 128. x = (short) InfoMouse.Position.X_Graphics; 129. y = (short) InfoMouse.Position.Y_Graphics; 130. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) 131. { 132. if ((m_Section[c0].Btn != NULL) && (m_Section[c0].x <= x) && (m_Section[c0].y <= y) && ((m_Section[c0].x + m_Section[c0].w) >= x) && ((m_Section[c0].y + m_Section[c0].h) >= y)) 133. return c0; 134. } 135. 136. return eNull; 137. } 138. //+------------------------------------------------------------------+ 139. public : 140. //+------------------------------------------------------------------+ 141. C_Controls(const long Arg0, const string szShortName, C_Mouse *MousePtr) 142. :C_Terminal(Arg0), 143. m_MousePtr(MousePtr) 144. { 145. if ((!IndicatorCheckPass(szShortName)) || (CheckPointer(m_MousePtr) == POINTER_INVALID)) SetUserError(C_Terminal::ERR_Unknown); 146. if (_LastError >= ERR_USER_ERROR_FIRST) return; 147. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 148. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("")); 149. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 150. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) 151. { 152. m_Section[c0].h = m_Section[c0].w = def_SizeButtons; 153. m_Section[c0].y = 25; 154. m_Section[c0].Btn = NULL; 155. } 156. m_Section[ePlay].x = def_PosXObjects; 157. m_Section[eLeft].x = m_Section[ePlay].x + 47; 158. m_Section[eRight].x = m_Section[ePlay].x + 511; 159. m_Slider.Minimal = eTriState; 160. } 161. //+------------------------------------------------------------------+ 162. ~C_Controls() 163. { 164. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) delete m_Section[c0].Btn; 165. ObjectsDeleteAll(GetInfoTerminal().ID, def_ObjectCtrlName("")); 166. delete m_MousePtr; 167. } 168. //+------------------------------------------------------------------+ 169. void SetBuffer(const int rates_total, double &Buff[]) 170. { 171. uCast_Double info; 172. 173. info._16b[eCtrlPosition] = m_Slider.Minimal; 174. info._16b[eCtrlStatus] = (ushort)(m_Slider.Minimal > def_MaxPosSlider ? m_Slider.Minimal : (m_Section[ePlay].state ? ePlay : ePause)); 175. info._8b[def_IndexTimeFrame] = (uchar) def_IndicatorTimeFrame; 176. if (rates_total > 0) 177. Buff[rates_total - 1] = info.dValue; 178. } 179. //+------------------------------------------------------------------+ 180. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 181. { 182. short x, y; 183. static ushort iPinPosX = 0; 184. static short six = -1, sps; 185. uCast_Double info; 186. 187. switch (id) 188. { 189. case (CHARTEVENT_CUSTOM + evCtrlReplayInit): 190. info.dValue = dparam; 191. if ((info._8b[7] != 'D') || (info._8b[6] != 'M')) break; 192. iPinPosX = m_Slider.Minimal = (info._16b[eCtrlPosition] > def_MaxPosSlider ? def_MaxPosSlider : (info._16b[eCtrlPosition] < iPinPosX ? iPinPosX : info._16b[eCtrlPosition])); 193. SetPlay((eObjectControl)(info._16b[eCtrlStatus]) == ePlay); 194. break; 195. case CHARTEVENT_OBJECT_DELETE: 196. if (StringSubstr(sparam, 0, StringLen(def_ObjectCtrlName(""))) == def_ObjectCtrlName("")) 197. { 198. if (sparam == def_ObjectCtrlName(ePlay)) 199. { 200. delete m_Section[ePlay].Btn; 201. m_Section[ePlay].Btn = NULL; 202. SetPlay(m_Section[ePlay].state); 203. }else 204. { 205. RemoveCtrlSlider(); 206. CreateCtrlSlider(); 207. } 208. } 209. break; 210. case CHARTEVENT_MOUSE_MOVE: 211. if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft)) switch (CheckPositionMouseClick(x, y)) 212. { 213. case ePlay: 214. SetPlay(!m_Section[ePlay].state); 215. if (m_Section[ePlay].state) 216. { 217. RemoveCtrlSlider(); 218. m_Slider.Minimal = iPinPosX; 219. }else CreateCtrlSlider(); 220. break; 221. case eLeft: 222. PositionPinSlider(iPinPosX = (iPinPosX > m_Slider.Minimal ? iPinPosX - 1 : m_Slider.Minimal)); 223. break; 224. case eRight: 225. PositionPinSlider(iPinPosX = (iPinPosX < def_MaxPosSlider ? iPinPosX + 1 : def_MaxPosSlider)); 226. break; 227. case ePin: 228. if (six == -1) 229. { 230. six = x; 231. sps = (short)iPinPosX; 232. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false); 233. } 234. iPinPosX = sps + x - six; 235. PositionPinSlider(iPinPosX = (iPinPosX < m_Slider.Minimal ? m_Slider.Minimal : (iPinPosX > def_MaxPosSlider ? def_MaxPosSlider : iPinPosX))); 236. break; 237. }else if (six > 0) 238. { 239. six = -1; 240. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); 241. } 242. break; 243. } 244. ChartRedraw(GetInfoTerminal().ID); 245. } 246. //+------------------------------------------------------------------+ 247. }; 248. //+------------------------------------------------------------------+ 249. #undef def_PosXObjects 250. #undef def_ButtonPlay 251. #undef def_ButtonPause 252. #undef def_ButtonCtrl 253. #undef def_ButtonCtrlBlock 254. #undef def_ButtonPin 255. #undef def_PathBMP 256. //+------------------------------------------------------------------+
C_Controls.mqhファイルのソースコード
前回までの記事でいくつかの変更を加えてきたため、C_Controls.mqhヘッダーファイルに対して少なくともいくつかの修正を理解し、適用しておく必要があります。加えて、これから紹介するいくつかの変更にも注意してください。まずは10行目を見てください。この行ではCtrl.bmpという画像が参照されていますが、これは一体どの画像でしょうか。これは実際には、以前LEFT.BMPと呼ばれていた画像と同じものです。同様に、11行目で参照されているのは、かつてLEFT_BLOCK.BMPとして知られていた画像です。今後はこれらの画像を異なる名前で使用することになります。いずれにしても、この記事にはこれらの画像ファイルが添付されているので、必要に応じてプロジェクトに簡単に追加できるようになっています。
次にもう一つ注目すべき変更があります。それはC_Controlsクラスのコンストラクタ内です。146行目を見てください。コンストラクタは、コード内で開発者が明示的に指定したエラーに限って早期に終了するようになりました。それ以外のエラーについては、少なくとも今のところは無視されます。
そして最も重要なのは、175行目にあります。ここでは、本記事で提案しているように、インジケーターバッファに対して「どこに」「何を」書き込むかが定義されています。ただし、この情報を活用することは、単にバッファに書き込むよりも少し複雑であるため、ここではまだ完全なコードは紹介しません。というのも、MetaTrader 5のプログラミングに慣れていない読者のために、仕組みをきちんと説明することが重要だからです。ですので、読者の皆さんの中にすでに経験豊富な方がいらっしゃれば、今すぐ完全なコードを提示しないことをお詫びします。
最後に
サービスのコードおよびインジケーターのコードに対して、今後加えるいくつかの変更点についてまだ説明が必要なため、本記事では完全なコードは提示しません。すでにMQL5に関してしっかりとした経験をお持ちの方々には申し訳ありません。ただ、これまでの記事を公開してきた中で、多くの読者がまだMQL5を始めたばかりであり、学ぶ意欲を持った見習いプログラマーであることに気づきました。ですので、なるべく丁寧に説明するようにしています。とはいえ、本記事には2つの画像が添付されています。これらはC_Controls.mqhヘッダーファイルで参照されている画像です。
この記事を引き続き読み進めたい方は、プロジェクトにこの調整を忘れずに適用してください。
次回の記事では、コントロールインジケーターのソースコードに加えた修正点を提示し、それについて説明します。ヘッダーファイルの変更はすでに確認しましたが、インジケーター本体のコードも変更する必要があります。また、C_Replay.mqhヘッダーファイルのソースコードをどのように調整するかについても示します。これらの変更がなければ、サービスはチャート上のインジケーターを「見て」、時間足が変更されたことを判断することができません。
それでは、また次回お会いしましょう。そして、この内容をしっかりと学習するのを忘れないようにしてください。
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12362





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