English Русский Deutsch
preview
MQL5入門(第35回):MQL5のAPIとWebRequest関数の習得(IX)

MQL5入門(第35回):MQL5のAPIとWebRequest関数の習得(IX)

MetaTrader 5統合 |
14 0
ALGOYIN LTD
Israel Pelumi Abioye

はじめに

連載「MQL5入門」の第35回へようこそ。前回の記事では、プロジェクトのフロントエンドとして、MetaTrader 5チャート上へのインタラクティブなコントロールパネルの構築に焦点を当てました。パネルのレイアウト作成、ボタンや入力ボックスの配置、パネル内へのテキスト表示方法について学びました。この時点では、パネルはまだ外部サービスとは連携しておらず、あくまで表示用のインターフェースに留まっていました。 今回は、このコントロールパネルをバックエンドロジックへ接続し、プロジェクトをさらに一歩前進させます。主なテーマは、チャートイベントを用いたユーザー操作の処理、送信ボタンのクリック検出、そしてWebRequest関数を使用した外部APIへのユーザーデータ送信の準備です。

さらに、サーバーからの応答を取得してパネル表示用に加工するための基本的なフレームワークについても解説します。 これまで同様、すべての概念を網羅的に説明するのではなく、実装に必要な要点に絞って解説します。これにより、不要な情報に惑わされることなく、実践的に学習を進めることができます。記事が終わるころには、あなたのコントロールパネルは単なる静的なUIではなくなり、外部APIサーバーと連携し、ユーザー入力に応じて動的に反応するようになっています。


チャートイベントを使用したボタンクリックの検出

このセクションでは、MetaTrader 5がどのようにユーザー入力を検出し、それに応答するのかについて解説します。MetaTrader 5では、ボタン(たとえばパネル内の送信ボタン)がクリックされたとしても、それが直接ボタン自身の処理として実行されるわけではありません。その代わりに、チャートイベントが生成されます。チャート上でマウスクリックやキー入力、あるいはUIコントロールとのやり取りが発生すると、MetaTrader 5はそれをチャートイベントとしてプログラムに送信します。 重要なのは、ボタンや各種コントロールはそれ自体で処理ロジックを持つのではなく、ユーザーが何らかの操作をおこなったという事実を通知する役割に限定されています。そのため、実際の処理内容はすべてプログラム側で判断します。これはチャートイベント処理システムによって実現されており、コードはイベントを監視し、送信ボタンが押された際に検知できるようになっています。

MetaTrader 5は、ボタンが押された際に、その操作に関する情報を含むイベントを発行します。この情報には、イベントの種類と、それを発生させたコントロールの識別情報が含まれます。このデータを確認することで、送信ボタンが押されたのか、それともチャート上の別の場所で発生したイベントなのかを判別できます。 送信ボタンによるイベントであることが確認できた場合、次にユーザー入力データを読み取り、API呼び出し用に準備する処理へ進みます。このように処理を分離することは非常に重要です。チャートイベントはユーザーのアクション(ボタンなど)を通知し、プログラムはその後の処理内容を決定します。この流れを理解することが、コントロールパネルを後続セクションで扱うAIとのバックエンド通信ロジックに接続するための基盤となります。

例:
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int32_t id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id == CHARTEVENT_OBJECT_CLICK)
     {
      if(sparam == send_button_name)
        {

         Comment("BUTTON WORKING PERFECTLY");

        }
     }
  }

出力:

図1:チャートイベント

説明:

MetaTrader 5は、チャート上で何らかのイベントが発生するたびに、独自のイベントハンドラであるOnChartEventメソッドを自動的に呼び出します。これは常時実行されるOnTickとは異なり、操作やイベントが発生したときにのみ動作します。マウスクリック、キー入力、オブジェクト操作、コントロールパネルの操作などは、そのようなイベントの代表例です。この関数の役割は、それらのイベントを監視し、発生した際に適切に反応することです。発生したチャートイベントの種類は、最初のパラメータであるidによって示されます。MetaTrader 5では、オブジェクトのクリック、キー入力、チャートのリサイズなどの操作を、あらかじめ定義された定数で表現します。ここでは、イベントの種類が「オブジェクトのクリック」に該当するかどうかをコードで判定しています。これにより、プログラムは他のすべてのチャートイベントを無視し、ユーザーがオブジェクトをクリックした場合のみに反応するようになります。

次の処理では、オブジェクトがクリックされたことを確認したうえで、そのイベントを発生させたオブジェクトを特定します。このとき使用されるのがsparamパラメータです。sparamには、操作されたオブジェクトの名前が格納されています。これを送信ボタンの名前と比較することで、クリックが送信ボタンによるものなのか、それともチャート上の別のオブジェクトによるものなのかを判定できます。この2つの条件(オブジェクトがクリックされたこと、かつそれが送信ボタンであること)が成立した場合に限り、このブロック内のコードが実行されます。この例では、ボタンのクリックを正常に検知できたことを示すメッセージをチャート上に表示します。このシンプルなテストにより、ボタンがチャートイベントシステムに正しく接続されていることを確認できます。

ここで重要なのは「責任の分離」という考え方です。ボタン自体はクリックされても何も処理を行いません。代わりにチャートへイベントを通知するだけです。その後、チャートがイベントハンドラを呼び出し、あなたのコードがそのイベント情報をもとに処理内容を決定します。後にこの送信ボタンを、ユーザー入力の読み取りやAPIリクエスト送信のロジックへ接続する際には、ユーザー操作に対するプログラムの挙動を完全に制御できることが重要になります。

比喩的な説明

チャートをアクティブな作業スペース、OnChartEvent関数をその空間を監視する警備員だと考えてみてください。警備員は普段は何もしませんが、ボタンが押されるなど何らかの出来事があると、即座に気付いて対応します。イベント識別子は、受付係に「どのような種類の出来事が発生したのか」を伝える役割を持ちます。ボタンが押されたのか、誰かがノックしたのか、それとも電話が鳴ったのか、といったことです。ただし、この例で受付係が関心を持っているのは「机の上のボタンが押されたかどうか」の1つだけです。他の出来事はすべて無視されます。

次に受付係は、どのボタンが押されたのかを確認します。これはボタンのラベルを読み取る動作に相当します。ラベルが送信ボタンと一致すれば、受付係は訪問者の要求を正確に理解できます。一致しない場合は、特に何もおこないません。送信ボタンであることが確認された場合のみ、必要な処理が実行されます。この段階では単純に「ボタンが正しく動作していることを確認した」というメッセージを返すだけですが、後にこの処理はユーザー入力の取得やサーバーへの送信へと拡張されます。この例でおこなっていることは、受付係が「リクエストを受け付けた」と伝えるのと同じです。

この比喩が示している重要な点は、アクション自体はボタンが実行するものではないということです。ボタンは単に「押された」という事実を通知するだけです。実際の意思決定は受付デスク(チャートイベントハンドラ)側でおこなわれます。そのためMetaTrader 5では、ボタン自体ではなくチャートイベントを通じてボタンクリックを処理します。


ユーザー操作に応じたAPIリクエストの送信

ユーザーがコントロールパネルにプロンプトを入力し、送信ボタンをクリックしたとき、そのクリックはソフトウェアが処理を進めてAPIとやり取りするための合図となります。ソフトウェアが一貫して効率的に動作するように、クエリを自動的に送信したり、ティックごとに送信したりするのではなく、意図的にユーザーからの入力を待つようにします。このロジックと人間の入力との連携は、MetaTrader 5ではチャートイベントによって管理されます。送信ボタンが押されると、プログラムはチャートイベントを発生させて記録します。プログラムはこのイベントハンドラ内でどのコントロールがクリックされたかを判定し、それに応じた処理を実行できます。WebRequestのロジックは、送信ボタンがイベントの発生源であることが確認された後に実行されます。これにより、APIリクエストはユーザーが明示的に要求した場合にのみ送信されることが保証されます。

この方法が重要な理由はいくつかあります。まず、不要なクエリがサーバーに送信されるのを防ぎ、APIのレート制限を超えることを回避できます。次に、データの送受信に関してユーザーに完全な制御を与えることができます。さらに、バックグラウンドロジックとユーザー操作を分離することで、コード構造を整理された状態に保つことができます。バックエンドのロジックはAPIとの通信を管理し、インターフェースは入力とクリックの処理を担当します。WebRequestをユーザー操作に直接結び付けることで、明確でレスポンスの良いワークフローを構築できます。コントロールパネルは単なる視覚的なコンポーネントではなく、プログラムが外部サービスとどのように、そしていつやり取りするかをユーザーが制御できるインタラクティブな入口として機能します。

例:
#include <Controls\Dialog.mqh>
#include <Controls\Edit.mqh>
#include <Controls\Button.mqh>
#include <Controls\Label.mqh>

CAppDialog panel;
CEdit input_box;
CButton send_button;
string send_button_name = "SEND BUTTON";

CLabel  response_display;
string response_text_name = "AI REPONSE";


int panel_x = 32;
int panel_y = 82;
int panel_w = 600;
int panel_h = 200;
ulong chart_ID = ChartID();
string panel_name = "Google Generative AI";
string input_box_name = "INPUT BOX";

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {

   panel.Create(chart_ID,panel_name,0,panel_x,panel_y,panel_w,panel_h);

   input_box.Create(chart_ID,input_box_name,0,5,55,0,0);
   input_box.Width(500);
   input_box.Height(30);
   panel.Add(input_box);

   send_button.Create(chart_ID,send_button_name,0,510,55,556,85);
   send_button.Text("Send");
   panel.Add(send_button);

   response_display.Create(0, "PanelText", 0, 0, 0, 0, 0);
   response_display.Text("THIS WILL BE THE SERVER RESPONSE......");
   panel.Add(response_display);

   panel.Run();

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {

   panel.Destroy(reason);

  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---

  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int32_t id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id == CHARTEVENT_OBJECT_CLICK)
     {
      if(sparam == send_button_name)
        {

         //Comment("BUTTON WORKING PERFECTLY");
         string API_KEY = "AbcdefKJXiFPdvvM6f4ivPZ-zA2Qnoq612345";
         string url =  "https://generativelanguage.googleapis.com/v1beta/models/"
                       "gemini-2.5-flash-lite:generateContent?key=" + API_KEY;

         string headers = "Content-Type: application/json\r\n";

         string body = "{"
                       "\"contents\": ["
                       "{"
                       "\"parts\": ["
                       "{"
                       "\"text\": \"" + input_box.Text() + "\""
                       "}"
                       "]"
                       "}"
                       "]"
                       "}";

         char data[];
         int copied = StringToCharArray(body, data, 0, WHOLE_ARRAY, CP_UTF8);

         if(copied > 0)
            ArrayResize(data, copied - 1);

         char result[];
         string result_headers;
         int timeout = 15000;

         int response = WebRequest("POST",url,headers,timeout,data,result,result_headers);

         if(response == -1)
           {
            Print("WebRequest failed. Error: ", GetLastError());
            return;
           }

         string response_text = CharArrayToString(result);
         Print(response_text);

        }
     }
  }

出力:

図2:APIリクエスト

説明:

ユーザーがボタンを押した瞬間からサーバーが応答するまでの間に何が起こるかを理解するために、このコードをステップごとに見ていきましょう。すべてはチャートイベントハンドラから始まります。MetaTrader 5は、チャート上でマウスクリック、キー入力、またはコントロールパネルの要素が操作されるたびに、この関数を即座に呼び出します。この関数が呼び出される際、MetaTraderは4つのパラメータを渡します。この中で特に重要なのは、発生したアクションの種類を示すイベント識別子と、操作対象のオブジェクト名を示す文字列引数です。

最初の条件では、イベント識別子がオブジェクトクリックに対応する値かどうかを確認します。これにより、プログラムの処理ははチャートオブジェクトがクリックされた場合にのみ実行されるようになります。このチェックがない場合、コードはマウス移動やチャートの変化といった他のイベントにも反応してしまい、予期しない動作を引き起こす可能性があります。次の条件では、クリックイベントであることが確認された後、イベントに含まれるオブジェクト名が送信ボタンの名前と一致するかどうかを確認します。チャート上には複数のオブジェクトが同時に存在するため、この確認は不可欠です。オブジェクト名を照合することで、そのクリックが送信ボタンによるものかどうかを判定します。この条件が満たされない限り、プログラムは処理を続行しません。送信ボタンが押されたことが確認された場合、

Google Generative AI APIと通信するための準備をおこないます。まずAPIキーを定義します。このキーはアプリケーションをGoogleに識別させるだけでなく、サーバー側で使用状況の計測、制限の適用、リクエストの認証を行うために使用されます。このキーがない場合、リクエストは拒否されます。次にリクエストURLを構築します。このURLには、実行する処理、接続先のサーバー、使用するAIモデル、そしてAPIのバージョンが含まれます。APIキーはURLに付加され、サーバーがリクエスト元を識別できるようにします。これにより、アプリケーションはリクエスト先と利用するサービスの両方を明確に指定できます。

次にリクエストヘッダーを設定します。Content-Typeヘッダーは、データがAPIの仕様に従ってJSON形式で送信されることをサーバーに伝えます。このヘッダーがないと、サーバーはリクエストを正しく解釈できないことがあります。ヘッダーの設定後、リクエストボディを構築します。このボディにはAIに送信する実際のメッセージが含まれます。テキストはハードコードされたものではなく、コントロールパネルの入力ボックスから直接取得されます。つまり、ユーザーが入力したプロンプトがそのままAIに送信されます。このテキストは適切なJSONフィールドに配置され、APIが要求する形式に従って構成されます。

ボディは最初に文字列として生成されますが、その後文字配列に変換する必要があります。これは、MQL5のWebRequest関数がリクエストボディとして文字列を直接受け付けないためです。この変換では、特殊文字を含めすべての文字を正しく送信するためにUTF-8エンコーディングが使用されます。また、変換時に自動的に追加される余分なヌル文字を削除するために、配列のサイズを調整します。これによりJSON構造が正しく維持されます。

MQL5のWebRequest関数はその形式のデータのみを受け付けるため、リクエストボディが文字列として構築された場合、それは文字配列に変換されます。この変換の過程では、特殊文字を含むすべての文字が正しく伝達されるようにUTF-8エンコーディングが使用されます。次に、配列のサイズを変更して変換中に挿入された余分なヌル文字を削除することで、JSON構造が保持されます。次に、サーバーの応答を記録するための変数が設定されます。応答データと応答ヘッダーは別々の変数に格納されます。また、プログラムが応答を待機する時間を制御するためにタイムアウトが設定されます。

その後、WebRequest関数が実行されます。この時点でリクエストが実際にサーバーへ送信されます。関数はHTTPメソッド、URL、ヘッダー、タイムアウト、およびリクエストボディを送信し、サーバーからの応答をあらかじめ用意された変数に格納します。また、リクエストが成功したかどうかを示す値を返します。コードはリクエスト直後にこの戻り値を確認し、失敗を示している場合には処理を停止し、エラーメッセージと直近のエラーコードを表示します。このエラー処理は、デバッグを容易にし、不正または不完全なデータでプログラムが実行されるのを防ぐために重要です。リクエストが成功した場合、文字配列として受け取った応答データは読みやすい文字列に変換されます。この文字列がAIサーバーから返された生の応答を表します。これにより、プログラムは一連の処理を正常に完了します。すなわち、ボタンのクリックをきっかけにユーザー入力を取得し、それをAPIに送信し、表示や追加処理、または後続の利用のために保存可能な応答を受け取ります。

比喩的な説明

パネルを小さな机、チャートを忙しいオフィスだと想像してください。入力ボックスはメモ帳のような役割を持ち、そこにメッセージを書き込むことができます。送信ボタンは、机の上にある「Send」と書かれた物理ボタンのようなものです。AIサーバーは、問い合わせに対応する専門家がいる遠隔のオフィスにたとえられます。チャートイベント関数は、常に周囲の動きを監視している受付係のような存在です。受付係は、誰かがボタンに触れるといった出来事を検知して報告しますが、すぐに行動するわけではありません。まずその出来事の詳細を確認し、「ボタンクリック」のイベントであれば、どのボタンが押されたのかを特定します。そして「Send」ボタンであると分かれば、メッセージを送る必要があると判断します。

ボタンクリックが確認されると、プログラムはメッセージ送信に必要なすべての情報を準備します。APIキーは、専門家のオフィスにアクセスするための秘密のパスコードのような役割を果たします。URLはそのオフィスの正確な住所です。ヘッダーは、「このメッセージは理解可能な形式で書かれています」と伝えるための注意書きのようなものです。その後、メモ帳に書かれたメッセージは封筒に丁寧に移されます。これは、文字列を文字配列に変換する処理に相当します。余分なものが含まれないように、封筒はメッセージがちょうど収まるようにサイズが調整されます。

準備が整うと、その封筒はWebRequestという宅配サービスに渡され、タイムアウトという期限とともに送信されます。配達員はそれを専門家のオフィスに届けた後、返答を待ちます。配達できなかった場合は、何らかの問題が発生したことを示すエラー通知が届きます。配達できた場合、専門家の返答が入った封筒が返送されます。その封筒を開封し、読み取れる形式に変換することで、ユーザーは内容を確認できます。つまり、ユーザーがメッセージを書いて送信ボタンをクリックすると、受付係がその操作を確認し、必要な情報を整えてメッセージを送り出し、最終的に専門家からの返答が机の上に届けられるという流れになります。これは、MetaTrader 5におけるイベント駆動型のインタラクティブなフロントエンドがバックエンドサービスと通信する仕組みそのものです。

 

API応答から有用なデータを抽出する

WebRequestは通常、JSONのような構造化された形式でサーバーからの応答を受け取ります。この応答には、AIが生成した応答に加えて、メタデータや内部情報なども含まれています。このプロジェクトでは、人間の入力に対してAIが生成した実際のテキストのみを取得することに焦点を当てます。情報を抽出するために、まずWebRequestから取得した生のバイト配列を読みやすい文字列に変換します。これにより、サーバーからの完全な応答を操作可能な形で扱えるようになります。次に、その文字列の中からAIのテキストが含まれている部分を特定します。これは通常、Google Generative AIの応答における「text」のような特定のキーによって示されます。文字列関数を用いてこのキーの開始位置を特定し、その位置からAIの実際のメッセージを含む部分文字列を抽出します。

抽出されたテキストは、パネルへの表示などアプリケーション内のさまざまな場所で利用でき、変数として保存することも可能です。必要なデータのみを分離することで、サーバーからのその他の情報が表示や後続の処理に影響を与えないようにします。この工程は、アプリケーションの表示を簡潔かつ目的に沿ったものに保ち、ユーザーがAIによって生成された重要な結果のみを確認できるようにするために重要です。

例:
#include <Controls\Dialog.mqh>
#include <Controls\Edit.mqh>
#include <Controls\Button.mqh>
#include <Controls\Label.mqh>

CAppDialog panel;
CEdit input_box;
CButton send_button;
string send_button_name = "SEND BUTTON";

CLabel  response_display;
string response_text_name = "AI REPONSE";

int panel_x = 32;
int panel_y = 82;
int panel_w = 600;
int panel_h = 200;
ulong chart_ID = ChartID();
string panel_name = "Google Generative AI";
string input_box_name = "INPUT BOX";

string ai_response;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   panel.Create(chart_ID,panel_name,0,panel_x,panel_y,panel_w,panel_h);

   input_box.Create(chart_ID,input_box_name,0,5,55,0,0);
   input_box.Width(500);
   input_box.Height(30);
   panel.Add(input_box);

   send_button.Create(chart_ID,send_button_name,0,510,55,556,85);
   send_button.Text("Send");
   panel.Add(send_button);

   response_display.Create(0, "PanelText", 0, 0, 0, 0, 0);
   panel.Add(response_display);

   panel.Run();

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   panel.Destroy(reason);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int32_t id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id == CHARTEVENT_OBJECT_CLICK)
     {
      if(sparam == send_button_name)
        {
         //Comment("BUTTON WORKING PERFECTLY");

         string API_KEY = "AIzaSyCKJXiFPdvvM6f4ivPZ-zA2Qnoq6g62X7M";

         string url =  "https://generativelanguage.googleapis.com/v1beta/models/"
                       "gemini-2.5-flash-lite:generateContent?key=" + API_KEY;

         string headers = "Content-Type: application/json\r\n";

         string body = "{"
                       "\"contents\": ["
                       "{"
                       "\"parts\": ["
                       "{"
                       "\"text\": \"" + input_box.Text() + "\""
                       "}"
                       "]"
                       "}"
                       "]"
                       "}";

         char data[];
         int copied = StringToCharArray(body, data, 0, WHOLE_ARRAY, CP_UTF8);

         if(copied > 0)
            ArrayResize(data, copied - 1);

         char result[];
         string result_headers;
         int timeout = 15000;

         int response = WebRequest("POST",url,headers,timeout,data,result,result_headers);

         if(response == -1)
           {
            Print("WebRequest failed. Error: ", GetLastError());
            return;
           }

         string response_text = CharArrayToString(result);
         //  Print(response_text);

         string pattern = "\"text\": ";
         int pattern_lenght = StringFind(response_text,pattern);
         pattern_lenght += StringLen(pattern);

         int end = StringFind(response_text,"}",pattern_lenght + 1);

         ai_response = StringSubstr(response_text,pattern_lenght,end - pattern_lenght);

         response_display.Text(ai_response);

        }
     }
  }

出力:

図3:AIの応答

説明:

AIの応答を保存するために、まず変数を宣言します。このai_response変数には、長いサーバー応答から抽出され、メタデータなどの不要な情報から分離されたテキストのみが格納されます。次に、サーバー応答の中でAIが生成したテキストが格納されているキーを示すために、パターン文字列を定義します。Google Generative AIのJSON応答では、実際の応答は「text」というキーで表されるため、このパターンはプログラムに対して目的のメッセージの開始位置を特定するための手がかりとなります。次に、文字列検索関数を用いて応答全体の中からこのパターンを検索します。これにより、パターンが最初に出現する位置が取得されます。その位置にパターンの長さを加えることで、キーの直後から抽出を開始できるようにし、AIのメッセージの正確な開始位置を得ます。

次に、その開始位置からメッセージの終了位置を特定するために、次の閉じ括弧「}」を検索します。これによりAIのテキストがどこで終わるかを判断できるため、応答全体の中から必要な部分のみを抽出し、それ以外を除外することができます。開始位置と終了位置が分かったら、StringSubstr関数を使用してその範囲の文字列を取得します。これにより、ai_response変数には生成されたテキストのみが格納されます。最後に、ラベルの内容を更新して抽出したテキストをパネル上に表示します。この処理により、コントロールパネルのインターフェースにAIの応答が直接ユーザーへ表示されるようになります。

比喩的な説明

あなたが司書で、出版社から分厚い封筒が届いたとします。その中には報告書やポスター、告知、請求書など多くの資料が含まれていますが、あなたが必要としているのはその中の書評の一部だけです。あなたの責任は、それらの書類をすべて確認し、該当するページを見つけ出し、それを取り出して図書館の掲示板に掲示し、誰もが読めるようにすることです。書評だけを保存するためには、ai_responseという名前の専用フォルダを定義する必要があります。このai_response変数には、長いサーバー応答から抽出され、メタデータやその他のデータから分離された関連テキストのみが格納されます。次に、すべての手紙には「text:」というヘッダーが付いていることに気づきます。これは、「メッセージはここから始まる」と書かれた付箋が封筒に貼られているようなもので、あなたへの手がかりです。あなたは、そのラベルが付いた最初の箇所を書類の中から探します。あなたが抽出したい手紙の開始位置は、そのラベルを見つけた場所になります。

しかし、単にラベルの位置を見つけるだけでは不十分です。誤って追加の文書を表示に含めてしまわないようにするためには、手紙の終わりがどこかを把握する必要があります。そのために、JSONの各セクションの後に現れる閉じ括弧を探します。これは、封筒を開けて手紙の最後の一文を読み、その後に続くものは別の内容であると理解するのと同じです。開始位置と終了位置が確実になった後で、残りの郵便物からその手紙だけを丁寧に切り出します。これこそがStringSubstr関数の役割であり、開始と終了の間にあるテキストだけを抽出することで、余計な情報を含まない明確な書評を得ることができます。最後に、その手紙をresponse_displayラベルに配置します。これにより、そのメッセージは図書館を訪れるすべての人にとってすぐに見える状態になります。必要な情報がフレーム内に整理されて提示されるため、訪問者は書類の山から自分で仕分けを行う必要がなくなります。


コントロールパネルにスクロール可能なテキストを実装する

このセクションでは、コントロールパネル内でAIの回答をスクロール表示させる方法について説明します。MQL5のコントロールパネルは、一般的なプログラミング環境のテキストエリアとは異なり、「\n」による改行を正しく認識しません。つまり、サーバーからの応答が長い場合でもテキストは自動的に折り返されたり改行されたりせず、表示領域からはみ出してしまいます。この問題を解決するために、ラベル上で表示されるテキストの範囲を徐々にずらしていくスクロール機構を実装します。

目的は、常に全文を保持しながら、表示する範囲だけを段階的に変化させることです。これを実現するために、定期的に関数を呼び出すタイマーを使用します。この関数は実行されるたびに、応答の異なる一部分をラベルに表示することで、連続的にスクロールしているような動作を生み出します。この方法により、非常に長いAIの応答であっても、パネルの限られた表示領域内で読み取ることができ、重要な情報が失われることもなく、手動での煩雑なスクロール操作も不要になります。スクロール可能なテキストを導入することで、ユーザーはAIの応答全体を容易に読み取ることができ、同時にパネルはコンパクトなままチャート内に自然に統合されます。以降のセクションでは、コンテンツの抽出、ラベルの更新処理、スクロール速度の制御といったコードロジックについて順に解説し、滑らかで分かりやすい表示を実現する方法を示します。

例:
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   EventSetMillisecondTimer(150);
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {

   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Timer function for animation                                     |
//+------------------------------------------------------------------+
void OnTimer()
  {
   Print("EURUSD")
  }

説明:

プログラムの初期化部分では、短い間隔で動作するミリ秒単位のタイマーが開始されます。このタイマーによって一定かつ予測可能なリズムが生成され、ソフトウェアはユーザーインターフェースを繰り返し更新できるようになります。タイマーを使用することで、サーバーから受信した応答全体を一度に表示するのではなく、表示されるテキストを時間経過とともに段階的に更新することが可能になります。これはコントロールパネル上では現実的ではないため、このような方法が採用されています。タイマーイベントが発生するたびにタイマーハンドラが自動的に呼び出されます。これにより、呼び出しごとにテキストの表示範囲をわずかに変更できます。たとえば、表示位置を上方向や横方向にずらしたり、テキストの開始位置を変更したり、次の文字列の範囲を表示したりすることが可能です。タイマーが高頻度で動作するため、テキストは急に切り替わるのではなく、滑らかにスクロールしているように見えます。この仕組みにより、改行文字を使用せずにスクロール表示を再現できます。

一方で、初期化解除処理では、EAがチャートから削除された場合やチャート状態が変化した場合にタイマーを停止します。スクロール処理は継続的に更新を行う仕組みであるため、これは非常に重要です。パネルが消えた後もタイマーが動作し続けると、不要な処理がバックグラウンドで実行されてしまいます。そのため、タイマーを停止することでスクロール処理を確実に終了させ、余計な負荷を防ぎ、プラットフォームの安定性を維持します。

比喩的な説明

コントロールパネルを、表示できる文字数に制限のある小型の電子掲示板だと想像してみてください。サーバーからの応答は、その掲示板の画面よりもはるかに長い紙に書かれた長文の手紙のようなものです。画面にはメッセージ全体を一度に表示することができず、また改行にも対応していないため、メッセージの異なる部分が時間とともに順に表示されるように、紙をゆっくりと動かす仕組みが必要になります。これは、初期化時にタイマーを設定することで、掲示板内部の小さなモーターが動き出すようなものです。このモーターは一定の間隔で動作し、この場合は150ミリ秒ごとに作動します。モーターは、一定の刻みで動くたびに紙を少しずつ動かすようシステムに指示します。一定のリズムで動作するため、その動きは不規則ではなく、滑らかで制御されたものになります。

実際の処理はタイマーハンドラ内でおこなわれます。モーターが動くたびに、画面に表示されるメッセージの表示範囲を変更できます。画面そのもののサイズが変わるわけでも、新しい機能が追加されるわけでもありませんが、ユーザーからはテキストがスクロールしているように見えます。毎回表示されるのは、同じ長いメッセージの中の異なる一部分にすぎません。掲示板が取り外されたり電源が切られたりすると、そのモーターは停止します。これにより、表示装置が存在しない状態で処理が継続することを防ぎます。同様に、アプリケーション終了時にタイマーを停止することで、スクロール処理が正しく終了し、無駄なリソース消費を防ぐことができます。

次に、OnTimerイベントハンドラを用いてテキストをスクロールさせる方法を見ていきます。タイマーを利用して、コントロールパネルに表示されるテキストを定期的に更新し、サーバーからの応答全体を一度に表示しないようにします。タイマーが鳴るたびに応答の一部を表示し、次のタイミングでその表示範囲を少しずつずらしていきます。これを繰り返すことで、テキストが画面上を滑らかに流れているように見えます。コントロールパネルは複数行テキストや改行をサポートしていませんが、この方法により長いAI応答をシンプルかつ分かりやすく扱うことが可能になります。

例:
int    display_length;
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int32_t id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id == CHARTEVENT_OBJECT_CLICK)
     {
      if(sparam == send_button_name)
        {

         //Comment("BUTTON WORKING PERFECTLY");

         string API_KEY = "AIzaSyCKJXiFPdvvM6f4ivPZ-zA2Qnoq6g62X7M";

         string url =  "https://generativelanguage.googleapis.com/v1beta/models/"
                       "gemini-2.5-flash-lite:generateContent?key=" + API_KEY;

         string headers = "Content-Type: application/json\r\n";

         string body = "{"
                       "\"contents\": ["
                       "{"
                       "\"parts\": ["
                       "{"
                       "\"text\": \"" + input_box.Text() + "\""
                       "}"
                       "]"
                       "}"
                       "]"
                       "}";

         char data[];
         int copied = StringToCharArray(body, data, 0, WHOLE_ARRAY, CP_UTF8);

         if(copied > 0)
            ArrayResize(data, copied - 1);

         char result[];
         string result_headers;
         int timeout = 15000;

         int response = WebRequest("POST",url,headers,timeout,data,result,result_headers);

         if(response == -1)
           {
            Print("WebRequest failed. Error: ", GetLastError());
            return;
           }

         string response_text = CharArrayToString(result);
         //  Print(response_text);

         string pattern = "\"text\": ";
         int pattern_lenght = StringFind(response_text,pattern);
         pattern_lenght += StringLen(pattern);

         int end = StringFind(response_text,"}",pattern_lenght + 1);

         ai_response = StringSubstr(response_text,pattern_lenght,end - pattern_lenght);

         //   response_display.Text(ai_response);
         Print(ai_response);

         int res_lenght = StringLen(ai_response);
         if(res_lenght < 100)
           {

            display_length = res_lenght;

           }
         if(res_lenght >= 100)
           {

            display_length = 100;

           }
        }
     }
  }

説明:

この場合、このコードの役割は、AIが生成したテキストのうち、コントロールパネルに一度に表示する文字数を決定することです。まず応答の長さを計算します。これは、テキスト内の各文字を数えることでおこなわれます。この処理により、プログラムは表示を制御する前に応答のサイズを正確に把握できるようになります。その長さに基づいて、単純な条件分岐がおこなわれます。応答が100文字未満の場合、プログラムは表示文字数を応答全体の長さに設定します。テキストがすでにパネル内に収まっているため、そのまま全体を表示でき、スクロールは必要ありません。

一方、AIの応答が100文字を超える場合は、表示文字数を100文字に制限します。最初はその範囲のみが表示され、残りのテキストはスクロールによって順次表示されていきます。これにより、パネルの表示が崩れることなく整理された状態を保ちつつ、長いメッセージ全体を時間をかけて確認できるようになります。

比喩的な説明

本棚に設けられた小さな窓を想像してください。本棚に並んだ大量の本はAIの応答を表しています。まず、その冊数を数えることは応答の長さを測ることに相当します。もし100冊未満であれば、窓からすべての本を同時に見ることができるため、窓を動かす必要はありません。しかし、その窓には一度に100冊しか入らないため、100冊を超える場合、残りを見るためには窓を少しずつ動かす必要があります。すべての本を見ながらも棚を見やすく保つために表示範囲を制限するという考え方は、窓のサイズを適切に設定してオーバーフローを防ぐことに似ています。

OnTimerイベントハンドラは、この表示制御をもとにテキストをスクロールさせるために使用されます。パネルの固定領域内で長いAI応答を少しずつ表示するために、OnTimer関数はテキストの表示範囲を1文字ずつ繰り返し移動させる仕組みとして機能します。これにより、パネルは内容全体を保持しつつ、ユーザーには滑らかなスクロール表示として見えるようになります。

例:

#include <Controls\Dialog.mqh>
#include <Controls\Edit.mqh>
#include <Controls\Button.mqh>
#include <Controls\Label.mqh>

CAppDialog panel;
CEdit input_box;
CButton send_button;
string send_button_name = "SEND BUTTON";

CLabel  response_display;
string response_text_name = "AI REPONSE";

int panel_x = 32;
int panel_y = 82;
int panel_w = 600;
int panel_h = 200;
ulong chart_ID = ChartID();
string panel_name = "Google Generative AI";
string input_box_name = "INPUT BOX";

string ai_response = " ";

int    display_length; // Number of characters visible at once
int    scroll_pos = 0;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   panel.Create(chart_ID,panel_name,0,panel_x,panel_y,panel_w,panel_h);

   input_box.Create(chart_ID,input_box_name,0,5,55,0,0);
   input_box.Width(500);
   input_box.Height(30);
   panel.Add(input_box);

   send_button.Create(chart_ID,send_button_name,0,510,55,556,85);
   send_button.Text("Send");
   panel.Add(send_button);

   response_display.Create(0, "PanelText", 0, 0, 0, 0, 0);
   response_display.Text(ai_response);
   panel.Add(response_display);

   EventSetMillisecondTimer(150);
   panel.Run();

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   EventKillTimer();
   panel.Destroy(reason);
  }
//+------------------------------------------------------------------+
//| Timer function for animation                                     |
//+------------------------------------------------------------------+
void OnTimer()
  {
   int message_len = StringLen(ai_response);

// SAFETY CHECK (very important)
   if(message_len == 0)
      return;

   string visible_text = "";

   for(int i = 0; i < display_length; i++)
     {
      int char_index = (scroll_pos + i) % message_len;
      visible_text += StringSubstr(ai_response, char_index, 1);
     }

   response_display.Text(visible_text);

   scroll_pos++;
   if(scroll_pos >= message_len)
      scroll_pos = 0;
  }

出力:

図5:スクロールテキスト

説明:

まず、この関数はAI応答文字列全体の長さを決定します。これにより、アプリケーションはメッセージの文字数を取得でき、スクロール中にテキストをどのタイミングで折り返すかを判断するために重要な情報となります。これは、長いリボンをディスプレイの窓に通す前に、その全長を測定することに例えることができます。その後、安全確認がおこなわれます。AIの応答が空の場合、関数は直ちに処理を停止し、何も実行しません。これにより、存在しない文字列をスクロールしようとすることを防ぎます。これは、ディスプレイに通す前にリボンが正しく存在するかを確認するのと同じです。次に、パネルに表示されるAI応答の一部を保持するための一時的な文字列が用意されます。長いリボンに小さな窓を開け、その窓から一部分だけが見えるようにするイメージです。その後のループ処理では、表示する文字数に応じて、AI応答の各文字を順番に取り出し、一時的な表示用文字列に追加していきます。これは、リボンを窓の前で少しずつ動かしながら、見える部分を更新していくのと同じです。

ループの中では、次に表示するAI応答の文字位置が決定されます。モジュロ演算により、応答の末尾に達した場合でも先頭に戻るため、スクロールが途切れずに続くようになっています。選択された文字は1文字ずつ表示用文字列に追加され、ユーザーには連続したテキストとして表示されます。現在のフレームに必要なすべての文字が揃った後、その文字列がパネルに設定され、ユーザーに表示されます。これは、リボンの該当部分を窓の中にぴったり収めて表示するのと同じです。表示内容が更新されると、スクロール位置が1文字分進み、次のタイマーティックで次の部分が表示されることで、滑らかなスクロール効果が得られます。

比喩的な説明

AIの応答を、メッセージが書かれた長いリボンだと想像してください。表示パネルは壁に設置された小さな窓のようなもので、一度にリボンの一部分しか見ることができません。プログラムはまずリボンの長さを測定します。これは、窓に通す前にリボン全体の長さを把握する作業に相当します。全体の長さを知ることは非常に重要で、どこで終わり、どこで先頭に戻る必要があるかを判断するための基準になります。次に安全確認がおこなわれます。テキストが存在しない場合、リボンは動かさず何も表示しません。これによりエラーや空白表示を防ぎます。その後、ウィンドウ越しに表示されるリボンの一部分を保持するための空の文字列が用意されます。これは、どの部分を表示するかを準備する作業に相当します。

このウィンドウを埋めるために、ループが実行されます。表示長に応じてリボンから対応する文字を順番に選択し、それを表示用の文字列に追加していきます。メッセージを表示可能にするために、これはリボンをウィンドウの前へ少しずつスライドさせながら、各文字を一つずつ取り出していく作業に似ています。 現在のフレームに必要なすべての文字が揃った時点で、その部分のリボンをパネルに表示します。これは、ガラス越しにリボンの現在の部分がはっきりと見える状態に相当します。その後、次のタイマーティックでウィンドウに次のリボン部分を表示できるようにするため、表示後にスクロール位置が1文字分進められます。メッセージがウィンドウ上を滑らかに流れるようにするため、これはリボンを少しずつ前へ押し出す動作に似ています。

最後に、スクロールがリボンの終端に達した場合、プログラムは位置を先頭に戻します。これによりメッセージは継続してループし、無限に表示される状態が維持されます。これは、リボンを先頭に戻してメッセージを繰り返し表示するのと同じです。


結論

本記事では、ユーザーの操作を検知してAPIにリクエストを送信することで、MetaTrader 5のコントロールパネルをインタラクティブにする方法について解説しました。チャートイベントの処理方法、ユーザーが送信ボタンをクリックした際にプログラムがどのように応答するか、そしてサーバーからの応答を処理してAIが生成した有用なテキストを抽出する方法を学びました。また、OnTimerイベントハンドラを用いて、パネルに一度に表示しきれない長さの応答を滑らかに連続表示するためのスクロールテキストの概念も導入しました。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/20859

添付されたファイル |
Python-MetaTrader 5ストラテジーテスター(第4回):テスター入門 Python-MetaTrader 5ストラテジーテスター(第4回):テスター入門
シミュレーター上で初めての自動売買ロボットを構築し、MetaTrader 5のストラテジーテスター風にストラテジーテスト処理を実行します。その上で、カスタムシミュレーションで生成された結果を、普段使用しているターミナルの結果と比較します。
Python-MetaTrader 5ストラテジーテスター(第3回):MetaTrader 5風の取引操作 — 処理と管理 Python-MetaTrader 5ストラテジーテスター(第3回):MetaTrader 5風の取引操作 — 処理と管理
シミュレーター内で注文の開始、終了、変更などの取引操作を処理するための、Python-MetaTrader5と同様の方法を紹介します。シミュレーションがMT5と同様の動作となるように、取引リクエストに対して厳密な検証処理が実装されており、銘柄取引パラメータや一般的なブローカーの制限事項が考慮されています。
MQL5取引ツール(第11回):ヒートマップおよび標準モード対応相関行列ダッシュボード(ピアソン、スピアマン、ケンドール) MQL5取引ツール(第11回):ヒートマップおよび標準モード対応相関行列ダッシュボード(ピアソン、スピアマン、ケンドール)
MQL5で相関行列ダッシュボードを構築し、ピアソン、スピアマン、ケンドールの各手法を用いて、指定した時間足およびバー数に基づいて資産間の相関関係を算出します。色の閾値と星印によってp値の有意性を示す標準モードに加え、相関の強さをグラデーションで可視化するヒートマップモードを実装します。さらに、時間足選択ツール、モード切り替え、動的な凡例を備えたインタラクティブなユーザーインターフェースを搭載しており、銘柄間の依存関係を効率的に分析できます。
MQL5でカスタムインジケーターを作成する(第5回):WaveTrend Crossover Evolution:Canvasを用いたフォグ状グラデーション、シグナルバブル、リスク管理 MQL5でカスタムインジケーターを作成する(第5回):WaveTrend Crossover Evolution:Canvasを用いたフォグ状グラデーション、シグナルバブル、リスク管理
MQL5におけるSmart WaveTrend Crossoverンジケーターを拡張し、Canvasを用いた描画機能を組み込むことで、霧状のグラデーションオーバーレイ、ブレイクアウトを検出するシグナルボックス、さらに買いシグナルや売りシグナルをバブルや三角形で表示する視覚的アラート機能を追加します。さらに、リスク管理機能として、ローソク足倍率またはパーセンテージに基づいて計算される動的なテイクプロフィットおよびストップロスレベルを導入し、ライン表示およびテーブル表示によって可視化します。加えて、トレンドフィルタリングやボックス延長機能といったオプションも提供します。