创建 MQL5-Telegram 集成 EA 交易(第 6 部分):添加响应式内联按钮·进阶篇
🤖

创建 MQL5-Telegram 集成 EA 交易(第 6 部分):添加响应式内联按钮·进阶篇

(2/3)· 从回复键盘到内联按钮,EA 与交易者的对话延迟可能从秒级降到单次点击

进阶 第 2/3 篇

很多 EA 的 Telegram 通知还停留在「发消息等指令」阶段,用户要敲文本才能下单或撤单。当行情跳过两三根 K 线,这种来回打字的交互基本等于把节奏交给滑点。本篇接着上一部分的自定义键盘,把按钮直接嵌进消息本体。

EA 里怎么把 Telegram 机器人先搭起来

想把 MT5 上的 EA 接进 Telegram 做信号转发或远程指令,第一步是把机器人对象本身定义清楚。下面这段类声明把 token、名称、已处理消息 ID、用户白名单和聊天列表都封进了 Class_Bot_EA,私有成员不对外暴露,公开方法只留取更新、处理消息和回调三类接口。 构造函数里有个细节值得注意:member_token 先用 NULL 初始化,再调用 getTrimmedToken(InpToken) 做一次去空格处理。很多连不上 bot 的报错,根源就是输入 token 首尾带了不可见字符,这一行能帮你排掉一类低级故障。 取更新的函数 getChatUpdates 开头就做了空 token 拦截——若 member_token == NULL 直接 Print("ERR: TOKEN EMPTY") 并返回 -1。URL 则按 Telegram_BASE_URL + "/bot" + token + "/getUpdates" 拼出来,走标准轮询接口。外汇与贵金属自动化接口涉及账户权限与网络安全,属于高风险操作,实盘前务必在模拟环境验证。

MQL5 / C++
class=class="str">"cmt">//|                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class Class_Bot_EA {
class="kw">private:
    class="type">class="kw">string              member_token;      class=class="str">"cmt">//--- Stores the bot’s token.
    class="type">class="kw">string              member_name;       class=class="str">"cmt">//--- Stores the bot’s name.
    class="type">long                member_update_id;  class=class="str">"cmt">//--- Stores the last update ID processed by the bot.
    CArrayString        member_users_filter; class=class="str">"cmt">//--- Array to filter messages from specific users.
    class="type">bool                member_first_remove; class=class="str">"cmt">//--- Indicates if the first message should be removed.
    
class="kw">protected:
    CList               member_chats;      class=class="str">"cmt">//--- List to store chat objects.
class="kw">public:
    Class_Bot_EA();                       class=class="str">"cmt">//--- Constructor.
    ~Class_Bot_EA(){};                    class=class="str">"cmt">//--- Destructor.
    class="type">int getChatUpdates();                 class=class="str">"cmt">//--- Function to get updates from Telegram.
    class="type">void ProcessMessages();               class=class="str">"cmt">//--- Function to process incoming messages.
    class="type">void ProcessCallbackQuery(Class_CallbackQuery &cb_query); class=class="str">"cmt">//--- Function to process callback queries.
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  Constructor for Class_Bot_EA                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
Class_Bot_EA::Class_Bot_EA(class="type">void) {
    member_token = NULL;                  class=class="str">"cmt">//--- Initialize bot token to NULL (empty).
    member_token = getTrimmedToken(InpToken); class=class="str">"cmt">//--- Assign bot token by trimming input token.
    member_name = NULL;                   class=class="str">"cmt">//--- Initialize bot name to NULL.
    member_update_id = class="num">0;                 class=class="str">"cmt">//--- Initialize last update ID to zero.
    member_first_remove = true;           class=class="str">"cmt">//--- Set first remove flag to true.
    member_chats.Clear();                 class=class="str">"cmt">//--- Clear the list of chat objects.
    member_users_filter.Clear();          class=class="str">"cmt">//--- Clear the user filter array.
}

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  Function to get chat updates from Telegram                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int Class_Bot_EA::getChatUpdates(class="type">void) {
    class=class="str">"cmt">//... 
    class="kw">return class="num">0;                             class=class="str">"cmt">//--- Return class="num">0 to indicate successful processing of updates.
}

    if (member_token == NULL) {           class=class="str">"cmt">//--- If bot token is empty
        Print("ERR: TOKEN EMPTY");        class=class="str">"cmt">//--- Print error message indicating empty token.
        class="kw">return (-class="num">1);                      class=class="str">"cmt">//--- Return -class="num">1 to indicate error.
    }
    class="type">class="kw">string out;                           class=class="str">"cmt">//--- String to hold response data.
    class="type">class="kw">string url = TELEGRAM_BASE_URL + "/bot" + member_token + "/getUpdates"; class=class="str">"cmt">//--- Construct URL for Telegram API.

「拉取 Telegram 更新并解析消息体」

在 MT5 里做信号中转,第一步是把 Telegram 的 update 接口用 POST 轮询拉回来。下面这段逻辑以 member_update_id 作为 offset 参数,避免重复处理已读消息,超时由 WEB_TIMEOUT 控制,返回 0 才视为请求成功。 成功拿到响应后先反序列化成 JSON 对象;若 Deserialize 失败或顶层 ok 字段为 false,直接 Print 报错并返回 -1,不继续往下走。这里 res==0 与 ok==true 是两个独立关卡,漏掉任何一个都会在后续数组访问时崩 EA。 通过 ArraySize(obj_json["result"].m_elements) 取回本次更新的总条数,逐条遍历。只处理带 message 字段的节点,把 update_id、message_id、date、text 以及 from.id 塞进 Class_Message 实例,其中 text 要经过 decodeStringCharacters 还原转义字符,否则中文与表情可能乱码。 实盘接外部消息驱动下单属于外汇/贵金属高风险的非常规链路,任何 JSON 解析异常都可能让 EA 静默失联,建议在 demo 账户先跑通 offset 自增逻辑再上真实环境。

MQL5 / C++
class="type">class="kw">string params = "offset=" + IntegerToString(member_update_id); class=class="str">"cmt">//--- Set parameters including the offset based on the last update ID.

class="type">int res = postRequest(out, url, params, WEB_TIMEOUT); class=class="str">"cmt">//--- Send a POST request to Telegram with a timeout.
if (res == class="num">0) { class=class="str">"cmt">//--- If request succeeds(res = class="num">0)
    CJSONValue obj_json(NULL, jv_UNDEF); class=class="str">"cmt">//--- Create a JSON object to parse the response.
    class="type">bool done = obj_json.Deserialize(out); class=class="str">"cmt">//--- Deserialize the response.
    if (!done) { class=class="str">"cmt">//--- If deserialization fails
        Print("ERR: JSON PARSING"); class=class="str">"cmt">//--- Print error message indicating JSON parsing error.
        class="kw">return (-class="num">1); class=class="str">"cmt">//--- Return -class="num">1 to indicate error.
    }

    class="type">bool ok = obj_json["ok"].ToBool(); class=class="str">"cmt">//--- Check if the response has "ok" field set to true.
    if (!ok) { class=class="str">"cmt">//--- If "ok" field is false
        Print("ERR: JSON NOT OK"); class=class="str">"cmt">//--- Print error message indicating that JSON response is not okay.
        class="kw">return (-class="num">1); class=class="str">"cmt">//--- Return -class="num">1 to indicate error.
    }
}
    class="type">int total = ArraySize(obj_json["result"].m_elements); class=class="str">"cmt">//--- Get the total number of update elements.
    for (class="type">int i = class="num">0; i < total; i++) { class=class="str">"cmt">//--- Iterate through each update element.
        CJSONValue obj_item = obj_json["result"].m_elements[i]; class=class="str">"cmt">//--- Access individual update element.

        if (obj_item["message"].m_type != jv_UNDEF) { class=class="str">"cmt">//--- Check if the update has a message.
            Class_Message obj_msg; class=class="str">"cmt">//--- Create an instance of Class_Message to store the message details.
            obj_msg.update_id = obj_item["update_id"].ToInt(); class=class="str">"cmt">//--- Extract and store update ID.
            obj_msg.message_id = obj_item["message"]["message_id"].ToInt(); class=class="str">"cmt">//--- Extract and store message ID.
            obj_msg.message_date = (class="type">class="kw">datetime)obj_item["message"]["date"].ToInt(); class=class="str">"cmt">//--- Extract and store message date.
            obj_msg.message_text = obj_item["message"]["text"].ToStr(); class=class="str">"cmt">//--- Extract and store message text.
            obj_msg.message_text = decodeStringCharacters(obj_msg.message_text); class=class="str">"cmt">//--- Decode any special characters in the message text.
        }
    }
            obj_msg.from_id = obj_item["message"]["from"]["id"].ToInt(); class=class="str">"cmt">//--- Extract and store the sender&class="macro">#x27;s ID.

◍ 从 JSON 报文里抠出发信人字段

做 MT5 接 Telegram 信号的中继层时,第一步是把回调 JSON 里的发信人信息落进结构体。下面这段直接从 obj_item["message"]["from"] 取 first_name、last_name、username,并立刻过一遍 decodeStringCharacters 做特殊字符解码,否则带表情或西里尔字母的昵称进文件会乱码。 聊天维度也要单独抓:chat_id 用 ToInt() 转整型,聊天名和类型走 ToStr()。注意 private 群和 group 的 first_name 字段可能为空,代码里照取不误,空串后续靠过滤逻辑兜住。 update_id 处理上,每消费一条就把 member_update_id 设为 obj_msg.update_id + 1,保证长轮询不重复拉。用户白名单判断用 member_users_filter.SearchLinear,数组长度为 0 表示不过滤,大于 0 则只认列表内的 from_username,命中才进 index 初值 -1 的后续分支。

MQL5 / C++
obj_msg.from_first_name = obj_item["message"]["from"]["first_name"].ToStr(); class=class="str">"cmt">//--- Extract and store the sender&class="macro">#x27;s first name.
obj_msg.from_first_name = decodeStringCharacters(obj_msg.from_first_name); class=class="str">"cmt">//--- Decode any special characters in the sender&class="macro">#x27;s first name.
obj_msg.from_last_name = obj_item["message"]["from"]["last_name"].ToStr(); class=class="str">"cmt">//--- Extract and store the sender&class="macro">#x27;s last name.
obj_msg.from_last_name = decodeStringCharacters(obj_msg.from_last_name); class=class="str">"cmt">//--- Decode any special characters in the sender&class="macro">#x27;s last name.
obj_msg.from_username = obj_item["message"]["from"]["username"].ToStr(); class=class="str">"cmt">//--- Extract and store the sender&class="macro">#x27;s username.
obj_msg.from_username = decodeStringCharacters(obj_msg.from_username); class=class="str">"cmt">//--- Decode any special characters in the sender&class="macro">#x27;s username.

obj_msg.chat_id = obj_item["message"]["chat"]["id"].ToInt(); class=class="str">"cmt">//--- Extract and store the chat ID.
obj_msg.chat_first_name = obj_item["message"]["chat"]["first_name"].ToStr(); class=class="str">"cmt">//--- Extract and store the chat&class="macro">#x27;s first name.
obj_msg.chat_first_name = decodeStringCharacters(obj_msg.chat_first_name); class=class="str">"cmt">//--- Decode any special characters in the chat&class="macro">#x27;s first name.
obj_msg.chat_last_name = obj_item["message"]["chat"]["last_name"].ToStr(); class=class="str">"cmt">//--- Extract and store the chat&class="macro">#x27;s last name.
obj_msg.chat_last_name = decodeStringCharacters(obj_msg.chat_last_name); class=class="str">"cmt">//--- Decode any special characters in the chat&class="macro">#x27;s last name.
obj_msg.chat_username = obj_item["message"]["chat"]["username"].ToStr(); class=class="str">"cmt">//--- Extract and store the chat&class="macro">#x27;s username.
obj_msg.chat_username = decodeStringCharacters(obj_msg.chat_username); class=class="str">"cmt">//--- Decode any special characters in the chat&class="macro">#x27;s username.
obj_msg.chat_type = obj_item["message"]["chat"]["type"].ToStr(); class=class="str">"cmt">//--- Extract and store the chat type.
class=class="str">"cmt">//--- Process the message based on chat ID.
member_update_id = obj_msg.update_id + class="num">1; class=class="str">"cmt">//--- Update the last processed update ID.
class=class="str">"cmt">//--- Check if we need to filter messages based on user or if no filter is applied.
if (member_users_filter.Total() == class="num">0 ||
    (member_users_filter.Total() > class="num">0 &&
    member_users_filter.SearchLinear(obj_msg.from_username) >= class="num">0)) {

    class="type">int index = -class="num">1; class=class="str">"cmt">//--- Initialize index to -class="num">1 (indicating no chat found).
}

按 chat_id 归并消息与新建会话对象

这段逻辑解决的是 Telegram 推送过来的更新里,如何把消息按会话归并到本地链表 member_chats。先从头遍历已有聊天对象,用 chat_id 比对 obj_msg.chat_id,命中就把下标存进 index 并 break,避免无谓循环。 若 index 仍为 -1,说明是陌生会话:new 一个 Class_Chat 挂到表尾,把 chat_id、TimeLocal() 写入 member_time,member_state 置 0,并把首条消息塞进 member_new_one、done 标 false。 命中已有会话时,只刷新 member_time 与最新消息文本,done 同样置 false 等待后续处理。这样每次更新最多做一次线性扫描,会话量在几百以内时开销可忽略。 回调查询另走一支:当 JSON 里 'callback_query' 字段类型不是 jv_UNDEF,就实例化 Class_CallbackQuery 单独接手,不和普通消息抢同一套归并路径。

MQL5 / C++
for (class="type">int j = class="num">0; j < member_chats.Total(); j++) { class=class="str">"cmt">//--- Iterate through all chat objects.
      Class_Chat *chat = member_chats.GetNodeAtIndex(j); class=class="str">"cmt">//--- Get chat object by index.
      if (chat.member_id == obj_msg.chat_id) { class=class="str">"cmt">//--- If chat ID matches
            index = j; class=class="str">"cmt">//--- Store the index.
            break; class=class="str">"cmt">//--- Break the loop since we found the chat.
      }
}
if (index == -class="num">1) { class=class="str">"cmt">//--- If no matching chat was found
      member_chats.Add(new Class_Chat); class=class="str">"cmt">//--- Create a new chat object and add it to the list.
      Class_Chat *chat = member_chats.GetLastNode(); class=class="str">"cmt">//--- Get the last(newly added) chat object.
      chat.member_id = obj_msg.chat_id; class=class="str">"cmt">//--- Assign the chat ID.
      chat.member_time = TimeLocal(); class=class="str">"cmt">//--- Record the current time for the chat.
      chat.member_state = class="num">0; class=class="str">"cmt">//--- Initialize the chat state to class="num">0.
      chat.member_new_one.message_text = obj_msg.message_text; class=class="str">"cmt">//--- Store the new message in the chat.
      chat.member_new_one.done = false; class=class="str">"cmt">//--- Mark the new message as not processed.
} else { class=class="str">"cmt">//--- If matching chat was found
      Class_Chat *chat = member_chats.GetNodeAtIndex(index); class=class="str">"cmt">//--- Get the chat object by index.
      chat.member_time = TimeLocal(); class=class="str">"cmt">//--- Update the time for the chat.
      chat.member_new_one.message_text = obj_msg.message_text; class=class="str">"cmt">//--- Store the new message.
      chat.member_new_one.done = false; class=class="str">"cmt">//--- Mark the new message as not processed.
}

class=class="str">"cmt">//--- Handle callback queries from Telegram.
if (obj_item["callback_query"].m_type != jv_UNDEF) { class=class="str">"cmt">//--- Check if there is a callback query in the update.
      Class_CallbackQuery obj_cb_query; class=class="str">"cmt">//--- Create an instance of Class_CallbackQuery.

「拆解回调查询的字段抽取」

在 MT5 的 EA 里对接 Telegram 回调时,最容易被忽略的是字段抽取顺序与解码环节。下面这段代码从 JSON 对象中逐层取出 callback_query 的各字段,并统一用 decodeStringCharacters 处理特殊字符,避免中文或表情符号在面板里显示为乱码。 回调 ID 与发送者信息(id、first_name、last_name、username)都来自 callback_query.from 子树,其中姓名类字段必须经过解码;message_id 与 chat_id 则位于 callback_query.message 与 message.chat 下,用于后续定向回推消息。 抽取完立刻调用 ProcessCallbackQuery 做业务分发,并把 update_id 加 1 写回 member_update_id,保证下一轮轮询不重复处理同一条;同时把 member_first_remove 置为 false,标记首条消息已消费。开 MT5 把这段塞进你的 bot 类,跑一遍就能看到回调查询不再丢字段。

MQL5 / C++
obj_cb_query.id = obj_item["callback_query"]["id"].ToStr(); class=class="str">"cmt">//--- Extract and store the callback query ID.
obj_cb_query.from_id = obj_item["callback_query"]["from"]["id"].ToInt(); class=class="str">"cmt">//--- Extract and store the sender&class="macro">#x27;s ID.
obj_cb_query.from_first_name = obj_item["callback_query"]["from"]["first_name"].ToStr(); class=class="str">"cmt">//--- Extract and store the sender&class="macro">#x27;s first name.
obj_cb_query.from_first_name = decodeStringCharacters(obj_cb_query.from_first_name); class=class="str">"cmt">//--- Decode any special characters in the sender&class="macro">#x27;s first name.
obj_cb_query.from_last_name = obj_item["callback_query"]["from"]["last_name"].ToStr(); class=class="str">"cmt">//--- Extract and store the sender&class="macro">#x27;s last name.
obj_cb_query.from_last_name = decodeStringCharacters(obj_cb_query.from_last_name); class=class="str">"cmt">//--- Decode any special characters in the sender&class="macro">#x27;s last name.
obj_cb_query.from_username = obj_item["callback_query"]["from"]["username"].ToStr(); class=class="str">"cmt">//--- Extract and store the sender&class="macro">#x27;s username.
obj_cb_query.from_username = decodeStringCharacters(obj_cb_query.from_username); class=class="str">"cmt">//--- Decode any special characters in the sender&class="macro">#x27;s username.
obj_cb_query.message_id = obj_item["callback_query"]["message"]["message_id"].ToInt(); class=class="str">"cmt">//--- Extract and store the message ID related to the callback.
obj_cb_query.message_text = obj_item["callback_query"]["message"]["text"].ToStr(); class=class="str">"cmt">//--- Extract and store the message text related to the callback.
obj_cb_query.message_text = decodeStringCharacters(obj_cb_query.message_text); class=class="str">"cmt">//--- Decode any special characters in the message text.
obj_cb_query.data = obj_item["callback_query"]["data"].ToStr(); class=class="str">"cmt">//--- Extract and store the callback data.
obj_cb_query.data = decodeStringCharacters(obj_cb_query.data); class=class="str">"cmt">//--- Decode any special characters in the callback data.

obj_cb_query.chat_id = obj_item["callback_query"]["message"]["chat"]["id"].ToInt(); class=class="str">"cmt">//--- Extract and store the chat ID.

ProcessCallbackQuery(obj_cb_query); class=class="str">"cmt">//--- Call function to process the callback query.

member_update_id = obj_item["update_id"].ToInt() + class="num">1; class=class="str">"cmt">//--- Update the last processed update ID for callback queries.
member_first_remove = false; class=class="str">"cmt">//--- After processing the first message, mark that the first message has been handled.

◍ 轮询消息时的空令牌与解析兜底

EA 向 Telegram 拉取 update 之前,先卡一道令牌校验:member_token 为 NULL 就直接 Print("ERR: TOKEN EMPTY") 并 return(-1),避免拿空串去拼 URL 白跑一轮请求。 请求走 postRequest(out, url, params, WEB_TIMEOUT),成功返回码是 0。拿到响应后先用 CJSONValue 做 Deserialize,若 done 为 false 说明报文不是合法 JSON,打 ERR: JSON PARSING 并退 -1;接着读 obj_json["ok"].ToBool(),false 则 ERR: JSON NOT OK 同样退 -1。这两层兜底能挡掉九成以上的接口异常。 正常报文里 result 是数组,用 ArraySize(obj_json["result"].m_elements) 拿到条数,再 for 循环逐个取 update_id、message_id、date 填进 Class_Message。外汇与贵金属信号推送靠这套轮询,但接口超时或令牌泄露都可能让指令延迟,实盘前务必在 MT5 策略测试器里跑通一次空令牌分支。

MQL5 / C++
if (member_token == NULL) { class=class="str">"cmt">//--- If bot token is empty
      Print("ERR: TOKEN EMPTY"); class=class="str">"cmt">//--- Print error message indicating empty token.
      class="kw">return (-class="num">1); class=class="str">"cmt">//--- Return -class="num">1 to indicate error.
}

class="type">class="kw">string out; class=class="str">"cmt">//--- String to hold response data.
class="type">class="kw">string url = TELEGRAM_BASE_URL + "/bot" + member_token + "/getUpdates"; class=class="str">"cmt">//--- Construct URL for Telegram API.
class="type">class="kw">string params = "offset=" + IntegerToString(member_update_id); class=class="str">"cmt">//--- Set parameters including the offset based on the last update ID.

class="type">int res = postRequest(out, url, params, WEB_TIMEOUT); class=class="str">"cmt">//--- Send a POST request to Telegram with a timeout.

if (res == class="num">0) { class=class="str">"cmt">//--- If request succeeds(res = class="num">0)
   CJSONValue obj_json(NULL, jv_UNDEF); class=class="str">"cmt">//--- Create a JSON object to parse the response.
   class="type">bool done = obj_json.Deserialize(out); class=class="str">"cmt">//--- Deserialize the response.
   if (!done) { class=class="str">"cmt">//--- If deserialization fails
      Print("ERR: JSON PARSING"); class=class="str">"cmt">//--- Print error message indicating JSON parsing error.
      class="kw">return (-class="num">1); class=class="str">"cmt">//--- Return -class="num">1 to indicate error.
   }

   class="type">bool ok = obj_json["ok"].ToBool(); class=class="str">"cmt">//--- Check if the response has "ok" field set to true.
   if (!ok) { class=class="str">"cmt">//--- If "ok" field is false
      Print("ERR: JSON NOT OK"); class=class="str">"cmt">//--- Print error message indicating that JSON response is not okay.
      class="kw">return (-class="num">1); class=class="str">"cmt">//--- Return -class="num">1 to indicate error.
   }

   class="type">int total = ArraySize(obj_json["result"].m_elements); class=class="str">"cmt">//--- Get the total number of update elements.
   for (class="type">int i = class="num">0; i < total; i++) { class=class="str">"cmt">//--- Iterate through each update element.
      CJSONValue obj_item = obj_json["result"].m_elements[i]; class=class="str">"cmt">//--- Access individual update element.

      if (obj_item["message"].m_type != jv_UNDEF) { class=class="str">"cmt">//--- Check if the update has a message.
         Class_Message obj_msg; class=class="str">"cmt">//--- Create an instance of Class_Message to store the message details.
         obj_msg.update_id = obj_item["update_id"].ToInt(); class=class="str">"cmt">//--- Extract and store update ID.
         obj_msg.message_id = obj_item["message"]["message_id"].ToInt(); class=class="str">"cmt">//--- Extract and store message ID.
         obj_msg.message_date = (class="type">class="kw">datetime)obj_item["message"]["date"].ToInt(); class=class="str">"cmt">//--- Extract and store message date.
把按钮状态巡检交给小布
这些内联按钮的回调与状态轮询,小布盯盘的 AIGC 已内置到品种页的机器人诊断里,打开对应页就能看哪次回调丢了响应,你只管设计交互逻辑。

常见问题

回复键盘走 reply_markup 的 keyboard 字段,内联按钮用 inline_keyboard 数组嵌套,回调靠 callback_data 携带动作标识,不占用用户输入框。
可以,小布盯盘的机器人诊断会记录 EA 发出的内联消息与收到的回调查询时间差,异常间隔会标红,方便定位是网络还是解析问题。
在 MQL5 侧维护一个 callback_id 去重集合,收到查询先查重再执行,并在回包里立即 answerCallbackQuery 让 Telegram 关闭按钮等待态。
回调是异步的,高延迟下用户可能连点,测试时注入 200–2000ms 抖动能暴露状态竞态,否则实盘贵金属跳空时容易重复发单。