リプレイシステムの開発(第77回):新しいChart Trade (IV)
はじめに
前回の「リプレイシステムの開発(第76回):新しいChart Trade (III)」では、DispatchMessageコードの最も重要な部分について説明し、通信プロセス、より正確には通信プロトコルの設計方法について議論を始めました。
本題に入る前に、前回提示したコードに小さな修正を加える必要があります。これまで説明した内容はすべて有効ですが、システムを安定化させるためにはこの変更が必要です。その修正をおこなった後、この記事の本来のテーマに移ることができます。
DispatchMessageコードの安定性向上
マウスインジケーターとChart Tradeの相互作用に一部問題があるため、少し修正が必要です。なぜ相互作用が時々失敗するのかを完全に説明することはできません。しかし、以下のコード調整をおこなうことで、その問題は解消されます。
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)) 293. { 294. switch (CheckMousePosition(x = (short)lparam, y = (short)dparam)) 295. { 296. case MSG_MAX_MIN: 297. if (sz < 0) m_Info.IsMaximized = (m_Info.IsMaximized ? false : true); 298. break; 299. case MSG_DAY_TRADE: 300. if ((m_Info.IsMaximized) && (sz < 0)) m_Info.IsDayTrade = (m_Info.IsDayTrade ? false : true); 301. break; 302. case MSG_LEVERAGE_VALUE: 303. if ((m_Info.IsMaximized) && (sz < 0)) CreateObjectEditable(obj = MSG_LEVERAGE_VALUE, m_Info.Leverage); 304. break; 305. case MSG_TAKE_VALUE: 306. if ((m_Info.IsMaximized) && (sz < 0)) CreateObjectEditable(obj = MSG_TAKE_VALUE, m_Info.FinanceTake); 307. break; 308. case MSG_STOP_VALUE: 309. if ((m_Info.IsMaximized) && (sz < 0)) CreateObjectEditable(obj = MSG_STOP_VALUE, m_Info.FinanceStop); 310. break; 311. case MSG_TITLE_IDE: 312. if (sx < 0) 313. { 314. DeleteObjectEdit(); 315. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false); 316. sx = x - (m_Info.IsMaximized ? m_Info.x : m_Info.minx); 317. sy = y - (m_Info.IsMaximized ? m_Info.y : m_Info.miny); 318. } 319. if ((mx = x - sx) > 0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_XDISTANCE, mx); 320. if ((my = y - sy) > 0) ObjectSetInteger(GetInfoTerminal().ID, m_Info.szObj_Chart, OBJPROP_YDISTANCE, my); 321. if (m_Info.IsMaximized) 322. { 323. m_Info.x = (mx > 0 ? mx : m_Info.x); 324. m_Info.y = (my > 0 ? my : m_Info.y); 325. }else 326. { 327. m_Info.minx = (mx > 0 ? mx : m_Info.minx); 328. m_Info.miny = (my > 0 ? my : m_Info.miny); 329. } 330. break; 331. case MSG_BUY_MARKET: 332. ev = evChartTradeBuy; 333. case MSG_SELL_MARKET: 334. ev = (ev != evChartTradeBuy ? evChartTradeSell : ev); 335. case MSG_CLOSE_POSITION: 336. if ((m_Info.IsMaximized) && (sz < 0)) 337. { 338. string szTmp = StringFormat("%d?%s?%c?%d?%.2f?%.2f", ev, _Symbol, (m_Info.IsDayTrade ? 'D' : 'S'), m_Info.Leverage, 339. FinanceToPoints(m_Info.FinanceTake, m_Info.Leverage), FinanceToPoints(m_Info.FinanceStop, m_Info.Leverage)); 340. PrintFormat("Send %s - Args ( %s )", EnumToString((EnumEvents) ev), szTmp); 341. sz = x; 342. EventChartCustom(GetInfoTerminal().ID, ev, 0, 0, szTmp); 343. } 344. break; 345. } 346. if (sz < 0) 347. { 348. sz = x; 349. AdjustTemplate(); 350. if (obj == MSG_NULL) DeleteObjectEdit(); 351. } 352. }else 353. { 354. sz = -1; 355. if (sx > 0) 356. { 357. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); 358. sx = sy = -1; 359. } 360. } 361. break; 362. case CHARTEVENT_OBJECT_ENDEDIT: 363. switch (obj) 364. { 365. case MSG_LEVERAGE_VALUE: 366. case MSG_TAKE_VALUE: 367. case MSG_STOP_VALUE: 368. dvalue = StringToDouble(ObjectGetString(GetInfoTerminal().ID, m_Info.szObj_Editable, OBJPROP_TEXT)); 369. if (obj == MSG_TAKE_VALUE) 370. m_Info.FinanceTake = (dvalue <= 0 ? m_Info.FinanceTake : dvalue); 371. else if (obj == MSG_STOP_VALUE) 372. m_Info.FinanceStop = (dvalue <= 0 ? m_Info.FinanceStop : dvalue); 373. else 374. m_Info.Leverage = (dvalue <= 0 ? m_Info.Leverage : (short)MathFloor(dvalue)); 375. AdjustTemplate(); 376. obj = MSG_NULL; 377. ObjectDelete(GetInfoTerminal().ID, m_Info.szObj_Editable); 378. break; 379. } 380. break; 381. case CHARTEVENT_OBJECT_CLICK: 382. if (sparam == m_Info.szObj_Chart) if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft)) switch (obj = CheckMousePosition(x = (short)lparam, y = (short)dparam)) 383. { 384. case MSG_DAY_TRADE: 385. m_Info.IsDayTrade = (m_Info.IsDayTrade ? false : true); 386. DeleteObjectEdit(); 387. break; 388. case MSG_MAX_MIN: 389. m_Info.IsMaximized = (m_Info.IsMaximized ? false : true); 390. DeleteObjectEdit(); 391. break; 392. case MSG_LEVERAGE_VALUE: 393. CreateObjectEditable(obj, m_Info.Leverage); 394. break; 395. case MSG_TAKE_VALUE: 396. CreateObjectEditable(obj, m_Info.FinanceTake); 397. break; 398. case MSG_STOP_VALUE: 399. CreateObjectEditable(obj, m_Info.FinanceStop); 400. break; 401. } 402. if (obj != MSG_NULL) AdjustTemplate(); 403. break; 404. case CHARTEVENT_OBJECT_DELETE: 405. if (sparam == m_Info.szObj_Chart) macro_CloseIndicator(C_Terminal::ERR_Unknown); 406. break; 407. } 408. ChartRedraw(); 409. } 410. //+------------------------------------------------------------------+
C_ChartFloatingRAD.mqh(抜粋)
314行目や341行目など、一部の行が取り消し線で示されていることに注意してください。これらの行は、346行目にあるテスト内へ移動されました。この調整により、特定のコントロールをクリックした際に発生していた安定性の問題が解消されます。変数szは各オブジェクトで使用されており、その様子は297、300、303、306、309行目、および312行目と336行目の条件テストで確認できます。
前のバージョンと比較すると、この修正は特にマウスインジケーターとChart Tradeの相互作用を安定させることを目的としています。以前は、マウスインジケーターが先にロードされると、一部のChart Tradeコントロールが正しく反応しないことがありました。この問題の回避策は、インジケーターをチャートから一度削除して再挿入することだけでした。その後でようやくコントロールが正しく動作するという、やや奇妙な挙動でした。
このため、CHARTEVENT_OBJECT_CLICKイベントは処理コードから削除する必要があります。前回の記事で示したコードの381行目から403行目までのすべての行を削除してください。これらの変更は、これまでに説明した内容に影響を与えないため、この記事の本題に進むことができます。
メッセージプロトコル設計の背景を理解する
読者の皆さんがコンピュータ通信システムについてどの程度ご存知かを前提にすることはできません。すべての読者が理解できるようにするため、ここでは基礎から説明します。すでにこうしたプロトコルの経験がある方は、わかりやすい部分も多いかと思いますので、その場合は次のセクションに進んでください。
前回の記事では、数値を文字列に変換して送信する必要がある理由を説明しました。たとえば、バイナリ値「00010011」を送信する場合、まず文字列「19」に変換する必要があります。つまり、1バイトではなく2バイトを送信することになります。一見非効率に思えますが、ここでの目的は効率ではなく、受信側で正確に情報を理解してもらうことです。もちろん効率も望ましいですが、正確性が優先されます。
前回説明したように、ここではsparamフィールドを使用します。ここでの課題は、1つの文字列内に情報をすべて格納する場合、どこで1つのデータが終わり、次のデータが始まるのかをどのように判定するか、という点です。これは通信プロトコル設計において非常に重要なポイントです。
このための戦略を設計することが重要です。データを正確に抽出できる形式を設計することが不可欠です。いくつかのアプローチが考えられ、それぞれに利点と欠点があります。ひとつの方法は、各フィールドを固定長の配列として定義し、これらを連結して送信文字列を作る方法です。このアプローチには利点と欠点があります。
固定長配列を使う利点は、インデックスが簡単になる点です。各ブロックは常に同じサイズだからです。しかし、フィールドが割り当てられた容量を完全に使用しない場合、メモリや帯域を無駄にしてしまいます。実際、空の部分も帯域(ここではメモリ)を消費します。
より明確にするために、以下の図をご覧ください。

青いブロックは文字列の終端を示すNULL文字です。配列の一部に空の位置があり、スペースが無駄になっているのがわかります。このような状況では、固定長配列の使用は非効率的です。
一方、可変長配列を使用し、データに応じて正確なサイズを割り当てる方法もあります。これにより、無駄なスペースを避けられ、データの抽出も容易になります。しかし、コードが複雑化し、作成後のテスト作業も増加します。作成したコードをテストする手間も増えます。また、データが予想サイズを超えた場合、情報が失われるリスクも生じます。図2はこの方法の理想的なイメージです。

ご覧の通り、最初の画像では、青いブロックが行の終端を示す文字を表しています。ここでは、各色が行に圧縮される情報を示す理想化された状態です。場合によっては文字列の抽出が複雑になることもありますが、元の情報を完全かつ正しく復元するには十分適切です。しかし、この場合も各セットは固定サイズになります。これにより、最初の図よりも高度な方法が示されています。しかし、問題が発生する場合もあります。たとえば、2バイトを想定していたフィールドが突然3バイト必要になった場合です。
これは最悪のパターンです。この場合、緑色のセットを組み立てる過程で1バイトが失われてしまいます。では、緑色のセットを拡張して追加のバイトを格納できるでしょうか。答えは「いいえ」です。拡張すると、緑色セット以降のすべての情報が破損し、受信側は緑色セットに3バイトあることを理解できず、2バイトしかないと誤解してしまいます。
この例から、プロトコル設計が簡単ではないことが分かります。受信側が固定サイズを期待している場合に送信側がサイズを変更すると、通信は失敗します。したがって、別のアプローチを取る必要があります。
1つの方法として、各フィールドを必要なだけのサイズにできる可変長ブロックを使うことが考えられます。これであれば柔軟性は高くなります。しかし、新たな課題も発生します。受信側は、どこで1つのフィールドが終わり、次のフィールドが始まるのかをどのように判断すればよいでしょうか。このためには区切り文字やマーカーが必要です。しかし注意しなければ、送信側では正しくても受信側で正しく理解されないメッセージが送られてしまう可能性があります。なぜ受信側で理解されにくくなるのでしょうか。
考えてみてください。メッセージの各セクションは任意のサイズを取ることができ、ほぼすべての種類の情報を送信可能です。情報は必ず一定の順序で並べる必要がありますが、その順序は常に守る必要があります。ここまでは問題ありません。しかし、
1つのセクションが終わり次のセクションが始まることをどのように示すかが最も難しい部分です。文字列に格納するデータの種類によって対応が異なります。1バイトで使用可能な255の値(NULL文字を避けるため256ではなく255)すべてを使う場合、ブロック内で別の情報を示す方法が問題となります。値を32〜127の文字に制限すると、追加の組み立て作業が必要ですが、128〜255の値をマーカーとして利用できます。
さらに制限することも可能です。必要な情報を英数字のみで伝え、句読点などの記号を区切り文字として予約する方法です。ここで可能な理由は、送信するデータが比較的単純だからです。資産名やレバレッジ、TP/SL(テイクプロフィット/ストップロス)レベルなどの数値情報を送信するだけです。これらはChart Tradeで設定されますが、エキスパートアドバイザー(EA)に正確に送信する必要があります。
加えて、これらの簡単な値に加え、操作種別も送信します。MetaTrader 5ですでに処理されますが、通信を確実に管理するために含めます。これは必須ではありません。
通信は必ずしも1つのターミナル内に限定されません。適切なネットワークプロトコルを用いれば、1台のコンピュータがターミナルを実行し、別のコンピュータが注文やポジションを管理することも可能です。比較的非力なマシンでも、強力なマシンと同等に取引を処理できます。
ここではさらに詳しい説明はおこないません。あくまで、実際の接続がどのように動作するかを示すことが目的です。
両方の長所を組み合わせる
今回採用する解決策は、固定長と可変長の両方の長所を組み合わせたものです。データは英数字文字列として送信し、区切り文字で明確化しつつ、インデックスも可能にします。各フィールドは必要な文字数を確保できるため、情報が失われることはありません。
前回のコード断片の338行目を参照してください。さらにわかりやすくするために、実際の送信例を見てみましょう。

図3は、Chart Tradeの送信例を示しています。一見するとメッセージは複雑に見えますが、理解するためには断片の338行目で何が行われているかを確認する必要があります。このメッセージは明確なプロトコルに従っています。基本的に、ここでは「両方の長所を組み合わせている」と言えます。ブロックは任意のサイズにできる一方で、行内の情報を特定の方法でインデックス化しています。
インデックスの仕組みは一目で理解できないかもしれませんが、存在しています。メッセージ内の「D」記号に注目してください。この文字は、前後に同じ文字が配置されています。この時点で、このブロックには「D」だけが含まれた1つのブロックが存在しています。この状況はメッセージ内の他の場所では発生していません。つまり、この単一文字は何らかの方法でインデックス化可能であることを示しています。詳細は後ほど説明します。現時点では、ここで何が起きているかを理解することに集中してください。
このメッセージの最初のブロックは、今回の例では1文字のみですが、実行する操作の種類を示しています。再度言いますが、MetaTrader 5はこの情報をEAに提供します。ここでは、Chart TradeがEAと通信することを前提にしています(ネットワーク経由、メール、その他手段を含む)。そのため、実行する操作の種類を明示的に指定しています。
注目すべき点として、値「9」はマーケットでの買いイベントに対応します。しかし、この値は状況により異なる場合があります。たとえば、ブロックが「11」を含む場合、これは全ポジションの決済を意味します。この場合、ブロックは1文字ではなく2文字になります。では、なぜ「9」が買いで、「11」が全決済なのか?その値の由来はどこから来るのか?これは非常に妥当な疑問です。338行目を見ると、文字列の最初に配置される値はushort型です。しかし、これだけでは、なぜ9が買いで、11が全決済であるかは説明できません。
では、278行目を見てください。この値はどこから来るのでしょうか。これは、Defines.mqhヘッダーファイルに由来します。Defines.mqhにはEnumEventsという列挙型があり、ゼロから開始し、新しい要素ごとにコンパイラが値を1ずつ増加させます。最初のイベント「evHideMouse」から数えて、9番目のイベントがevChartTradeBuy、11番目のイベントがevChartTradeCloseAllです。つまり、文字列の先頭に現れる値は、EnumEvents列挙型に由来しています。
次に、すべての「?」記号が紫色で強調されていることに注目してください。文字列の終端を示すNULL文字は青色で表示されています。各ブロックは「?」で区切られているため、必要なだけの英数字を挿入してメッセージを送信可能です。ただし、重要な点があります。このメッセージは特定の順序で構築される必要があります。受信側は、特定の順序でデータを受信することを期待しています。理論上は情報をランダムな順序で配置できますが、受信側(ここで示すバージョン)はそれを期待していません。
次のブロックは、注文対象となる資産の名前を提供します。繰り返しになりますが、EAとChart Tradeが同じチャート上で動作している場合、この情報は不要です。しかし、ここでは別のケースを想定しています。
さらに将来的には、このメッセージの詳細は他の目的にも利用可能です。例では資産は「BOVA11」というETFです。区切り文字がない場合、資産名は複雑になります。市場によっては資産記号が4文字の場合もあれば5文字の場合もあります。この例では5文字です。ブラジル証券取引所(B3)でも、4文字の銘柄が多く使われます。
もう一つ注目すべき点があります。ここでの目的は、Chart Tradeをリプレイ/シミュレーションでも利用可能に設計することです。この場合、銘柄名は任意の文字数の英数字で構成される可能性があります。そのため、動的サイズのブロックは非常に望ましいです。
再び「D」文字に戻ります。338行目を再度確認してください。操作が同日決済(デイトレード)ではない場合、このブロック内の文字は「S」に置き換わります。任意の文字を選ぶことも可能ですが、受信側も更新しなければ通信が失敗します。
この直後に、リテラル値「250」があります。これは何を意味するのでしょうか。再び338行目を確認すると、この値は希望するレバレッジレベルを表しています。ここで興味深い点は、レバレッジ値を表すために3文字の数字を使用していることです。
バイナリ値を使うことはできないのでしょうか。レバレッジはゼロにならないため、一見適切に思えます。しかし、この方法には制約があります。問題は、文字列内で使用する区切り文字自体にあります。ASCII表で「?」の値を確認すると、63です。
わかりやすくするために、文字0から文字128までのASCIIテーブルを以下に記載しました。

この63が重要な理由は、もしレバレッジ値に63が含まれると、受信側(後ほど示します)がそれを区切り文字として解釈してしまうためです。つまり、4番目のブロックがレバレッジを示す部分であっても、受信側は正しく認識できなくなります。
「では、63を加えてオフセットすればよいのでは?」と思うかもしれません。理論上はレバレッジはゼロにならないため、最初の有効値は64から始まります。問題解決に見えますが、そう簡単ではありません。63をレバレッジに加えると、単に問題を先送りしているだけだからです。
なぜそうなるのでしょうか。疑問に思うかもしれません。「何が問題なのでしょうか。63を加えれば、すべての値は63以上になるのでは?」確かにそうです。しかし、問題の本質はここにあります。プログラミングにおいて、どの値も無限ではありません。すべての値には上限があり、その上限は使用しているワードサイズによって決まります。たとえ64ビットプロセッサ(現在のMetaTrader 5が使用するような)であっても、文字の扱いは8ビット単位に依存しています。
たとえ64ビットのプロセッサであっても、文字としては実際に2^64 (18,446,744,073,709,551,615)まで数えることはできません。文字として扱える最大値は、あくまで255 (2^8)です。なぜでしょうか。これを回避することは可能なのでしょうか。可能です。この問題を回避する方法のひとつとして、ASCII以外の文字セットを使用することが考えられます。たとえばUnicodeです。
しかし別の問題があります。MQL5のStringFormatは、少なくとも執筆時点ではUnicodeを使用しません。MQL5の文字列関数は、C/C++の原則に従いASCIIを使用します。C/C++自体はUnicodeを扱えますが、元々はASCIIベースでした。
そのため、63を加えたとしても、255ごとに「複合値」を生成する必要があります。この複合値は、カウントサイクルの回数(係数)と現在のカウント値の組み合わせで表されます。たとえば、値575は「係数2」に63を加えたものです。。
正確に表現するためには、2バイトが必要です。1バイト目は係数、すなわち最大値255に達した回数を示し、2バイト目が63となります。この計算に伴う数学的な詳細は、本記事の範囲外とします。
メッセージプロトコルを構築する際には、このように文字コードや区切り文字の制約を考慮する必要があります。さらに、プロトコル内には、doubleやfloatで表現できる値も2つ含まれています。しかし、レバレッジと同様の理由で、これらの値もリテラルとして文字列に書き出されます。そのため、画像に示されている形式のまま送信されます。
ここで、なぜこれらの値はこのようになるのか、それらは何を表しているかと疑問に思うかもしれません。奇妙に思えるのは、コードスニペットの338行目を確認し忘れているからかもしれません。この行では、通貨価値がポイントに変換されていることがわかります。たとえば、3.60は$900に、3.02は$755に対応しています。
通貨価値を直接送信せず、ポイントで表す理由は単純です。事前に変換された値を使った方が、EAの実装が簡単になるためです。現時点では完全には理解できないかもしれませんが、後でその利点が明確になります。事前に変換した値を直接EAに送信する利点について、さらに詳しく解説する予定です。しかし、これは将来の話です。
結論
本記事では、通信プロトコルをどのように作成するかについて、できる限り詳細に説明してきました。まだ完全に説明し終えたわけではありません。というのも、受信側の処理部分、すなわち受信メッセージを処理する部分については、まだ確認する必要があるからです。しかし、この記事の主な目的は達成できたと考えています。それは、通信プロトコルを設計する際に注意すべき理由、特にここで示した方法と異なる方式を採用する場合に注意が必要であることを理解してもらうことです。
これは、今すぐに計画しておくべき事項です。後回しにすると、プロトコルが情報を正しく伝達できずに苦労することになります。そして、この情報はEAが何をどのように実行すべきかを判断する上で不可欠です。後回しにせず、今すぐ学び、必要だと思う調整を実装し始めてください。次回の記事では、ついに受信側、すなわちEA自体について見ていきます。
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12476
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
取引におけるニューラルネットワーク:マルチエージェント自己適応モデル(最終回)
初級から中級まで:定義(II)
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
取引におけるニューラルネットワーク:マルチエージェント自己適応モデル(MASA)
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索