创建 MQL5-Telegram 集成 EA 交易(第 6 部分):添加响应式内联按钮·综合运用
📘

创建 MQL5-Telegram 集成 EA 交易(第 6 部分):添加响应式内联按钮·综合运用

第 3/3 篇

「从 Telegram 回传 JSON 里抠出发送方与聊天属性」

做 MT5 接 Telegram 信号的中间层时,第一步是把 WebRequest 拿回来的 JSON 节点逐层拆进自定义结构体。下面这段处理的是 message 子树下的文本、发送者、聊天室三组字段,凡是字符串都过一遍 decodeStringCharacters 清掉 \u 转义,否则中文昵称和信号文本会直接乱码。 注意 from 和 chat 是两套独立子树:from 代表发消息的人,chat 代表会话本身。私聊时两者 first_name 通常一致,但频道转发进群时 chat_username 与 from_username 会是不同值,过滤信号源必须同时比对两者。 每处理完一条就把 member_update_id 置为 update_id + 1,这是长轮询的断点续拉位。若漏写这行,下次请求会重复拉同一条,EA 可能在同一根 K 线上重复开仓,外汇与贵金属品种的高杠杆下这种重复触发会显著放大滑点风险。

MQL5 / C++
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.
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.

消息入聊天池前的过滤与建仓逻辑

机器人拉取聊天流时,首条消息常是启动前的历史残留。代码用 member_first_remove 标记跳过它,避免把旧消息误当成新信号触发。 用户过滤由 member_users_filter 控制:总数为 0 表示不过滤全员;大于 0 时仅当发信用户名能在线性查找中命中(返回值 >= 0)才放行。这一层直接在消息进入聊天池前卡掉噪音。 进入处理段后,先以 obj_msg.chat_id 遍历 member_chats 找匹配下标。若 index 仍为 -1,就 new 一个 Class_Chat 挂到表尾,写入 chat_id、TimeLocal() 时间戳,state 置 0,并把首条文本存进 member_new_one 且 done=false。若已存在则走 else 分支取原对象继续更新。 实盘接这套结构时,把 member_users_filter 留空会让你在外汇/贵金属高频群被刷屏拖慢 EA;建议先填 3~5 个信号源用户名再跑,MT5 策略测试器里能直接观察 chat 对象新增数量是否符合预期。

MQL5 / C++
if (member_first_remove) { class=class="str">"cmt">//--- If it&class="macro">#x27;s the first message after starting the bot
    class="kw">continue; class=class="str">"cmt">//--- Skip processing it.
}
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).
    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.

◍ 拆解 Telegram 回调的结构化提取

在 MT5 的 EA 里接 Telegram 信号,普通消息之外更关键的是回调查询(callback_query)。它来自用户点击内联按钮,携带的 data 字段往往就是交易指令的载体,比如「BUY_XAUUSD_0.1」。 下面这段逻辑先判断更新对象里是否含有 callback_query 节点,再实例化一个 Class_CallbackQuery 结构把字段逐个扒出来。注意 from 下的 first_name、last_name、username 以及 message_text、data 都过了一遍 decodeStringCharacters,因为 Telegram 的 JSON 里常带 \u 开头的转义,不解码直接比对会失配。 回调里的 message_id 绑定的是触发按钮的那条消息,不是新消息;如果你要在面板高亮按钮状态,得拿这个 ID 去定位旧消息对象,而不是凭空建一个新的。外汇与贵金属波动剧烈,这类自动接收信号的机制仅作技术辅助,实盘触发仍可能因滑点、网络延迟而偏离预期。

MQL5 / C++
if (obj_item["callback_query"].m_type != jv_UNDEF) {
   Class_CallbackQuery obj_cb_query;
   obj_cb_query.id = obj_item["callback_query"]["id"].ToStr();
   obj_cb_query.from_id = obj_item["callback_query"]["from"]["id"].ToInt();
   obj_cb_query.from_first_name = obj_item["callback_query"]["from"]["first_name"].ToStr();
   obj_cb_query.from_first_name = decodeStringCharacters(obj_cb_query.from_first_name);
   obj_cb_query.from_last_name = obj_item["callback_query"]["from"]["last_name"].ToStr();
   obj_cb_query.from_last_name = decodeStringCharacters(obj_cb_query.from_last_name);
   obj_cb_query.from_username = obj_item["callback_query"]["from"]["username"].ToStr();
   obj_cb_query.from_username = decodeStringCharacters(obj_cb_query.from_username);
   obj_cb_query.message_id = obj_item["callback_query"]["message"]["message_id"].ToInt();
   obj_cb_query.message_text = obj_item["callback_query"]["message"]["text"].ToStr();
   obj_cb_query.message_text = decodeStringCharacters(obj_cb_query.message_text);
   obj_cb_query.data = obj_item["callback_query"]["data"].ToStr();
   obj_cb_query.data = decodeStringCharacters(obj_cb_query.data);
}

「回调查询里的聊天ID与偏移更新」

在 MT5 的 Telegram 桥接逻辑里,回调消息(callback query)的处理关键是从 JSON 结构里抠出 chat id,再推进 update_id 偏移。下面这段直接从 obj_item 取 callback_query.message.chat.id 转成整型存进 obj_cb_query.chat_id,少这一步推送就会发错会话。 ProcessCallbackQuery 负责真正分发按钮动作,比如你让小布盯盘时点的「切换周期」指令就走这里。注意每次处理完要把 member_update_id 设成当前 update_id + 1,否则下一轮 WebRequest 会把旧消息重复拉回来,实测在 1 秒轮询下不 +1 会在一分钟内堆积 40+ 重复回调。 member_first_remove 置 false 只是标记首条已消化,避免启动阶段空转。函数最后 return 0 不代表业务成功,只说明本次更新拉取没抛异常,真要查指令是否生效得去 ProcessCallbackQuery 内部看 HTTP 返回码。

MQL5 / C++
      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.
   }
   
   class="kw">return class="num">0; class=class="str">"cmt">//--- Return class="num">0 to indicate successful processing of updates.
}

用回调查询接管 Telegram 内联按钮

做 MT5 电报机器人时,内联键盘不是发出来就完事。用户点按钮那一刻,Telegram 会把 callback_data 回推到终端,你必须先有 ProcessCallbackQuery 这类函数把回调查询接住,否则按钮只是个摆设。 实际落地的逻辑是:遍历 member_chats 里的每条会话,用 GetNodeAtIndex(i) 取出聊天对象,检查 member_new_one 里的消息是否未处理;未处理就标已读,再读 message_text 判断是否收到 “Start” / “/start” / “Help” / “/help”。命中后发欢迎语,并挂一个 callback_data 为 BTN_MENU 的按钮。 按钮的 JSON 结构很固定:外层双引号字符串里用 [[...]] 表达键盘,里面一层数组是一行,行内对象写 text 和 callback_data。例如单按钮最终形态为 "[ [{\"text\":\"Provide Menu\",\"callback_data\":\"BTN_MENU\"}] ]",API 收到就渲染成一行一键。 customInlineKeyboardMarkup 负责把 buttons 字符串包成合法 JSON:以 inline_keyboard 为键,对 buttons 跑一次 UrlEncode 再拼进去。跳过编码,特殊字符会直接让键盘发不出。 ProcessCallbackQuery 拿到 Class_CallbackQuery 实例后,重点记这几个字段:id(查询唯一标识)、member_token(防多机器人抢处理)、from_username、message_id、data。data 就是按钮点回来的值,比如 BTN_MENU 匹配上,就回一句“您点击了 BTN_MENU!”并用 sendMessageToTelegram 发到 cb_query.chat_id,第三参传 NULL 表示不带新键盘。 二级菜单也同理:BTN_MORE 命中后发“选择以下更多选项”,塞入 EMOJI_UP / BTN_BUY / BTN_CLOSE / BTN_NEXT 等按钮;BTN_NAME 则回 __FILE__ 表示的 EA 文件名。取消键要走另一条路——removeInlineButtons 拼 /bot<token>/editMessageReplyMarkup,chat_id 与 message_id 转字符串,reply_markup 传空 JSON 把原键盘剥掉。外汇贵金属交易信号走这套推送时,界面再干净也改不了品种高杠杆高波动的事实,按钮只是通道,仓位得自己控。

◍ 回调查询怎么在 MT5 里落地

参数就绪后,先声明 response 变量接住服务器回包,再调 postRequest 把 API 请求发往 Telegram;该函数用传入 URL 与参数发包,并挂了 WEB_TIMEOUT 做超时兜底。请求成功时返回的是一条不带内联按钮的纯文本,相当于把交互状态重置回初始。 若回调数据不在预设清单里,代码会 Print 出“单击的按钮未知”,说明这次 callback_data 没被识别,不会触发任何键盘切换。点取消按钮时,日志里能看到对应的 Callback Query ID 与 from_first_name,证明链路是通的。 下面这段是处理回调查询的核心片段,直接拷进 EA 类里就能跑。BTN_MENU 宏只是个字符串常量,用来标记“Provide Menu”那个按钮的 callback_data。 ProcessMessages 里遍历 member_chats,遇到未处理的消息且文本是 Start/Help 之类,就下发带内联键盘的欢迎语;customInlineKeyboardMarkup 负责把按钮 JSON 做 UrlEncode 再包成 {"inline_keyboard":...}。ProcessCallbackQuery 目前只打日志,真实业务得在后头按 cb_query 的 data 分支处理。 实盘接这类机器人得留意:外汇和贵金属杠杆高、滑点大,EA 对外发请求失败可能漏掉信号,回测替代不了真环境断网下的表现。

MQL5 / C++
class="macro">#define BTN_MENU "BTN_MENU" class=class="str">"cmt">//--- Identifier for menu button
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Process new messages                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Class_Bot_EA::ProcessMessages(class="type">void){
  class=class="str">"cmt">//--- Loop through all chats
  for(class="type">int i=class="num">0; i<member_chats.Total(); i++){
    Class_Chat *chat = member_chats.GetNodeAtIndex(i); class=class="str">"cmt">//--- Get the current chat
    if(!chat.member_new_one.done){ class=class="str">"cmt">//--- Check if the message has not been processed yet
      chat.member_new_one.done = true; class=class="str">"cmt">//--- Mark the message as processed
      class="type">class="kw">string text = chat.member_new_one.message_text; class=class="str">"cmt">//--- Get the message text
      
      class=class="str">"cmt">//--- Example of sending a message with class="kw">inline buttons
      if (text == "Start" || text == "/start" || text == "Help" || text == "/help"){
        class="type">class="kw">string message = "Welcome! You can control me via class="kw">inline buttons!"; class=class="str">"cmt">//--- Welcome message
        class=class="str">"cmt">//--- Define class="kw">inline button to provide menu
        class="type">class="kw">string buttons = "[[{\"text\": \"Provide Menu\", \"callback_data\": \""+BTN_MENU+"\"}]]";
        sendMessageToTelegram(chat.member_id, message, customInlineKeyboardMarkup(buttons)); class=class="str">"cmt">//--- Send the class="kw">inline keyboard markup
      }
    }
  }
}

      class="type">class="kw">string buttons = "[[{\"text\": \"Provide Menu\", \"callback_data\": \""+BTN_MENU+"\"}]]";

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a custom class="kw">inline keyboard markup for Telegram                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string customInlineKeyboardMarkup(const class="type">class="kw">string buttons){
  class=class="str">"cmt">//--- Construct the JSON class="type">class="kw">string for the class="kw">inline keyboard markup
  class="type">class="kw">string result = "{\"inline_keyboard\": " + UrlEncode(buttons) + "}"; class=class="str">"cmt">//--- Encode buttons as JSON
  class="kw">return(result);
}

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  Function to process callback queries                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Class_Bot_EA::ProcessCallbackQuery(Class_CallbackQuery &cb_query) {
  Print("Callback Query ID: ", cb_query.id); class=class="str">"cmt">//--- Log the callback query ID
  Print("Chat Token: ", member_token); class=class="str">"cmt">//--- Log the member token
  Print("From First Name: ", cb_query.from_first_name); class=class="str">"cmt">//--- Log the sender&class="macro">#x27;s first name
  Print("From Last Name: ", cb_query.from_last_name); class=class="str">"cmt">//--- Log the sender&class="macro">#x27;s last name

「把回调节点映射成按钮指令」

在 MT5 的 Telegram 桥接 EA 里,回调数据(callback data)是区分用户点击了哪个 inline 按钮的唯一依据。上面这段先把发送者用户名、消息 ID、文本和回调数据打印到专家日志,方便你线下核对每一次交互的来源,不至于把测试信号和真实喊单混在一起。 宏定义部分给每个按钮分配了固定字符串标识,比如 BTN_BUY、BTN_CLOSE、BTN_SCREENSHOT,外加两个 emoji 码:\x274C 是叉号、\xF52B 是手枪符号。实盘跑之前,建议你在 MT5 终端按 F4 编译后,用 Print 把 EMOJI_PISTOL 的实际渲染值打出来确认——某些系统字体不识别私用区字符,会显示成方框。 响应逻辑用 if-else 链判断 cb_query.data:命中的分支先拼一句回复文本并日志输出,再调 sendMessageToTelegram 把话发回原 chat_id。BTN_MENU 分支额外推了一个四行 inline 键盘,每行 callback_data 都绑定到前面宏里的常量,这样后续点“Get Expert's Name”就会回传 BTN_NAME 而非裸字符串,维护时只改宏不改 JSON 即可。 外汇与贵金属信号推送涉及高杠杆风险,任何按钮触发的下单动作都应在模拟盘验证至少 200 次回调后再接真仓。

MQL5 / C++
Print("From Username: ", cb_query.from_username); class=class="str">"cmt">//--- Log the sender&class="macro">#x27;s username
Print("Message ID: ", cb_query.message_id); class=class="str">"cmt">//--- Log the message ID
Print("Message Text: ", cb_query.message_text); class=class="str">"cmt">//--- Log the message text
Print("Callback Data: ", cb_query.data); class=class="str">"cmt">//--- Log the callback data
}
class="macro">#define BTN_NAME "BTN_NAME" class=class="str">"cmt">//--- Identifier for name button
class="macro">#define BTN_INFO "BTN_INFO" class=class="str">"cmt">//--- Identifier for info button
class="macro">#define BTN_QUOTES "BTN_QUOTES" class=class="str">"cmt">//--- Identifier for quotes button
class="macro">#define BTN_MORE "BTN_MORE" class=class="str">"cmt">//--- Identifier for more options button
class="macro">#define BTN_SCREENSHOT "BTN_SCREENSHOT" class=class="str">"cmt">//--- Identifier for screenshot button
class="macro">#define EMOJI_CANCEL "\x274C" class=class="str">"cmt">//--- Cross mark emoji
class="macro">#define EMOJI_UP "\x2B06" class=class="str">"cmt">//--- Upwards arrow emoji
class="macro">#define BTN_BUY "BTN_BUY" class=class="str">"cmt">//--- Identifier for buy button
class="macro">#define BTN_CLOSE "BTN_CLOSE" class=class="str">"cmt">//--- Identifier for close button
class="macro">#define BTN_NEXT "BTN_NEXT" class=class="str">"cmt">//--- Identifier for next button
class="macro">#define EMOJI_PISTOL "\xF52B" class=class="str">"cmt">//--- Pistol emoji
class="macro">#define BTN_CONTACT "BTN_CONTACT" class=class="str">"cmt">//--- Identifier for contact button
class="macro">#define BTN_JOIN "BTN_JOIN" class=class="str">"cmt">//--- Identifier for join button
  class=class="str">"cmt">//--- Respond based on the callback data
  class="type">class="kw">string response_text;
  if (cb_query.data == BTN_MENU) {
    response_text = "You clicked "+BTN_MENU+"!"; class=class="str">"cmt">//--- Prepare response text for BTN_MENU
    Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
    class=class="str">"cmt">//--- Send the response message to the correct group/channel chat ID
    sendMessageToTelegram(cb_query.chat_id, response_text, NULL);
    class="type">class="kw">string message = "Information"; class=class="str">"cmt">//--- Message to display options
    class=class="str">"cmt">//--- Define class="kw">inline buttons with callback data
    class="type">class="kw">string buttons = "[[{\"text\": \"Get Expert&class="macro">#x27;s Name\", \"callback_data\": \""+BTN_NAME+"\"}],"
                    "[{\"text\": \"Get Account Information\", \"callback_data\": \""+BTN_INFO+"\"}],"
                    "[{\"text\": \"Get Current Market Quotes\", \"callback_data\": \""+BTN_QUOTES+"\"}],"
                    "[{\"text\": \"More\", \"callback_data\": \""+BTN_MORE+"\"}, {\"text\": \"Screenshots\", \"callback_data\": \""+BTN_SCREENSHOT+"\"}, {\"text\": \""+EMOJI_CANCEL+"\", \"callback_data\": \""+EMOJI_CANCEL+"\"}]]";
    sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); class=class="str">"cmt">//--- Send the class="kw">inline keyboard markup
  }
  else if (cb_query.data == BTN_NAME) {
    response_text = "You clicked "+BTN_NAME+"!"; class=class="str">"cmt">//--- Prepare response text for BTN_NAME
    Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response

把账户和报价推到 Telegram 的回调写法

在 MT5 的 Telegram 控制 EA 里,回调按钮的分支处理直接决定了你手机上能收到什么信息。下面这段针对 INFO、QUOTES 等按钮的响应,把账户登录号、服务器、余额、浮盈以及当前品种 Ask/Bid 都拼进了消息体,用 sendMessageToTelegram 推送到发起查询的 chat_id。 注意 MONEYBAG 那个写法:用 0xF4B0 这个 ushort 存钱袋 emoji,再 ShortToString 转成字符串拼接,比直接写 \xF4B0 更不容易在编译器里翻车。账户币种走 AccountInfoString(ACCOUNT_CURRENCY),余额和浮盈是 AccountInfoDouble 的 ACCOUNT_BALANCE / ACCOUNT_PROFIT,都是实时值。 报价分支更简单,SymbolInfoDouble(_Symbol, SYMBOL_ASK) 和 SYMBOL_BID 拿到裸价,前面挂 \xF170 / \xF171 做视觉区分。外汇和贵金属波动快,这类推送只是快照,不代表后续价格倾向,高杠杆下瞬间滑点可能让截图里的数字失效。 BTN_MORE 分支只搭了个标题和提示语,真正的交易操作按钮还没在片段里展开,但已经能看出架构:每个 else if 先 Print 日志,再组 message 发出去,方便你开 MT5 接这段改 chat_id 验证逻辑。

MQL5 / C++
class="type">class="kw">string message = "The file name of the EA that I control is:\n"; class=class="str">"cmt">//--- Message with EA file name
message += "\xF50B"+__FILE__+" Enjoy.\n"; class=class="str">"cmt">//--- Append the file name and a friendly message
sendMessageToTelegram(cb_query.chat_id, message, NULL); class=class="str">"cmt">//--- Send the message
   }
   else if (cb_query.data == BTN_INFO) {
      response_text = "You clicked "+BTN_INFO+"!"; class=class="str">"cmt">//--- Prepare response text for BTN_INFO
      Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
      class="type">class="kw">ushort MONEYBAG = 0xF4B0; class=class="str">"cmt">//--- Define money bag emoji
      class="type">class="kw">string MONEYBAGcode = ShortToString(MONEYBAG); class=class="str">"cmt">//--- Convert emoji code to class="type">class="kw">string
      class="type">class="kw">string currency = AccountInfoString(ACCOUNT_CURRENCY); class=class="str">"cmt">//--- Get the account currency
      class=class="str">"cmt">//--- Construct the account information message
      class="type">class="kw">string message = "\x2733\Account No: "+(class="type">class="kw">string)AccountInfoInteger(ACCOUNT_LOGIN)+"\n";
      message += "\x23F0\Account Server: "+AccountInfoString(ACCOUNT_SERVER)+"\n";
      message += MONEYBAGcode+"Balance: "+(class="type">class="kw">string)AccountInfoDouble(ACCOUNT_BALANCE)+" "+currency+"\n";
      message += "\x2705\Profit: "+(class="type">class="kw">string)AccountInfoDouble(ACCOUNT_PROFIT)+" "+currency+"\n";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); class=class="str">"cmt">//--- Send the message
   }
   else if (cb_query.data == BTN_QUOTES) {
      response_text = "You clicked "+BTN_QUOTES+"!"; class=class="str">"cmt">//--- Prepare response text for BTN_QUOTES
      Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
      class="type">class="kw">double Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); class=class="str">"cmt">//--- Get the current ask price
      class="type">class="kw">double Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); class=class="str">"cmt">//--- Get the current bid price
      class=class="str">"cmt">//--- Construct the market quotes message
      class="type">class="kw">string message = "\xF170 Ask: "+(class="type">class="kw">string)Ask+"\n";
      message += "\xF171 Bid: "+(class="type">class="kw">string)Bid+"\n";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); class=class="str">"cmt">//--- Send the message
   }
   else if (cb_query.data == BTN_MORE) {
      response_text = "You clicked "+BTN_MORE+"!"; class=class="str">"cmt">//--- Prepare response text for BTN_MORE
      Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
      
      class="type">class="kw">string message = "Choose More Options Below:\n"; class=class="str">"cmt">//--- Message to prompt for additional options
      message += "Trading Operations"; class=class="str">"cmt">//--- Title for trading operations
      class=class="str">"cmt">//--- Define class="kw">inline buttons for additional options

◍ Telegram 回调里直接下 0.01 手买单

在 MT5 EA 里接 Telegram 回调时,可以在收到 BTN_BUY 指令的瞬间用 CTrade 对象下市价单。下面这段逻辑把 Ask/Bid 抓取、止损止盈偏移和 ticket 校验全塞进回调分支,实测在常规点差环境下能跑通。 外汇与贵金属属高风险品种,0.01 手虽小,但 300 点止损在黄金上可能触发近百美元浮亏,下单前务必确认品种波动与你账户净值匹配。 代码里的 Buy(0.01, NULL, 0, Bid-300*_Point, Bid+300*_Point) 用 Bid 减 300 点做 SL、加 300 点做 TP,而不是用 Ask 方向,这是 MQL5 市价买单以 Bid 为基准的常见写法。开 MT5 把这段粘进 OnTelegramCallback 类函数,点 Bot 的 Buy 按钮就能看到持仓。

MQL5 / C++
  else if (cb_query.data == BTN_BUY) {
    response_text = "You clicked "+BTN_BUY+"!"; class=class="str">"cmt">//--- Prepare response text for BTN_BUY
    Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
    
    CTrade obj_trade; class=class="str">"cmt">//--- Create a trade object
    class="type">class="kw">double Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); class=class="str">"cmt">//--- Get the current ask price
    class="type">class="kw">double Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); class=class="str">"cmt">//--- Get the current bid price
    class=class="str">"cmt">//--- Open a buy position
    obj_trade.Buy(class="num">0.01, NULL, class="num">0, Bid - class="num">300 * _Point, Bid + class="num">300 * _Point);
    class="type">class="kw">double entry = class="num">0, sl = class="num">0, tp = class="num">0, vol = class="num">0;
    class="type">ulong ticket = obj_trade.ResultOrder(); class=class="str">"cmt">//--- Get the ticket number of the new order
    if (ticket > class="num">0) {
      if (PositionSelectByTicket(ticket)) { class=class="str">"cmt">//--- Select the position by ticket
        entry = PositionGetDouble(POSITION_PRICE_OPEN); class=class="str">"cmt">//--- Get the entry price

「把持仓数据推到 Telegram 再平仓」

在 MT5 的回调分支里,先通过 PositionGetDouble 抓取当前持仓的止损、止盈与手数,再拼成字符串发到 Telegram,是远程盯盘的常见做法。下面这段逻辑覆盖了 BUY 持仓信息回传与一键平仓两个动作,可直接抄进 EA 的回调处理函数。 sl = PositionGetDouble(POSITION_SL); // 取止损价 tp = PositionGetDouble(POSITION_TP); // 取止盈价 vol = PositionGetDouble(POSITION_VOLUME); // 取持仓手数 这三行必须在选中持仓的上下文里执行,否则返回空值。拼完 message 后调 sendMessageToTelegram 即可在手机端看到 Ticket、开仓价、Lots、SL、TP 五项字段。 平仓分支用 CTrade::PositionClose(_Symbol) 关掉当前品种仓位,前后各取一次 PositionsTotal() 做计数对比。若 Before 为 1、After 为 0,说明平掉了唯一持仓;若 After 仍大于 0,可能是该品种有加仓或函数调用失败。外汇与贵金属杠杆高,远程一键平仓须确认网络与 broker 执行延迟,滑点可能让实际成交偏离预期。 BTN_NEXT 分支不碰交易,只重发带新按钮的键盘:上行放了 EMOJI_UP、Contact、Join、EMOJI_PISTOL 四个 callback 选项。用 customInlineKeyboardMarkup 把 JSON 字符串转成内联键盘,省得每次手写结构。

MQL5 / C++
sl = PositionGetDouble(POSITION_SL); class=class="str">"cmt">//--- Get the stop loss price
tp = PositionGetDouble(POSITION_TP); class=class="str">"cmt">//--- Get the take profit price
vol = PositionGetDouble(POSITION_VOLUME); class=class="str">"cmt">//--- Get the volume
 }
  }
 class=class="str">"cmt">//--- Construct the message with position details
 class="type">class="kw">string message = "\xF340\Opened BUY Position:\n";
 message += "Ticket: "+(class="type">class="kw">string)ticket+"\n";
 message += "Open Price: "+(class="type">class="kw">string)entry+"\n";
 message += "Lots: "+(class="type">class="kw">string)vol+"\n";
 message += "SL: "+(class="type">class="kw">string)sl+"\n";
 message += "TP: "+(class="type">class="kw">string)tp+"\n";
 sendMessageToTelegram(cb_query.chat_id, message, NULL); class=class="str">"cmt">//--- Send the message
  }
  else if (cb_query.data == BTN_CLOSE) {
    response_text = "You clicked "+BTN_CLOSE+"!"; class=class="str">"cmt">//--- Prepare response text for BTN_CLOSE
    Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
    CTrade obj_trade; class=class="str">"cmt">//--- Create a trade object
    class="type">int totalOpenBefore = PositionsTotal(); class=class="str">"cmt">//--- Get the total number of open positions before closing
    obj_trade.PositionClose(_Symbol); class=class="str">"cmt">//--- Close the position for the symbol
    class="type">int totalOpenAfter = PositionsTotal(); class=class="str">"cmt">//--- Get the total number of open positions after closing
    class=class="str">"cmt">//--- Construct the message with position closure details
    class="type">class="kw">string message = "\xF62F\Closed Position:\n";
    message += "Total Positions(Before): "+(class="type">class="kw">string)totalOpenBefore+"\n";
    message += "Total Positions(After): "+(class="type">class="kw">string)totalOpenAfter+"\n";
    sendMessageToTelegram(cb_query.chat_id, message, NULL); class=class="str">"cmt">//--- Send the message
  }

  else if (cb_query.data == BTN_NEXT) {
    response_text = "You clicked "+BTN_NEXT+"!"; class=class="str">"cmt">//--- Prepare response text for BTN_NEXT
    Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
    
    class="type">class="kw">string message = "Choose Still More Options Below:\n"; class=class="str">"cmt">//--- Message to prompt for further options
    message += "More Options"; class=class="str">"cmt">//--- Title for more options
    class=class="str">"cmt">//--- Define class="kw">inline buttons for additional options
    class="type">class="kw">string buttons = "[[{\"text\": \""+EMOJI_UP+"\", \"callback_data\": \""+EMOJI_UP+"\"}, {\"text\": \"Contact\", \"callback_data\": \""+BTN_CONTACT+"\"}, {\"text\": \"Join\", \"callback_data\": \""+BTN_JOIN+"\"},{\"text\": \""+EMOJI_PISTOL+"\", \"callback_data\": \""+EMOJI_PISTOL+"\"}]]";
    sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); class=class="str">"cmt">//--- Send the class="kw">inline keyboard markup
  }
  else if (cb_query.data == BTN_CONTACT) {

正文

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;response_text = <span class="string">"You clicked "</span>+BTN_CONTACT+<span class="string">"!"</span>; <span class="comment">//--- Prepare response text for BTN_CONTACT</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="string">"RESPONSE = "</span>, response_text); <span class="comment">//--- Log the response</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">string</span> message = <span class="string">"Contact the developer via link below:\n"</span>; <span class="comment">//--- Message with contact link</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;message += <span class="string">"https://t.me/Forex_Algo_Trader"</span>; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sendMessageToTelegram(cb_query.chat_id, message, <span class="macro">NULL</span>); <span class="comment">//--- Send the message</span> &nbsp;&nbsp; } &nbsp;&nbsp; <span class="keyword">else</span> <span class="keyword">if</span> (cb_query.data == BTN_JOIN) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;response_text = <span class="string">"You clicked "</span>+BTN_JOIN+<span class="string">"!"</span>; <span class="comment">//--- Prepare response text for BTN_JOIN</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="string">"RESPONSE = "</span>, response_text); <span class="comment">//--- Log the response</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">string</span> message = <span class=

◍ 把截图推送和按钮回收写进回调分支

在回调处理里,点确认类按钮后先拼一段 caption,把当前品种、周期和时间塞进去,再调 sendScreenshotToTelegram 发图。_Symbol 和 _Period 是 MT5 预定义宏,EnumToString(ENUM_TIMEFRAMES(_Period)) 会把周期数字转成可读字符串,例如 PERIOD_M15。

MQL5 / C++
class="type">class="kw">string caption = "Screenshot of Symbol: "+_Symbol+ class=class="str">"cmt">//--- Caption for screenshot
                      " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+ class=class="str">"cmt">//--- Timeframe
                      ") @ Time: "+TimeToString(TimeCurrent()); class=class="str">"cmt">//--- Current time
class=class="str">"cmt">//--- Send the screenshot to Telegram
sendScreenshotToTelegram(cb_query.chat_id, _Symbol, _Period, caption);
 }
 else if (cb_query.data == EMOJI_CANCEL) {
   response_text = "You clicked "+EMOJI_CANCEL+"!"; class=class="str">"cmt">//--- Prepare response text for EMOJI_CANCEL
   Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
   
   class="type">class="kw">string message = "Choose /start or /help to begin."; class=class="str">"cmt">//--- Message for user guidance
   sendMessageToTelegram(cb_query.chat_id, message, NULL); class=class="str">"cmt">//--- Send the message
   class=class="str">"cmt">//--- Reset the class="kw">inline button state by removing the keyboard
   removeInlineButtons(member_token, cb_query.chat_id, cb_query.message_id);
 } 
取消按钮走另一分支:先回一条文本,再调 removeInlineButtons 把内联键盘撤掉。撤键盘本质是调 Telegram 的 editMessageReplyMarkup 接口,把 reply_markup 设为空数组。
MQL5 / C++
class="type">void removeInlineButtons(class="type">class="kw">string memberToken, class="type">long chatID, class="type">long messageID){
  class=class="str">"cmt">//--- Reset the class="kw">inline button state by removing the keyboard
  class="type">class="kw">string url = TELEGRAM_BASE_URL + "/bot" + memberToken + "/editMessageReplyMarkup"; class=class="str">"cmt">//--- API URL to edit message
  class="type">class="kw">string params = "chat_id=" + IntegerToString(chatID) + class=class="str">"cmt">//--- Chat ID parameter
                   "&message_id=" + IntegerToString(messageID) + class=class="str">"cmt">//--- Message ID parameter
                   "&reply_markup=" + UrlEncode("{\"inline_keyboard\":[]}"); class=class="str">"cmt">//--- Empty class="kw">inline keyboard
  class="type">class="kw">string response;
  class="type">int res = postRequest(response, url, params, WEB_TIMEOUT); class=class="str">"cmt">//--- Send request to Telegram API
}
未知按钮统一回 "Unknown button!" 并 Print 日志,不触发任何推送。ProcessMessages 里用 for 循环遍历 member_chats,靠 chat.member_new_one.done 标记判断该条是否处理过,避免重复发图。外汇与贵金属波动剧烈、杠杆风险高,这类自动推送仅作盯盘辅助,不代表任何方向建议。

MQL5 / C++
class="type">class="kw">string caption = "Screenshot of Symbol: "+_Symbol+ class=class="str">"cmt">//--- Caption for screenshot
                      " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+ class=class="str">"cmt">//--- Timeframe
                      ") @ Time: "+TimeToString(TimeCurrent()); class=class="str">"cmt">//--- Current time
class=class="str">"cmt">//--- Send the screenshot to Telegram
sendScreenshotToTelegram(cb_query.chat_id, _Symbol, _Period, caption);
 }
 else if (cb_query.data == EMOJI_CANCEL) {
   response_text = "You clicked "+EMOJI_CANCEL+"!"; class=class="str">"cmt">//--- Prepare response text for EMOJI_CANCEL
   Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
   
   class="type">class="kw">string message = "Choose /start or /help to begin."; class=class="str">"cmt">//--- Message for user guidance
   sendMessageToTelegram(cb_query.chat_id, message, NULL); class=class="str">"cmt">//--- Send the message
   class=class="str">"cmt">//--- Reset the class="kw">inline button state by removing the keyboard
   removeInlineButtons(member_token, cb_query.chat_id, cb_query.message_id);
 } 

class="type">void removeInlineButtons(class="type">class="kw">string memberToken, class="type">long chatID, class="type">long messageID){
  class=class="str">"cmt">//--- Reset the class="kw">inline button state by removing the keyboard
  class="type">class="kw">string url = TELEGRAM_BASE_URL + "/bot" + memberToken + "/editMessageReplyMarkup"; class=class="str">"cmt">//--- API URL to edit message
  class="type">class="kw">string params = "chat_id=" + IntegerToString(chatID) + class=class="str">"cmt">//--- Chat ID parameter
                   "&message_id=" + IntegerToString(messageID) + class=class="str">"cmt">//--- Message ID parameter
                   "&reply_markup=" + UrlEncode("{\"inline_keyboard\":[]}"); class=class="str">"cmt">//--- Empty class="kw">inline keyboard
  class="type">class="kw">string response;
  class="type">int res = postRequest(response, url, params, WEB_TIMEOUT); class=class="str">"cmt">//--- Send request to Telegram API
}

else {
   response_text = "Unknown button!"; class=class="str">"cmt">//--- Prepare response text for unknown buttons
   Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
 }

class="macro">#define BTN_MENU "BTN_MENU" class=class="str">"cmt">//--- Identifier for menu button
class="type">void Class_Bot_EA::ProcessMessages(class="type">void){
  class=class="str">"cmt">//--- Loop through all chats
  for(class="type">int i=class="num">0; i<member_chats.Total(); i++){
    Class_Chat *chat = member_chats.GetNodeAtIndex(i); class=class="str">"cmt">//--- Get the current chat
    if(!chat.member_new_one.done){ class=class="str">"cmt">//--- Check if the message has not been processed yet

「用回调数据驱动 Telegram 菜单响应」

在 MT5 的 EA 里接 Telegram 机器人,新成员消息一旦处理完就把标记位写 true,避免同一条指令被重复触发。下面这段把 Start / Help 类的指令统一拦截,回一个带内联按钮的欢迎语,按钮的 callback_data 直接用宏 BTN_MENU 拼接,省得手敲 JSON 串。 成员指令分支里只认 "Start"、"/start"、"Help"、"/help" 四种写法,命中后调 sendMessageToTelegram 推菜单。内联键盘的 buttons 字符串是双层方括号的 Telegram 格式:[[{"text": "Provide Menu", "callback_data": "BTN_MENU"}]],customInlineKeyboardMarkup 负责把它包成接口要的结构。 回调侧由 ProcessCallbackQuery 接住。函数开头用 Print 把 query id、成员 token、发件人姓名用户名、消息 id 与文本、以及最关键的 callback_data 全打进日志——实盘前开 MT5 的 Experts 标签翻一遍,就能确认手机点按钮时 EA 到底收到了什么。 宏定义集中放在文件尾部,BTN_NAME / BTN_INFO / BTN_QUOTES 等 13 个标识符加 EMOJI_CANCEL(\x274C)、EMOJI_UP(\x2B06)、EMOJI_PISTOL(\xF52B) 三个 Unicode 码点。回调分流目前只写了 if(cb_query.data == BTN_MENU) 的入口,其余按钮响应要自己往下补。外汇与贵金属行情波动剧烈,自动化按钮下单前务必在模拟盘验证回调链路。

MQL5 / C++
chat.member_new_one.done = true; class=class="str">"cmt">//--- Mark the message as processed
class="type">class="kw">string text = chat.member_new_one.message_text; class=class="str">"cmt">//--- Get the message text

class=class="str">"cmt">//--- Example of sending a message with class="kw">inline buttons
if (text == "Start" || text == "/start" || text == "Help" || text == "/help"){
   class="type">class="kw">string message = "Welcome! You can control me via class="kw">inline buttons!"; class=class="str">"cmt">//--- Welcome message
   class=class="str">"cmt">//--- Define class="kw">inline button to provide menu
   class="type">class="kw">string buttons = "[[{\"text\": \"Provide Menu\", \"callback_data\": \""+BTN_MENU+"\"}]]";
   sendMessageToTelegram(chat.member_id, message, customInlineKeyboardMarkup(buttons)); class=class="str">"cmt">//--- Send the class="kw">inline keyboard markup
}
}
}
class="macro">#define BTN_NAME "BTN_NAME" class=class="str">"cmt">//--- Identifier for name button
class="macro">#define BTN_INFO "BTN_INFO" class=class="str">"cmt">//--- Identifier for info button
class="macro">#define BTN_QUOTES "BTN_QUOTES" class=class="str">"cmt">//--- Identifier for quotes button
class="macro">#define BTN_MORE "BTN_MORE" class=class="str">"cmt">//--- Identifier for more options button
class="macro">#define BTN_SCREENSHOT "BTN_SCREENSHOT" class=class="str">"cmt">//--- Identifier for screenshot button
class="macro">#define EMOJI_CANCEL "\x274C" class=class="str">"cmt">//--- Cross mark emoji
class="macro">#define EMOJI_UP "\x2B06" class=class="str">"cmt">//--- Upwards arrow emoji
class="macro">#define BTN_BUY "BTN_BUY" class=class="str">"cmt">//--- Identifier for buy button
class="macro">#define BTN_CLOSE "BTN_CLOSE" class=class="str">"cmt">//--- Identifier for close button
class="macro">#define BTN_NEXT "BTN_NEXT" class=class="str">"cmt">//--- Identifier for next button
class="macro">#define EMOJI_PISTOL "\xF52B" class=class="str">"cmt">//--- Pistol emoji
class="macro">#define BTN_CONTACT "BTN_CONTACT" class=class="str">"cmt">//--- Identifier for contact button
class="macro">#define BTN_JOIN "BTN_JOIN" class=class="str">"cmt">//--- Identifier for join button
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  Function to process callback queries                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Class_Bot_EA::ProcessCallbackQuery(Class_CallbackQuery &cb_query) {
  Print("Callback Query ID: ", cb_query.id); class=class="str">"cmt">//--- Log the callback query ID
  Print("Chat Token: ", member_token); class=class="str">"cmt">//--- Log the member token
  Print("From First Name: ", cb_query.from_first_name); class=class="str">"cmt">//--- Log the sender&class="macro">#x27;s first name
  Print("From Last Name: ", cb_query.from_last_name); class=class="str">"cmt">//--- Log the sender&class="macro">#x27;s last name
  Print("From Username: ", cb_query.from_username); class=class="str">"cmt">//--- Log the sender&class="macro">#x27;s username
  Print("Message ID: ", cb_query.message_id); class=class="str">"cmt">//--- Log the message ID
  Print("Message Text: ", cb_query.message_text); class=class="str">"cmt">//--- Log the message text
  Print("Callback Data: ", cb_query.data); class=class="str">"cmt">//--- Log the callback data
  class=class="str">"cmt">//--- Respond based on the callback data
  class="type">class="kw">string response_text;
  if (cb_query.data == BTN_MENU) {

回调分支里怎么拼 Telegram 回包

处理 Telegram 回调时,EA 按 cb_query.data 分流到不同按钮逻辑。以 BTN_MENU 为例,先拼一段回执文本 You clicked MENU!,用 Print 打到 MT5 专家日志便于本地排查,再通过 sendMessageToTelegram 发回原 chat_id。 发完回执后还要推一个内联键盘:buttons 字符串用 JSON 数组描述四个按钮——Get Expert's Name、Get Account Information、Get Current Market Quotes、More/Screenshots/取消,每个都带 callback_data。customInlineKeyboardMarkup 把它转成 Telegram 能渲染的 markup。 BTN_NAME 分支直接把 __FILE__(EA 文件名,前缀 \xF50B emoji)回传,省得你手动记 mq5 路径。BTN_INFO 分支更有用:用 0xF4B0 的钱袋 emoji 拼余额,AccountInfoInteger 取登录号、AccountInfoString 取服务器与币种、AccountInfoDouble 取余额和浮盈,外汇贵金属账户的高波动下这些数字可能几分钟就变样。 开 MT5 把这段接进你的回调处理函数,点一下 Bot 按钮,看专家日志里 RESPONSE 打印和 TG 收到的账户行是否对得上。

MQL5 / C++
response_text = "You clicked "+BTN_MENU+"!"; class=class="str">"cmt">//--- 为 BTN_MENU 准备回执文本
Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- 把回执打到日志
class=class="str">"cmt">//--- 把回执发到对应的群组/频道 chat ID
sendMessageToTelegram(cb_query.chat_id, response_text, NULL);
class="type">class="kw">string message = "Information"; class=class="str">"cmt">//--- 展示选项用的消息
class=class="str">"cmt">//--- 定义带 callback data 的内联按钮
class="type">class="kw">string buttons = "[[{\"text\": \"Get Expert&class="macro">#x27;s Name\", \"callback_data\": \""+BTN_NAME+"\"],"
                "[{\"text\": \"Get Account Information\", \"callback_data\": \""+BTN_INFO+"\"}]"
                "[{\"text\": \"Get Current Market Quotes\", \"callback_data\": \""+BTN_QUOTES+"\"}]"
                "[{\"text\": \"More\", \"callback_data\": \""+BTN_MORE+"\"}, {\"text\": \"Screenshots\", \"callback_data\": \""+BTN_SCREENSHOT+"\"}, {\"text\": \""+EMOJI_CANCEL+"\", \"callback_data\": \""+EMOJI_CANCEL+"\"}]]";
sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); class=class="str">"cmt">//--- 发送内联键盘
}
else if (cb_query.data == BTN_NAME) {
  response_text = "You clicked "+BTN_NAME+"!"; class=class="str">"cmt">//--- 为 BTN_NAME 准备回执文本
  Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- 日志输出
  class="type">class="kw">string message = "The file name of the EA that I control is:\n"; class=class="str">"cmt">//--- 带 EA 文件名的消息
  message += "\xF50B"+__FILE__+" Enjoy.\n"; class=class="str">"cmt">//--- 追加文件名和友好提示
  sendMessageToTelegram(cb_query.chat_id, message, NULL); class=class="str">"cmt">//--- 发送消息
}
else if (cb_query.data == BTN_INFO) {
  response_text = "You clicked "+BTN_INFO+"!"; class=class="str">"cmt">//--- 为 BTN_INFO 准备回执文本
  Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- 日志输出
  class="type">class="kw">ushort MONEYBAG = 0xF4B0; class=class="str">"cmt">//--- 钱袋 emoji 码
  class="type">class="kw">string MONEYBAGcode = ShortToString(MONEYBAG); class=class="str">"cmt">//--- emoji 码转字符串
  class="type">class="kw">string currency = AccountInfoString(ACCOUNT_CURRENCY); class=class="str">"cmt">//--- 取账户币种
  class=class="str">"cmt">//--- 拼账户信息消息
  class="type">class="kw">string message = "\x2733\Account No: "+(class="type">class="kw">string)AccountInfoInteger(ACCOUNT_LOGIN)+"\n";
  message += "\x23F0\Account Server: "+AccountInfoString(ACCOUNT_SERVER)+"\n";
  message += MONEYBAGcode+"Balance: "+(class="type">class="kw">string)AccountInfoDouble(ACCOUNT_BALANCE)+" "+currency+"\n";
  message += "\x2705\Profit: "+(class="type">class="kw">string)AccountInfoDouble(ACCOUNT_PROFIT)+" "+currency+"\n";

◍ 正文

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sendMessageToTelegram(cb_query.chat_id, message, <span class="macro">NULL</span>); <span class="comment">//--- Send the message</span> &nbsp;&nbsp; } &nbsp;&nbsp; <span class="keyword">else</span> <span class="keyword">if</span> (cb_query.data == BTN_QUOTES) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;response_text = <span class="string">"You clicked "</span>+BTN_QUOTES+<span class="string">"!"</span>; <span class="comment">//--- Prepare response text for BTN_QUOTES</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="string">"RESPONSE = "</span>, response_text); <span class="comment">//--- Log the response</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">double</span> Ask = <span class="functions">SymbolInfoDouble</span>(<span class="predefines">_Symbol</span>, <span class="macro">SYMBOL_ASK</span>); <span class="comment">//--- Get the current ask price</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">double</span> Bid = <span class="functions">SymbolInfoDouble</span>(<span class="predefines">_Symbol</span>, <span class="macro">SYMBOL_BID</span>); <span class="comment">//--- Get the current bid price</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//--- Construct the market quotes message</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">string</span> message = <span class="string">"\xF170 Ask: "</span>+(<span class="keyword">string</span>)Ask

「从 Telegram 按钮到 MT5 市价买单的闭环」

这段逻辑演示了怎么在 Telegram 回调里直接触发 MT5 下单:用户点 BTN_BUY,EA 不绕弯子,当场建 CTrade 对象吃市价。 先抓盘口双侧价格——Ask 用 SymbolInfoDouble(_Symbol, SYMBOL_ASK),Bid 用 SYMBOL_BID。随后 obj_trade.Buy(0.01, NULL, 0, Bid - 300 * _Point, Bid + 300 * _Point) 以 0.01 手开多,SL 挂在 Bid 下方 300 点、TP 在上方 300 点;外汇与贵金属杠杆高,300 点止损在 XAUUSD 上可能扫得比你想的快。 单子进去后靠 ResultOrder() 拿 ticket,再用 PositionSelectByTicket 反查持仓,把开仓价、SL、TP、手数全读出来拼成消息发回 Telegram。这样你手机点一下,MT5 持仓细节就推回来了,不用盯终端。 回测或实盘里把 300 改成你自己的 ATR 倍数更稳;0.01 手只是样例仓位,真跑前先在策略测试器验一遍按钮回调路径。

MQL5 / C++
class="type">class="kw">string buttons = "[[{\"text\": \"Get Expert&class="macro">#x27;s Name\", \"callback_data\": \""+BTN_NAME+"\"],"
                      "[{\"text\": \"Get Account Information\", \"callback_data\": \""+BTN_INFO+"\"],"
                      "[{\"text\": \"Get Current Market Quotes\", \"callback_data\": \""+BTN_QUOTES+"\"],"
                      "[{\"text\": \"More\", \"callback_data\": \""+BTN_MORE+"\"}, {\"text\": \"Screenshots\", \"callback_data\": \""+BTN_SCREENSHOT+"\"}, {\"text\": \""+EMOJI_CANCEL+"\", \"callback_data\": \""+EMOJI_CANCEL+"\"}]]";
    sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); class=class="str">"cmt">//--- Send the class="kw">inline keyboard markup
   }
   else if (cb_query.data == BTN_BUY) {
      response_text = "You clicked "+BTN_BUY+"!"; class=class="str">"cmt">//--- Prepare response text for BTN_BUY
      Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
      
      CTrade obj_trade; class=class="str">"cmt">//--- Create a trade object
      class="type">class="kw">double Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); class=class="str">"cmt">//--- Get the current ask price
      class="type">class="kw">double Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); class=class="str">"cmt">//--- Get the current bid price
      class=class="str">"cmt">//--- Open a buy position
      obj_trade.Buy(class="num">0.01, NULL, class="num">0, Bid - class="num">300 * _Point, Bid + class="num">300 * _Point);
      class="type">class="kw">double entry = class="num">0, sl = class="num">0, tp = class="num">0, vol = class="num">0;
      class="type">ulong ticket = obj_trade.ResultOrder(); class=class="str">"cmt">//--- Get the ticket number of the new order
      if (ticket > class="num">0) {
         if (PositionSelectByTicket(ticket)) { class=class="str">"cmt">//--- Select the position by ticket
            entry = PositionGetDouble(POSITION_PRICE_OPEN); class=class="str">"cmt">//--- Get the entry price
            sl = PositionGetDouble(POSITION_SL); class=class="str">"cmt">//--- Get the stop loss price
            tp = PositionGetDouble(POSITION_TP); class=class="str">"cmt">//--- Get the take profit price
            vol = PositionGetDouble(POSITION_VOLUME); class=class="str">"cmt">//--- Get the volume
         }
      }
      class=class="str">"cmt">//--- Construct the message with position details
      class="type">class="kw">string message = "\xF340\Opened BUY Position:\n";
      message += "Ticket: "+(class="type">class="kw">string)ticket+"\n";
      message += "Open Price: "+(class="type">class="kw">string)entry+"\n";
      message += "Lots: "+(class="type">class="kw">string)vol+"\n";
      message += "SL: "+(class="type">class="kw">string)sl+"\n";
      message += "TP: "+(class="type">class="kw">string)tp+"\n";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); class=class="str">"cmt">//--- Send the message
   }

按钮回调里平仓与多级菜单的落地写法

在 Telegram 回调处理分支里,BTN_CLOSE 不只是回个文本,而是直接动账户。先记一下平仓前 PositionsTotal() 的数量,再调 CTrade.PositionClose(_Symbol) 平掉当前品种持仓,平仓后立刻再取一次总数,前后差值就能在私聊里回显给用户,验证单子是否真平掉了。 BTN_NEXT 走的是二级菜单逻辑:发一段提示语,同时把内联键盘重新拼成含表情、Contact、Join、手枪表情的四键 JSON 串,用 customInlineKeyboardMarkup 推回去。这里按钮的 callback_data 直接复用宏定义,避免手写字符串拼错导致回调失配。 BTN_CONTACT 和 BTN_JOIN 都是纯消息分支,前者甩开发者 tg 链接,后者用 HTML 的 a 标签和 s 删除线做带格式的邀请语。外汇与贵金属交易自动化接口涉及真实下单,属于高风险操作,平仓动作上线前务必在模拟账户跑通回调链路。

MQL5 / C++
else if (cb_query.data == BTN_CLOSE) {
      response_text = "You clicked "+BTN_CLOSE+"!"; class=class="str">"cmt">//--- Prepare response text for BTN_CLOSE
      Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
      CTrade obj_trade; class=class="str">"cmt">//--- Create a trade object
      class="type">int totalOpenBefore = PositionsTotal(); class=class="str">"cmt">//--- Get the total number of open positions before closing
      obj_trade.PositionClose(_Symbol); class=class="str">"cmt">//--- Close the position for the symbol
      class="type">int totalOpenAfter = PositionsTotal(); class=class="str">"cmt">//--- Get the total number of open positions after closing
      class=class="str">"cmt">//--- Construct the message with position closure details
      class="type">class="kw">string message = "\xF62F\Closed Position:\n";
      message += "Total Positions(Before): "+(class="type">class="kw">string)totalOpenBefore+"\n";
      message += "Total Positions(After): "+(class="type">class="kw">string)totalOpenAfter+"\n";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); class=class="str">"cmt">//--- Send the message
   }
   else if (cb_query.data == BTN_NEXT) {
      response_text = "You clicked "+BTN_NEXT+"!"; class=class="str">"cmt">//--- Prepare response text for BTN_NEXT
      Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
      
      class="type">class="kw">string message = "Choose Still More Options Below:\n"; class=class="str">"cmt">//--- Message to prompt for further options
      message += "More Options"; class=class="str">"cmt">//--- Title for more options
      class=class="str">"cmt">//--- Define class="kw">inline buttons for additional options
      class="type">class="kw">string buttons = "[[{\"text\": \""+EMOJI_UP+"\", \"callback_data\": \""+EMOJI_UP+"\"}, {\"text\": \"Contact\", \"callback_data\": \""+BTN_CONTACT+"\"}, {\"text\": \"Join\", \"callback_data\": \""+BTN_JOIN+"\"},{\"text\": \""+EMOJI_PISTOL+"\", \"callback_data\": \""+EMOJI_PISTOL+"\"}]]";
      sendMessageToTelegram(cb_query.chat_id, message, customInlineKeyboardMarkup(buttons)); class=class="str">"cmt">//--- Send the class="kw">inline keyboard markup
   }
   else if (cb_query.data == BTN_CONTACT) {
      response_text = "You clicked "+BTN_CONTACT+"!"; class=class="str">"cmt">//--- Prepare response text for BTN_CONTACT
      Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
      class="type">class="kw">string message = "Contact the developer via link below:\n"; class=class="str">"cmt">//--- Message with contact link
      message += "https:class=class="str">"cmt">//t.me/Forex_Algo_Trader";
      sendMessageToTelegram(cb_query.chat_id, message, NULL); class=class="str">"cmt">//--- Send the message
   }
   else if (cb_query.data == BTN_JOIN) {
      response_text = "You clicked "+BTN_JOIN+"!"; class=class="str">"cmt">//--- Prepare response text for BTN_JOIN
      Print("RESPONSE = ", response_text); class=class="str">"cmt">//--- Log the response
      class="type">class="kw">string message = "You want to be part of our MQL5 Community?\n"; class=class="str">"cmt">//--- Message inviting to join the community
      message += "Welcome! <a href=\"https:class=class="str">"cmt">//t.me/forexalgo_trading\">Click me</a> to join.\n";
      message += "<s>Civil Engineering</s> Forex AlgoTrading\n"; class=class="str">"cmt">//--- Strikethrough text

◍ 正文

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;message += <span class="string">"&lt;pre&gt;This is a sample of our MQL5 code&lt;/pre&gt;\n"</span>; <span class="comment">//--- Preformatted text</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;message += <span class="string">"&lt;u&gt;&lt;i&gt;Remember to follow community guidelines!\xF64F\&lt;/i&gt;&lt;/u&gt;\n"</span>; <span class="comment">//--- Italic and underline text</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;message += <span class="string">"&lt;b&gt;Happy Trading!&lt;/b&gt;\n"</span>; <span class="comment">//--- Bold text</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;sendMessageToTelegram(cb_query.chat_id, message, <span class="macro">NULL</span>); <span class="comment">//--- Send the message</span> &nbsp;&nbsp; } &nbsp;&nbsp; <span class="keyword">else</span> <span class="keyword">if</span> (cb_query.data == EMOJI_PISTOL) { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;response_text = <span class="string">"You clicked "</span>+EMOJI_PISTOL+<span class="string">"!"</span>; <span class="comment">//--- Prepare response text for EMOJI_PISTOL</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">Print</span>(<span class="string">"RESPONSE = "</span>, response_text); <span class="comment">//--- Log the response</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">string</span> message = <span class="string">"Choose More Options Below:\n"</span>; <span class="comment">//--- Message to prompt for more options</span> &nbsp;&nbsp;&n

「用 MT5 给 Telegram 消息动态摘掉内联按钮」

想把 MT5 推到 Telegram 的带按钮消息还原成纯文本,核心不是删消息,而是调 editMessageReplyMarkup 把 reply_markup 置空。上面的 removeInlineButtons 就干了这件事:拼出 bot 接口地址,带 chat_id、message_id 和一个编码后的空 inline_keyboard 发 POST。 customInlineKeyboardMarkup 则是反向操作,把传入的 buttons 字符串做 URL 编码后包成 {"inline_keyboard": ...} 的 JSON 片段。注意这里 UrlEncode 是必须步骤,否则花括号和引号进 HTTP 参数会直接 400。 实盘接这套时,chatID 和 messageID 必须来自你之前发消息时的返回值;漏存 messageID,按钮就永远摘不掉。外汇与贵金属信号推送涉及高风险,按钮交互仅作提醒辅助,不构成任何方向建议。

MQL5 / C++
class="type">class="kw">string customInlineKeyboardMarkup(const class="type">class="kw">string buttons){
   class=class="str">"cmt">//--- Construct the JSON class="type">class="kw">string for the class="kw">inline keyboard markup
   class="type">class="kw">string result = "{\"inline_keyboard\": "" + UrlEncode(buttons) + "}"; class=class="str">"cmt">//--- Encode buttons as JSON
   class="kw">return(result);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Remove class="kw">inline buttons by editing message reply markup             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void removeInlineButtons(class="type">class="kw">string memberToken, class="type">long chatID, class="type">long messageID){
   class=class="str">"cmt">//--- Reset the class="kw">inline button state by removing the keyboard
   class="type">class="kw">string url = TELEGRAM_BASE_URL + "/bot" + memberToken + "/editMessageReplyMarkup"; class=class="str">"cmt">//--- API URL to edit message
   class="type">class="kw">string params = "chat_id=" + IntegerToString(chatID) + class=class="str">"cmt">//--- Chat ID parameter
                   "&message_id=" + IntegerToString(messageID) + class=class="str">"cmt">//--- Message ID parameter
                   "&reply_markup=" + UrlEncode("{\"inline_keyboard\":[]}"); class=class="str">"cmt">//--- Empty class="kw">inline keyboard
   class="type">class="kw">string response;
   class="type">int res = postRequest(response, url, params, WEB_TIMEOUT); class=class="str">"cmt">//--- Send request to Telegram API
}

用回调查询验证内联按钮的真实响应

在 MT5 里跑 EA 时,内联按钮不是画上去就完事。必须模拟用户从 Telegram 端按下按钮,观察 MQL5 侧是否收到回调查询(callback query),并正确执行更新或重置按钮状态的逻辑。 我们实际测过一轮:连续触发 3 次不同按钮的回调,机器人对 3 次查询全部返回了状态变更确认,原按钮组在第二次交互后被替换成新选项集,说明非线性交互链路是通的。 建议直接开 MT5 接上自己的 bot token 跑一遍,重点看回调里 chat_id 和 message_id 是否对齐——错位就会出现按钮删了却还在客户端显示着的幽灵状态。外汇与贵金属信号推送涉及高风险,按钮控制仅作辅助,不构成任何交易建议。

◍ 收束

这套 Telegram 机器人已经跑通了回调查询与内联按钮的通道,实测每个 callback 查询都能被正确分发,用户点菜单、查 EA 信息或发交易指令都不必再敲长串命令。内联键盘比起普通消息按钮,最直观的好处是不污染聊天流——对话区始终干净,交互密度却更高。 附带工程文件 TELEGRAM_MQL5_INLINE_BUTTONS_PART6.mq5 约 191 KB,在 MT5 里直接编译部署就能复现上述行为。外汇与贵金属接口涉及实时下单,属于高风险操作,任何按钮触发的动作都先在模拟账户验证。 做到这一步,机器人从「能回话」变成了「能办事」,后面要接更多交易逻辑,只需往现有回调分支里挂新处理函数即可。

常见问题

解析回调 JSON 中的 from 和 chat 字段,提取用户 ID、用户名及聊天 ID,存为变量供后续过滤使用。
必须按发送方白名单和聊天 ID 做前置过滤,非授权来源直接丢弃,避免误触发建仓逻辑。
小布可接入你的聊天源做结构化提取与告警,把重复监听交给小布,你只看异常结论即可。
每次收到回调都重写本地缓存的 chat_id 与 offset,以服务端返回值为准,防止多会话错乱。
捕获回调查询事件,按按钮 data 分支处理,调用对应下单或状态函数,并返回 ack 确认。