リプレイシステムの開発(第78回):新しいChart Trade(V)
はじめに
前回の「リプレイシステムの開発(第77回):新しいChart Trade (IV)」では、できる限り丁寧に、しかし複雑になりすぎないように注意しながら説明しました。今回のテーマは「通信プロトコルをどのように開発するか」です。これは、異なるアプリケーションやプログラム間で情報をやり取りする際に不可欠な知識です。プログラムが同じ環境内で動作している場合でも、異なる環境で動作している場合でも必要となります。今回の具体的なケースでは、Chart Tradeインジケーターがエキスパートアドバイザー(EA)に対して、どのような操作をおこなうべきか指示できるようにします。つまり、ユーザーがChart Tradeで「売り」を指示すれば、EAは売り注文を実行します。「買い」を指示すれば、EAは成行買い注文を発注します。
最近の記事では主に、Chart Tradeインジケーターを作成する理由やプログラミングを開始する前にメッセージプロトコルを設計する方法について解説してきました。しかし、ここまではインジケーター側にのみ焦点を当ててきたため、説明としては十分ではありませんでした。
この説明を補完するには、まず受信側、すなわちEAの実装を確認する必要があります。なぜなら、MetaTrader 5ではインジケーターが直接注文を送信したり、ポジションを管理したり、取引サーバーと通信するシステムに干渉することを明示的に禁止しているからです。つまり、インジケーター単体ではポジションの開設や決済、変更をおこなうことはできません。
抜け道が存在すると主張する人もいるかもしれません。しかし実際には全く異なる仕組みであり、これは今後の記事で開発する別のツールで詳しく解説する予定です。本記事では、まだ注文システムの構築初期段階にあるとご理解ください。
現時点での主な関心事は、EAが状況をどのように把握するかという点です。これは非常に重要です。ユーザーはEAに直接操作をおこなうことはなく、あくまでChart Tradeインジケーターを通じて操作をおこないます。Chart Tradeはその指示をEAに伝える必要があります。インジケーターだけでは、実際に市場に注文を送信する役割を持つEAが状況を把握できなければ意味がありません。繰り返しになりますが、MetaTrader 5では、注文を実行できるのはEAだけであり、他のプログラムはこれをおこなうことができません。
したがって、本記事の中心となる問いは以下の通りです。
EAはChart Tradeをどのように理解するのか?
まず、この非常に基本的な問いから始めましょう。もしまだこの仕組みがよくわからない場合は、前回の記事に立ち戻ることをおすすめします。そこでは、メッセージプロトコルの設計方法を解説しました。この基礎知識は不可欠です。なぜなら、ここでは「魔法のように」動作させる方法を見ていくからです。Chart TradeインジケーターもEAも、互いの存在を認識していません。そして、認識する必要もありません。両者に必要なのは、メッセージが確実に届けられ、正しく理解されると信頼することだけです。
たとえチャート上EAが存在していることを知らなくても、Chart Tradeインジケーターはユーザーの意図をEAに伝えることができます。このプロセスは非常に興味深く観察することができます。
さらに重要なのは、この仕組みを理解すると、MetaTrader 5向けにより汎用性の高いプログラムやアプリケーションを設計できるようになるという点です。この知識は、MetaTrader 5の枠を超えて応用可能です。
そうです。このようなプログラムやプロセス間のメッセージ交換は、WindowsやLinux、macOSといったオペレーティングシステムでも広く用いられています。現代のシステムはすべてこの原則に依存しています。複数の小さなプログラムを開発し、それらが互いに通信できるようにすることで、広範で持続可能なエコシステムが形成されます。
各アプリケーションは特定のタスクに最適化できます。しかし、組み合わせることで、ほぼ無限のことを低コストで実現できるのです。実装、保守、改良のコストも抑えられます。
ここから先は、大規模で複雑なオールインワン型アプリケーションを作る考え方から離れることをおすすめします。これらは保守や更新が煩雑になりがちです。代わりに、より小さくシンプルなプログラムを作ることを考えましょう。このシンプルさが俊敏性を生み、アプリケーションの改良や改善が容易になります。多くの人にとって、これは新しいことかもしれません。しかし、業界の大多数はこの方法で開発しています。完全なモノリシックシステムを構築することは、コストが高く、非実用的だからです。特に、変更や改良が必要なときには大きな負担となります。
シンプルなアプリケーションは、デバッグ、調整、最適化がはるかに容易です。このため、EAはMetaTrader 5で必要な操作、すなわち注文やポジションの送信や変更のみを担当します。それ以外の処理は、他のプログラムやアプリケーションに委任されます。
では、EAがChart Tradeをどのように解釈するのかを真に理解するために、まず非常にシンプルなコードを作成してみましょう。本当にシンプルです。この段階では、注文を送信したり、ポジションを決済したり、取引を反転させたりすることはありません。今それをおこなうと、かえって複雑になってしまいます。
必要なのは、受信側がどのように動作するかを理解するための、可能な限りシンプルな例です。これにより、Chart Tradeインジケーター(EAの存在を知らない)が、どのようにしてEAに動作を促すのか、メッセージプロトコルの仕組みが明確に理解できるようになります。
同時に、EAもChart Tradeの存在を知らなくても、ユーザーのリクエストに応答できます。繰り返しますが、ユーザーはEAに直接操作をおこなうことはありません。
この目的を達成するための最もシンプルなコードを以下に示します。完全な形で掲載しています。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. #property description "Demo version between interaction" 04. #property description "of Chart Trade and Expert Advisor" 05. #property version "1.78" 06. #property link "https://www.mql5.com/pt/articles/11760" 07. //+------------------------------------------------------------------+ 08. #include <Market Replay\Defines.mqh> 09. //+------------------------------------------------------------------+ 10. class C_Decode 11. { 12. private : 13. struct stInfoEvent 14. { 15. EnumEvents ev; 16. string szSymbol; 17. bool IsDayTrade; 18. ushort Leverange; 19. double PointsTake, 20. PointsStop; 21. }info[1]; 22. public : 23. //+------------------------------------------------------------------+ 24. C_Decode() 25. { 26. info[0].szSymbol = _Symbol; 27. } 28. //+------------------------------------------------------------------+ 29. bool Decode(const int id, const string sparam) 30. { 31. string Res[]; 32. 33. if (StringSplit(sparam, '?', Res) != 6) return false; 34. stInfoEvent loc = {(EnumEvents) StringToInteger(Res[0]), Res[1], (bool)(Res[2] == "D"), (ushort) StringToInteger(Res[3]), StringToDouble(Res[4]), StringToDouble(Res[5])}; 35. if ((id == loc.ev) && (loc.szSymbol == info[0].szSymbol)) info[0] = loc; 36. 37. ArrayPrint(info, 2); 38. 39. return true; 40. } 41. //+------------------------------------------------------------------+ 42. }*GL_Decode; 43. //+------------------------------------------------------------------+ 44. int OnInit() 45. { 46. GL_Decode = new C_Decode; 47. 48. return INIT_SUCCEEDED; 49. } 50. //+------------------------------------------------------------------+ 51. void OnTick() {} 52. //+------------------------------------------------------------------+ 53. void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) 54. { 55. switch (id) 56. { 57. case CHARTEVENT_CUSTOM + evChartTradeBuy : 58. case CHARTEVENT_CUSTOM + evChartTradeSell : 59. case CHARTEVENT_CUSTOM + evChartTradeCloseAll: 60. (*GL_Decode).Decode(id - CHARTEVENT_CUSTOM, sparam); 61. break; 62. } 63. } 64. //+------------------------------------------------------------------+ 65. void OnDeinit(const int reason) 66. { 67. delete GL_Decode; 68. } 69. //+------------------------------------------------------------------+
EAのソースコード
正直言って、先ほどのコードが「シンプル」と言われても、そう感じられない方も多いでしょう。私も最初に見たときは、非常に複雑に見え、ほとんど理解できませんでした。もしこれが「シンプル」だとすれば、本当に複雑なコードは一体どうなるのか想像するしかありません。確かに、このコードは比較的シンプルです。しかし、普段あまり目にしない要素を使っているため、多くの方にとっては馴染みがないかもしれません。あるいは、MQL5で可能であることすら気づかない要素かもしれません。もしそうであり、将来的にプロのプログラマーになりたいと本気で考えているのであれば、ぜひこのまま読み進めてください。これからおこなう解説を理解すれば、見た目よりずっとシンプルであることがわかるはずです。このコードは非常に直接的で、必要な動作を正確に実現しています。
さらに、ここで学ぶ知識は、落ち着いて物事を考え、問題に対処する力を養う助けにもなります。また、プログラミングは楽しむものであるべきだということを思い出させてくれます。もしプログラミングをただ退屈で難しく、複雑な作業だと捉えているなら、別のことを検討したほうがよいかもしれません。プログラミングするときは、まるでお菓子屋さんの子どものように、どのお菓子を最初に選ぼうか迷う楽しさを感じるべきです。
さて、脱線はこのくらいにして、コードが実際にどのように動作するか見ていきましょう。最初の7行は誰でも理解できるはずです。初心者でも簡単に追うことができます。しかし、8行目は少し馴染みのある構文です。この行ではコードにファイルをインクルードしています。このファイルはヘッダーファイルで、後ほど必要となる定義が含まれています。なお、10行目から42行目までのコードは、ここでは一旦飛ばします。後ほど丁寧に解説しますので、まずはいくつかの小さいですが重要なポイントを押さえましょう。
ここで、コード内で最も重要な関数、OnInitにたどり着きます。これは44行目から始まります。42行目の処理の影響で、予想とは少し異なる動作になる場合があります。46行目ではnew演算子を使ってC_Decodeクラスのメモリを確保し、初期化しています。なぜわざわざメモリを手動で確保するのでしょうか。コンパイラに任せれば簡単ではないかと思われるかもしれません。確かに簡単です。しかし、プログラマーとしては、クラスがいつ、どこで使われるかを自分で制御できることが重要です。コンパイラ任せにすると、予期しない値でクラスを使用してしまうことがあります。
特に、大規模なコードベースではクラスが複数箇所で宣言されているため、混乱は避けられません。ある時点でコードが複雑に絡み合うこともあります。newとdeleteを明示的に使うことで、コードの開始と終了を完全に制御できるのです。多くの初心者プログラマーは、この概念に苦労します。奇妙に聞こえるかもしれませんが、自分のコードがどこで終わるのか、あるいはどこから始まるのかが明確に分からない人もいます。しかし実際にそういうことは起こります。
46行目が実行されると、C_Decodeクラスのメモリが確保され、同時に24行目、すなわちC_Decodeクラスのコンストラクタが呼び出されます。このコンストラクタでは、1つの変数だけが初期化されます。それが銘柄名、つまりEAが動作する資産の名前です。ここで注意してください。このモデルでは、クロスオーダー(複数銘柄間での操作)は許可されていません。この点はまだ完全には理解できないかもしれませんが、読み進めれば、なぜクロスオーダーが許可されていないのかが分かります。ごく簡単な修正をおこなえばクロスオーダーを有効にできますが、現段階では気にする必要はありません。このコードはあくまでメッセージプロトコルの動作を示すためのもので、取引サーバーに実際のリクエストを送信する目的ではありません。
OnInit関数に戻ります。46行目の処理が終わると、48行目で初期化が成功したかどうかを示す値を返します。これは非常に重要です。返り値がINIT_SUCCEEDEDでない場合、MetaTrader 5は自動的にEAを無効化する手順を実行します。その一つがDeinitイベントを呼び出すことで、EA内のOnDeinit手続きが実行されます。
コード内にこの手続きがなければ、MetaTrader 5はデフォルトの処理をおこないます。いずれにしても、アプリケーションはCPU時間の割り当てを失い、実行スケジュールから外されます。
また、チャート上に残されたアプリケーション固有の情報も無視されます。多くのアプリケーションはチャート上にオブジェクトを生成して配置しますが、Deinitが発動し、コードがそれらを削除しない場合、オブジェクトはチャート上に残り、誤った情報として表示されることがあります。
したがって、OnDeinit手続きを必ず実装することが推奨されます。多くの場合、その内容は非常に少ないものですが、今回の場合は67行目に重要な処理があります。ここではdelete演算子をを使い、先ほど確保したメモリを解放します。この時点でC_Decodeのデストラクタが呼び出されます。デストラクタを明示的に宣言していなくても、deleteを使用することでコンパイラが自動的に生成してくれます。delete演算子にはデストラクタが必要だからです。細かいことは気にする必要はありませんが、内部でどのように動作しているか知っておくのは有益です。
これで、EAコードの開始と終了がどこかを理解できました。次に進みましょう。51行目では、資産のティックを受信するたびに呼び出される手続きがあります。これはすべてのEAに必須の手続きです。しかし、この中にあまり多くのロジックを入れすぎないようにしましょう。その理由は、自動売買EA構築シリーズの記事で詳しく解説されています。ある程度の自動化を伴うEAを作りたい場合は、その連載を読むことを強くおすすめします。
この連載は「自動的に動作するEAを作る(第1回):概念と構造」から始まる全15記事です。この連載を読むことで、大きく理解が進みます。実際、このリプレイ/シミュレーションシリーズでも、自動売買EAシリーズのいくつかの概念を再利用しています。どちらにしても、省略せずに一通り目を通してください。
さて、次に重要な手続きは53行目にあります。それがOnChartEventです。ここからが本当に面白くなってきます。しかし、整理して解説するため、詳しい内容は次のセクションで説明します。
イベントからメッセージを解読する方法
このリプレイ/シミュレーターシリーズの以前の記事では、EventChartCustomとOnChartEventがどのように連携しているかを解説しました。私たちはこの連携を活用し、リプレイ/シミュレーターサービスからコントロール用インジケーターへデータを送信できるようにしました。これは、インジケーターがシミュレーションやリプレイのどの段階にあるのかを正確に把握するために必要です。もしこれらの記事を読んでいない場合(連載のこの時点で初めて読んでいる場合)は、ぜひ過去の記事に戻って確認してください。
一例として「リプレイシステムの開発(第60回):サービスの再生(I)」があります。このテーマについては全7記事があり、すべて読むことで、技術的な詳細だけでなく、このアプローチがなぜ理にかなっているのかも理解できます。
当時、サービスは数値パラメータを使ってコントロール用インジケーターと通信していました。これは比較的簡単でした。なぜなら、lparamやdparamといったパラメータに情報がそのまま入っていたからです。しかし、今回の方法は少し異なります。今回はsparamパラメータを使用します。つまり、メッセージは文字列で送信され、その中に必要な情報が含まれています。ポイントは、この情報がエンコードされていることです。どのようにエンコードされているかは、メッセージプロトコルによって定義されています。この概念は前回の記事で解説しました。ここでは、そのメッセージをプロトコルに従ってデコードする方法に焦点を当てます。
前回の記事で、メッセージにはイベントそのものが含まれると述べました。これは重要です。なぜなら、受信側、つまりEAがメッセージが破損していないかどうかを確認できるからです。
同じプロトコルは3つの認識済みイベントタイプすべてで使用されるため、すべてを一貫して処理できます。57~59行目では、EAがインターセプトすべき3つのイベントが確認できます。そして、すべて同じデコード処理を通過できるため、実際に重要なのは60行目だけです。その行は29行目から始まるコードを呼び出しています。ここは非常に重要です。もしすぐに理解できなくても、もう一度読み返してください。珍しいケースではありますが、極めて重要です。
31行目では、ローカル変数として動的配列を宣言しています。動的配列なので、メモリの割り当てはプログラムが自動的におこないます。そのため、特に心配する必要はありません。次に33行目でStringSplitを呼び出します。これにより、指定した文字で区切られたサブ文字列が配列に格納されます。今回の区切り文字は疑問符(?)です。なぜこの文字を選んだのでしょうか。
この点については前回の記事で解説しました。疑問符は値のグループ化のための区切り文字として選ばれています。分割処理によって、固定数のサブ文字列が得られることを期待しています。もしメッセージが有効(あるいは最低限有効)であれば、6つの文字列が得られるはずです。StringSplitが6つより多い、または少ない文字列を返した場合、テストは失敗し、falseを返します。6つぴったりであれば、次の処理に進みます。
そして34行目が、本当に興味深い部分です。普段あまり見ないコードですが、特殊というわけではありません。理解するためには、13行目で宣言された構造体と並べて考える必要があります。ここに注意してください。34行目の各要素は、stInfoEvent構造体のフィールドに直接対応しています。私は意図的にこのように書きました。stInfoEventで宣言された変数の順序に注目してください。これは非常に重要です。次に34行目の値の順序を見てください。見事に一致しています。結果として、より明示的に書くと次のようなコードと同等です。
stInfoEvent loc; loc.ev = (EnumEvents) StringToInteger(Res[0]); loc.szSymbol = Res[1]; loc.IsDayTrade = (Res[2] == "D"); loc.Leverange = (ushort) StringToInteger(Res[3]); loc.PointsTake = StringToDouble(Res[4]); loc.PointsStop = StringToDouble(Res[5]);
コードスニペット:モデル01
この2つのアプローチは、機能的には同じです。しかし、重要な注意点があります。34行目にはリスクが伴います。このリスクは、前述のコードバージョンでは回避されています。このリスクは、stInfoEvent構造体を変更した場合に発生します。なぜでしょうか。34行目のようにコードを書く場合、構造体を変更すると問題が生じますが、先ほどの明示的なコードでは問題になりません。
理由は非常に単純です。一見些細なことに思えるかもしれませんが、実際には多くの頭痛の種になり、なぜすべてがうまくいかないのかを何時間も考える羽目になります。たとえば、19行目と20行目で宣言された変数を入れ替えたとします。先ほどの明示的なコードであれば、何も壊れず、代入は正しくマッピングされます。
しかし、34行目のコンパクトな代入方式では、すべての順序がずれてしまいます。本来PointsTakeに代入されるべき値がPointsStopに入り、その逆も同様です。冗談のように聞こえるかもしれませんが、実際に起こる現象です。
これはコンパイラがメモリ内で変数をどのように整理するかに関係しています。この原理は、古いC言語プログラムで意図的に使用されることもあります。開発者がメモリレイアウトを直接操作して特定の動作を強制するのです。しかし、これは本記事の範囲を超えています。
ここでの結論は簡単です。コンパクトな代入スタイルを使うのは、構造体が完全に定義され、最終化された後に限定してください。そして、一度最終化したら構造体を変更しないことが重要です。もし変更する場合は、34行目のようなコンパクト代入をすべて見直して調整する必要があります。テスト用にこのコードを使い、34行目やstInfoEvent構造体内の変数宣言順序を変更すると、その影響を確認できます。
コードに戻りましょう。35行目では、データが期待通りの値と一致しているかを確認します。具体的には、資産名とイベントタイプです。一致すれば、値を静的配列に代入します。注意:この代入はあくまでテストおよびデモ用です。現時点では、stInfoEventを配列に格納できれば十分です。その理由は37行目にあります。この行では、MQL5の関数ArrayPrintを使用しています。この関数は配列の内容をターミナルに表示します。結果の一例は以下の画像の通りです。

結論
本記事で解説した通り、EAはまだ取引サーバーにリクエストを送信することはできません。これは意図的な設計です。その前に、コードがどのように動作するのか、そして何よりも、Chart TradeとEA間の通信プロトコルがどのように機能するのかを完全に理解する必要があります。この基礎がないまま進めると、重大な問題に直面するリスクがあります。
とはいえ、この段階でこのように提示することには利点があります。その過程で、将来的に遭遇する可能性のある概念について解説できました。最初はなぜそれが機能するのか、あるいは機能しないのか理解しにくいかもしれません。しかし、ともあれ、次に進むための基礎ブロックは整いました。以下の動画では、システムが実際にどのように動作するかをご覧いただけます。それでは、次回の記事まで、さらに面白くなる内容をお楽しみに。
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/12492
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
高度なICT取引システムの開発:オーダーブロックインジケーターでのシグナルの実装
取引におけるニューラルネットワーク:Attentionメカニズムを備えたエージェントのアンサンブル(MASAAT)
取引におけるニューラルネットワーク:Attentionメカニズムを備えたエージェントのアンサンブル(最終回)
取引におけるニューラルネットワーク:マルチエージェント自己適応モデル(最終回)
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索