リプレイシステムの開発(第55回):コントロールモジュール
はじめに
前回の「リプレイシステムの開発(第54回):最初のモジュールの誕生」では、新しいリプレイ/シミュレーターシステムにおける最初の実用的なモジュールを組み立てました。開発中のシステムで活用できるだけでなく、こうしたシステムの構築に伴う膨大なプログラミング作業を回避するために、モジュールを個別に適用し、カスタマイズすることも可能です。いずれにせよ、一度モジュールを構築すれば、再コンパイルすることなく簡単にカスタマイズできるようになります。
これを実現するには、モジュールにメッセージを送信し、外観や動作を変更するだけで済みます。この処理は、シンプルなスクリプトを用いることで容易に実行できます。
前回の記事の内容を踏まえると、私たちは実際の口座とデモ口座の両方で使用可能なシステムを構築する機会を得ています。しかし、それだけではありません。リアル口座やデモ口座での挙動に極めて近い動作をするリプレイ/シミュレーターシステムを構築することも可能です。
とはいえ、この新しいモデルを導入する最大の利点は、リプレイ/シミュレーターシステムとMetaTrader5での日常業務の両方において、同じツールやアプリケーションを使用できることです。これにより、デモ口座での訓練やリアル口座での取引に一貫した環境を提供できます。
さて、マウスインジケーターの準備が整ったので、次にコントロールインジケーターを作成し、モジュール方式で動作するように調整していきます。この点について、簡単に説明を加えておきます。
これまで、リプレイ/シミュレーターシステムではグローバルターミナル変数を使用し、必要なプログラム間で通信を行い、リプレイ/シミュレーターサービスとの対話、制御、アクセスを可能にしていました。
しかし、新たにユーザーイベントを用いたメッセージングによるモジュールシステムを導入したため、もはやグローバルターミナル変数を使用する必要はなくなりました。これにより、従来使用していたすべてのグローバルターミナル変数を削除できます。ただし、この変更に伴い、プログラム間での情報の流れを維持するためにシステムを適応させる必要があります。
情報伝達システムの設計は、慎重を期すべき作業です。なぜなら、一度送信された情報を後から読み取る手段がないためです。もしカスタムイベント経由で情報を受信した際に、対象のプログラムやアプリケーションがチャート上に存在しなければ、その情報は失われてしまいます。そのため、情報が確実に対象のアプリケーションやプログラムに受信されたことを確認できるまで、同じ情報を再送信する仕組みが必要になります。
この方針に基づき、リプレイ/シミュレーターシステムには、最低限の機能を実現するために必要な3つのコアプログラムを含めることにしました。これらのうち、ユーザーが直接目にするのは「サービス本体を管理するプログラム」と「マウスインジケーター」の2つのみです。一方、コントロールインジケーターはサービスプログラムの一部として扱われ、サービスを提供する目的以外では使用できません。
以上を踏まえた上で、次にコントロールインジケーターの変更点を見ていきましょう。ここでは、リプレイ/シミュレーターサービスの管理を担うように改良された点について説明します。
コントロールインジケーターの変更
コントロールインジケーターの変更点はそれほど多くありません。というのも、前の段階ですでにグローバルターミナル変数の削除を進めていたためです。ただし、メッセージ交換の仕組みを正しく理解しなければ、システムがどのようにタスクを実行するのかを把握することはできません。
そこで、今後の記事を読む際に混乱しないよう、最初の段階からすべてを明確にしておきましょう。
インジケーターをチャートに配置すると、ユーザーは複数のパラメータを設定できます。これらのパラメータは、インジケーターの入力変数として機能します。しかし、状況によっては、これらの変数が便利であるどころか、むしろ邪魔になることもあります。誤解しないでください。私は根本的な仕様変更を提案しているわけではありません。しかし、ユーザーがインジケーターの設定を事前に調整できるようにすると(このケースでは)、思わぬ問題が発生する可能性があります。
ユーザーが誤って設定を変更してしまうリスクはあるものの、それを除けば、これらの変数は非常に有用です。例えば、チャートIDをインジケーターに渡す際にも活用されています。実際のところ、インジケーター自体はこの情報を必要としません。しかし、インジケーターがチャートに配置されたタイミングによっては、チャートのIDが期待したオブジェクトのIDと異なる可能性があることを考慮する必要があります。この点については、過去の記事でも取り上げました。
確かに、メッセージングシステムを使えば、チャートIDをインジケーターに渡すことはできます。なぜなら、チャートはサービスによって開かれ、そのサービス側ではチャートIDを把握しているからです。しかし、そのような方法を採用すると、サービスおよびインジケーターのコードが不要に複雑化してしまいます。したがって、これまでのやり方を維持することにしました。ただし、グローバルターミナル変数を使用しなくなるため、コントロールインジケーターのコードには若干の修正が必要になります。
以下に、C_Control.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_ButtonLeft def_PathBMP + "Left.bmp" 011. #define def_ButtonLeftBlock def_PathBMP + "Left_Block.bmp" 012. #define def_ButtonRight def_PathBMP + "Right.bmp" 013. #define def_ButtonRightBlock def_PathBMP + "Right_Block.bmp" 014. #define def_ButtonPin def_PathBMP + "Pin.bmp" 015. #resource "\\" + def_ButtonPlay 016. #resource "\\" + def_ButtonPause 017. #resource "\\" + def_ButtonLeft 018. #resource "\\" + def_ButtonLeftBlock 019. #resource "\\" + def_ButtonRight 020. #resource "\\" + def_ButtonRightBlock 021. #resource "\\" + def_ButtonPin 022. //+------------------------------------------------------------------+ 023. #define def_PrefixCtrlName "MarketReplayCTRL_" 024. #define def_PosXObjects 120 025. //+------------------------------------------------------------------+ 026. #define def_SizeButtons 32 027. #define def_ColorFilter 0xFF00FF 028. //+------------------------------------------------------------------+ 029. #include "..\Auxiliar\C_Terminal.mqh" 030. #include "..\Auxiliar\C_Mouse.mqh" 031. //+------------------------------------------------------------------+ 032. class C_Controls : private C_Terminal 033. { 034. protected: 035. private : 036. //+------------------------------------------------------------------+ 037. enum eObjectControl {ePlay, eLeft, eRight, ePin, eNull}; 038. //+------------------------------------------------------------------+ 039. struct st_00 040. { 041. string szBarSlider, 042. szBarSliderBlock; 043. int Minimal; 044. }m_Slider; 045. struct st_01 046. { 047. C_DrawImage *Btn; 048. bool state; 049. int x, y, w, h; 050. }m_Section[eObjectControl::eNull]; 051. C_Mouse *m_MousePtr; 052. //+------------------------------------------------------------------+ 053. inline void CreteBarSlider(int x, int size) 054. { 055. ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSlider = def_PrefixCtrlName + "B1", OBJ_RECTANGLE_LABEL, 0, 0, 0); 056. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_XDISTANCE, def_PosXObjects + x); 057. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_YDISTANCE, m_Section[ePin].y + 11); 058. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_XSIZE, size); 059. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_YSIZE, 9); 060. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BGCOLOR, clrLightSkyBlue); 061. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_COLOR, clrBlack); 062. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_WIDTH, 3); 063. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSlider, OBJPROP_BORDER_TYPE, BORDER_FLAT); 064. ObjectCreate(GetInfoTerminal().ID, m_Slider.szBarSliderBlock = def_PrefixCtrlName + "B2", OBJ_RECTANGLE_LABEL, 0, 0, 0); 065. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XDISTANCE, def_PosXObjects + x); 066. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YDISTANCE, m_Section[ePin].y + 6); 067. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_YSIZE, 19); 068. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BGCOLOR, clrRosyBrown); 069. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_BORDER_TYPE, BORDER_RAISED); 070. } 071. //+------------------------------------------------------------------+ 072. void SetPlay(bool state) 073. { 074. if (m_Section[ePlay].Btn == NULL) 075. m_Section[ePlay].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_PrefixCtrlName + EnumToString(ePlay), def_ColorFilter, "::" + def_ButtonPlay, "::" + def_ButtonPause); 076. 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) ? 0 : 1)); 077. } 078. //+------------------------------------------------------------------+ 079. void CreateCtrlSlider(void) 080. { 081. CreteBarSlider(77, 436); 082. m_Section[eLeft].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_PrefixCtrlName + EnumToString(eLeft), def_ColorFilter, "::" + def_ButtonLeft, "::" + def_ButtonLeftBlock); 083. m_Section[eRight].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_PrefixCtrlName + EnumToString(eRight), def_ColorFilter, "::" + def_ButtonRight, "::" + def_ButtonRightBlock); 084. m_Section[ePin].Btn = new C_DrawImage(GetInfoTerminal().ID, 0, def_PrefixCtrlName + EnumToString(ePin), def_ColorFilter, "::" + def_ButtonPin); 085. PositionPinSlider(m_Slider.Minimal); 086. } 087. //+------------------------------------------------------------------+ 088. inline void RemoveCtrlSlider(void) 089. { 090. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 091. for (eObjectControl c0 = ePlay + 1; c0 < eNull; c0++) 092. { 093. delete m_Section[c0].Btn; 094. m_Section[c0].Btn = NULL; 095. } 096. ObjectsDeleteAll(GetInfoTerminal().ID, def_PrefixCtrlName + "B"); 097. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 098. } 099. //+------------------------------------------------------------------+ 100. inline void PositionPinSlider(int p) 101. { 102. int iL, iR; 103. 104. m_Section[ePin].x = (p < m_Slider.Minimal ? m_Slider.Minimal : (p > def_MaxPosSlider ? def_MaxPosSlider : p)); 105. iL = (m_Section[ePin].x != m_Slider.Minimal ? 0 : 1); 106. iR = (m_Section[ePin].x < def_MaxPosSlider ? 0 : 1); 107. m_Section[ePin].x += def_PosXObjects; 108. m_Section[ePin].x += 95 - (def_SizeButtons / 2); 109. for (eObjectControl c0 = ePlay + 1; c0 < eNull; c0++) 110. 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))); 111. ObjectSetInteger(GetInfoTerminal().ID, m_Slider.szBarSliderBlock, OBJPROP_XSIZE, m_Slider.Minimal + 2); 112. } 113. //+------------------------------------------------------------------+ 114. inline eObjectControl CheckPositionMouseClick(int &x, int &y) 115. { 116. C_Mouse::st_Mouse InfoMouse; 117. 118. InfoMouse = (*m_MousePtr).GetInfoMouse(); 119. x = InfoMouse.Position.X_Graphics; 120. y = InfoMouse.Position.Y_Graphics; 121. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) 122. { 123. 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)) 124. return c0; 125. } 126. 127. return eNull; 128. } 129. //+------------------------------------------------------------------+ 130. public : 131. //+------------------------------------------------------------------+ 132. C_Controls(const long Arg0, const string szShortName, C_Mouse *MousePtr) 133. :C_Terminal(Arg0), 134. m_MousePtr(MousePtr) 135. { 136. if ((!IndicatorCheckPass(szShortName)) || (CheckPointer(m_MousePtr) == POINTER_INVALID)) SetUserError(C_Terminal::ERR_Unknown); 137. if (_LastError != ERR_SUCCESS) return; 138. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, false); 139. ObjectsDeleteAll(GetInfoTerminal().ID, def_PrefixCtrlName); 140. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); 141. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) 142. { 143. m_Section[c0].h = m_Section[c0].w = def_SizeButtons; 144. m_Section[c0].y = 25; 145. m_Section[c0].Btn = NULL; 146. } 147. m_Section[ePlay].x = def_PosXObjects; 148. m_Section[eLeft].x = m_Section[ePlay].x + 47; 149. m_Section[eRight].x = m_Section[ePlay].x + 511; 150. m_Slider.Minimal = INT_MIN; 151. } 152. //+------------------------------------------------------------------+ 153. ~C_Controls() 154. { 155. for (eObjectControl c0 = ePlay; c0 < eNull; c0++) delete m_Section[c0].Btn; 156. ObjectsDeleteAll(GetInfoTerminal().ID, def_PrefixCtrlName); 157. delete m_MousePtr; 158. } 159. //+------------------------------------------------------------------+ 160. void SetBuff(const int rates_total, double &Buff[]) 161. { 162. uCast_Double info; 163. 164. info._int[0] = m_Slider.Minimal; 165. info._int[1] = (m_Section[ePlay].state ? INT_MAX : INT_MIN); 166. Buff[rates_total - 1] = info.dValue; 167. } 168. //+------------------------------------------------------------------+ 169. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 170. { 171. int x, y; 172. static int iPinPosX = -1, six = -1, sps; 173. uCast_Double info; 174. 175. switch (id) 176. { 177. case (CHARTEVENT_CUSTOM + evCtrlReplayInit): 178. info.dValue = dparam; 179. iPinPosX = m_Slider.Minimal = info._int[0]; 180. if (info._int[1] == 0) SetUserError(C_Terminal::ERR_Unknown); else 181. { 182. SetPlay(info._int[1] == INT_MAX); 183. if (info._int[1] == INT_MIN) CreateCtrlSlider(); 184. } 185. break; 186. case CHARTEVENT_OBJECT_DELETE: 187. if (StringSubstr(sparam, 0, StringLen(def_PrefixCtrlName)) == def_PrefixCtrlName) 188. { 189. if (sparam == (def_PrefixCtrlName + EnumToString(ePlay))) 190. { 191. delete m_Section[ePlay].Btn; 192. m_Section[ePlay].Btn = NULL; 193. SetPlay(m_Section[ePlay].state); 194. }else 195. { 196. RemoveCtrlSlider(); 197. CreateCtrlSlider(); 198. } 199. } 200. break; 201. case CHARTEVENT_MOUSE_MOVE: 202. if ((*m_MousePtr).CheckClick(C_Mouse::eClickLeft)) switch (CheckPositionMouseClick(x, y)) 203. { 204. case ePlay: 205. SetPlay(!m_Section[ePlay].state); 206. if (m_Section[ePlay].state) 207. { 208. RemoveCtrlSlider(); 209. m_Slider.Minimal = iPinPosX; 210. }else CreateCtrlSlider(); 211. break; 212. case eLeft: 213. PositionPinSlider(iPinPosX = (iPinPosX > m_Slider.Minimal ? iPinPosX - 1 : m_Slider.Minimal)); 214. break; 215. case eRight: 216. PositionPinSlider(iPinPosX = (iPinPosX < def_MaxPosSlider ? iPinPosX + 1 : def_MaxPosSlider)); 217. break; 218. case ePin: 219. if (six == -1) 220. { 221. six = x; 222. sps = iPinPosX; 223. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false); 224. } 225. iPinPosX = sps + x - six; 226. PositionPinSlider(iPinPosX = (iPinPosX < m_Slider.Minimal ? m_Slider.Minimal : (iPinPosX > def_MaxPosSlider ? def_MaxPosSlider : iPinPosX))); 227. break; 228. }else if (six > 0) 229. { 230. six = -1; 231. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); 232. } 233. break; 234. } 235. ChartRedraw(GetInfoTerminal().ID); 236. } 237. //+------------------------------------------------------------------+ 238. }; 239. //+------------------------------------------------------------------+ 240. #undef def_PosXObjects 241. #undef def_ButtonPlay 242. #undef def_ButtonPause 243. #undef def_ButtonLeft 244. #undef def_ButtonRight 245. #undef def_ButtonPin 246. #undef def_PrefixCtrlName 247. #undef def_PathBMP 248. //+------------------------------------------------------------------+
C_Control.mqhのソースコード
コードには一見奇妙に見える点がいくつかあります。最初に注目すべきは150行目で、MQL5の定数 INT_MINを使用してスライダーの最小オフセット値を指定していることです。これは負の値であり、整数型変数が取り得る最小の値です。このようにした理由はすぐには分かりにくいかもしれませんが、150行目を完全に理解するには、まず整理すべき他の要素があるため、しばらくお待ちください。
次に重要なのが160行目で、コントロールインジケーターバッファへデータを書き込む処理があります。この段階では、2つの値のみを記録しています。将来的に追加の値が必要になる可能性はありますが、現時点ではこの2つの値を1つのdouble型の値に圧縮し、バッファ内の1つの位置だけを使用するようにしています。
この圧縮処理には、162行目で宣言されたunion(共用体)を使用します。そして、164行目で、スライダーを操作する際にユーザーが調整する値を格納します。注:ここで保存するのは、ユーザーが変更したスライダーの位置です。続く165行目では、コントロールインジケーターの状態(再生モードか一時停止モードか)を指定します。
ここで重要なポイントがあります。ユーザーが再生ボタンを押した場合は特定の値が保存され、一時停止モードの場合は別の値が保存されます。このとき使用する値は、整数型データが取り得る範囲の両端です。この方法により、null値を使うことで生じる曖昧さを回避しつつ、データの整合性を確保できるため、その後のテストが容易になります。
この点は非常に重要です。コントロールインジケーターの構築段階でこの仕組みを理解していないと、後の説明が難しくなる可能性があります。忘れないでください。情報はコントロールインジケーターからサービスに直接送られるのではなく、バッファという経路を通ります。この点については、今後さらに詳しく説明します。
最後に、166行目では、圧縮した値をインジケーターバッファ内の特定の位置に保存します。この特定の位置にデータを保存する理由については、本連載の別の記事ですでに説明しているので、疑問がある場合は過去の記事を参照してください。
次に、説明が必要な部分は169行目から始まるメッセージハンドラです。特に注目すべきポイントが2つあります。まずは、比較的シンプルで、先ほど説明した内容とも関係がある部分から見ていきましょう。では、209行目へ進みます。
ここは興味深い部分です。209行目では、スライダーを動かすときにユーザーが設定した値を m_Slider.Minimal変数に保存しています。その理由は、処理を簡素化するためです。こうすることで、コードの重要な部分にすべての処理を集約できます。もし209行目がなければ、コードのどこかでユーザーが設定した位置をバッファに渡す処理を追加する必要が出てきます。あるいは、さらに悪いことに、端末のグローバル変数を使用せずに、ユーザーが指定した値を直接サービスに渡す手段を考えなければならなくなるでしょう。以前は、この処理にグローバル変数を使用していました。しかし今回はバッファを使用します。そのため、繰り返しの確認や調整を避けるために、値を簡単にアクセスできる場所に保存するようにしました。なお、この値がここに保存されるのは、ユーザーが再生ボタンをクリックした後のみである点に注意してください。
ここで、前のコードに戻り、177行目を見てみましょう。ここでは、スライダーを適切な位置に設定するために、コントロールインジケーターを初期化するカスタムイベントを発生させています。
このカスタムイベントは定期的に発生しますが、そのデータはdouble型の値にパックされた形式で格納されています。したがって、安全性とデータの正確性を確保しながら、情報を適切に解読する必要があります。受信した情報は、バッファに保存されている情報と同じ形式です。ただし、ここではデータの整合性チェックが必要になります。
例えば、180行目に小さなチェックがあります。ここでは、システムが再生モードまたは一時停止モードを示す値が0かどうかを確認しています。もし0だった場合、何か問題が発生し、コントロールインジケーターが誤ったデータを受け取っていることを意味します。このため、SetUserError を呼び出してエラー処理をおこないます。通常、この関数が実行されることはありませんが、万が一エラーが発生した場合は、適切な対応を取る必要があります。この対応策については、インジケーターのコード内で後ほど詳しく説明します。
すべてが正常であれば、さらに2つの処理を実行します。1つ目は182行目で、再生ボタンまたは一時停止ボタンを表示する関数を呼び出します。2つ目は、行183に示されているチェックです。値が最小の場合は一時停止モードになっていることを意味するため、ユーザーが調整できるようにスライダーを再作成する必要があります。
基本的な仕組みはこうなっています。インジケーターがチャートに追加された直後は機能せず、カスタムイベントによって初期化される必要があります。この初期化のプロセスについては、後ほど詳しく解説します。また、マウスポインターとコントロールインジケーターの相互作用によって、リプレイ/シミュレーターサービスを適切に制御するために必要なメッセージのサイクルが生成されます。
それでは、コントロールインジケーターのコード全体を確認していきましょう。この時点で、今後のモジュール構築の基礎となる重要な部分を説明しているため、慎重に読み進めてください。
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.55" 07. #property link "https://www.mql5.com/pt/articles/11988" 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; 20. //+------------------------------------------------------------------+ 21. int OnInit() 22. { 23. ResetLastError(); 24. if (CheckPointer(control = new C_Controls(user00, "Market Replay Control", new C_Mouse(user00, "Indicator Mouse Study"))) == POINTER_INVALID) 25. SetUserError(C_Terminal::ERR_PointerInvalid); 26. if (_LastError != ERR_SUCCESS) 27. { 28. Print("Control indicator failed on initialization."); 29. return INIT_FAILED; 30. } 31. SetIndexBuffer(0, m_Buff, INDICATOR_DATA); 32. ArrayInitialize(m_Buff, EMPTY_VALUE); 33. 34. return INIT_SUCCEEDED; 35. } 36. //+------------------------------------------------------------------+ 37. int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) 38. { 39. return m_RatesTotal = rates_total; 40. } 41. //+------------------------------------------------------------------+ 42. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 43. { 44. (*control).DispatchMessage(id, lparam, dparam, sparam); 45. if (_LastError >= ERR_USER_ERROR_FIRST + C_Terminal::ERR_Unknown) 46. { 47. Print("Internal failure in the messaging system..."); 48. ChartClose(user00); 49. } 50. (*control).SetBuff(m_RatesTotal, m_Buff); 51. } 52. //+------------------------------------------------------------------+ 53. void OnDeinit(const int reason) 54. { 55. switch (reason) 56. { 57. case REASON_TEMPLATE: 58. Print("Modified template. Replay/simulation system shutting down."); 59. case REASON_INITFAILED: 60. case REASON_PARAMETERS: 61. case REASON_REMOVE: 62. case REASON_CHARTCLOSE: 63. ChartClose(user00); 64. break; 65. } 66. delete control; 67. } 68. //+------------------------------------------------------------------+
コントロール指標のソースコード
10行目では、MQL5に対してバッファが必要であることを宣言し、9行目ではチャートに情報を表示しないことを指定しています。つまり、このバッファはインジケーターの内部でのみ使用され、ユーザーには表示されませんが、適切な方法を知っているコードからはアクセス可能ということです。
バッファを使用するため、まずそれを宣言する必要があります。18行目でこれをおこない、次に31行目でバッファを宣言してインジケーターの外部からもアクセスできるようにします。32行目では、バッファが完全に空であることを確認します。ここから先の説明は重要なので、注意して読み進めてください。
ユーザーがMetaTrader 5プラットフォームを操作すると(例えば時間枠を変更するなど)、MetaTrader 5はチャートを一度クリアしてから再読み込みします。この際、すべてのコードがリセットされます。どのインジケーターでも、これはOnInitイベントが再び呼び出されることを意味し、再初期化のプロセスが発生します。私たちのコントロールインジケーターは特定の計算を行うものではなく、単にカスタムシンボルを使ってチャート上にバーを表示するためのサービスを、ユーザーが操作・制御するための手段を提供するものです。
したがって、ユーザーが時間枠を変更すると、インジケーター内のすべての値が失われます。このため、すべてが適切な方法で処理されるようにする必要があります。インジケーターが制御するサービスは、チャート上で何が起こっているかを知ることはできません。同様に、インジケーターもサービスがどのように動作しているかを直接把握することはできません。サービスとインジケーターが互いに何をしているのかを認識する方法は、メッセージを交換することです。
以前は、このメッセージ交換はグローバル端末変数を通じておこなわれていました。しかし現在は異なる方法を採用しており、ユーザーの操作にかかわらず、インジケーターとサービスが常に一貫性を保ち、何が起こっているのかを認識できるようにする必要があります。このため、インジケーターの状態をサービスに通知するために、インジケーターバッファを使用し、発生しているプロセスに関するすべての情報をそこに格納します。
インジケーターは、入力パラメータ(16行目で宣言)と、OnChartEvent内で処理されるユーザーイベントを通じて、サービスが何を実行しているのかを知ることができます。
データをすでにチャート上にあるインジケーターへ渡す際に入力パラメータを使用するのは、実用的ではありません。可能ではありますが、非効率的であり、私たちはこの方法を採用しません。なぜなら、サービスがインジケーターをチャートに配置した後は、入力パラメータを介した操作ができなくなるからです。そのため、ここではカスタムイベントを利用します。
では、さらに詳しく説明します。ユーザーが時間枠を変更すると、インジケーターは以前の状態やシフト位置に関する情報を失います。しかし、サービス側にはこの情報が保持されています。では、サービスはどのようにしてこのデータをインジケーターに渡し、一貫性を維持するのでしょうか。サービスは、MetaTrader 5がいつインジケーターを復元するのかを知ることはできませんが、インジケーターバッファを監視することは可能です。これが、ここでの重要なポイントとなります。これが主なトリックです。
インジケーターをチャートに配置すると、バッファの初期値はゼロになります。C_Controlsクラスのコンストラクタが実行されると、150行目で特定の値が初期化されます(詳細はクラスコードを参照)。しかし、この値はOnChartEventが呼ばれるまでバッファに書き込まれず、その後、インジケーターコードの50行目でバッファが更新されます。
したがって、MetaTrader 5がチャート上のコントロールインジケーターを復元した後にサービスがバッファを読み取ると、ゼロまたは異常な値が見えることになります。この時点で、MetaTrader 5に特別なイベントを発生させ、サービスがインジケーターに最新の値を通知し、それらが正しく表示されるようにします。これにより、ボタンやスライダーが正しく配置されます。
もしこれを別の方法でおこなおうとすると、失われたデータを復元する手段を考えなければなりません。結果的に、異なる手法を用いたとしても、最終的にはインジケーターの初期化に帰結することになります。しかし、一部の方法ではユーザーによる操作が可能となり、サービスとインジケーター間の通信管理が複雑になるリスクがあります。今回の方法では、実際に情報を必要とする部分のみにデータを提供することで、セキュリティを強化しながら、一貫した動作を確保できます。
ここまでの説明は、特にプログラミングを始めたばかりの方には少し難しく感じられるかもしれません。メッセージ交換や制御された初期化といった概念に馴染みのない方も多いでしょう。では、これが実際にどのように機能するのかをどのように示せばよいのでしょうか。そのために、コントロールインジケーターとマウスインジケーターを使用します。しかし、実際のシステムがどのように動作するかを理解する前に、
まずこの考え方をわかりやすく示すためのシンプルなコードを作成する必要があります。以下に、そのシンプルなコードを示します。
01. //+------------------------------------------------------------------+ 02. #property service 03. #property copyright "Daniel Jose" 04. #property description "Data synchronization demo service." 05. #property version "1.00" 06. //+------------------------------------------------------------------+ 07. #include <Market Replay\Defines.mqh> 08. //+------------------------------------------------------------------+ 09. #define def_IndicatorControl "Indicators\\Market Replay.ex5" 10. #resource "\\" + def_IndicatorControl 11. //+------------------------------------------------------------------+ 12. input string user00 = "BOVA11"; //Symbol 13. //+------------------------------------------------------------------+ 14. #define def_Loop ((!_StopFlag) && (ChartSymbol(id) != "")) 15. //+------------------------------------------------------------------+ 16. void OnStart() 17. { 18. uCast_Double info; 19. long id; 20. int handle, iPos, iMode; 21. double Buff[]; 22. 23. SymbolSelect(user00, true); 24. id = ChartOpen(user00, PERIOD_H1); 25. if ((handle = iCustom(ChartSymbol(id), ChartPeriod(id), "::" + def_IndicatorControl, id)) != INVALID_HANDLE) 26. ChartIndicatorAdd(id, 0, handle); 27. IndicatorRelease(handle); 28. if ((handle = iCustom(ChartSymbol(id), ChartPeriod(id), "\\Indicators\\Mouse Study.ex5", id)) != INVALID_HANDLE) 29. ChartIndicatorAdd(id, 0, handle); 30. IndicatorRelease(handle); 31. Print("Service maintaining sync state..."); 32. iPos = 0; 33. iMode = INT_MIN; 34. while (def_Loop) 35. { 36. while (def_Loop && ((handle = ChartIndicatorGet(id, 0, "Market Replay Control")) == INVALID_HANDLE)) Sleep(50); 37. info.dValue = 0; 38. if (CopyBuffer(handle, 0, 0, 1, Buff) == 1) info.dValue = Buff[0]; 39. IndicatorRelease(handle); 40. if (info._int[0] == INT_MIN) 41. { 42. info._int[0] = iPos; 43. info._int[1] = iMode; 44. EventChartCustom(id, evCtrlReplayInit, 0, info.dValue, ""); 45. }else if (info._int[1] != 0) 46. { 47. iPos = info._int[0]; 48. iMode = info._int[1]; 49. } 50. Sleep(250); 51. } 52. ChartClose(id); 53. Print("Finished service..."); 54. } 55. //+------------------------------------------------------------------+
デモサービスのソースコード
コントロールインジケーターをサービスリソースに変換する10行目に注目してください。したがって、これはここでのシステム以外では役に立たないため、インジケーターのリストに含める必要はありません。12行目には、システムをテストするための銘柄を指定します。後で有効性はチェックされないので、必ず有効な文字を使用してください。14行目には、サービスを適切にシャットダウンできるようにするためにいくつかの条件をチェックする定義があります。
23行目では、銘柄が気配値表示ウィンドウに存在しない場合に備えて、銘柄を表示します。24行目では、ユーザー(この場合はあなた)が指定した銘柄を含むグラフィックウィンドウを開きます。これが完了すると、チャートIDが作成され、それを使用してチャートにインジケーターを配置できるようになります。
25行目では、新しく開いたチャートにコントロールインジケーターを配置します。次に、29行目でマウスインジケーターを追加します。マウスインジケーターはインジケーターリストに表示されますが、コントロールインジケーターは表示されません。とはいえ、完全なテストには両方が必要です。
31行目では、サービスがアクティブになり、チャート上で何が起こっているのかを監視していることをターミナルに通知します。
この時点で、マウスインジケーターはすでにチャート上に表示されているはずですが、コントロールインジケーターはリストにのみ存在し、表示されません。コントロールインジケーターの初期化方法については先ほど説明しましたが、現時点ではそのバッファに意味のないランダムな値が格納されており、直接操作はできません。しかし、コントロールインジケーターが正しく初期化され、そのバッファに値が書き込まれていれば、特定の値が得られます。そこで、34行目では、14行目で定義した条件が満たされる限り、ループが継続します。
36行目では、コントロールインジケーターが実際にチャートに追加されているか確認しています。なぜこのチェックをおこなうのか、そしてその完了を待つ必要があるのかというと、コードの実行があまりにも早いため、MetaTrader 5が状況を安定させるために時間を要するからです。このため、36行目のループを使って待機します。
すべてが正常に動作した後、コントロールインジケーターのバッファを読み取ろうとしますが、このインジケーターはチャートに表示されません。
バッファの読み取りに成功すると、info.dValue変数にゼロ以外の値が追加されます。これにより、コントロールインジケーターの状態を確認することができます。40行目では、そのインジケーターがすでに初期化されているかをチェックします。今回は初回なので、インジケーターはまだ初期化されていません。42行目と43行目では、インジケーターに渡す値を生成し、MetaTrader 5にカスタムイベントを発生させるようにリクエストします。44行目では、このイベントが表示され、コントロールインジケーターが初期化されるようメッセージが送られます。
それ以外のタイミングでは、インジケーターが一時停止状態か再生状態かをチェックします。これらの状態にある場合、45行目でユーザーが指定した値をインジケーターに保存します。このため、ユーザーが時間枠を変更すると、40行目のチェックでtrueが返され、サービスに保存された値がインジケーターに戻され、時間枠変更後もインジケーターが正しく初期化されることが確保されます。
最後に、52行目でチャートを閉じ、53行目でサービス終了メッセージを表示します。
もしまだ実際に試すのが心配であれば、下のビデオでシステムの動作を確認できます。また、実行可能ファイルも添付していますので、どのように動作するのかも確認できます。
デモ映像
結論
この記事では、今後の記事で取り上げる内容の基盤を築き始めました。内容がかなり凝縮されていることは理解していますので、焦らず落ち着いて勉強を進めてください。これから先、さらに複雑になっていきますので。
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/11988
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
リプレイシステムの開発(第56回):モジュールの適応
MQL5経済指標カレンダーを使った取引(第1回):MQL5経済指標カレンダーの機能をマスターする
ニューラルネットワークの実践:擬似逆行列(II)
知っておくべきMQL5ウィザードのテクニック(第44回):ATR (Average True Range)テクニカル指標
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索