リプレイシステムの開発(第76回):新しいChart Trade(III)
はじめに
前回の「リプレイシステムの開発(第75回):新しいChart Trade (II)」では、C_ChartFloatingRADクラスのいくつかの側面について説明しました。ただし内容がかなり濃かったため、必要に応じてできる限り詳細な説明をおこなうことを心がけました。ところが、まだ取り上げていない手続きが1つ残っていました。前回の記事で、ヘッダファイル C_ChartFloatingRAD.mqhに記載された内容だけで説明を試みても、十分ではなかったでしょう。というのも、DispatchMessage手続きの動作を完全に理解するには、もう一つ関連するトピックを併せて説明する必要があるからです。
この記事では、DispatchMessage手続きが実際にどのように動作するのかを詳しく説明することができます。これはC_ChartFloatingRADクラスにおいて最も重要な手続きであり、MetaTrader 5がChart Tradeに送信するイベントを生成し、処理する役割を担っています。
ここでの内容は、前回の記事と一緒に読むべきものです。前回の内容を十分に理解していない状態で今回の記事に取り組むことはお勧めしません。前回と今回の記事を合わせて初めて、チャートトレードインジケーターの概念的な基盤が完成するのです。したがって、それぞれをしっかり理解することが重要です。
それでは、DispatchMessageの説明に移りましょう。
DispatchMessageの仕組みを理解する
もしまだ「Chart Tradeで多数のオブジェクトがコーディングされるのでは」と考えているなら、仕組みを十分に理解できていない証拠です。ここで先に進む前に、改めて前回の記事を読み直すことを強くお勧めします。今回は、Chart Tradeを機能させる本質、すなわちイベントに応答し、イベントを生成する点だけを扱います。ここで新しいオブジェクトを作成することはありません。私たちの役割は、MetaTrader 5が報告してくるイベントを正しく処理できるようにすることです。
前回の記事で、ヘッダファイルC_ChartFloatingRAD.mqhにコードが欠けている部分があったことに気づいたかもしれません。その不足していたコードは以下のスニペットに示すとおりです。
259. //+------------------------------------------------------------------+ 260. void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) 261. { 262. #define macro_AdjustMinX(A, B) { \ 263. B = (A + m_Info.Regions[MSG_TITLE_IDE].w) > x; \ 264. mx = x - m_Info.Regions[MSG_TITLE_IDE].w; \ 265. A = (B ? (mx > 0 ? mx : 0) : A); \ 266. } 267. #define macro_AdjustMinY(A, B) { \ 268. B = (A + m_Info.Regions[MSG_TITLE_IDE].h) > y; \ 269. my = y - m_Info.Regions[MSG_TITLE_IDE].h; \ 270. A = (B ? (my > 0 ? my : 0) : A); \ 271. } 272. 273. static short sx = -1, sy = -1, sz = -1; 274. static eObjectsIDE obj = MSG_NULL; 275. short x, y, mx, my; 276. double dvalue; 277. bool b1, b2, b3, b4; 278. ushort ev = evChartTradeCloseAll; 279. 280. switch (id) 281. { 282. case CHARTEVENT_CHART_CHANGE: 283. x = (short)ChartGetInteger(GetInfoTerminal().ID, CHART_WIDTH_IN_PIXELS); 284. y = (short)ChartGetInteger(GetInfoTerminal().ID, CHART_HEIGHT_IN_PIXELS); 285. macro_AdjustMinX(m_Info.x, b1); 286. macro_AdjustMinY(m_Info.y, b2); 287. macro_AdjustMinX(m_Info.minx, b3); 288. macro_AdjustMinY(m_Info.miny, b4); 289. if (b1 || b2 || b3 || b4) AdjustTemplate(); 290. break; 291. case CHARTEVENT_MOUSE_MOVE: 292. if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft)) switch (CheckMousePosition(x = (short)lparam, y = (short)dparam)) 293. { 294. case MSG_TITLE_IDE: 295. if (sx < 0) 296. { 297. DeleteObjectEdit(); 298. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false); 299. sx = x - (m_Info.IsMaximized ? m_Info.x : m_Info.minx); 300. sy = y - (m_Info.IsMaximized ? m_Info.y : m_Info.miny); 301. } 302. if ((mx = x - sx) > 0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XDISTANCE, mx); 303. if ((my = y - sy) > 0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YDISTANCE, my); 304. if (m_Info.IsMaximized) 305. { 306. m_Info.x = (mx > 0 ? mx : m_Info.x); 307. m_Info.y = (my > 0 ? my : m_Info.y); 308. }else 309. { 310. m_Info.minx = (mx > 0 ? mx : m_Info.minx); 311. m_Info.miny = (my > 0 ? my : m_Info.miny); 312. } 313. break; 314. case MSG_BUY_MARKET: 315. ev = evChartTradeBuy; 316. case MSG_SELL_MARKET: 317. ev = (ev != evChartTradeBuy ? evChartTradeSell : ev); 318. case MSG_CLOSE_POSITION: 319. if ((m_Info.IsMaximized) && (sz < 0)) 320. { 321. string szTmp = StringFormat("%d?%s?%c?%d?%.2f?%.2f", ev, _Symbol, (m_Info.IsDayTrade ? 'D' : 'S'), m_Info.Leverage, 322. FinanceToPoints(m_Info.FinanceTake, m_Info.Leverage), FinanceToPoints(m_Info.FinanceStop, m_Info.Leverage)); 323. PrintFormat("Send %s - Args ( %s )", EnumToString((EnumEvents) ev), szTmp); 324. sz = x; 325. EventChartCustom(GetInfoTerminal().ID, ev, 0, 0, szTmp); 326. DeleteObjectEdit(); 327. } 328. break; 329. }else 330. { 331. sz = -1; 332. if (sx > 0) 333. { 334. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); 335. sx = sy = -1; 336. } 337. } 338. break; 339. case CHARTEVENT_OBJECT_ENDEDIT: 340. switch (obj) 341. { 342. case MSG_LEVERAGE_VALUE: 343. case MSG_TAKE_VALUE: 344. case MSG_STOP_VALUE: 345. dvalue = StringToDouble(ObjectGetString(GetInfoTerminal().ID, m_Info.szObj_Editable, OBJPROP_TEXT)); 346. if (obj == MSG_TAKE_VALUE) 347. m_Info.FinanceTake = (dvalue <= 0 ? m_Info.FinanceTake : dvalue); 348. else if (obj == MSG_STOP_VALUE) 349. m_Info.FinanceStop = (dvalue <= 0 ? m_Info.FinanceStop : dvalue); 350. else 351. m_Info.Leverage = (dvalue <= 0 ? m_Info.Leverage : (short)MathFloor(dvalue)); 352. AdjustTemplate(); 353. obj = MSG_NULL; 354. ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable); 355. break; 356. } 357. break; 358. case CHARTEVENT_OBJECT_CLICK: 359. if (sparam == m_Info.szObj_Chart) if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft)) switch (obj = CheckMousePosition(x = (short)lparam, y = (short)dparam)) 360. { 361. case MSG_DAY_TRADE: 362. m_Info.IsDayTrade = (m_Info.IsDayTrade ? false : true); 363. DeleteObjectEdit(); 364. break; 365. case MSG_MAX_MIN: 366. m_Info.IsMaximized = (m_Info.IsMaximized ? false : true); 367. DeleteObjectEdit(); 368. break; 369. case MSG_LEVERAGE_VALUE: 370. CreateObjectEditable(obj, m_Info.Leverage); 371. break; 372. case MSG_TAKE_VALUE: 373. CreateObjectEditable(obj, m_Info.FinanceTake); 374. break; 375. case MSG_STOP_VALUE: 376. CreateObjectEditable(obj, m_Info.FinanceStop); 377. break; 378. } 379. if (obj != MSG_NULL) AdjustTemplate(); 380. break; 381. case CHARTEVENT_OBJECT_DELETE: 382. if (sparam == m_Info.szObj_Chart) macro_CloseIndicator(C_Terminal::ERR_Unknown); 383. break; 384. } 385. ChartRedraw(); 386. } 387. //+------------------------------------------------------------------+ 388. }; 389. //+------------------------------------------------------------------+
C_ChartFloatingRAD.mqhで欠落していたコード
不足していたコードは、その空白部分にぴったり収まります。以下の強調表示されたスニペットの通りです。C_ChartFloatingRADクラスのコードを完成させるには、この断片を示された行番号に従ってその場所に正確に挿入する必要があります。これは比較的簡単におこなうことができるはずです。それでは、このコードがどのように動作するのかを見ていきましょう。
最初に注目すべき点は、このコード断片にはオブジェクトを作成する呼び出しが一切含まれていないということです。先ほど述べた通り、ここではオブジェクトをプログラミングしていません。Chart Tradeはテンプレートとして作成され、保存されます。これはすべてMetaTrader 5内でおこなわれます。前回の記事の最後に、この概念を理解する助けとなるいくつかの参考情報を残しておきました。
この種のプログラミングは、多くの開発者にはあまり使われません。しかし私の考えでは、多くのケースにおいてこの方法の方がはるかに実用的でシンプルです。すべてのオブジェクトを手作業で実装・配置する必要はありません。代わりに、それらをテンプレートとして参照するだけで済みます。MetaTrader 5が直接構成できない場合のみ、プログラムで作成します。すでに触れたように、要素を作成してテンプレートとして保存する作業はむしろ稀です。唯一この方法で扱えなかったオブジェクトはOBJ_EDITでした。これは、テキスト入力や操作のロジックを作成するより、OBJ_EDITオブジェクト自体を直接作成する方が容易だったからです。したがって、実際に作成されるオブジェクトはこれだけです。
スニペットを見ると、このコードはイベント処理だけを扱っていることが分かります。前回の記事にある動画や添付された実行ファイルを見た方には驚きかもしれません。なぜなら、コードには多くのオブジェクトが含まれていないように見えるのに、実際にはそのまま動作しているからです。DispatchMessageの中に他のオブジェクトがあるはずだと思った方もいるでしょう。しかし、ご覧の通り、他にオブジェクトはありません。MetaTrader 5からのイベント処理だけです。では、どのように動いているのでしょうか。
ここからは段階的に見ていきましょう。ここではMetaTrader 5から送られる5種類のイベントを処理します。それぞれを見る前に、まず行番号273~278にある変数宣言に注目してください。staticとして宣言された変数は、クラスのグローバルスコープに置くことも可能ですが、ここではこの手続き内部でのみ必要です。staticによって、呼び出し間で値が保持されます。一方、行278で宣言されている変数はstaticではありません。これは宣言と同時に初期化され、誤った使い方による問題を避けるためです。この初期値はDefines.mqhファイルに定義されているイベントに対応しています。
まず、行282~290にあるCHARTEVENT_CHART_CHANGEイベント処理から始めましょう。このイベントはチャートが変化するたびに発生します。処理内容は単純で、OBJ_CHARTオブジェクトがチャートウィンドウ内で正しく位置を維持するための計算をおこないます。
行291~338に示されている2番目のCHARTEVENT_MOUSE_MOVEイベント処理は、DispatchMessageの中で最も長いハンドラです。マウスが動いたりボタンが押されたりするたびに発生します。ただし、デフォルトではMetaTrader 5はこのイベントを送信しません。マウスイベントを受信することを明示的に指定する必要があります。これを実現するのがマウスインジケーターです。このインジケーターについては過去の記事で詳細に説明しました。ユーザーが作成した要素と対話できるようにするため、マウスインジケーターの理解は非常に重要です。
ここで特筆すべきは、マウスイベントを直接処理していない点です。行292で最初におこなうのは、左クリックが発生したかどうかを確認することですが、マウスインジケータによる有効性の確認も必要です。なぜでしょうか。なぜハンドラ内でクリックの有効性を直接チェックしないのでしょうか。なぜマウスインジケータに問い合わせる必要があるのでしょうか。
理由は、マウスインジケーターが「リサーチモード」で動作している可能性があるということです。このモードがユーザーの要求で有効になっている場合、クリックは処理されるべきではありません。マウスインジケーターがいつどのくらいの間リサーチモードになるかを正確に検出することはできません。そのため、最も確実な方法はマウスインジケータに問い合わせることです。クリックが有効な場合、MetaTrader 5から取得した位置データを変換し、どのオブジェクトがクリックされたかをCheckMousePositionを使って確認します。この際、マウスインジケーターが提供する座標を使うこともできます。しかし、その場合はインジケーターバッファを読み取る必要があります。バッファの値はMetaTrader 5が直接提供する値と同じなので、わざわざバッファを読む意味はありません。
実際のクリック処理を分析する前に、行329を見てみましょう。ここでは、CHARTEVENT_MOUSE_MOVEイベントで有効なクリックが発生しなかった場合、あるいはユーザーがマウスを動かした場合の処理がおこなわれます。この場合、行331でsz変数に負の値を代入し、行332でsx変数が正の値かどうかを確認します。正の場合はマウスイベントを処理しますが、ここではその処理は後回しにします。行334ではMetaTrader 5に対してチャートをマウスで動かせることを通知し、行335で変数sxとsyに負の値を代入します。これにより行332の条件が偽になり、行334が不要に実行されることを防ぎます。
注意すべきは、マウスイベント(CHARTEVENT_MOUSE_MOVE)は非常に高頻度で発生するということです。場合によってはOnTickよりも頻繁に発生します。そのため、これらのイベントを不用意に処理するとMetaTrader 5の動作が著しく遅くなる可能性があります。これがデフォルトで無効になっている理由です。
次です。行293~328では、特定のChart Trade要素とユーザーがやり取りするロジックが記述されています。CheckMousePositionが値を返す場合、その値はコード内で扱われるオブジェクトのいずれかを指します。ここで扱うオブジェクトは、タイトルバー、[Market Buy]ボタン、[Market Sell]ボタン、[Close Position]ボタンの4種類です。他の要素は別の箇所で処理され、後ほど説明されますが、この4つの要素はそれぞれ異なるアプローチが必要です。まず、最も複雑なタイトルバーから見ていきましょう。
タイトルバーがクリックされると、ユーザーはChart Tradeを移動させたい場合があります。そのため、行295~313にはその処理に必要なロジックが含まれています。この処理の詳細はここでは省略します。なぜなら、この処理は本連載内の別の記事ですでに解説されており、Chart Tradeの初期バージョンを作成した際に説明済みだからです。ご質問がある場合は、以前の記事をお読みください。コードは突然現れるものではなく、段階的に作成・実装され、現在の形に至ったことを理解しておくことが重要です。
私たちが注目すべき部分は、行314~328です。この範囲では、行278で定義された変数の値を使って、3つの条件のうち2つをテストします。ここで扱う3つの条件は、Market Buy、Market Sell、Close Positionの各イベントです。まず最初に、[Market Buy]ボタンがクリックされたかどうかを確認します。もしクリックされていれば、行315でev変数にMarket Buyイベントを示す値を代入します。
次に行316では、[Market Sell]ボタンがクリックされたかどうかを確認します。しかし同時に、[Market Buy]ボタンがクリックされていないかどうかも確認する必要があります。なぜ別のイベントを扱うのに、他のボタンの状態も確認するのでしょうか。確かに、これは別のイベントです。
しかし、重要なのは、Market Buy、Market Sell、Close Positionの3つのイベントはすべて同じ種類のアクションを実行するという点です。イベント間には中断はなく、逆に連続して並んで処理されます。そのため、行317では、前のイベントがev変数の値を変更していないかどうかを確認し、変更されている場合、次のイベント処理は実行しません。そうでなければ、最後のイベントのみが実行され、前のイベントは無視されます。
いずれにしても、常に行318に到達し、Close Positionイベントが処理されます。このイベントでは、MetaTrader 5がカスタムイベントを実行できるようにするためのチェックが必要です。さて、ここまでたどり着くために、どのようにすれば失敗を避けられるかを見ていきましょう。
行319では、Chart Tradeが最大化されているかどうかをチェックします。このチェックは重要です。なぜならCheckMousePosition関数はウィンドウが最大化されているかどうかを考慮しないからです。もしこのチェックをおこなわずにボタンの位置をクリックすると、ウィンドウが最大化されていない場合でもカスタムイベントが発生してしまいます。この問題を避けるためには、オブジェクトがチャート上で表示されている場合にのみイベントを発生させる必要があります。
ここで重要な点があります。ウィンドウは最大化されている必要があります。つまり、ボタンはユーザーから見える状態でなければなりません。しかし、このチェックには小さな問題があります。それは、ウィンドウが最大化されているかどうかを判定しているのがChart Tradeのコードであり、MetaTrader 5自体ではないという点です。なぜこれが大事かというと、ボタンが他のオブジェクトで覆われている場合に問題になるからです。ボタンの上に別のオブジェクトがあり、Chart Tradeが最大化されている場合、ボタンの位置をクリックしてもカスタムイベントを発生させるのはMetaTrader 5ではなくChart Tradeのコードです。これは将来的に修正予定の不具合ですが、現時点では重大な問題ではないため許容できます。しかし、リアル口座でChart Tradeを使用する場合は注意してください。Chart Tradeが最大化されているとき、ボタンの前に何も置かないようにしてください。
同じ行319には、クリック後にマウスを移動したことで複数イベントが発生するのを防ぐチェックも含まれています。これは簡単なチェックで、szの値が負であるかどうかを確認しています。もし正の値であれば、イベントはすでに発生しており、マウスボタンはまだ押されている状態です。ボタンを離してマウスを動かした場合、行331で新しいユーザーイベントが発生します。このシンプルなチェックだけでも、多くの問題を回避できます。
すべての「魔法」は行321で起こります。長くならないように、この行を2つに分けて説明しています。つまり、行322も行321の一部として考えます。まず次の4行を見てください。行323では、イベントをMetaTrader 5のツールボックスにログとして記録します。これは必須ではないと思うかもしれませんが、実際には多くの問題を避ける助けになります。これは非常に重要なデバッグ手法です。MetaTrader 5ツールボックスに表示されるメッセージを無視してはいけません。多くのメッセージが重要な情報を含んでいます。
行324では、前述の条件を防ぐためのロック処理がおこなわれます。これは、行319で使用したsz変数をチェックする際に必要です。そして行325で、カスタムイベントが実際に発生します。ここでは、前回のコントロールインジケーターとは異なり、データは文字列フィールドに送信されます。文字列フィールドが使用できない場合、アプリケーション間で大量の情報をやり取りする際に問題が発生します。
ここで重要なポイントがあります。MQL5の文字列はC/C++と同じ原理に従っており、文字列はNULL文字で終端されます。なぜこのことを説明するかというと、カスタムイベントで送信する際にStringFormat関数を使用する必要がある理由を理解するためです。カスタムイベントで送信される内容を表示する必要がないと思う場合は、単純に行321の内容を取り出し、行325に置き、変数szTmpをStringFormat関数に置き換え、行321とまったく同じ式を使用すればよいでしょう。
しかし、送信する文字列には数値を含める必要があるため、そのままでは正しく伝達できません。ここでなぜlparamやdparamを使わずにsparamを使うのか疑問に思うかもしれません。理由は単純です。送信するデータの量が多いためです。もし少数のパラメータだけを送るのであれば、lparamやdparamを使うことも可能です。しかし、情報量が多いため、sparamフィールドを使わざるを得ません。初心者が特に誤解しやすいポイントです。
たとえば、値65を見たとき、あるアプリケーションはこれを数値65と解釈するかもしれませんが、別のアプリケーションは文字Aと解釈するかもしれません。多くの読者には混乱するかもしれませんが、重要なのは、65を文字列として見ているのではなく、バイナリ形式での値として扱うということです。バイナリでは8ビットで0100 0001と表現されます。プログラミングを扱う際は、この考え方が正しいです。文字列を見るとき、人間が読める文字やテキストとしてではなく、非常に大きな数値の連続として考えるべきです。1バイト、256バイトといった固定長ではなく、潜在的に膨大なバイト列として扱います。
こう考えると、文字列の終端をどうやって示すかも理解しやすくなります。一部の言語では、文字列の先頭に文字数やバイト数を示す値を格納する場合があります。BASICや古いPascalがその例です。Pascalでは文字列の長さを先頭に保持します。
この場合、文字列(正確には配列)は0〜255の任意のバイト値を含むことができます。この方式は多くのケースでうまく機能しますが、固定長配列の場合、メモリの無駄が発生する場合があります。たとえば3バイトしか必要ない場合でも、コンパイラが最大50バイトと判断すると、50バイトを確保してしまうことがあります。
この非効率を避けるため、他の言語では文字列形式を使わず、文字列の終端を特定のコードで示す方式を採用しています。最も一般的な方法は、NULL文字を使用することです。NULLは0000 0000のバイナリ値です。この文字を選んだ理由は、コンパイラ実装上の都合によるものです。理論上は任意の文字を使うこともできます。重要なのは、文字列内でデータを送信する際に、この終端処理がどう影響するかです。
さて、数値データに戻ります。Chart Tradeでは主にshort型とdouble型の2種類を送信します。short型は16ビット、すなわち2バイトを使用します。double型についてはここでは触れず、short型の例を使います。Chart Trade内ではshortはレバレッジレベルに使用されます。最小レバレッジ値は1です。バイナリでは0000 0000 0000 0001(16ビット表記)となります。
しかし、shortは2バイトを使いますが、文字列は1バイト文字で構成されます。つまり、文字列内で値1の最初のバイトがNULL文字として解釈されてしまう可能性があります。その通りです。どの情報も読み取られる前に、文字列はすでに終了してしまいます。このため、値を印刷可能な文字に変換する必要があります。つまり、データがバイナリ形式であっても、送信の際には人間が読める形(表示可能な文字列)に変換し、送信後に再びバイナリ形式に戻すのです。これこそがStringFormat関数を使用する理由です。
最後に
この記事は唐突に終わるように見えるかもしれませんが、もうひとつ必要なテーマ、すなわちアプリケーション間の通信プロトコルについては、この短い紙幅で説明するのは適切ではないと考えます。このテーマは複雑であり、詳細に扱うに値します。ここでは単純に、行321が私たちが使用するプロトコルを作成している、と言うにとどめます。
しかし、このアプローチでは、プログラム間の通信に関する全体の問題が完全に曖昧なままとなり、この知識を自分のプログラムでどう使うかを理解しようとしても助けにはなりません。MetaTrader 5は長い間存在しており、多くのプログラマーが関心を持っていますが、ここで示す方法を使っている人を私は見たことがありません。多くの人は何かを達成したい場合、ゼロから作り始めるか、巨大なプログラムを作ります。しかし、それらはほとんどの場合、非常に維持や調整、改善が困難です。しかし、ここで共有する知識を用いれば、より小さく、ずっとシンプルなプログラムを開発でき、デバッグや改善もはるかに容易になります。
だからこそ、Chart Tradeを通して、このメッセージングプロセスの仕組みを詳細に説明する機会を設けました。初心者プログラマーの皆さんには、通常期待される以上のことが可能であることを実感してほしいと思います。
残りのイベント(CHARTEVENT_OBJECT_ENDEDIT、CHARTEVENT_OBJECT_CLICK、CHARTEVENT_OBJECT_DELETE)については、ここで説明した内容よりもコードがずっとシンプルなので、自力で理解できると私は確信しています。
次回の記事では、行321がなぜその特定の形式を持つのか、そしてそれがChart Tradeのイベントを受け取るエキスパートアドバイザー(EA)のコードにどのように影響し、ボタン操作に基づいて注文を実行できるようになるのかを正確に解説します。
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12443
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
初級から中級まで:定義(I)
時間、価格、ボリュームに基づいた3Dバーの作成
取引におけるニューラルネットワーク:Segment Attentionを備えたパラメータ効率重視Transformer(最終回)
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索