初級から中級まで:オブジェクト(III)
はじめに
前回の「初級から中級まで:マウスイベント」では、マウス関連のイベントや、マウスとキーボードを組み合わせたイベントの処理方法について、シンプルで分かりやすく、できるだけ客観的な形で解説しました。これらは、チャート上で特定の操作を実行するために、押下ボタンの組み合わせやボタン操作の組み合わせを扱ううえで必要です。
一見すると、前回取り上げた機能は比較的シンプルに思えるかもしれません。しかし、実際に実装してみると、想像以上に複雑であることに気づきます。その理由は、実装する機能の種類や実装方法によっては、一部のユーザーが必要な入力デバイスを持っていない場合があるということです。現在では一般的になりつつあるとはいえ、すべての人がデスクトップコンピュータを使用しているわけではありません。
また、デスクトップコンピューターを使用している場合でも、ご存じのとおり、3つ以上のボタンを備えたマウスを持っていないユーザーも少なくありません。そのため、多くのユーザーを対象とした機能を実装する際には、設計上の制約が生じることがあります。個人的な用途であれば大きな問題にはなりません。しかし、プログラミングを学んでアプリケーションを販売したいと考えている場合は、すべてのユーザーが必要な機器を持っているわけではないことを意識する必要があります。そのため、対象ユーザーの環境に合わせて設計するか、それとも一部のユーザーを切り捨てて市場の一部を失うリスクを受け入れるかを選択しなければなりません。こうした結果は、たった一つの設計判断によって左右されることもあります。
もっとも、本記事の目的は、作成したアプリケーションを販売する方法を学ぶことではありません。MQL5を使って自分のアイデアを形にすることです。前回の記事は、技術的な説明と簡単なデモに重点を置いた内容だったので、今回はそこで紹介した内容を実際に活用し、MetaTrader 5の十字カーソルツールの代替となる機能を作成してみます。
もちろん、MetaTrader 5の十字カーソルを置き換えること自体が目的ではありません。適切な考え方を理解していれば、限られた知識しかなくても実用的なものを作れることをお見せしたいのです。
カスタムインジケータ
今回ご紹介する内容は、以前執筆した「リプレイシステムの開発(第30回):エキスパートアドバイザープロジェクト - C_Mouseクラス(IV)」でも取り上げたことがあります。その記事では、オブジェクト指向プログラミング(OOP)を用いて、マウスを利用したインジケータの実装方法を紹介しました。今回はそれと似た内容を扱いますが、対象はプログラミング初心者です。そのため、よりシンプルなプログラムを使って解説を進めます。読者の皆さんにも取り組みやすい内容になっています。
「以前に説明した内容を、なぜもう一度取り上げるのか」と思われるかもしれません。しかし、実際に繰り返すのは考え方だけです。これまでの記事ではOOPについてはまだ説明しておらず、構造化プログラミングのみを扱ってきました。そこで今回は、専門家でなくても、自分の目的に合ったものを作れることを示したいと思います。これまで学んできた概念を活用しながら、最小限の知識でも十分に目的を達成できることを確認していきます。そして最後には、期待どおりの結果を得ることができます。つまり、一見すると物足りなく思えるようなシンプルな方法でも、十分に機能するということです。
それでは最初に、前回の記事で取り上げたコードの一つをもとに実装を始めます。以下がそのコードです。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. int OnInit() 07. { 08. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 09. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 10. //+----------------+ 11. ChartSetInteger(0, CHART_MOUSE_SCROLL, false); 12. ChartSetInteger(0, CHART_CONTEXT_MENU, false); 13. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 14. //+----------------+ 15. ChartSetInteger(0, CHART_KEYBOARD_CONTROL, false); 16. ChartSetInteger(0, CHART_QUICK_NAVIGATION, false); 17. //+----------------+ 18. 19. return INIT_SUCCEEDED; 20. }; 21. //+------------------------------------------------------------------+ 22. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 23. { 24. return rates_total; 25. }; 26. //+------------------------------------------------------------------+ 27. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 28. { 29. string sz = ""; 30. 31. switch (id) 32. { 33. case CHARTEVENT_KEYDOWN: 34. break; 35. case CHARTEVENT_MOUSE_MOVE: 36. Comment(sz); 37. sz += "Mouse position X: " + (string)(short)lparam; 38. sz += "\nMouse position Y: " + (string)(short)dparam; 39. Comment(sz); 40. if ((uchar)sparam != 0) PrintFormat("Hexadecimal mask of mouse buttons is: 0x%02X", (uchar)sparam); 41. break; 42. case CHARTEVENT_MOUSE_WHEEL: 43. break; 44. } 45. }; 46. //+------------------------------------------------------------------+ 47. void OnDeinit(const int reason) 48. { 49. Comment(""); 50. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 51. //+----------------+ 52. ChartSetInteger(0, CHART_MOUSE_SCROLL, true); 53. ChartSetInteger(0, CHART_CONTEXT_MENU, true); 54. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 55. //+----------------+ 56. ChartSetInteger(0, CHART_KEYBOARD_CONTROL, true); 57. ChartSetInteger(0, CHART_QUICK_NAVIGATION, true); 58. //+----------------+ 59. }; 60. //+------------------------------------------------------------------+
コード01
コード01は、前回の記事で説明したコードを修正したものです。これにより、より簡単にテストすることができます。40行目ではターミナルに何らかの値を出力していることに注目してください。この値こそが、私たちが使用する必要のある値です。そのため、コード01を実行すると、次の図のような表示が得られます。

図01
図01で強調表示されているのが、実際に使用するべきマスク値です。これは、解析用のカスタム十字カーソルを表示するイベントを作成するために必要です。この点に注意してください。図01に表示されている値は、マウスの中央ボタンが押されたときに記録された値です。したがって、中央ボタンを押したときに解析用のカスタム十字カーソルを作成するのであれば、その実装方法を考え始めることができます。ただし、もう一つ理解しておく必要があることがあります。
前回の記事で示したコードを実際に試してみた方であれば、コード01の06行目にあるOnInit関数内でおこなわれている各呼び出しが、MetaTrader 5の標準プロパティを有効または無効にしていることに気付いたはずです。しかし、私たちが必要としているのは解析用のカスタム十字カーソルを作成することだけです。そのため、これらすべてのイベントを無効にする必要はありません。そのようにすると、一部のユーザーにとってMetaTrader 5の利用が過度に制限されてしまうからです。無効にすべきなのは、MetaTrader 5にデフォルトでは使用してほしくないイベントだけです。この場合、それはコード01の13行目にあるイベントです。
これで、コード01で本当に変更する必要がある最初の項目が分かりました。しかし、次に実装するコードへ進む前に、十字カーソルの作成に取り掛かることができます。これから説明する内容には十分注意してください。非常に重要です。
「初級から中級まで:オブジェクト(II)」では、チャート上で垂直線と水平線という2つのオブジェクトを同時に扱う方法を示しました。
ここで重要なのは、これら2つのオブジェクトは、コンピュータグラフィックスで一般的に用いられる画面座標(X座標およびY座標)を使用しないということです。OBJ_VLINEとOBJ_HLINEは、どちらも時間と価格に基づく座標を使用します。つまり、これらのチャート座標は価格と時間を基準にしています。一方、オペレーティングシステムが提供するのはX座標とY座標です。そのため、これらの画面座標を時間と価格の座標へ変換する必要があります。この変換を手作業でおこなうのは非常に面倒です。価格と時間をX座標およびY座標と対応付けるには、最小価格・最大価格、および最小時刻・最大時刻を考慮しなければならないためです。
幸いなことに、この処理はMQL5標準ライブラリに用意されている関数を使用して実行できるため、作業は大幅に簡単になります。それでは、チャート上に描画される十字カーソルについて見ていく前に、まずX座標とY座標を時間と価格に基づくチャート座標へ変換する方法を見ていきましょう。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. int OnInit() 07. { 08. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 09. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 10. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 11. 12. return INIT_SUCCEEDED; 13. }; 14. //+------------------------------------------------------------------+ 15. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 16. { 17. return rates_total; 18. }; 19. //+------------------------------------------------------------------+ 20. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 21. { 22. string sz = ""; 23. short x, y; 24. int sub; 25. datetime dt; 26. double price; 27. 28. switch (id) 29. { 30. case CHARTEVENT_KEYDOWN: 31. break; 32. case CHARTEVENT_MOUSE_MOVE: 33. x = (short)lparam; 34. y = (short)dparam; 35. ChartXYToTimePrice(0, x, y, sub, dt, price); 36. Comment(sz); 37. sz += "Mouse position X: " + (string)x; 38. sz += "\nMouse position Y: " + (string)y; 39. sz += "\nMouse position time: " + TimeToString(dt, TIME_DATE | TIME_MINUTES); 40. sz += "\nMouse position price: " + (string)price; 41. Comment(sz); 42. if ((uchar)sparam != 0) PrintFormat("Hexadecimal mask of mouse buttons is: 0x%02X", (uchar)sparam); 43. break; 44. case CHARTEVENT_MOUSE_WHEEL: 45. break; 46. } 47. }; 48. //+------------------------------------------------------------------+ 49. void OnDeinit(const int reason) 50. { 51. Comment(""); 52. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 53. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 54. }; 55. //+------------------------------------------------------------------+
コード02
以下は、本当に必要な機能だけを有効または無効にするための、よりシンプルなコードです。それ以外の動作については、すべてMetaTrader 5の標準動作のままとなります。マウスイベントハンドラに対応する部分に、小さな変更を加えている点に注目してください。また、35行目には、値を実際に必要な、あるいは使用したい形式へ変換するためのライブラリ関数が記述されています。これらの値をOBJ_VLINEおよびOBJ_HLINEオブジェクトの制御に直接使用する前に、この点を理解しておくことが重要です。これらの点を示すために、次のアニメーションをご覧ください。

アニメーション01
アニメーション01では、チャートが日足で表示されていることが分かります。しかし、アニメーション内に表示されている時刻の値を見ると、それらは日中(イントラデイ)のデータと考えられる値になっています。そのため、「日足チャートを見ているのに、なぜイントラデイの値が表示されるのか」と疑問に思い、混乱する方もいるかもしれません。
実際には、これらの値が表示されるのは、ChartXYToTimePrice関数が、期待する形式へ値を変換しているのではなく、画面座標を時間と価格の座標へ変換しているためです。そして、軸方向のわずかな変化によって時間には端数が生じるため、その結果として日中の時刻を表す値になります。そのため、一見すると意味のないように思える値が表示されるのです。したがって、これはエラーではなく、画面座標を時間・価格座標へ変換した結果として生じる自然な挙動です。
いずれにしても、ここまで見てきた内容を踏まえ、少し想像力と常識を働かせれば、OBJ_HLINEとOBJ_VLINEオブジェクトをどのように制御すればよいかを考えるために必要なものは、すでにすべて揃っていることが分かるでしょう。これは「初級から中級まで:オブジェクト(II)」で示した方法とは異なります。あちらではキーボードを使って制御していましたが、今回はマウスを使用して制御します。
さて、それではどのように実現するのでしょうか。ここからが最も興味深い部分です。まず最初に、チャート上に常に表示され続ける十字カーソルを作成します。そのためのコードを以下に示します。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. #define macro_NameObject def_Prefix + (string)ObjectsTotal(0) 07. //+------------------------------------------------------------------+ 08. string gl_Objs[2]; 09. //+------------------------------------------------------------------+ 10. int OnInit() 11. { 12. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 13. ObjectCreate(0, gl_Objs[0] = macro_NameObject, OBJ_VLINE, 0, 0, 0); 14. ObjectCreate(0, gl_Objs[1] = macro_NameObject, OBJ_HLINE, 0, 0, 0); 15. ObjectSetString(0, gl_Objs[0], OBJPROP_TOOLTIP, "\n"); 16. ObjectSetString(0, gl_Objs[1], OBJPROP_TOOLTIP, "\n"); 17. ObjectSetInteger(0, gl_Objs[0], OBJPROP_COLOR, clrBlue); 18. ObjectSetInteger(0, gl_Objs[1], OBJPROP_COLOR, clrBlue); 19. 20. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 21. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 22. 23. return INIT_SUCCEEDED; 24. }; 25. //+------------------------------------------------------------------+ 26. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 27. { 28. return rates_total; 29. }; 30. //+------------------------------------------------------------------+ 31. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 32. { 33. int sub; 34. datetime dt; 35. double price; 36. 37. switch (id) 38. { 39. case CHARTEVENT_KEYDOWN: 40. break; 41. case CHARTEVENT_MOUSE_MOVE: 42. ChartXYToTimePrice(0, (short)lparam, (short)dparam, sub, dt, price); 43. ObjectMove(0, gl_Objs[0], 0, dt, price); 44. ObjectMove(0, gl_Objs[1], 0, dt, price); 45. if ((uchar)sparam != 0) PrintFormat("Hexadecimal mask of mouse buttons is: 0x%02X", (uchar)sparam); 46. break; 47. case CHARTEVENT_MOUSE_WHEEL: 48. break; 49. } 50. ChartRedraw(); 51. }; 52. //+------------------------------------------------------------------+ 53. void OnDeinit(const int reason) 54. { 55. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 56. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 57. 58. ObjectsDeleteAll(0, def_Prefix); 59. ChartRedraw(); 60. }; 61. //+------------------------------------------------------------------+
コード03
コード03をチャートに適用すると、次のような結果になります。

アニメーション02
アニメーション02の冒頭では、垂直線は変化していないにもかかわらず、時刻の値が変化していることに注目してください。これはマウスがわずかに動いているためであり、ごく正常で問題のない動作です。マウスポインタが移動すると、それに合わせて垂直線と水平線も移動し、チャート上に十字カーソルが常に表示されることが分かります。
ここでおこなっていることの一部については、すでに「初級から中級まで:オブジェクト(II)」で説明しています。そのため、ここでは新しい部分だけに注目します。今回新しい点は、移動が、ライブラリ関数によって画面座標が時間と価格の座標へ変換された際に返される値に直接関連付けられていることです。
しかし、これは私たちが本当に実現したかったものではありません。私たちが実現したかったのは、中央ボタンを押した後にのみ同じ十字カーソルを表示することでした。そのためには、コードにいくつか変更を加える必要があります。その前に、15行目、16行目、および58行目がなぜ必要なのかを理解しておきましょう。まず、15行目と16行目は、オブジェクトの上にマウスポインタがあるときに、MetaTrader 5が小さなメッセージを表示するのを防ぐためのものです。オブジェクト自体を説明するメッセージを用意したい一方で、そそのメッセージをチャートに出しっぱなしにしたくない場合、15行目と16行目の手法を使って意味のあるメッセージを設定してください。すると、マウスカーソルがオブジェクトの上でしばらく静止したときに、そのメッセージが表示されます。
一方、58行目では、指定したプレフィックスに一致する名前を持つチャート上のすべてのオブジェクトが削除されます。オブジェクトをインジケータ自身のショートネームに関連付けているため、インジケータに関連するオブジェクトだけを簡単に削除できます。オブジェクトをチャート上に残したい場合は、インジケータ名とは異なるプレフィックスを使用してください。それでは、次のコードを見ていきましょう。
001. //+------------------------------------------------------------------+ 002. #property copyright "Daniel Jose" 003. //+------------------------------------------------------------------+ 004. #define def_Prefix "Demo" 005. //+------------------------------------------------------------------+ 006. #define macro_NameObject def_Prefix + (string)ObjectsTotal(0) 007. //+------------------------------------------------------------------+ 008. enum eBtnMouse { 009. MOUSE_LEFT = 0x01, 010. MOUSE_RIGHT = 0x02, 011. MOUSE_KEY_SHIFT = 0x04, 012. MOUSE_KEY_CTRL = 0x08, 013. MOUSE_MIDDLE = 0x10, 014. MOUSE_EXTRA_1 = 0x20, 015. MOUSE_EXTRA_2 = 0x40 016. }; 017. //+------------------------------------------------------------------+ 018. struct st_Cross 019. { 020. private : 021. //+----------------+ 022. bool IsView; 023. string Objs[2]; 024. //+----------------+ 025. void Create(char index, ENUM_OBJECT type) 026. { 027. ObjectCreate(0, Objs[index] = macro_NameObject, type, 0, 0, 0); 028. ObjectSetString(0, Objs[index], OBJPROP_TOOLTIP, "\n"); 029. ObjectSetInteger(0, Objs[index], OBJPROP_COLOR, clrBlue); 030. } 031. //+----------------+ 032. public : 033. //+----------------+ 034. void Show(void) 035. { 036. if (IsView) return; 037. Create(0, OBJ_VLINE); 038. Create(1, OBJ_HLINE); 039. IsView = true; 040. ChartRedraw(); 041. } 042. //+----------------+ 043. void Hide(void) 044. { 045. if (!IsView) return; 046. for(uint c = 0; c < Objs.Size(); c++) 047. ObjectDelete(0, Objs[c]); 048. ChartRedraw(); 049. IsView = false; 050. } 051. //+----------------+ 052. void Move(short x, short y) 053. { 054. int sub; 055. datetime dt; 056. double price; 057. 058. if (!IsView) return; 059. ChartXYToTimePrice(0, x, y, sub, dt, price); 060. ObjectMove(0, Objs[0], 0, dt, price); 061. ObjectMove(0, Objs[1], 0, dt, price); 062. ChartRedraw(); 063. } 064. //+----------------+ 065. }gl_Cross; 066. //+------------------------------------------------------------------+ 067. int OnInit() 068. { 069. gl_Cross.Hide(); 070. 071. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 072. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 073. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 074. 075. return INIT_SUCCEEDED; 076. }; 077. //+------------------------------------------------------------------+ 078. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 079. { 080. return rates_total; 081. }; 082. //+------------------------------------------------------------------+ 083. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 084. { 085. 086. switch (id) 087. { 088. case CHARTEVENT_KEYDOWN: 089. break; 090. case CHARTEVENT_MOUSE_MOVE: 091. if (((uchar)sparam & MOUSE_MIDDLE) != 0) gl_Cross.Show(); 092. gl_Cross.Move((ushort)lparam, (ushort)dparam); 093. break; 094. case CHARTEVENT_MOUSE_WHEEL: 095. break; 096. } 097. ChartRedraw(); 098. }; 099. //+------------------------------------------------------------------+ 100. void OnDeinit(const int reason) 101. { 102. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 103. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 104. 105. gl_Cross.Hide(); 106. }; 107. //+------------------------------------------------------------------+
コード04
さて、このコード04は何をしているのでしょうか。最終的な動作は、次のアニメーションで示されているとおりです。

アニメーション03
「なるほど。でも、構造化コードを使うように切り替えていますね。なぜですか。」その理由は、読者の皆さん、構造化コードのほうが十字カーソルそのものを作成・保守しやすいということです。構造体の目的は、特定の目的に直接関係する一連の関数やプロシージャをまとめることにあります。
「初級から中級まで:構造(VII)」では、コードを構造化する方法を説明しましたが、その内容はすべて抽象的な形で示していました。しかし、今回のように、より具体的な観点から見てみると、すべてが意味を持ち始めます。
このコード04は、構造化プログラミングとオブジェクト指向的な書き方のちょうど境目にあると言えます。その理由は、コード04では、構造体そのものを使用する前に、特定のメンバーを初期化しなければならないことが明らかだということです。そして、これこそがOOPが生まれた理由の一つでもあります。次の点に注目してください。65行目でgl_Cross変数を使用するためにst_Cross構造体を宣言した時点では、IsViewやObjs配列にどのような値が入っているのか確信することはできません。このことが、特に特定の種類のデータを扱うことを目的としたコードでは、ある種のコードを作成する際の難しさにつながります。
しかし、ここでは状況は比較的単純であるため、69行目で構造体を準備して、その後で使用を開始できます。これは最善の方法ではありませんが、すでに述べたように、ここで扱っている内容は比較的単純です。そのため、91行目で中央ボタンが押されているかどうかを確認した際に、解析用のシンプルな十字カーソルを構成するオブジェクトを表示するかどうかを選択できます。これは、st_Cross構造体のShow関数を呼び出すことで実現しています。
いずれにしても、92行目では、オブジェクトの位置をマウスポインターに追従するよう調整しています。しかし、オブジェクトがチャート上に存在しない場合は、IsViewの値はfalseになります。その結果、58行目で処理は停止し、直ちに呼び出し元へ制御が返されます。この場合の呼び出し元は92行目です。この状態は中央ボタンが押されるまで続き、中央ボタンが押されると十字カーソルが作成されます。その時点からは、マウスが動くたびに更新されます。これは、IsViewがtrueになるため、58行目の条件が成立しなくなり、92行目で位置が更新されるようになるからです。
では、一度表示された十字カーソルをどのように消せばよいのでしょうか。これは、十字カーソルをどのような目的で使用し、どのように動作させるかによって異なります。しかし、ほとんど手間をかけず、多くのコードを追加することなく、その動作を完全に実装するまでの間、キー操作によってチャートから十字カーソルを非表示にする機能を追加できます。そのために、以下のコード断片に示すように、イベント処理コードを変更し、この方法でチャートから十字カーソルを削除できるようにしました。
. . . 082. //+------------------------------------------------------------------+ 083. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 084. { 085. 086. switch (id) 087. { 088. case CHARTEVENT_KEYDOWN: 089. if (TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) gl_Cross.Hide(); 090. break; 091. case CHARTEVENT_MOUSE_MOVE: 092. if (((uchar)sparam & MOUSE_MIDDLE) != 0) gl_Cross.Show(); 093. gl_Cross.Move((ushort)lparam, (ushort)dparam); 094. break; 095. case CHARTEVENT_MOUSE_WHEEL: 096. break; 097. } 098. ChartRedraw(); 099. }; 100. //+------------------------------------------------------------------+ . . .
コード05
89行目でおこなったこの単純な変更により、次のアニメーションで示すような動作を実現できます。

アニメーション04
これで、解析用の十字カーソルを必要に応じて表示したり非表示にしたりできるようになったことが分かります。しかし、ここで、アニメーション04には重要な問題が一つあります。それは、銘柄のティックサイズ、つまり価格が何ポイント単位で変動するかという点です。多くの場合とは異なり、この先物契約、ここではドル先物契約ですが、価格は1単位ではなく0.5単位で変動します。これを確認するには、契約仕様を見れば分かります。

図02
では、図02は何を示しているのでしょうか。ここには、銘柄の価格がどれだけの値幅で変動するかが示されています。ここで私が強調したいのは、まさにこの情報です。この情報は、価格ラインが常に市場で有効な価格を反映するような適切な方法を実装するために重要です。実際には決して使用されない価格を価格ラインに表示しても意味がありません。なぜなら、そのような価格は、例えば注文を送信する際には使用できないからです。
この問題を解決するには、もう一度コードを変更する必要があります。ただし、今回はイベントハンドラではなく、以下に示すようにst_Cross構造体を変更します。
. . . 017. //+------------------------------------------------------------------+ 018. struct st_Cross 019. { 020. private : 021. //+----------------+ 022. bool IsView; 023. string Objs[2]; 024. int nDigits; 025. double PointPerTick; 026. //+----------------+ 027. void Create(char index, ENUM_OBJECT type) 028. { 029. ObjectCreate(0, Objs[index] = macro_NameObject, type, 0, 0, 0); 030. ObjectSetString(0, Objs[index], OBJPROP_TOOLTIP, "\n"); 031. ObjectSetInteger(0, Objs[index], OBJPROP_COLOR, clrBlue); 032. } 033. //+----------------+ 034. public : 035. //+----------------+ 036. void Init(void) 037. { 038. nDigits = (int) SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); 039. PointPerTick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); 040. Hide(); 041. } 042. //+----------------+ . . . 060. //+----------------+ 061. void Move(short x, short y) 062. { 063. int sub; 064. datetime dt; 065. double price; 066. 067. if (!IsView) return; 068. ChartXYToTimePrice(0, x, y, sub, dt, price); 069. price = NormalizeDouble(MathRound(price / PointPerTick) * PointPerTick, nDigits); 070. ObjectMove(0, Objs[0], 0, dt, price); 071. ObjectMove(0, Objs[1], 0, dt, price); 072. ChartRedraw(); 073. } 074. //+----------------+ 075. }gl_Cross; 076. //+------------------------------------------------------------------+ 077. int OnInit() 078. { 079. gl_Cross.Init(); 080. 081. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 082. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 083. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 084. 085. return INIT_SUCCEEDED; 086. }; 087. //+------------------------------------------------------------------+ . . .
コード06
コード断片06では、非常にシンプルな変更をおこなっていることに注目してください。まず、構造体に新しい変数を追加しました。これにより、同じ情報を何度も読み取る必要がなくなります。その直後に、新しい手順Initを追加しました。これは36行目でおこなわれており、ここで構造体をより適切な方法で初期化しています。そのため、OnInitメソッドでは、以前Hideプロシージャを呼び出していた部分を変更し、79行目に示されているようにInitへ呼び出し先を変更しました。
そして、価格ラインのこのずれを修正するために、69行目を使用して価格を適切に調整します。その結果、最終的には次のアニメーションに示されている状態になります。

アニメーション05
完璧です。非常にシンプルなコードだけで、基本的でありながら完全にカスタマイズ可能なインジケータを作成することができました。ここで一つ疑問が生じます。それは、このインジケータを使用して、どのような種類の分析を実行できるのかということです。なぜなら、ここでは特定の種類の分析に焦点を当てていないからです。さて、読者の皆さん、この質問に対する単純な答えはありません。その理由は、このインジケータを使えば、どのような種類の分析でも実行できるということです。すべては、私たちが本当に何を達成したいのか、そして後に作成したものをどのように利用する予定なのかによって決まります。
覚えておいてください。これらの記事の内容は、教育を目的としたものです。そして、ここで取り上げた内容のどの部分も、私たちが達成しようとしているあらかじめ決められた目的に直接対応しているわけではありません。そのため、一つの実装方法や別の実装方法に固執しないでください。重要なのは、概念を理解しようとすることです。そして、一度その概念を理解すれば、あとは自分が望むもの、または必要とするものを実装するだけです。
では、このインジケータを、実際に役立つもの、非常に興味深く、そして魅力的なものへ変えるにはどうすればよいでしょうか。次のようにしましょう。st_Cross構造体をヘッダーファイルへ移動します。これにより、さまざまなインジケータを最小限の労力で実装できるようになります。さらに、後でクラスについて説明する段階になったときに、この構造体をクラスへ発展させられるよう準備しておくこともできます。これで、ファイルは2つになります。そのうちの一つを以下に示します。これはヘッダーファイルです。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. enum eBtnMouse { 05. MOUSE_LEFT = 0x01, 06. MOUSE_RIGHT = 0x02, 07. MOUSE_KEY_SHIFT = 0x04, 08. MOUSE_KEY_CTRL = 0x08, 09. MOUSE_MIDDLE = 0x10, 10. MOUSE_EXTRA_1 = 0x20, 11. MOUSE_EXTRA_2 = 0x40 12. }; 13. //+------------------------------------------------------------------+ 14. struct st_TimePrice 15. { 16. double Price; 17. datetime Time; 18. }; 19. //+------------------------------------------------------------------+ 20. struct st_Cross 21. { 22. private : 23. //+----------------+ 24. bool IsView; 25. string Objs[2]; 26. int nDigits; 27. double PointPerTick; 28. //+----------------+ 29. void Create(char index, ENUM_OBJECT type) 30. { 31. ObjectCreate(0, Objs[index] = macro_NameObject, type, 0, 0, 0); 32. ObjectSetString(0, Objs[index], OBJPROP_TOOLTIP, "\n"); 33. ObjectSetInteger(0, Objs[index], OBJPROP_COLOR, clrBlue); 34. } 35. //+----------------+ 36. public : 37. //+----------------+ 38. void Init(void) 39. { 40. nDigits = (int) SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); 41. PointPerTick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); 42. Hide(); 43. } 44. //+----------------+ 45. void Show(void) 46. { 47. if (IsView) return; 48. Create(0, OBJ_VLINE); 49. Create(1, OBJ_HLINE); 50. IsView = true; 51. ChartRedraw(); 52. } 53. //+----------------+ 54. void Hide(void) 55. { 56. if (!IsView) return; 57. for(uint c = 0; c < Objs.Size(); c++) 58. ObjectDelete(0, Objs[c]); 59. ChartRedraw(); 60. IsView = false; 61. } 62. //+----------------+ 63. st_TimePrice Move(short x, short y) 64. { 65. int sub; 66. st_TimePrice tp; 67. 68. ChartXYToTimePrice(0, x, y, sub, tp.Time, tp.Price); 69. tp.Price = NormalizeDouble(MathRound(tp.Price / PointPerTick) * PointPerTick, nDigits); 70. if (IsView) 71. { 72. ObjectMove(0, Objs[0], 0, tp.Time, tp.Price); 73. ObjectMove(0, Objs[1], 0, tp.Time, tp.Price); 74. ChartRedraw(); 75. } 76. 77. return tp; 78. } 79. //+----------------+ 80. }; 81. //+------------------------------------------------------------------+
コード07
そして、これが私たちが実装したいインジケータに対応します。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. #define macro_NameObject def_Prefix + (string)ObjectsTotal(0) 07. //+------------------------------------------------------------------+ 08. #include <Tutorial\File 01.mqh> 09. //+------------------------------------------------------------------+ 10. st_Cross gl_Cross; 11. //+------------------------------------------------------------------+ 12. int OnInit() 13. { 14. gl_Cross.Init(); 15. 16. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 17. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 18. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 19. 20. return INIT_SUCCEEDED; 21. }; 22. //+------------------------------------------------------------------+ 23. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 24. { 25. return rates_total; 26. }; 27. //+------------------------------------------------------------------+ 28. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 29. { 30. 31. switch (id) 32. { 33. case CHARTEVENT_KEYDOWN: 34. if (TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) gl_Cross.Hide(); 35. break; 36. case CHARTEVENT_MOUSE_MOVE: 37. if (((uchar)sparam & MOUSE_MIDDLE) != 0) gl_Cross.Show(); 38. gl_Cross.Move((ushort)lparam, (ushort)dparam); 39. break; 40. case CHARTEVENT_MOUSE_WHEEL: 41. break; 42. } 43. ChartRedraw(); 44. }; 45. //+------------------------------------------------------------------+ 46. void OnDeinit(const int reason) 47. { 48. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 49. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 50. 51. gl_Cross.Hide(); 52. }; 53. //+------------------------------------------------------------------+
コード08
コード07では、新しい構造体を追加したことに注目してください。この構造体の目的は、マウスの移動と位置に基づいて現在の値を返すことです。そしてコード08では、ヘッダーファイルが定義部分の後に追加されている点に注目してください。これは、ファイルをコンパイルできるようにするために重要です。これにより、私たちはこれからかなり長い時間取り組むことのできる基盤を手に入れました。
それでは、比較的シンプルで軽量なインジケータから始めましょう。ここで「低コスト」と表現しているのは、実装に多くの労力を必要とせず、少しの注意だけで作成できるという意味です。ただし、内容を適切に分けるために、次の新しいテーマへ進みましょう。
トレンドラインインジケータ
最もシンプルで、手軽に実装できる、負担の少ないインジケータの一つがトレンドラインです。次の例を考えてみましょう。MetaTrader 5のメニューからトレンドラインオブジェクトを選択することなく、トレンドラインを追加できるインジケータを作成したいとします。そのため、この作業を効率化するためのインジケータを作成することにします。オブジェクトを使用するあらゆる種類のインジケータを作成するための信頼できるツールであるコード08を基にして、トレンドラインを使用するインジケータを作成するために、コードへ小さな変更を加えていきます。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. #define macro_NameObject def_Prefix + (string)ObjectsTotal(0) 07. //+------------------------------------------------------------------+ 08. #include <Tutorial\File 01.mqh> 09. //+------------------------------------------------------------------+ 10. st_Cross gl_Cross; 11. //+------------------------------------------------------------------+ 12. int OnInit() 13. { 14. gl_Cross.Init(); 15. 16. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 17. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 18. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 19. 20. return INIT_SUCCEEDED; 21. }; 22. //+------------------------------------------------------------------+ 23. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 24. { 25. return rates_total; 26. }; 27. //+------------------------------------------------------------------+ 28. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 29. { 30. st_TimePrice tp; 31. static string isPaint = ""; 32. 33. switch (id) 34. { 35. case CHARTEVENT_KEYDOWN: 36. if (TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) gl_Cross.Hide(); 37. break; 38. case CHARTEVENT_MOUSE_MOVE: 39. if (((uchar)sparam & MOUSE_MIDDLE) != 0) 40. { 41. gl_Cross.Show(); 42. tp = gl_Cross.Move((ushort)lparam, (ushort)dparam); 43. if (isPaint == "") 44. { 45. ObjectCreate(0, isPaint = macro_NameObject, OBJ_TREND, 0, tp.Time, tp.Price); 46. ObjectSetInteger(0, isPaint, OBJPROP_SELECTABLE, true); 47. ObjectSetInteger(0, isPaint, OBJPROP_COLOR, clrMagenta); 48. ObjectSetInteger(0, isPaint, OBJPROP_WIDTH, 3); 49. ObjectSetInteger(0, isPaint, OBJPROP_RAY_RIGHT, true); 50. } 51. ObjectMove(0, isPaint, 1, tp.Time, tp.Price); 52. }else 53. { 54. isPaint = ""; 55. gl_Cross.Hide(); 56. } 57. break; 58. case CHARTEVENT_MOUSE_WHEEL: 59. break; 60. } 61. ChartRedraw(); 62. }; 63. //+------------------------------------------------------------------+ 64. void OnDeinit(const int reason) 65. { 66. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 67. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 68. 69. gl_Cross.Hide(); 70. 71. if (reason == REASON_REMOVE) 72. ObjectsDeleteAll(0, def_Prefix); 73. }; 74. //+------------------------------------------------------------------+
コード09
コード09は、多くの初心者が知らないあることを説明するのに役立ちます。
プログラミングでは、要素を組み合わせる順番によって結果が変わります。
この単純な言葉は、すべてのプログラマーが日々下す多くの判断には影響しないように思えるかもしれません。しかし、この一見単純な言葉こそ、多くの初心者がまだ実感していない重要な事実なのです。まさにそのため、コード09をチャート上で実行すると、以下に示す結果が得られます。

アニメーション06
つまり、私たちのアイデアは機能しています。しかし、ここで次のように考えるかもしれません。「何らかの理由でトレンドラインの描画方法を変更したい場合はどうすればよいのか。どのように変更できるのか。」これは最も簡単な部分です、読者の皆さん。通常、オブジェクトを選択するには、そのオブジェクトをダブルクリックする必要があります。しかし、次の図に示されているパラメータを変更すると、オブジェクトを選択するために必要なのは1回のクリックだけになります。

図03
ただし、この設定が適用されるのは選択可能なオブジェクトだけであることを覚えておいてください。この点については、後ほど詳しく説明します。現時点では、MetaTrader 5ではすべてのオブジェクトが初期状態で選択可能になっています。さて、図03で示されているオプションを使用すると、次のようなことができます。

アニメーション07
つまり、あるコードを別のコードから作成できたことに、すぐに達成感を覚えることができます。しかし、それでもここには欠点があります。以下のアニメーションに示すように、新しいトレンドラインを作成しようとすると、何が起こるか見てみましょう。

アニメーション08
「何だか、もう何も理解できません。なんて不思議なことでしょう。なぜ以前のトレンドラインが削除されたのでしょうか。そして、新しいトレンドラインを作成しようとしたとき、交差しているラインのうち一つが表示されなかったのはなぜでしょうか。これは非常に奇妙な状況です。アニメーション06を見る限り、コードは完全に動作しているように見えたので、このようなことが起こるとは思いませんでした。」さて、読者の皆さん、問題はオブジェクトが間違った順序で作成されていることです。奇妙に聞こえるかもしれません。しかし、そうです。アニメーション08で確認できる現象は、まさにオブジェクトを間違った順序で作成した結果なのです。
コード09を見ると、最初に十字カーソルを作成していることが分かります。これは41行目でおこなわれています。その直後、45行目でトレンドラインを作成しています。この作成順序であれば、もちろん新しいオブジェクトを作成しようとしない限り、問題は発生しません。しかし、そのような操作をおこなうと、アニメーション08で見られるように問題が発生します。実際には、二つの問題が発生します。最初の問題は、コード09の6行目にあるマクロが、チャート上にすでに存在しているオブジェクトを確認していないことです。そのため、同じプレフィックスを持つオブジェクトが一覧に含まれている場合、ある時点で、すでにチャート上に存在しているオブジェクトに遭遇することになります。
しかし、それはどのようにして可能なのでしょうか。では、最初の時点ではチャート上にオブジェクトが存在しないと仮定しましょう。十字カーソルを作成した後、2つのオブジェクトが存在します。一つは「Demo0」、もう一つは「Demo1」という名前になります。ここまでは問題ありません。コード09の45行目でトレンドラインを作成すると、3つ目のオブジェクト「Demo2」が追加されます。
これで、アニメーション06に示されている線に対応する地点に到達しました。新しいトレンドラインを作成しようとすると、十字カーソルは「Demo1」と「Demo2」というオブジェクトを作成しようとします。しかし、少し待ってください。チャート上にはすでに「Demo2」という名前のオブジェクトが存在しています。それは以前に作成したトレンドラインです。そうです、読者の皆さん。これが、十字カーソルのラインのうち一つが作成されない理由です。そして、その結果がアニメーション08の冒頭で確認できる状態になります。しかし、十字カーソルを削除すると、「Demo1」と「Demo2」の両方が消え、それと同時に以前のトレンドラインも削除されます。そしてその時点では、チャート上にはオブジェクトが一つも存在しなくなり、トレンドラインを作成する最初の状態へ戻ることになります。
ここで注意すべき点は、最初の問題はチャート上に存在するオブジェクト数の数え方によって発生したということです。しかし、2つ目の問題は、オブジェクトが作成される順序に関係しています。したがって、両方の問題を解決するには、コード09を変更し、次のような形にする必要があります。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. #define def_Prefix "Demo" 05. //+------------------------------------------------------------------+ 06. #define macro_NameObject def_Prefix + (string)(ObjectsTotal(0) + 1) 07. //+------------------------------------------------------------------+ 08. #include <Tutorial\File 01.mqh> 09. //+------------------------------------------------------------------+ 10. st_Cross gl_Cross; 11. //+------------------------------------------------------------------+ 12. int OnInit() 13. { 14. gl_Cross.Init(); 15. 16. IndicatorSetString(INDICATOR_SHORTNAME, def_Prefix); 17. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); 18. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, false); 19. 20. return INIT_SUCCEEDED; 21. }; 22. //+------------------------------------------------------------------+ 23. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 24. { 25. return rates_total; 26. }; 27. //+------------------------------------------------------------------+ 28. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 29. { 30. st_TimePrice tp; 31. static string isPaint = ""; 32. 33. switch (id) 34. { 35. case CHARTEVENT_KEYDOWN: 36. if (TerminalInfoInteger(TERMINAL_KEYSTATE_ESCAPE)) gl_Cross.Hide(); 37. break; 38. case CHARTEVENT_MOUSE_MOVE: 39. if (((uchar)sparam & MOUSE_MIDDLE) != 0) 40. { 41. tp = gl_Cross.Move((ushort)lparam, (ushort)dparam); 42. if (isPaint == "") 43. { 44. ObjectCreate(0, isPaint = macro_NameObject, OBJ_TREND, 0, tp.Time, tp.Price); 45. ObjectSetInteger(0, isPaint, OBJPROP_SELECTABLE, true); 46. ObjectSetInteger(0, isPaint, OBJPROP_COLOR, clrMagenta); 47. ObjectSetInteger(0, isPaint, OBJPROP_WIDTH, 3); 48. ObjectSetInteger(0, isPaint, OBJPROP_RAY_RIGHT, true); 49. } 50. ObjectMove(0, isPaint, 1, tp.Time, tp.Price); 51. gl_Cross.Show(); 52. }else 53. { 54. isPaint = ""; 55. gl_Cross.Hide(); 56. } 57. break; 58. case CHARTEVENT_MOUSE_WHEEL: 59. break; 60. } 61. ChartRedraw(); 62. }; 63. //+------------------------------------------------------------------+ 64. void OnDeinit(const int reason) 65. { 66. ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false); 67. ChartSetInteger(0, CHART_CROSSHAIR_TOOL, true); 68. 69. gl_Cross.Hide(); 70. 71. if (reason == REASON_REMOVE) 72. ObjectsDeleteAll(0, def_Prefix); 73. }; 74. //+------------------------------------------------------------------+
コード10
コード10を実行すると、次のアニメーションに示されている結果が表示されます。

アニメーション09
ここで注目してほしいのは、これで必要な数だけトレンドラインを追加できるようになったという点です。そして、これは何の問題もなく実行できます。なぜなら、コード10の6行目によって名前の競合問題が解決されているからです。また、チャート上に配置されるトレンドラインが作成された後にのみ、十字カーソルが表示されます。これは51行目で確認できます。ここで、実際に解析用のシンプルな十字カーソルを作成しています。つまり、実行順序を単純に変更することで、目的のオブジェクトをチャート上に残すことを妨げていた2つ目の問題を解決できたということです。
最後に
本記事では、多くの人が難しいと考えている、あるいは少なくとも実用的な実装方法を見つけられないと考えていることを、実際にどのように実装するかについて見てきました。つまり、インジケータ内でオブジェクトを直接操作し、それらをチャート上に配置し、その後利用する方法について説明しました。
確かに、ここでは比較的シンプルなオブジェクト、つまりトレンドラインを使用して説明しました。しかし、それだけを理由に、ここで紹介したアプリケーションが役に立たないと考えないでください。ここで示した内容を利用して、さまざまなスキルを練習しながら、読者の皆さん自身にとって本当に役立つアプリケーションを開発するためのトレーニングや実践に活用できます。ぜひ、この内容を研究し、この記事で示したものにどのような改善を加えられるか考えてみてください。
忘れないでください。私たちはまだクラスを扱っていません。そのため、この課題はさらに興味深いものになります。解決策をどのように実装するかによって、実装方法の一部には改善できる点があることに気付き始めるでしょう。こうした点が、まだこの短い連載では紹介していない新しいプログラミングスタイルへと私たちを導いていきます。しかし、そのスタイルはまもなく私たちのレパートリーに加わることになります。
それでは、添付ファイル内のコードを楽しんでください。そして次回の記事でお会いしましょう。次回は、チャート上のオブジェクトに対しておこなえる別の操作について見ていきます。
| MQ5ファイル | 詳細 |
|---|---|
| コード01 | オブジェクトの動作例 |
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/16021
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
初級から中級まで:オブジェクト(IV)
初級から中級まで:マウスイベントの処理
時間帯別・曜日別・月の日付別の季節性インジケータ
ニューラルネットワークの実践:習うより慣れよ
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索