English Русский Español Deutsch
preview
MQL5-Telegram統合エキスパートアドバイザーの作成(第5回):TelegramからMQL5にコマンドを送信し、リアルタイムの応答を受信する

MQL5-Telegram統合エキスパートアドバイザーの作成(第5回):TelegramからMQL5にコマンドを送信し、リアルタイムの応答を受信する

MetaTrader 5トレーディングシステム | 11 11月 2024, 11:18
187 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

本記事は連載第5回であり、引き続きMetaQuotes Language 5 (MQL5)とTelegramを統合し、MetaTrader 5 (MT5)とTelegram間の相互作用を改善することに重点を置いています。以前、連載第4回で、MQL5からTelegramに複雑なメッセージとチャート画像を送信するための基礎を築き、これらのプラットフォーム間の通信ブリッジを確立しました。今回は、エキスパートアドバイザー(EA)がTelegramユーザーから直接コマンドを受信して解釈できるようにすることで、その基礎を拡張することを目指しています。EAがシグナルを生成し、市場ポジションを開き、定義済みのメッセージをTelegramチャットに送信することで自身を制御するのではなく、コマンドをEAに中継することでTelegramチャットからEAを制御します。EAはコマンドをデコードして解釈し、知的で適切なリクエストの返信と応答を返します。

まず、この通信を容易にするために必要な環境を設定し、シームレスなやり取りのためにすべてが整っていることを確認します。この記事の核となるのは、JavaScript Object Notation (JSON)データ(この場合はTelegramのコマンドとリクエスト)からチャットの更新を自動的に取得するクラスを作成することです。これにより、EAはTelegramからのユーザーコマンドを理解して処理できるようになります。このステップは、ボットがメッセージを送信するだけでなく、ユーザー入力にインテリジェントに応答する動的な双方向通信を確立する上で非常に重要です。

さらに、受信データのデコードと解釈に焦点を当て、EAがTelegramアプリケーションプログラミングインタフェース(API)からのさまざまな種類のコマンドを効果的に管理できるようにします。このプロセスを示すために、Telegram、MetaTrader 5、MQL5コードエディター間の通信フローを示す詳細なビジュアルガイドを提供し、これらのコンポーネントがどのように連携するかを理解しやすくなります。

統合プロセスフロー

提供されている図では、統合コンポーネントを明確に示しています。フローは次のようになります。Telegramは、EAが接続されている取引端末にコマンドを送信し、EAはMQL5にコマンドを送信します。MQL5はメッセージをデコードして解釈し、それぞれの応答を準備します。次に、応答を取引端末とTelegramに送信します。理解を容易にするために、プロセス全体を次のトピックに細分化します。

  1. 環境の設定
  2. JSONからチャットの更新を取得するためのクラスの作成
  3. Telegram APIからのデータのデコードと解析
  4. 応答の処理
  5. 実装のテスト
  6. 結論

この記事の最後までに、TelegramからMQL5にコマンドとリクエストを送信し、Telegramチャットで応答として監視された返答を受け取る、完全に統合されたEAが完成します。では始めましょう。


環境の設定

クラスと関数を作成する実際の作業を開始する前に、EAがTelegramとインターフェイスできる環境を確立することが重要です。EAは、MQL5での取引、配列、文字列の管理を容易にするいくつかの重要なライブラリにアクセスする必要があります。これらの重要なライブラリを利用できるようにすることで、EAが充実した関数とクラスライブラリにアクセスできるようになります。これにより、EAの実装が大幅にスムーズになります。これは次の図のようになります。

#include <Trade/Trade.mqh>
#include <Arrays/List.mqh>
#include <Arrays/ArrayString.mqh>
    

ここで、ライブラリ<Trade/Trade.mqh>は、取引関数の完全なセットを提供します。このライブラリにより、EAは取引を実行し、ポジションを管理し、その他の取引関連のタスクを実行できます。これは、市場とのやり取りを目的とするすべてのEAにとって重要なコンポーネントです。次のライブラリ<Arrays/List.mqh>および<Arrays/ArrayString.mqh>は、データ構造の管理をサポートするためのものです。最初のライブラリは、動的リストを管理するために使用され、2番目のライブラリは文字列の配列を操作するために役立ちます。これらのライブラリは、Telegramから受信した取引シグナルの処理において特に有用です。専門用語が多かったことと思いますが、次の章ではこれらのライブラリの役割をさらに詳しく説明し、それぞれの機能についても触れます。Arraysライブラリにアクセスするには、ナビゲータを開き、Includeフォルダを展開して、以下のようにいずれかのライブラリをオンにしてください。

配列ライブラリ

最後に、以下に示すように、TelegramのベースURL、タイムアウト、ボットのトークンを定義する必要があります。

#define TELEGRAM_BASE_URL  "https://api.telegram.org"
#define WEB_TIMEOUT        5000
//#define InpToken "7456439661:AAELUurPxI1jloZZl3Rt-zWHRDEvBk2venc"
#define InpToken "7456439661:AAELUurPxI1jloZZl3Rt-zWHRDEvBk2venc"

ライブラリをインクルードしてプログラムをコンパイルすると、Telegramコマンドから受信した複雑なデータ構造を処理するために必要な環境がすべて整い、実装に進むことができます。 


JSONからチャットの更新を取得するためのクラスの作成

このセクションでは、EAがTelegramからリアルタイムで更新を受信できるようにするためのコア機能の開発に焦点を当てます。具体的には、Telegram APIによって返されるJSONデータを解析し、チャットの更新やユーザーコマンドなど、必要な情報を抽出するクラスを作成する必要があります。このステップは、TelegramとMetaTrader 5の間で応答性の高い通信ループを確立するために非常に重要です。まず、プロセスをシミュレートしてみましょう。以下のように、ブラウザでチャットの更新を取得するためのデフォルト関数を再読み込みし、クラス実装に必要なデータ構造を取得します。

空のデータ

読み込み時に、プロセスによって、成功したがデータ構造が空であることを示すtrueが返されます。これは、過去24時間以内にTelegramチャットからメッセージが送信されていないためです。更新情報を取得するにはメッセージを送信する必要があります。このためは、以下に示すように、Telegramチャットから初期化メッセージを送信します。

TELEGRAM最初の初期化メッセージ

メッセージを送信すると、更新がおこなわれ、ブラウザリンクを再読み込みして送信されたデータの構造を取得できるようになります。

データ構造1

上の画像から、送信したメッセージに基づいて構築されたデータ構造の詳細を確認できます。このデータはクラスにコピーされ、メッセージ更新が送信されるたびにループ処理される必要があります。したがって、すべてのメンバー変数を含むクラスを構築しましょう。まずは、一般的なクラスの設計図を作成します。

//+------------------------------------------------------------------+
//|        Class_Message                                             |
//+------------------------------------------------------------------+
class Class_Message : public CObject{//Defines a class named Class_Message that inherits from CObject.
   public:
      Class_Message(); // constructor
      ~Class_Message(){}; // Declares a destructor for the class, which is empty.
};

後で作業がスムーズに進むように、先に宣言したクラスプロトタイプに注目しましょう。クラスを宣言するには、classキーワードに続けてクラス名を指定します。この場合、クラス名はClass_Messageとなります。多くの類似したデータ構造が取得されるため、CObjectという別のクラスを継承し、publicキーワードを使って、外部からその継承メンバーにアクセスできるようにします。次に、クラスの最初のメンバーをpublicとして宣言します。さらに進む前に、これらの概念について詳しく説明しましょう。キーワードは、一般的にアクセス指定子と呼ばれる4つの修飾子の1つであり、コンパイラが変数、構造体のメンバー、またはクラスにアクセスする方法を定義する修飾子で、以下の4つの種類があります。

それぞれについて分解して説明します。

  • Public:publicアクセス指定子で宣言されたメンバーは、クラスが表示されているコードのどこからでもアクセスできます。つまり、クラス外部の関数や変数、その他のオブジェクトから直接アクセスして使用できるということです。一般的に、他のクラスや関数、スクリプトからアクセスされるべき関数や変数に使われます。
  • Protected:protectedアクセス指定子で宣言されたメンバーは、クラス外部からはアクセスできませんが、クラス内、派生クラス(このクラスから継承されたサブクラス)、およびフレンドクラス/関数からはアクセス可能です。これは、サブクラスでのみ使用されるべきデータをカプセル化する際に便利です。通常、サブクラスが基本クラスの特定の変数や関数にアクセスしたり変更したりできるようにしながら、それらをプログラムの他の部分から隠蔽するために使われます。
  • Private:privateアクセス指定子で宣言されたメンバーは、クラス内でのみアクセス可能です。派生クラスやプログラムの他の部分からは、privateメンバーに直接アクセスしたり変更したりすることはできません。これは最も制限の厳しいアクセスレベルであり、通常、クラス外部からアクセスできない変数やヘルパー関数に使用されます。privateメンバーはデータ隠蔽を実現するために使われ、オブジェクトの内部状態はpublicインターフェイス(メソッド)を通じてのみ変更できるようにします。
  • Virtual:このアクセス指定子はクラスメソッドにのみ適用され、構造体のメソッドには適用されません。仮想メソッドとして宣言することで、コンパイラにそのメソッドがクラスの仮想関数テーブルに配置されるべきであることを指示します。

上記の構文のうち、一般的に使用されるのは最初の3つだけです。さて、クラスのプロトタイプに戻って、それぞれの機能について詳しく見ていきましょう。

  • クラス宣言

class Class_Message : public CObject{...};:Class_Messageという名前の新しいクラスを宣言します。このクラスは、MQL5の基底クラスであるCObjectから派生し、カスタムオブジェクトの作成に使用されます。したがって、Class_Messageは、メモリ管理やオブジェクト指向プログラミングのその他の利点など、MQL5フレームワークによって提供される機能を使用して、プログラムでメッセージを簡単に表示できます。

  • コンストラクタ

Class_Message();:ここで、Class_Messageクラスのコンストラクタが宣言されます。コンストラクタは、クラスのインスタンス(またはオブジェクト)が作成されるときに自動的に呼び出される特別な関数です。コンストラクタの役割は、クラスのメンバー変数を初期化し、オブジェクトの作成時に実行する必要がある設定を実行することです。Class_Messageの場合、メンバー変数を初期化します。

  • デストラクタ

~Class_Message(){};:Class_Messageクラスではデストラクタを宣言します。デストラクタは、クラスのインスタンスが明示的に削除されるか、スコープ外になると自動的に呼び出されます。通常、デストラクタはクリーンアップを実行するために定義され、概念的には、クラスのインスタンスが作成されるときに呼び出されるコンストラクタの反対です。この場合、Class_Messageクラスのデストラクタは何もおこないません(クリーンアップタスクは実行しません)。これは、現時点では必要がないためです。 

コンストラクタとデストラクタの両方に基底クラスと同じ名前が含まれていますが、デストラクタのプレフィックスには潮汐(~)が付いている点に注意してください。次に、クラスのメンバーの定義に続きます。これらのメンバーはデータ構造で受け取ったものと同じなので、データ構造とそこから抽出する必要があるメンバーを以下のように可視化します。

メッセージの詳細をクリア

上記の画像から、クラスには最低14のメンバーが必要であることがわかります。メンバーは以下のように定義します。

      bool              done; //A boolean member variable TO INDICATE if a message has been processed.
      long              update_id; //Store the update ID from Telegram.
      long              message_id;//Stores the message ID.
      //---
      long              from_id;//Stores the sender’s ID.
      string            from_first_name;
      string            from_last_name;
      string            from_username;
      //---
      long              chat_id;
      string            chat_first_name;
      string            chat_last_name;
      string            chat_username;
      string            chat_type;
      //---
      datetime          message_date;
      string            message_text;

これで、必要なクラスメンバーが全部揃いました。最終的なクラス構造は以下のようになります。プロセス中のすべてが一目瞭然になるようにコメントを追加しました。

//+------------------------------------------------------------------+
//|        Class_Message                                             |
//+------------------------------------------------------------------+
class Class_Message : public CObject{//--- Defines a class named Class_Message that inherits from CObject.
   public:
      Class_Message(); //--- Constructor declaration.
      ~Class_Message(){}; //--- Declares an empty destructor for the class.
      
      //--- Member variables to track the status of the message.
      bool              done; //--- Indicates if a message has been processed.
      long              update_id; //--- Stores the update ID from Telegram.
      long              message_id; //--- Stores the message ID.

      //--- Member variables to store sender-related information.
      long              from_id; //--- Stores the sender’s ID.
      string            from_first_name; //--- Stores the sender’s first name.
      string            from_last_name; //--- Stores the sender’s last name.
      string            from_username; //--- Stores the sender’s username.

      //--- Member variables to store chat-related information.
      long              chat_id; //--- Stores the chat ID.
      string            chat_first_name; //--- Stores the chat first name.
      string            chat_last_name; //--- Stores the chat last name.
      string            chat_username; //--- Stores the chat username.
      string            chat_type; //--- Stores the chat type.

      //--- Member variables to store message-related information.
      datetime          message_date; //--- Stores the date of the message.
      string            message_text; //--- Stores the text of the message.
};

メッセージクラスを定義した後は、データを受信できるようにメンバーを初期化する必要があります。これはクラスコンストラクタを呼び出すことによっておこないます。

//+------------------------------------------------------------------+
//|      Constructor to initialize class members                     |
//+------------------------------------------------------------------+
Class_Message::Class_Message(void){
   //--- Initialize the boolean 'done' to false, indicating the message is not processed.
   done = false;
   
   //--- Initialize message-related IDs to zero.
   update_id = 0;
   message_id = 0;
   
   //--- Initialize sender-related information.
   from_id = 0;
   from_first_name = NULL;
   from_last_name = NULL;
   from_username = NULL;
   
   //--- Initialize chat-related information.
   chat_id = 0;
   chat_first_name = NULL;
   chat_last_name = NULL;
   chat_username = NULL;
   chat_type = NULL;
   
   //--- Initialize the message date and text.
   message_date = 0;
   message_text = NULL;
}

まず、基底クラスを呼び出し、スコープ演算子(::)を使用してコンストラクタを定義します。次に、メンバー変数をデフォルト値に初期化します。doneブール値はfalseに設定されており、メッセージがまだ処理されていないことを意味します。メッセージと更新のデフォルトIDを表すmessage_idとupdate_idは両方とも0に初期化されます。送信者関連の情報については、from_idは0に設定され、変数from_first_name、from_last_name、from_usernameはNULLに初期化されます。つまり、送信者の詳細は設定されません。同様に、チャットに関連する変数、つまりchat_id、chat_first_name、chat_last_name、chat_username、chat_typeも、そのデータ型に応じて0またはNULLに初期化されます。つまり、チャット情報はまだ利用できません。最後に、message_dateは0に設定され、message_textはNULLに初期化されます。これは、メッセージの内容とメッセージの日付がまだ指定されていないことを意味します。技術的には、intデータ型の変数は0に初期化し、stringsはNULLに初期化します。

同様に、個々のTelegramチャットを保持するために使用される別のクラスインスタンスを定義する必要があります。このデータは、解析済みのデータとTelegramから受信したデータを比較するために使用します。たとえば、Ask価格を取得というコマンドを送信した場合、データを解析し、JSONから更新を取得して、受信したデータがJSONに格納されているコマンドと一致するかどうかを確認します。もし一致すれば、必要なアクションを実行します。これでいくつかの点が明確になったかと思いますが、さらに進めばもっと明確になるでしょう。以下は、そのためのクラスコードスニペットです。

//+------------------------------------------------------------------+
//|        Class_Chat                                                |
//+------------------------------------------------------------------+
class Class_Chat : public CObject{
   public:
      Class_Chat(){}; //Declares an empty constructor.
      ~Class_Chat(){}; // deconstructor
      long              member_id;//Stores the chat ID.
      int               member_state;//Stores the state of the chat.
      datetime          member_time;//Stores the time related to the chat.
      Class_Message     member_last;//An instance of Class_Message to store the last message.
      Class_Message     member_new_one;//An instance of Class_Message to store the new message.
};

個々のTelegramチャットの情報を処理し保持するために、Class_Chatクラスを定義します。このクラスには、空のコンストラクタとデストラクタに加えて、いくつかのメンバー変数が含まれます。具体的には、member_idにはチャットの一意のIDが格納され、member_stateにはチャットの状態が示されます。また、member_timeにはチャットに関連する時間情報が保持されます。このクラスには、以前に定義した基本クラスClass_Messageのインスタンスが2つ含まれており、それぞれ最後のメッセージと新しいメッセージを保持します。これらは、ユーザーが複数のコマンドを送信した際にメッセージを保存し、個別に処理するために必要です。この動作を説明するために、次のように初期化メッセージを送信します。

2番目の初期化メッセージ

チャットの更新を読み取ると、次のデータ構造が得られます。

2番目のメッセージデータ構造

受信した2番目のメッセージデータの構造を確認すると、最初のメッセージの更新IDは794283239、メッセージIDは664であり、2番目のメッセージは794283240と665で、差は1であることがわかります。このことから、別のクラスが必要であることが明確になります。これで、インタラクションフローをシームレスに制御するために使用する最後のデフォルトクラスの作成に進むことができます。その構造は以下のとおりです。

//+------------------------------------------------------------------+
//|   Class_Bot_EA                                                    |
//+------------------------------------------------------------------+
class Class_Bot_EA{
   private:
      string            member_token;         //--- Stores the bot’s token.
      string            member_name;          //--- Stores the bot’s name.
      long              member_update_id;     //--- Stores the last update ID processed by the bot.
      CArrayString      member_users_filter;  //--- An array to filter users.
      bool              member_first_remove;  //--- A boolean to indicate if the first message should be removed.
   
   protected:
      CList             member_chats;         //--- A list to store chat objects.

   public:
      void Class_Bot_EA();   //--- Declares the constructor.
      ~Class_Bot_EA(){};    //--- Declares the destructor.
      int getChatUpdates(); //--- Declares a function to get updates from Telegram.
      void ProcessMessages(); //--- Declares a function to process incoming messages.
};

TelegramボットとMQL5環境間のやり取りを管理するために、Class_Bot_EAというクラスを定義します。このクラスには、ボットの認証トークンを格納するmember_tokenや、ボットの名前を格納するmember_nameなどのprivateメンバーがいくつかあります。別のメンバーはmember_update_idで、最後に処理された更新を追跡します。他のいくつかのメンバーは、ユーザーインタラクションを管理およびフィルタ処理します。このクラスには、チャットオブジェクトのリストを保持するprotectedメンバーmember_chatsがあります。Publicメンバーの中で最も注目すべきは、インスタンスの必要な初期化とクリーンアップを実行するコンストラクタとデストラクタです。Publicメンバーには、Telegramから更新を取得するgetChatUpdatesと、受信メッセージの処理を処理するProcessMessagesという2つの注目すべき関数もあります。これらは、チャットの更新を取得して受信したコマンドを処理するために使用する最も重要な関数です。これらのメンバーは、最初のクラスでおこなったのと同様の形式を使用して、以下のように初期化します。

void Class_Bot_EA::Class_Bot_EA(void){ //--- Constructor
   member_token=NULL; //--- Initialize the bot's token as NULL.
   member_token=getTrimmedToken(InpToken); //--- Assign the trimmed bot token from InpToken.
   member_name=NULL; //--- Initialize the bot's name as NULL.
   member_update_id=0; //--- Initialize the last update ID to 0.
   member_first_remove=true; //--- Set the flag to remove the first message to true.
   member_chats.Clear(); //--- Clear the list of chat objects.
   member_users_filter.Clear(); //--- Clear the user filter array.
}

ここでは、Class_Bot_EAクラスのコンストラクタを呼び出して、メンバー変数を初期化し、ボットの環境を設定します。最初は、member_tokenがプレースホルダーとしてNULLに設定されています。次に、InpTokenのトリミングされたバージョンを割り当てます。この値は、ボットの認証を管理するため非常に重要です。トリミングされたプレースホルダーがコード内に残っていると、ボットは動作しません。member_nameもNULLに初期化され、member_update_idは0に設定されています。これは、更新がまだ処理されていないことを示します。member_first_remove変数はtrueに設定されています。これは、ボットが処理する最初のメッセージを削除するように構成されていることを意味します。最後に、member_chatsとmember_users_filterの両方がクリアされ、空で起動するようにします。ボットのトークンを取得するために別の関数を使用していることに気付いたかもしれません。関数は次のとおりです。

//+------------------------------------------------------------------+
//|        Function to get the Trimmed Bot's Token                   |
//+------------------------------------------------------------------+
string getTrimmedToken(const string bot_token){
   string token=getTrimmedString(bot_token); //--- Trim the bot_token using getTrimmedString function.
   if(token==""){ //--- Check if the trimmed token is empty.
      Print("ERR: TOKEN EMPTY"); //--- Print an error message if the token is empty.
      return("NULL"); //--- Return "NULL" if the token is empty.
   }
   return(token); //--- Return the trimmed token.
}

//+------------------------------------------------------------------+
//|        Function to get a Trimmed string                          |
//+------------------------------------------------------------------+
string getTrimmedString(string text){
   StringTrimLeft(text); //--- Remove leading whitespace from the string.
   StringTrimRight(text); //--- Remove trailing whitespace from the string.
   return(text); //--- Return the trimmed string.
}

ここでは、ボットのトークン文字列をクリーンアップし、検証するために連携して動作する2つの関数を定義します。最初の関数getTrimmedTokenは、入力としてbot_tokenを受け取り、次にgetTrimmedString関数を呼び出してトークンの先頭と末尾の空白を削除します。トリミング後、関数はトークンが空でないかを確認します。もしトリミング後にトークンが空であれば、エラーメッセージを出力し、関数はボットがこのトークンでそれ以上処理できないことを示すNULLを返します。一方、トークンが空でない場合は、有効なトリミングされたトークンを返します。

2番目の関数getTrimmedStringは、指定された文字列の両端から空白を削除する実際の処理をおこないます。StringTrimLeftを使用して先頭の空白を削除し、StringTrimRightを使用して末尾の空白を削除し、その結果得られたトリミングされた文字列を有効性テストを通過したトークンとして返します。

ここまでで、受信したメタデータを整理するために必要なデータ構造は整いました。次に、チャットの更新を取得して同時に処理する必要があります。明確な通信を確保するために、最初にクラス関数を呼び出すことが必要です。クラスメンバーにアクセスするためには、必要なアクセス権を提供するクラスに基づいてオブジェクトを作成する必要があります。これは以下のように実現されます。

Class_Bot_EA obj_bot; //--- Create an instance of the Class_Bot_EA class

クラスオブジェクトをobj_botとして宣言すると、ドット演算子を使用してクラスのメンバーにアクセスできます。更新を確認し、指定された時間間隔でメッセージを処理する必要があります。したがって、コンピューターリソースを消費する可能性のあるティック数をカウントするのに時間がかかるOnTickイベントハンドラを使用する代わりに、自動的にカウントをおこなうOnTimer関数を選択します。イベントハンドラを使用するには、以下のようにOnInitイベントハンドラで設定して初期化する必要があります。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){
   EventSetMillisecondTimer(3000); //--- Set a timer event to trigger every 3000 milliseconds (3 seconds)
   OnTimer(); //--- Call OnTimer() immediately to get the first update
   return(INIT_SUCCEEDED); //--- Return initialization success
}

ここでは、EventSetMillisecondTimer関数を使用して3000ミリ秒(3秒)ごとにトリガーされるタイマーイベントを設定し、EAを初期化します。これにより、EAは定期的に更新をチェックし続けることができます。次に、初期化後すぐに最初の更新を取得するためにOnTimerイベントハンドラを呼び出し、プロセスが遅延なく開始されるようにします。最後に、初期化が成功したことを示すためにINIT_SUCCEEDEDを返します。タイマーが設定されたため、プログラムの初期化を解除する際には、設定したタイマーを破棄してコンピュータリソースを解放する必要があります。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
   EventKillTimer(); //--- Kill the timer event to stop further triggering
   ChartRedraw(); //--- Redraw the chart to reflect any changes
}

ここで、EAが削除または停止されると、OnDeinitイベントハンドラで最初におこなうことは、タイマーイベントを停止することです。これは、EventSetMillisecondTimerの論理的対応物であるEventKillTimer関数を使用しておこなわれます。EAが機能しなくなっている場合は、タイマーを実行し続けないようにする必要があります。タイマーを停止した後、ChartRedraw関数を呼び出します。この関数の呼び出しは厳密には必要ではありませんが、変更を適用するためにチャートを更新する必要がある状況では役立ちます。最後に、タイマーイベントハンドラを呼び出して、カウントプロセスを処理します。

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer(){
   obj_bot.getChatUpdates(); //--- Call the function to get chat updates from Telegram
   obj_bot.ProcessMessages(); //--- Call the function to process incoming messages
}

最後に、OnTimerイベントハンドラを呼び出します。その中で、作成したオブジェクトobj_botを使用し、クラス関数にアクセスするためにドット演算子を使用して、チャートの更新を取得し、メッセージを処理するために必要な2つの重要な関数を呼び出します。ここまではすべてうまくいっています。次に、関数に集中します。これは次のセクションでおこないます。


Telegram APIからのデータのデコードと解析

最初におこなうべきことは、チャットの更新を取得することです。これにより、Telegramから受信したテキストと比較し、一致した場合に必要な応答をおこないます。したがって、更新を取得する関数内でこの処理を実行します。

//+------------------------------------------------------------------+
int Class_Bot_EA::getChatUpdates(void){

//--- ....

}

関数を呼び出した後、最初に有効なトークンがあるかどうかを確認し、ない場合は、ログにエラーメッセージを出力して-1を返し、トークンがないと先に進めないことを示します。これは次のようになります。

   //--- Check if the bot token is NULL
   if(member_token==NULL){
      Print("ERR: TOKEN EMPTY"); //--- Print an error message if the token is empty
      return(-1); //--- Return with an error code
   }

トークンが空でない場合は、指定されたチャットから更新を取得するためにTelegram APIに送信するリクエストの準備に進むことができます。

   string out; //--- Variable to store the response from the request
   string url=TELEGRAM_BASE_URL+"/bot"+member_token+"/getUpdates"; //--- Construct the URL for the Telegram API request
   string params="offset="+IntegerToString(member_update_id); //--- Set the offset parameter to get updates after the last processed ID
   
   //--- Send a POST request to get updates from Telegram
   int res=postRequest(out, url, params, WEB_TIMEOUT);

まず、APIリクエストから返された応答を保持するために、outという名前の変数を宣言します。リクエストのURLを作成するために、基本となるAPI URL (TELEGRAM_BASE_URL)、ボットのトークン(member_token)、呼び出すメソッド(/getUpdates)を組み合わせます。このメソッドは、ユーザーがボットに送信した更新を取得し、前回の更新をチェックしてその後のアクションを確認するために使用されます。次に、リクエストには1つのパラメータを追加します。それがoffsetで、これにより最後に取得した更新以降に発生した更新のみを取得することができます。最後に、APIにPOSTリクエストを送信します。リクエストの結果はout変数に格納され、応答のresフィールドに示されます。この操作はカスタム関数postRequestを使用しておこなわれます。以下のコードスニペットとその説明を確認してください。これは前述の処理に類似していますが、使用した変数に関するコメントが追加されています。

//+------------------------------------------------------------------+
//| Function to send a POST request and get the response             |
//+------------------------------------------------------------------+
int postRequest(string &response, const string url, const string params,
                const int timeout=5000){
   char data[]; //--- Array to store the data to be sent in the request
   int data_size=StringLen(params); //--- Get the length of the parameters
   StringToCharArray(params, data, 0, data_size); //--- Convert the parameters string to a char array

   uchar result[]; //--- Array to store the response data
   string result_headers; //--- Variable to store the response headers

   //--- Send a POST request to the specified URL with the given parameters and timeout
   int response_code=WebRequest("POST", url, NULL, NULL, timeout, data, data_size, result, result_headers);
   if(response_code==200){ //--- If the response code is 200 (OK)
      //--- Remove Byte Order Mark (BOM) if present
      int start_index=0; //--- Initialize the starting index for the response
      int size=ArraySize(result); //--- Get the size of the response data array
      // Loop through the first 8 bytes of the 'result' array or the entire array if it's smaller
      for(int i=0; i<fmin(size,8); i++){
         // Check if the current byte is part of the BOM
         if(result[i]==0xef || result[i]==0xbb || result[i]==0xbf){
            // Set 'start_index' to the byte after the BOM
            start_index=i+1;
         }
         else {break;}
      }
      //--- Convert the response data from char array to string, skipping the BOM
      response=CharArrayToString(result, start_index, WHOLE_ARRAY, CP_UTF8);
      //Print(response); //--- Optionally print the response for debugging

      return(0); //--- Return 0 to indicate success
   }
   else{
      if(response_code==-1){ //--- If there was an error with the WebRequest
         return(_LastError); //--- Return the last error code
      }
      else{
         //--- Handle HTTP errors
         if(response_code>=100 && response_code<=511){
            response=CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); //--- Convert the result to string
            Print(response); //--- Print the response for debugging
            Print("ERR: HTTP"); //--- Print an error message indicating an HTTP error
            return(-1); //--- Return -1 to indicate an HTTP error
         }
         return(response_code); //--- Return the response code for other errors
      }
   }
   return(0); //--- Return 0 in case of an unexpected error
}

ここでは、POSTリクエストの送信と応答の処理をおこないます。まず、入力パラメータを取得し、送信可能な形式に変換します。具体的には、StringToCharArrayを使って、パラメータ文字列を文字配列に変換します。次に、応答データと応答ヘッダーを格納するための2つの配列を定義します。最後に、WebRequest関数を使用して、必要なパラメータとタイムアウト設定を指定し、POSTリクエストを指定のURLに送信します。

リクエストが成功した場合(200 OK応答コードを受け取った場合)、応答データの先頭に処理を妨げるものがないか確認します。特に、バイト順マーク(BOM)が存在するかを確認し、あればそれを不必要な部分として削除します。その後、データを文字配列から文字列に変換します。この一連の処理が問題なく完了した場合、0を返してスムーズに進んだことを示します。

リクエストが失敗した場合は、受け取った応答コードを確認してエラーを処理します。WebRequest関数に問題があれば、設定されたエラーコードをユーザーに通知します。これが、問題の原因を突き止める唯一の方法です。HTTPエラーの場合は、返されたエラーメッセージをできるだけ解釈して、ユーザーに伝えます。その他の応答コードの場合は、返されたコードをそのまま返します。

さらに進む前に、応答をチェックしてデータを出力することで、送信されたデータを確認できます。これは次のロジックを使用して実現します。

   //--- If the request was successful
   if(res==0){
      Print(out); //--- Optionally print the response
   }

ここでは、POSTの結果がゼロかどうかを確認し、ゼロの場合はデバッグと検証のためにデータを出力します。実行すると、以下の結果が得られます。

応答データ

ここで、応答がtrueであることが確認できます。これは、更新の取得プロセスが正常に完了したことを意味します。次に、データ応答を取得する必要があります。そのためには、JSON解析をおこなう必要があります。解析を担当するコードについては詳述しませんが、それをファイルとして含め、プログラムのグローバルスコープに追加する形にします。追加後、以下のようにJSONオブジェクトを作成します。

      //--- Create a JSON object to parse the response
      CJSONValue obj_json(NULL, jv_UNDEF);

オブジェクトを作成した後は、以下のようにそれを使用して応答を逆シリアル化します。 

      //--- Deserialize the JSON response
      bool done=obj_json.Deserialize(out);

結果を格納するためにブール変数doneを宣言します。ここには、正しく解析された応答かどうかを示すフラグが保存されます。以下のようにデバッグ目的で出力できます。

      Print(done);

出力すると、次の応答が得られます。

応答の逆シリアル化

ここで、応答が正しく解析されたことがわかります。続行するには、応答がtrueである必要があります。もし応答がfalseの場合、残りのメッセージ更新にアクセスすることなく、プロセスを停止して終了する必要があります。そのため、応答がfalseであれば、プロセスを終了するようにします。

      if(!done){
         Print("ERR: JSON PARSING"); //--- Print an error message if parsing fails
         return(-1); //--- Return with an error code
      }

ここでは、ブール変数doneを評価することで、JSON解析が成功したかどうかを確認します。解析が失敗した場合(つまり、doneがfalseの場合)、エラーメッセージ「ERR:JSON PARSING」を表示して、JSON応答の解釈に問題があったことを示します。これに続いて、JSON解析プロセス中にエラーが発生したことを通知するために-1を返します。次に、次のロジックを使用して、応答が正常に処理されたことを確認します。

      //--- Check if the 'ok' field in the JSON is true
      bool ok=obj_json["ok"].ToBool();
      //--- If 'ok' is false, there was an error in the response
      if(!ok){
         Print("ERR: JSON NOT OK"); //--- Print an error message if 'ok' is false
         return(-1); //--- Return with an error code
      }

まず、応答から取得されたJSON内のokフィールドの値を確認します。これにより、リクエストが正常に処理されたかどうかがわかります。このフィールドを抽出し、okという名前のブール値に保存します。okの値がfalseの場合、リクエスト自体は成功したものの、応答にエラーまたは何らかの問題があったことを示します。この場合、ERR:「JSON NOT OK」を返し、何らかの問題が発生したことを示します。また、JSON応答の処理にも何らかの問題が発生したことを示すために-1を返します。すべてが成功した場合、メッセージの更新があり、その取得に進むことができることを意味します。したがって、次のようにメッセージクラスに基づいてオブジェクトを宣言します。

      //--- Create a message object to store message details
      Class_Message obj_msg;

これで、すべてのメッセージの更新をループし、作成されたオブジェクトを使用してクラスに保存できるようになりました。まず、更新の合計数を取得する必要があります。これは、次のロジックによって実現されます。

      //--- Get the total number of updates in the JSON array 'result'
      int total=ArraySize(obj_json["result"].m_elements);
      //--- Loop through each update
      for(int i=0; i<total; i++){

      }

各反復で、作業対象のJSON応答から個々の更新項目を取得します。

         //--- Get the individual update item as a JSON object
         CJSONValue obj_item=obj_json["result"].m_elements[i];

その後、個々のチャットの更新を取得できるようになります。まず、メッセージを更新します。

         //--- Extract message details from the JSON object
         obj_msg.update_id=obj_item["update_id"].ToInt(); //--- Get the update ID
         obj_msg.message_id=obj_item["message"]["message_id"].ToInt(); //--- Get the message ID
         obj_msg.message_date=(datetime)obj_item["message"]["date"].ToInt(); //--- Get the message date
         
         obj_msg.message_text=obj_item["message"]["text"].ToStr(); //--- Get the message text
         obj_msg.message_text=decodeStringCharacters(obj_msg.message_text); //--- Decode any HTML entities in the message text

ここでは、obj_itemで示される更新項目から個々のメッセージの詳細を取得します。まず、JSONオブジェクトから更新IDを取得し、obj_msg.update_idに格納します。その後、メッセージIDを取得してobj_msg.message_idに格納します。メッセージの日付は人間が判読できる形式ではありませんが、この項目には含まれています。これをobj_msg.message_dateのdatetimeオブジェクトとして格納し、人間が読みやすい形式に型変換します。その後、メッセージテキストを確認します。ほとんどの場合、テキストを取得してobj_msg.message_textに格納できますが、HTMLエンティティやエンコードされた特殊文字が含まれていることがあります。これらのケースでは、decodeStringCharacters関数を使用して処理します。この関数は以前に説明した通りで、単に呼び出すことでその機能を実行します。最後に、同様の手順で送信者の詳細を抽出します。

         //--- Extract sender details from the JSON object
         obj_msg.from_id=obj_item["message"]["from"]["id"].ToInt(); //--- Get the sender's ID
         obj_msg.from_first_name=obj_item["message"]["from"]["first_name"].ToStr(); //--- Get the sender's first name
         obj_msg.from_first_name=decodeStringCharacters(obj_msg.from_first_name); //--- Decode the first name
         obj_msg.from_last_name=obj_item["message"]["from"]["last_name"].ToStr(); //--- Get the sender's last name
         obj_msg.from_last_name=decodeStringCharacters(obj_msg.from_last_name); //--- Decode the last name
         obj_msg.from_username=obj_item["message"]["from"]["username"].ToStr(); //--- Get the sender's username
         obj_msg.from_username=decodeStringCharacters(obj_msg.from_username); //--- Decode the username

送信者の詳細を抽出した後、同様の方法でチャットの詳細も抽出します。

         //--- Extract chat details from the JSON object
         obj_msg.chat_id=obj_item["message"]["chat"]["id"].ToInt(); //--- Get the chat ID
         obj_msg.chat_first_name=obj_item["message"]["chat"]["first_name"].ToStr(); //--- Get the chat's first name
         obj_msg.chat_first_name=decodeStringCharacters(obj_msg.chat_first_name); //--- Decode the first name
         obj_msg.chat_last_name=obj_item["message"]["chat"]["last_name"].ToStr(); //--- Get the chat's last name
         obj_msg.chat_last_name=decodeStringCharacters(obj_msg.chat_last_name); //--- Decode the last name
         obj_msg.chat_username=obj_item["message"]["chat"]["username"].ToStr(); //--- Get the chat's username
         obj_msg.chat_username=decodeStringCharacters(obj_msg.chat_username); //--- Decode the username
         obj_msg.chat_type=obj_item["message"]["chat"]["type"].ToStr(); //--- Get the chat type

ここまでで、構造がブラウザからのデータ構造で提供したものとまったく同じであることに気づいたはずです。次に、更新IDの更新に進み、Telegramからの次の更新リクエストが適切なポイントで開始されるようにします。

         //--- Update the ID for the next request
         member_update_id=obj_msg.update_id+1;

ここで、member_update_idを更新して、Telegramからの次の更新リクエストが正しい場所から開始されるようにします。値obj_msg.update_id + 1を割り当てることで、次のリクエストに現在の更新が含まれないようにオフセットを設定し、実質的にこのIDの後に発生する新しい更新のみを取得します。これは、同じ更新を複数回処理したくないため、またボットの応答性を可能な限り維持したいため重要です。次に、新しい更新があるかどうかを確認します。

         //--- If it's the first update, skip processing
         if(member_first_remove){
            continue;
         }

ここでは、フラグmember_first_removeをチェックして、現在の更新が初期化後の最初の更新かどうかを確認します。member_first_removeがtrueの場合、これは初期化後の最初の更新(初期更新)であることを示しています。その場合、この更新の処理をスキップし、次の更新へ進みます。最後に、ユーザー名フィルタが適用されているかどうかに基づいて、チャットメッセージをフィルタ処理し、適切に管理します。

         //--- Filter messages based on username
         if(member_users_filter.Total()==0 || //--- If no filter is applied, process all messages
            (member_users_filter.Total()>0 && //--- If a filter is applied, check if the username is in the filter
            member_users_filter.SearchLinear(obj_msg.from_username)>=0)){

            //--- Find the chat in the list of chats
            int index=-1;
            for(int j=0; j<member_chats.Total(); j++){
               Class_Chat *chat=member_chats.GetNodeAtIndex(j);
               if(chat.member_id==obj_msg.chat_id){ //--- Check if the chat ID matches
                  index=j;
                  break;
               }
            }

            //--- If the chat is not found, add a new chat to the list
            if(index==-1){
               member_chats.Add(new Class_Chat); //--- Add a new chat to the list
               Class_Chat *chat=member_chats.GetLastNode();
               chat.member_id=obj_msg.chat_id; //--- Set the chat ID
               chat.member_time=TimeLocal(); //--- Set the current time for the chat
               chat.member_state=0; //--- Initialize the chat state
               chat.member_new_one.message_text=obj_msg.message_text; //--- Set the new message text
               chat.member_new_one.done=false; //--- Mark the new message as not processed
            }
            //--- If the chat is found, update the chat message
            else{
               Class_Chat *chat=member_chats.GetNodeAtIndex(index);
               chat.member_time=TimeLocal(); //--- Update the chat time
               chat.member_new_one.message_text=obj_msg.message_text; //--- Update the message text
               chat.member_new_one.done=false; //--- Mark the new message as not processed
            }
         }

まず、member_users_filter.Total()をチェックして、ユーザー名フィルターがアクティブかどうかを確認します。フィルターがない場合(Total() == 0)、すべてのメッセージを通常通り処理します。フィルターがある場合(Total() > 0)、member_users_filter.SearchLinear()を使用して、送信者のユーザー名(obj_msg.from_username)がフィルター内に含まれているかを確認します。ユーザー名が一致した場合、メッセージを処理します。

次に、member_chatsリストを反復処理し、チャットID(obj_msg.chat_id).を比較してチャットを検索します。チャットが見つからない場合(index == -1)、新しいClass_Chatオブジェクトをリストに追加します。このオブジェクトは、チャットID、現在の時刻、初期状態0、および新しいメッセージのテキストで初期化されます。また、新しいメッセージは未完了としてマークされます(done = false)。

チャットが既にリストに存在する場合は、既存のチャットオブジェクトをメッセージの新しいテキストと現在の時刻で更新し、メッセージを未処理としてマークします。これにより、各チャットの最新メッセージが記録され、適切に更新されることが保証されます。すべてが完了したら、最初の更新フラグをfalseに設定します。

      //--- After the first update, set the flag to false
      member_first_remove=false;

最後に、POSTリクエストの結果を返します。

   //--- Return the result of the POST request
   return(res);

この関数を使用すると、設定した時間間隔ごとにチャットの更新を取得して保存し、必要なときにいつでも処理できるようになります。メッセージの処理は次のセクションでおこないます。


応答の処理

チャットの更新を取得したら、取得したメッセージにアクセスし、比較をおこない、Telegramに応答を送り返すことができます。これは、クラスのProcessMessages関数を使用することで実現されます。

void Class_Bot_EA::ProcessMessages(void){

//---

}

最初におこなう必要があるのは、個々のチャットを処理することです。

   //--- Loop through all chats
   for(int i=0; i<member_chats.Total(); i++){
      Class_Chat *chat=member_chats.GetNodeAtIndex(i); //--- Get the current chat
      if(!chat.member_new_one.done){ //--- Check if the message has not been processed yet
         chat.member_new_one.done=true; //--- Mark the message as processed
         string text=chat.member_new_one.message_text; //--- Get the message text

         //---

      }
   }

ここでは、member_chatsコレクションを反復処理し、member_chatsのインデックス変数iを使用して各チャットに対応するチャットオブジェクトを取得します。すべてのチャットについて、現在のチャットに関連付けられたメッセージをチェックし、member_new_one構造体のdoneフラグを評価することで、メッセージがまだ処理されているかどうかを確認します。メッセージがまだ処理されていない場合は、このフラグをtrueに設定し、重複処理を防ぐためにメッセージを処理済みとしてマークします。最後に、member_new_one構造体からメッセージのテキストを抽出し、そのテキストを使用してメッセージの内容に基づいてどのような応答やアクションを実行するべきかを判断します。たとえば、ユーザーがTelegramから挨拶のテキストHelloを送信する場合を定義します。

         //--- Process the command based on the message text
         
         //--- If the message is "Hello"
         if(text=="Hello"){
            string message="Hello world! You just sent a 'Hello' text to MQL5 and has been processed successfully.";
            
         }

ここでは、メッセージテキストにHelloが含まれているかを確認します。もし含まれている場合、システムはHelloを受信して処理したことをユーザーに知らせる応答を作成します。この応答は、入力がMQL5コードによって正常に処理されたことの確認として機能します。その後、この確認応答をユーザーに送り返して、入力が正常に処理されたことを伝えます。応答を送信するためには、応答を処理する別の関数を作成する必要があります。

//+------------------------------------------------------------------+
//| Send a message to Telegram                                      |
//+------------------------------------------------------------------+
int sendMessageToTelegram(const long chat_id,const string text,
                const string reply_markup=NULL){
   string output; //--- Variable to store the response from the request
   string url=TELEGRAM_BASE_URL+"/bot"+getTrimmedToken(InpToken)+"/sendMessage"; //--- Construct the URL for the Telegram API request

   //--- Construct parameters for the API request
   string params="chat_id="+IntegerToString(chat_id)+"&text="+UrlEncode(text); //--- Set chat ID and message text
   if(reply_markup!=NULL){ //--- If a reply markup is provided
      params+="&reply_markup="+reply_markup; //--- Add reply markup to parameters
   }
   params+="&parse_mode=HTML"; //--- Set parse mode to HTML (can also be Markdown)
   params+="&disable_web_page_preview=true"; //--- Disable web page preview in the message

   //--- Send a POST request to the Telegram API
   int res=postRequest(output,url,params,WEB_TIMEOUT); //--- Call postRequest to send the message
   return(res); //--- Return the response code from the request
}

ここでは、Telegram Bot APIを使用して指定されたTelegramチャットにメッセージを送信する関数sendMessageToTelegramを定義します。まず、TelegramのベースURL、ボットトークン(getTrimmedTokenを使用して取得)、およびメッセージ送信用の特定メソッド(sendMessage)を組み合わせて、APIリクエストのURLを構築します。このURLは、APIリクエストを正しいエンドポイントに転送するために不可欠です。次に、リクエストのクエリパラメータを構築します。これには次のものが含まれます。

  • chat_id:メッセージが送信されるチャットのID
  • text:メッセージの内容。URLエンコードされ、正しく送信されます。

reply_markup(カスタム返信キーボードが指定されている場合):インタラクティブなボタンをメッセージに追加するためのパラメータ。これにより、メッセージ内にインタラクティブなボタンを追加できるようになります。追加のパラメータは次のとおりです。

  • parse_mode=HTML:メッセージをHTMLとして解釈し、フォーマットされたテキストを許可します。
  • disable_web_page_preview=true:メッセージ内のWebページプレビューを無効にします。

最後に、関数はpostRequest関数を使用してリクエストを送信します。この関数は、Telegram APIとの実際の通信を処理し、リクエストからの応答コードを返します。応答コードにより、メッセージが正常に送信されたかどうか、またはエラーが発生したかがわかります。

以下のようにパラメータを指定して関数を呼び出し、応答を送信します。

            //--- Send the response message 
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;

ここでは、まずsendMessageToTelegram関数を使用して、応答メッセージを適切なTelegramチャットに送信します。適切なコンテンツメッセージの適切なチャットをターゲットとするchat.member_idを使用して関数を呼び出します。reply_markupパラメータはNULLに設定されています。これは、送信されたメッセージにキーボードやインタラクティブ要素が付随していないことを意味します。メッセージを送信した後、continue文を使用します。これは、現在処理中のループ内の残りのコードをスキップし、そのループの次の反復に進みます。ここでのロジックは単純です。応答を処理して現在のメッセージに転送します。その後は、現在の反復で現在のチャットまたはメッセージのコードを処理せずに、ほぼ先に進みます。コンパイルすると、次のようになります。

HELLO WORLD

メッセージが数秒以内に受信され、処理されたことがわかります。次に、関数にカスタム返信キーボードを追加するプロシージャに進みます。

//+------------------------------------------------------------------+
//| Create a custom reply keyboard markup for Telegram               |
//+------------------------------------------------------------------+
string customReplyKeyboardMarkup(const string keyboard, const bool resize,
                           const bool one_time){
   // Construct the JSON string for the custom reply keyboard markup.
   // 'keyboard' specifies the layout of the custom keyboard.
   // 'resize' determines whether the keyboard should be resized to fit the screen.
   // 'one_time' specifies if the keyboard should disappear after being used once.
   
   // 'resize' > true: Resize the keyboard to fit the screen.
   // 'one_time' > true: The keyboard will disappear after the user has used it once.
   // 'selective' > false: The keyboard will be shown to all users, not just specific ones.
   
   string result = "{"
                   "\"keyboard\": " + UrlEncode(keyboard) + ", " //--- Encode and set the keyboard layout
                   "\"one_time_keyboard\": " + convertBoolToString(one_time) + ", " //--- Set whether the keyboard should disappear after use
                   "\"resize_keyboard\": " + convertBoolToString(resize) + ", " //--- Set whether the keyboard should be resized to fit the screen
                   "\"selective\": false" //--- Keyboard will be shown to all users
                   "}";
   
   return(result); //--- Return the JSON string for the custom reply keyboard
}

ここでは、Telegram用のカスタム返信キーボードを作成する関数customReplyKeyboardMarkupを定義します。この関数は、keyboard、resize、one_timeの3つのパラメータを取ります。keyboardパラメータは、JSON形式でカスタムキーボードのレイアウトを指定します。resizeパラメータは、キーボードがユーザーのデバイスの画面に合わせてサイズ変更されるかどうかを決定します。resizeパラメータがtrueに設定されている場合、キーボードはユーザーのデバイスの画面に合わせてサイズ変更されます。one_timeパラメータは、キーボードがワンタイムキーボードになり、ユーザーが操作した後に消えるかどうかを指定します。

関数内で、カスタム返信キーボードマークアップを表すJSON文字列が構築されます。キーボードパラメータがAPIリクエストに対して正しくフォーマットされるようにするには、UrlEncode関数を使用してエンコードします。次に、convertBoolToString関数を使用して、resizeとone_timeのブール値(これらの値がtrueとfalseのどちらと見なされるかを決定します)を文字列表現に変換します。最後に、構築された文字列が関数から返され、TelegramへのAPIリクエストで使用できます。使用するカスタム関数は次のとおりです。

//+------------------------------------------------------------------+
//| Convert boolean value to string                                 |
//+------------------------------------------------------------------+
string convertBoolToString(const bool _value){
   if(_value)
      return("true"); //--- Return "true" if the boolean value is true
   return("false"); //--- Return "false" if the boolean value is false
}

最後に、カスタムキーボードを非表示にして強制的に返信するには、次の関数を使用します。

//+------------------------------------------------------------------+
//| Create JSON for hiding custom reply keyboard                    |
//+------------------------------------------------------------------+
string hideCustomReplyKeyboard(){
   return("{\"hide_keyboard\": true}"); //--- JSON to hide the custom reply keyboard
}

//+------------------------------------------------------------------+
//| Create JSON for forcing a reply to a message                    |
//+------------------------------------------------------------------+
string forceReplyCustomKeyboard(){
   return("{\"force_reply\": true}"); //--- JSON to force a reply to the message
}

ここで、関数hideCustomReplyKeyboardとforceReplyCustomKeyboardは、Telegramのカスタムキーボード機能によって実行される特定のアクションを指定するJSON文字列を生成します。

hideCustomReplyKeyboard関数では、生成されるJSON文字列は{"hide_keyboard": true}となります。このJSON構成は、ユーザーがメッセージを送信した後、返信キーボードを非表示にするようTelegramに指示します。基本的に、この関数は、メッセージ送信後にキーボードを非表示にする役割を果たします。

一方、forceReplyCustomKeyboard関数では、生成されるJSON文字列は{"force_reply": true}となります。この文字列は、チャット内の他のUI要素と対話する前に、ユーザーからの応答を要求するようTelegramに指示します。この文字列は、ユーザーが送信されたメッセージとのみ対話できるようにするために役立ちます。

カスタム返信キーボード機能が整ったら、関数を呼び出して、Telegramで返信キーボードを構築してみましょう。

         //--- If the message is "Hello"
         if(text=="Hello"){
            string message="Hello world! You just sent a 'Hello' text to MQL5 and has been processed successfully.";
            
            //--- Send the response message 
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup("[[\"Hello\"]]",false,false));
            continue;
         }

Telegramでメッセージを送信すると、次の結果が得られます。

カスタム返信キーボードHELLO

成功したことがわかります。これで、ボタンをクリックするだけでメッセージを送信できるようになりました。ただし、ボタンはかなり大きいです。ここで、複数のボタンを追加できます。まず、行形式でボタンを追加しましょう。

            string message="Hello world! You just sent a 'Hello' text to MQL5 and has been processed successfully.";
            string buttons_rows = "[[\"Hello 1\"],[\"Hello 2\"],[\"Hello 3\"]]";
            //--- Send the response message 
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(buttons_rows,false,false));
            continue;

ここでは、変数buttons_rowsを使用して、カスタム返信キーボードレイアウトを定義します。この文字列[["Hello 1"],["Hello 2"],["Hello 3"]]は、それぞれHello 1、Hello 2、Hello 3というラベルが付いた3つのボタンがあるキーボードを表します。この文字列の形式はJSONで、Telegramはこれを使用してキーボードをレンダリングします。実行すると、次の結果が得られます。

行レイアウト

キーボードレイアウトを列形式で可視化するために、次のロジックを実装します。

            string message="Hello world! You just sent a 'Hello' text to MQL5 and has been processed successfully.";
            string buttons_rows = "[[\"Hello 1\",\"Hello 2\",\"Hello 3\"]]";
            //--- Send the response message 
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(buttons_rows,false,false));

プログラムを実行すると、次の出力が表示されます。

列レイアウト

受け取ったレイアウトが列形式であることが確認でき、これはプロセスが成功したことを意味します。より複雑なコマンドを作成し続けることができます。まず、ユーザーが迅速に処理できるコマンドのカスタムリストを準備しましょう。

         //--- If the message is "/start", "/help", "Start", or "Help"
         if(text=="/start" || text=="/help" || text=="Start" || text=="Help"){
            //chat.member_state=0; //--- Reset the chat state
            string message="I am a BOT \xF680 and I work with your MT5 Forex trading account.\n";
            message+="You can control me by sending these commands \xF648 :\n";
            message+="\nInformation\n";
            message+="/name - get EA name\n";
            message+="/info - get account information\n";
            message+="/quotes - get quotes\n";
            message+="/screenshot - get chart screenshot\n";
            message+="\nTrading Operations\n";
            message+="/buy - open buy position\n";
            message+="/close - close a position\n";
            message+="\nMore Options\n";
            message+="/contact - contact developer\n";
            message+="/join - join our MQL5 community\n";
            
            //--- Send the response message with the main keyboard
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_MAIN,false,false));
            continue;
         }

ここでは、受信メッセージが「/start」、「/help」、「Start」、「Help」などの事前に決められたコマンドのいずれかであるかどうかを確認します。これらのコマンドのいずれかである場合は、ボットをユーザーに紹介し、ボットと対話するためにボットに送信できるコマンドのリストを提供する歓迎メッセージを準備します。このリストの一部を省略し、他の部分を分類して、ボットで何ができるかをユーザーに概観させます。最後に、このメッセージとカスタムキーボードをユーザーに返します。カスタムキーボードは、コマンドラインよりもボットとの対話に適しています。カスタムキーボードは次のように定義されています。

   #define EMOJI_CANCEL "\x274C" //--- Cross mark emoji
   #define KEYB_MAIN    "[[\"Name\"],[\"Account Info\"],[\"Quotes\"],[\"More\",\"Screenshot\",\""+EMOJI_CANCEL+"\"]]" //--- Main keyboard layout

マクロ#defineを使用して、Telegramボットのユーザーインターフェイスで使用される2つの要素を定義します。まず、Unicode表現\x274Cを使用して、クロスマークの絵文字をEMOJI_CANCELとして定義します。この絵文字は、キーボードレイアウトでキャンセルオプションを示すために使用されます。絵文字のUnicode表現は次のとおりです。

クロスマークUnicode

次に、ボットのメインキーボードレイアウトを表すKEYB_MAINを定義します。キーボードは、ボタンの行を含むJSON配列として構成されます。レイアウトには、コマンドリストの名前、口座情報、相場、詳細、スクリーンショット、およびEMOJI_CANCELで表されるキャンセルボタンの行が含まれます。このキーボードはユーザーに表示され、ユーザーは手動でコマンドを入力する代わりにこれらのボタンを押してボットと対話することができます。プログラムを実行すると、次の出力が得られます。

TELEGRAM JSON UI 1

これで、JSON形式のカスタムキーボードと、ボットに送信できるコマンドのリストができました。残っているのは、Telegramから受信したコマンドに従ってそれぞれの応答を作成することです。まず、「/name」コマンドに応答します。 

         //--- If the message is "/name" or "Name"
         if (text=="/name" || text=="Name"){
            string message = "The file name of the EA that I control is:\n";
            message += "\xF50B"+__FILE__+" Enjoy.\n";
            sendMessageToTelegram(chat.member_id,message,NULL);
         }

ここでは、ユーザーから受信したメッセージが/nameかNameのどちらかであるかどうかを確認します。このチェックで肯定的な結果が得られた場合は、現在使用されているEAファイルの名前を含むユーザーへの返信の作成に取り掛かります。「The file name of the EA that I control is:\n」というテキストで始まるmessageという文字列変数を初期化します。この最初の宣言に続いて、本の絵文字(コード\xF50Bで表される)とEAファイルの名前を続けます。

ファイル名を取得するには、組み込みのMQL5マクロFILEを使用します。マクロはファイル名とパスを返します。次に、ユーザーに送信するメッセージを作成します。メッセージは、EAファイルの名前とそのパスで構成されます。作成したメッセージは、sendMessageToTelegram関数を使用して送信します。この関数は3つのパラメータを取ります。1つ目は、メッセージを送信するユーザーのチャットIDです。2つ目はメッセージ自体です。3つ目のパラメータはNULLに設定されており、メッセージと一緒にカスタムキーボードやボタンコマンドを送信しないことを示します。追加のキーボードを作成したくないため、これは重要です。「/name」コマンドまたはそのボタンをクリックすると、以下のようにそれぞれの応答が返されます。

名前コマンド

うまくいきました。同様に、口座情報コマンドと相場コマンドに対するそれぞれの応答を作成します。これは以下のコードスニペットで実現できます。

         //--- If the message is "/info" or "Account Info"
         ushort MONEYBAG = 0xF4B0; //--- Define money bag emoji
         string MONEYBAGcode = ShortToString(MONEYBAG); //--- Convert emoji to string
         if(text=="/info" || text=="Account Info"){
            string currency=AccountInfoString(ACCOUNT_CURRENCY); //--- Get the account currency
            string message="\x2733\Account No: "+(string)AccountInfoInteger(ACCOUNT_LOGIN)+"\n";
            message+="\x23F0\Account Server: "+AccountInfoString(ACCOUNT_SERVER)+"\n";
            message+=MONEYBAGcode+"Balance: "+(string)AccountInfoDouble(ACCOUNT_BALANCE)+" "+currency+"\n";
            message+="\x2705\Profit: "+(string)AccountInfoDouble(ACCOUNT_PROFIT)+" "+currency+"\n";
            
            //--- Send the response message
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;
         }

         //--- If the message is "/quotes" or "Quotes"
         if(text=="/quotes" || text=="Quotes"){
            double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); //--- Get the current ask price
            double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); //--- Get the current bid price
            string message="\xF170 Ask: "+(string)Ask+"\n";
            message+="\xF171 Bid: "+(string)Bid+"\n";
            
            //--- Send the response message
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;
         }

取引操作コマンド、特に買いポジションを開く場合、次のロジックを使用します。

         //--- If the message is "/buy" or "Buy"
         if (text=="/buy" || text=="Buy"){
            CTrade obj_trade; //--- Create a trade object
            double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); //--- Get the current ask price
            double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); //--- Get the current bid price
            obj_trade.Buy(0.01,NULL,0,Bid-300*_Point,Bid+300*_Point); //--- Open a buy position
            double entry=0,sl=0,tp=0,vol=0;
            ulong ticket = obj_trade.ResultOrder(); //--- Get the ticket number of the new order
            if (ticket > 0){
               if (PositionSelectByTicket(ticket)){ //--- Select the position by ticket
                  entry=PositionGetDouble(POSITION_PRICE_OPEN); //--- Get the entry price
                  sl=PositionGetDouble(POSITION_SL); //--- Get the stop loss price
                  tp=PositionGetDouble(POSITION_TP); //--- Get the take profit price
                  vol=PositionGetDouble(POSITION_VOLUME); //--- Get the volume
               }
            }
            string message="\xF340\Opened BUY Position:\n";
            message+="Ticket: "+(string)ticket+"\n";
            message+="Open Price: "+(string)entry+"\n";
            message+="Lots: "+(string)vol+"\n";
            message+="SL: "+(string)sl+"\n";
            message+="TP: "+(string)tp+"\n";
            
            //--- Send the response message
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;
         }

ここでは、ユーザーが「/buy」または「Buy」というメッセージを送信するシナリオを扱います。最初のステップは、取引操作を実行するために使用するobj_tradeという名前のCTradeオブジェクトを作成することです。次に、SymbolInfoDouble関数を呼び出して、現在のAsk値とBId値を取得します。買いポジションを建てるには、CTradeオブジェクトのBuy関数を使用します。取引量を0.01ロットに設定します。SL(ストップロス)とTP(テイクプロフィット)には、それぞれ、買い値から300ポイント引いた値と、買い値に300ポイントを加えた値を設定します。

ポジションが建てられたら、ResultOrder関数を介して新しい注文のチケット番号を確認します。チケットが手元にある場合は、PositionGetInteger関数を使用してチケットによるポジションを選択します。次に、エントリ価格、取引量、ストップロス、テイクプロフィットなどの重要な統計情報を取得します。これらの数値を使用して、ユーザーに買いポジションを開いたことを通知するメッセージを作成します。ポジションのクローズとcontactコマンドを処理するには、次のような同様のロジックを使用します。

         //--- If the message is "/close" or "Close"
         if (text=="/close" || text=="Close"){
            CTrade obj_trade; //--- Create a trade object
            int totalOpenBefore = PositionsTotal(); //--- Get the total number of open positions before closing
            obj_trade.PositionClose(_Symbol); //--- Close the position for the symbol
            int totalOpenAfter = PositionsTotal(); //--- Get the total number of open positions after closing
            string message="\xF62F\Closed Position:\n";
            message+="Total Positions (Before): "+(string)totalOpenBefore+"\n";
            message+="Total Positions (After): "+(string)totalOpenAfter+"\n";
            
            //--- Send the response message
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;
         }

         //--- If the message is "/contact" or "Contact"
         if (text=="/contact" || text=="Contact"){
            string message="Contact the developer via link below:\n";
            message+="https://t.me/Forex_Algo_Trader";
            
            //--- Send the contact message
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;
         }

これで、Telegramから送信されたコマンドに応答できることがわかりました。ここまでは、プレーンテキストメッセージのみを送信していましたが、もう少し凝って、HyperText Markup Language (HTML)(Markdownとも呼ばれる)を使用してテキストメッセージをフォーマットしてみましょう。お好みでどうぞ。

         //--- If the message is "/join" or "Join"
         if (text=="/join" || text=="Join"){
            string message="You want to be part of our MQL5 Community?\n";
            message+="Welcome! <a href=\"https://t.me/forexalgo_trading\">Click me</a> to join.\n";
            message+="<s>Civil Engineering</s> Forex AlgoTrading\n";//strikethrough
            message+="<pre>This is a sample of our MQL5 code</pre>\n";//preformat
            message+="<u><i>Remember to follow community guidelines!\xF64F\</i></u>\n";//italic, underline
            message+="<b>Happy Trading!</b>\n";//bold
            
            //--- Send the join message
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;
         }

ここでは、ユーザーが「/join」または「Join」というメッセージを送信したときに応答します。まず、MQL5コミュニティへの参加を促す招待メッセージを送信します。このメッセージには、ユーザーがクリックしてコミュニティに参加できるハイパーリンクと、TelegramでHTMLタグを使用してテキストをフォーマットする方法の例がいくつか含まれています。

  • 取り消し線テキスト:<s>タグを使い、「Civil Engineering」を取り消し線付きで表示することで、「Forex AlgoTrading」に重点を置く内容を強調します。
  • フォーマット済みテキスト:<pre>タグを使用してMQL5コードのサンプルを表示し、視覚的にわかりやすく整えます。
  • 斜体および下線付きのテキスト:<u>タグと<i>タグを組み合わせ、ユーザーにコミュニティガイドラインに従うよう促すリマインダーを下線付きの斜体で強調し、注意を引くためにUnicode絵文字も追加します。
  • 太字テキスト:<b>タグで「Happy Trading!」を太字にして、締めくくりのメッセージを視覚的に際立たせます。

最後に、これらのフォーマットを含んだメッセージをsendMessageToTelegram関数でTelegram経由でユーザーに送信し、視覚的に整った魅力的なMQL5コミュニティへの招待状がユーザーに届くようにします。この設定により、実行時に以下のような出力が得られます。

HTMLエンティティ

コマンドリストを使い果たしたので、引き続き応答キーボードを変更し、[more]ボタンがクリックされたときに新しいキーボードを生成してみましょう。以下のロジックが実装されています。

         //--- If the message is "more" or "More"
         if (text=="more" || text=="More"){
            chat.member_state=1; //--- Update chat state to show more options
            string message="Choose More Options Below:";
            
            //--- Send the more options message with the more options keyboard
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_MORE,false,true));
            continue;
         }

ユーザーから「more」または「More」というメッセージを受け取った場合、それはチャットボットに対して会話のコンテキストを更新する指示とみなされます。このメッセージは、ユーザーが現在のオプションに満足していない、あるいはまだ希望する情報が得られていないことを示している可能性があります。したがって、こうしたリクエストには、さらに多様な選択肢を提示することで応答する必要があります。この対応では、KEYB_MOREという新しいキーボードレイアウトを使用し、ユーザーに追加の選択肢を提供するメッセージを送信します。これは以下のようになります。

   #define EMOJI_UP    "\x2B06" //--- Upwards arrow emoji
   #define KEYB_MORE "[[\""+EMOJI_UP+"\"],[\"Buy\",\"Close\",\"Next\"]]" //--- More options keyboard layout

プログラムを実行すると、次の出力が得られます。

MORE GIF

うまくいきました。他のコマンドも同様に処理できます。

         //--- If the message is the up emoji
         if(text==EMOJI_UP){
            chat.member_state=0; //--- Reset chat state
            string message="Choose a menu item:";
            
            //--- Send the message with the main keyboard
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_MAIN,false,false));
            continue;
         }

         //--- If the message is "next" or "Next"
         if(text=="next" || text=="Next"){
            chat.member_state=2; //--- Update chat state to show next options
            string message="Choose Still More Options Below:";
            
            //--- Send the next options message with the next options keyboard
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_NEXT,false,true));
            continue;
         }

         //--- If the message is the pistol emoji
         if (text==EMOJI_PISTOL){
            if (chat.member_state==2){
               chat.member_state=1; //--- Change state to show more options
               string message="Choose More Options Below:";
               
               //--- Send the message with the more options keyboard
               sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_MORE,false,true));
            }
            else {
               chat.member_state=0; //--- Reset chat state
               string message="Choose a menu item:";
               
               //--- Send the message with the main keyboard
               sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_MAIN,false,false));
            }
            continue;
         }

         //--- If the message is the cancel emoji
         if (text==EMOJI_CANCEL){
            chat.member_state=0; //--- Reset chat state
            string message="Choose /start or /help to begin.";
            
            //--- Send the cancel message with hidden custom reply keyboard
            sendMessageToTelegram(chat.member_id,message,hideCustomReplyKeyboard());
            continue;
         }

ここでは、チャットインターフェイスを制御するために、さまざまなユーザーメッセージを処理します。ユーザーがupの絵文字を送信すると、それを信号として受け取り、チャット状態を0にリセットします。そして、メインのキーボードレイアウトとともに、ユーザーにもう一度メニュー項目を選択するよう促します。ユーザーがnextまたはNextを送信したら、チャット状態を2に更新し、再度メニュー項目の選択を促します。この場合、追加オプションが表示されるキーボードレイアウトを使用します。

ピストルの絵文字を受け取った場合、現在の状態に基づいてチャット状態を調整します。状態が2であれば、状態を1に切り替え、その他のオプションキーボードを表示します。それ以外の状態の場合は、状態を0にリセットし、メインメニューキーボードを表示します。キャンセルの絵文字が送信された場合、チャット状態を0にリセットし、開始するには「/start」または「/help」のいずれかを選択するように促すメッセージを送信します。このメッセージは非表示のカスタム返信キーボードとともに送信され、ユーザーのアクティブなカスタムキーボードがクリアされます。使用される追加のカスタムレイアウトは次のとおりです。

   #define EMOJI_PISTOL   "\xF52B" //--- Pistol emoji
   #define KEYB_NEXT "[[\""+EMOJI_UP+"\",\"Contact\",\"Join\",\""+EMOJI_PISTOL+"\"]]" //--- Next options keyboard layout

ここまでで、すべて完了です。あとはスクリーンショットコマンドを処理するだけです。チャート画像の受信モードを処理するために、次のロジックが実装されています。この目的には、手動で入力するのではなく、キーボードレイアウトが使用されます。

         //--- If the message is "/screenshot" or "Screenshot"
         static string symbol = _Symbol; //--- Default symbol
         static ENUM_TIMEFRAMES period = _Period; //--- Default period
         if (text=="/screenshot" || text=="Screenshot"){
            chat.member_state = 10; //--- Set state to screenshot request
            string message="Provide a symbol like 'AUDUSDm'";
            
            //--- Send the message with the symbols keyboard
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_SYMBOLS,false,false));
            continue;
         }

         //--- Handle state 10 (symbol selection for screenshot)
         if (chat.member_state==10){
            string user_symbol = text; //--- Get the user-provided symbol
            if (SymbolSelect(user_symbol,true)){ //--- Check if the symbol is valid
               chat.member_state = 11; //--- Update state to period request
               string message = "CORRECT: Symbol is found\n";
               message += "Now provide a Period like 'H1'";
               symbol = user_symbol; //--- Update symbol
               
               //--- Send the message with the periods keyboard
               sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_PERIODS,false,false));
            }
            else {
               string message = "WRONG: Symbol is invalid\n";
               message += "Provide a correct symbol name like 'AUDUSDm' to proceed.";
               
               //--- Send the invalid symbol message with the symbols keyboard
               sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_SYMBOLS,false,false));
            }
            continue;
         }

         //--- Handle state 11 (period selection for screenshot)
         if (chat.member_state==11){
            bool found=false; //--- Flag to check if period is valid
            int total=ArraySize(periods); //--- Get the number of defined periods
            for(int k=0; k<total; k++){
               string str_tf=StringSubstr(EnumToString(periods[k]),7); //--- Convert period enum to string
               if(StringCompare(str_tf,text,false)==0){ //--- Check if period matches
                  ENUM_TIMEFRAMES user_period=periods[k]; //--- Set user-selected period
                  period = user_period; //--- Update period
                  found=true;
                  break;
               }
            }
            if (found){
               string message = "CORRECT: Period is valid\n";
               message += "Screenshot sending process initiated \xF60E";
               
               //--- Send the valid period message with the periods keyboard
               sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_PERIODS,false,false));
               string caption = "Screenshot of Symbol: "+symbol+
                                " ("+EnumToString(ENUM_TIMEFRAMES(period))+
                                ") @ Time: "+TimeToString(TimeCurrent());
               
               //--- Send the screenshot to Telegram
               sendScreenshotToTelegram(chat.member_id,symbol,period,caption);
            }
            else {
               string message = "WRONG: Period is invalid\n";
               message += "Provide a correct period like 'H1' to proceed.";
               
               //--- Send the invalid period message with the periods keyboard
               sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_PERIODS,false,false));
            }
            continue;
         }

ここでは、チャットフローのさまざまな状態を管理することで、チャートのスクリーンショットを求めるユーザーのリクエストに対応します。ユーザーが「/screenshot」または「Screenshot」コマンドを送信したら、チャット状態を10に設定し、使用可能なシンボルが表示されたキーボードを表示して、ユーザーにシンボルを入力するよう促します。ここで重要なのは、チャット状態が1000など任意の数値で指定できる点です。これにより、応答処理中の状態を識別子や数量詞として保持できます。ユーザーが銘柄を指定した場合、その有効性をチェックします。有効であれば、使用可能な時間枠オプションが表示されたキーボードを使用し、ユーザーにチャートの時間(期間)を入力するよう促します。もし無効な銘柄が入力された場合は、その旨を通知し、再度有効な銘柄の入力を求めます。

ユーザーが時間枠を入力したら、その時間枠が定義済みの有効なオプションの1つであるかを確認します。有効な場合、チャット状態を更新し、指定された銘柄のスクリーンショットリクエストを最初に示したif-then文に基づきバックエンドに転送し、スクリーンショットプロセスを開始します。もしユーザーが有効な定義済みオプションに一致しない時間枠を指定した場合、誤りを通知し、初回の入力リクエストと共に有効なオプションのリストを再提示します。使用する銘柄と期間、および時間枠配列のカスタム応答キーボードは、以下に定義されています。

   #define KEYB_SYMBOLS "[[\""+EMOJI_UP+"\",\"AUDUSDm\",\"AUDCADm\"],[\"EURJPYm\",\"EURCHFm\",\"EURUSDm\"],[\"USDCHFm\",\"USDCADm\",\""+EMOJI_PISTOL+"\"]]" //--- Symbol selection keyboard layout
   #define KEYB_PERIODS "[[\""+EMOJI_UP+"\",\"M1\",\"M15\",\"M30\"],[\""+EMOJI_CANCEL+"\",\"H1\",\"H4\",\"D1\"]]" //--- Period selection keyboard layout

   //--- Define timeframes array for screenshot requests
   const ENUM_TIMEFRAMES periods[] = {PERIOD_M1,PERIOD_M15,PERIOD_M30,PERIOD_H1,PERIOD_H4,PERIOD_D1};

ここまでで、完全にカスタマイズされたキーボードと返信の設定がすべて完了しました。これを確かめるために、プログラムを実行します。得られた出力結果は次のとおりです。

スクリーンショットGIF

ここで、スクリーンショット送信プロセスが開始され、完了したことがわかります。無効なコマンドまたは入力は、有効なコマンドのみがユーザーによって送信されるように処理されます。すべてが意図したとおりに機能することを確認し、結果として生じる制限を正確に特定するには、実装を徹底的にテストする必要があります。これは次のセクションで説明します。


実装のテスト

テストは、プログラムが意図通りに動作していることを確認する上で重要な段階です。正しく機能するかを確かめるためには、テストが欠かせません。まず最初におこなうべきことは、リンクの応答においてWebページのプレビューを有効にすることです。リンクのプレビューが有効になると、ユーザーはクリック前にページ内容を簡単に確認でき、タイトルや画像が表示されることでリンク先の概要が分かります。これは、特にリンクのテキストだけでは内容の品質を判断しにくい場合に、ユーザー体験を向上させるものです。そのため、次のように無効なプレビューをfalseに設定します。

//+------------------------------------------------------------------+
//| Send a message to Telegram                                      |
//+------------------------------------------------------------------+
int sendMessageToTelegram( ... ){
   
   //--- ...

   params+="&disable_web_page_preview=false"; //--- Enable web page preview in the message

   //--- ...
   
}

これを実行すると、次の出力が得られます。

Webページのプレビューが有効

これで、図のようにWebページのプレビューを受信できるようになりました。うまくいきました。次に、次のように、書式設定エンティティまたは解析モードをHyperText Markup Language (HTML)からMarkdownに変更します。

//+------------------------------------------------------------------+
//| Send a message to Telegram                                      |
//+------------------------------------------------------------------+
int sendMessageToTelegram( ... ){

   //--- ...

   params+="&parse_mode=Markdown"; //--- Set parse mode to Markdown (can also be HTML)

   //--- ...

}

Markdown解析モードでは、Markdownエンティティを使用して初期コードのフォーマット構造全体を変更する必要があります。正しい形式は以下のようになります。 

      //--- If the message is "/join" or "Join"
      if (text=="/join" || text=="Join"){
         string message = "You want to be part of our MQL5 Community?\n";
         message += "Welcome! [Click me](https://t.me/forexalgo_trading) to join.\n"; // Link
         message += "~Civil Engineering~ Forex AlgoTrading\n"; // Strikethrough
         message += "```\nThis is a sample of our MQL5 code\n```"; // Preformatted text
         message += "*_Remember to follow community guidelines! \xF64F_*"; // Italic and underline
         message += "**Happy Trading!**\n"; // Bold
      
         //--- Send the join message
         sendMessageToTelegram(chat.member_id, message, NULL);
         continue;
      }

変更した点は次のとおりです:

  • リンク:Markdownでは、リンクは<a href="URL">text</a>ではなく[text](URL)で作成されます。
  • 取り消し線:取り消し線には、<s>text</s>の代わりに~text~を使用します。
  • フォーマット済みテキスト:フォーマット済みテキストをフォーマットするには、<pre>text</pre>の代わりに3つのバックティック(```)を使用します。
  • 斜体と下線:Markdownはネイティブでは下線をサポートしていません。最も近いのは、斜体の*text*または_text_です。HTMLの下線効果はMarkdownでは直接サポートされていないため、必要に応じてプレースホルダーに組み込まれます。
  • 太字:太字にするには、<b>text</b>の代わりに二重アスタリスク**text**を使用します。

プログラムを実行すると、次の出力が表示されます。

Markdown出力

テストプロセスを説明するために、プログラムの動作を紹介するビデオを用意しました。このビデオでは、実行したさまざまなテストケースを示し、プログラムがさまざまな入力にどのように応答し、必要なタスクをどの程度適切に実行したかを強調します。このビデオを見ると、テストプロセスが非常に明確にわかり、実装が期待される要件を満たしていることを疑いなく確認できます。ビデオは以下に掲載されています。

要約すると、添付のビデオで示されているように、実装の実行と検証が成功したことで、プログラムが意図したとおりに機能していることが確認されました。


結論

まとめると、私たちが開発したEAは、MetaQuotes Language 5 (MQL5)の取引プラットフォームとMetaTrader 5取引プラットフォームをTelegramメッセージングアプリに統合し、ユーザーが取引ロボットと対話できる環境を提供します。しない理由はありません。Telegramは、自動取引システムを制御するための、強力で使いやすい方法を提供してくれるプラットフォームです。これにより、ユーザーは簡単にコマンドを送信し、システムからリアルタイムの応答を受け取ることができます。

私たちの場合、EAがTelegramボットと直接接続されており、従来のようにシグナル生成や中継を待つ必要がありません。これにより、ユーザーはいつでもEAとスムーズにやり取りが可能となりました。また、ユーザーとボットの間で効果的な会話ができるような仕組みも設定し、Telegram経由で送信されたMQL5コマンドが確実に解釈されることを確認しています。多くのテストを経て、私たちのEAが信頼性の高い堅牢なシステムであると自信を持って言えます。

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

添付されたファイル |
MQL5入門(第9回):MQL5のオブジェクトの理解と使用 MQL5入門(第9回):MQL5のオブジェクトの理解と使用
現在のデータと履歴データを使用して、MQL5でチャートオブジェクトを作成およびカスタマイズする方法を学びます。このプロジェクトベースのガイドは、取引を可視化し、MQL5の概念を実際に適用するのに役立ち、取引のニーズに合わせたツールの構築が容易になります。
古典的な戦略を再構築する(第8回):USDCADをめぐる為替市場と貴金属市場 古典的な戦略を再構築する(第8回):USDCADをめぐる為替市場と貴金属市場
この連載では、よく知られた取引戦略を再検討し、AIを使って改善できるかどうかを検証します。本日のディスカッションでは、貴金属と通貨の間に信頼できる関係があるかどうかを検証します。
確率最適化と最適制御の例 確率最適化と最適制御の例
SMOC(Stochastic Model Optimal Controlの略と思われる)と名付けられたこのエキスパートアドバイザー(EA)は、MetaTrader 5用の高度なアルゴリズム取引システムのシンプルな例です。テクニカル指標、モデル予測制御、動的リスク管理を組み合わせて取引判断をおこないます。このEAには、適応パラメーター、ボラティリティに基づくポジションサイジング、トレンド分析が組み込まれており、さまざまな市場環境においてパフォーマンスを最適化します。
人工協調探索(ACS)アルゴリズム 人工協調探索(ACS)アルゴリズム
人工協調探索(ACS)は、バイナリ行列と、相互主義的関係と協調に基づく複数の動的な個体群を用いて、最適解を迅速かつ正確に探索する革新的な手法です。捕食者と被食者に対するACS独自のアプローチにより、数値最適化問題で優れた結果を出すことができます。