创建 MQL5-Telegram 集成 EA 交易(第 6 部分):添加响应式内联按钮(基础篇)
「给EA挂上能回应的Telegram内联按钮」
在 MT5 里做 Telegram 集成,最容易被忽略的是按钮的「响应性」:用户点完不反馈,等于没接指令。本文基于 2025-06-12 发布的第 6 部分实现,核心是把回调 ID 原样回吐,避免 Telegram 端一直转圈。 内联按钮本质是一段 callback_data,EA 收到 update 后必须调用 answerCallbackQuery 把 ack 发回去。否则按钮在客户端会显示「请求超时」,交易指令也可能不落地。外汇与贵金属品种波动剧烈,这类异步丢失在高波动时段会放大误操作风险。 下面这段是 MQL5 里处理回应的骨架,重点看 BotAnswerCallback 的调用时机——必须在解析完指令、准备执行前或执行后立刻发,不能等下一根 K 线。
class=class="str">"cmt">// 处理收到的回调查询 class="type">void OnTelegramCallback(const class="type">class="kw">string callback_id, const class="type">class="kw">string data) { class=class="str">"cmt">// 解析 data 决定动作 if(StringFind(data, "BUY_") == class="num">0) TradeBuy(); if(StringFind(data, "SELL_") == class="num">0) TradeSell(); class=class="str">"cmt">// 必须回 ack,否则按钮转圈 BotAnswerCallback(callback_id, "已接收"); }
给EA接上Telegram内联按钮
把 Telegram 机器人塞进 MQL5 EA,最常见的做法是靠自定义键盘按钮;但键盘占屏幕、切换麻烦,内联按钮直接贴在消息气泡下方,用户点一下就回传回调,不用来回切界面。外汇与贵金属交易本身高杠杆、高风险,任何交互延迟都可能错过出场窗口,内联按钮能压低这一步的操作摩擦。 本小节先讲清内联按钮在 Telegram 里的定位:它本质是一条带 callback_data 的消息附件,Bot 收到点击后拿到的是回调查询而非普通文本。相比第五部分做的自定义键盘,内联按钮随消息生命周期存在,适合触发「平仓一半」「切换周期」这类一次性动作。 落到 MQL5 侧,我们要让 EA 在发消息时把 inline_keyboard 结构一起推上去,并在 OnChartEvent 或自定义监听里区分 callback_query 与普通 update。后续会演示如何按按钮的 callback_data 分支处理,并动态改写原消息里的按钮状态——比如把「已风控」从灰变亮。 本系列第五部分已打好命令响应底子,这一节把它往前推一步:你开 MT5 加载同一套 EA,把消息体从纯文本改成带 inline_keyboard 的 JSON,就能在手机端直接点按钮控单,不用再敲指令。
◍ 把交易指令塞进 TG 消息里
Telegram 机器人发出来的内联按钮,是直接渲染在消息气泡里的可点元素,点一下就触发动作,不用再敲文字指令。它靠 JSON 格式的内联键盘标记来定结构和外观,和传统回复键盘不同,按钮不另开菜单、不刷屏,始终贴着那条消息。 相比回复键盘,内联按钮最大的实用点是「留在原消息里」——上下文不丢,交互路径短。JSON 结构能描述多按钮矩阵和回调数据,机器人拿到点击回传后做动态响应,适合挂单、撤单、切换周期这类高频操作。 对做 MT5 信号推送的人来说,这套机制能把「看到信号→手动开单」压缩成「看到信号→点按钮」。外汇与贵金属波动剧烈、杠杆风险高,任何一键下单都得先在小额账户验证回调逻辑,别直接接实盘大仓。
「用三个类把 Telegram 内联按钮接进 EA」
在 MT5 里做 Telegram 控盘,内联按钮不是直接读 message_text 就能搞定的。回调查询(callback query)携带的 data 字段在标准消息里不存在,所以得把消息和回调拆成两套容器类分别管,否则按钮按下和聊天发言会混在同一逻辑里互相踩。 先写 Class_Message 当消息容器:done 标记是否处理过,update_id 与 message_id 默认 0,from_* 和 chat_* 系列默认 NULL,message_date 置 0、message_text 置 NULL。每个实例出生即干净状态,等 API 拉回数据再填。 回调查询单独用 Class_CallbackQuery 装:id 是查询唯一标识,from_id 定位触发用户,data 存按钮回调内容——这是决定 EA 下一步动作的关键字段,chat_id 保证回复发回原会话。类分清楚了,EA 才能把“按按钮”和“发文字”区分对待。 拉更新的主力是 Class_Bot_EA 里的 getChatUpdates。member_token 为空直接打印 ERR:TOKEN EMPTY 并返回 -1,拦掉无效请求;通过后拼 TELEGRAM_BASE_URL+/bot+token+/getUpdates 发请求。res==0 才用 CJSONValue 解析,反序列化失败或 ok 字段为 false 都return -1。 解析成功后用 ArraySize 数 result.m_elements 总量进循环。每轮把 obj_item 塞进 Class_Message:update_id、message_id 转 int,date 填 datetime,text 经 decodeStringCharacters 清特殊字符。处理完把 member_update_id 设为 update_id+1,下次轮询直接跳过已读。 用户过滤靠 member_users_filter:空则全放行,非空用 SearchLinear 比 from_username。聊天匹配用 chat.member_id == obj_msg.chat_id,命中就刷新 member_time 并把新消息写进 member_new_one、done 设 false;没命中就新建聊天节点挂进 member_chats。 回调分支检查 callback_query.m_type != jv_UNDEF 才建 Class_CallbackQuery 实例。id 用 ToStr 存,from_id 用 ToInt,from_first_name / from_last_name / from_username 全部转字符串后跑一遍 decodeStringCharacters——不解码,带 emoji 或转义的用户名在 MT5 里可能直接乱码。
回调查询字段拆解与首条消息防重处理
解析回调查询时,先从 message 字段取出消息 ID 转成整数写入 obj_cb_query.message_id,消息文本转字符串存进 obj_cb_query.message_text,文本里的特殊字符要解码。回调数据同样提取、转字符串后放入 obj_cb_query.data,它也是特殊字符编码的,不能原样当明文比。 发送消息的聊天 ID 从回调查询中取出来转整数塞进 obj_cb_query.chat_id,到此用户信息、消息内容、回调数据、聊天 ID 就齐了。接着调 ProcessCallbackQuery(obj_cb_query),由这个函数按具体内容分流处理。 处理完要从 obj_item 的 update_id 取整数加 1 存到 member_update_id,作为已处理进度位,后续轮询就不会重复吃同一条。首条消息跑完后把 member_first_remove 置 false,标记首条已处理,依赖“未处理首条”的后续逻辑便不再触发。 下面这段 Class_Message 定义了聊天消息与回调查询的存储结构,构造里 done 默认 false、各 ID 默认 0,开 MT5 把类挂上就能直接接 Telegram 更新流做验证。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Class_Message |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class</span> Class_Message : <span class="keyword">class="kw">public</span> CObject { <span class="keyword">class="kw">public</span>: Class_Message(); <span class="comment">class=class="str">"cmt">// Constructor</span> ~Class_Message(){}; <span class="comment">class=class="str">"cmt">// Destructor</span> <span class="keyword">class="type">bool</span> done; <span class="comment">class=class="str">"cmt">//--- Indicates if a message has been processed.</span> <span class="keyword">class="type">long</span> update_id; <span class="comment">class=class="str">"cmt">//--- Stores the update ID from Telegram.</span> <span class="keyword">class="type">long</span> message_id; <span class="comment">class=class="str">"cmt">//--- Stores the message ID.</span> <span class="keyword">class="type">long</span> from_id; <span class="comment">class=class="str">"cmt">//--- Stores the sender’s ID.</span> <span class="keyword">class="type">class="kw">string</span> from_first_name; <span class="comment">class=class="str">"cmt">//--- Stores the sender’s first name.</span> <span class="keyword">class="type">class="kw">string</span> from_last_name; <span class="comment">class=class="str">"cmt">//--- Stores the sender’s last name.</span> <span class="keyword">class="type">class="kw">string</span> from_username; <span class="comment">class=class="str">"cmt">//--- Stores the sender’s username.</span> <span class="keyword">class="type">long</span> chat_id; <span class="comment">class=class="str">"cmt">//--- Stores the chat ID.</span> <span class="keyword">class="type">class="kw">string</span> chat_first_name; <span class="comment">class=class="str">"cmt">//--- Stores the chat’s first name.</span> <span class="keyword">class="type">class="kw">string</span> chat_last_name; <span class="comment">class=class="str">"cmt">//--- Stores the chat’s last name.</span> <span class="keyword">class="type">class="kw">string</span> chat_username; <span class="comment">class=class="str">"cmt">//--- Stores the chat’s username.</span> <span class="keyword">class="type">class="kw">string</span> chat_type; <span class="comment">class=class="str">"cmt">//--- Stores the chat type.</span> <span class="keyword">class="type">class="kw">datetime</span> message_date; <span class="comment">class=class="str">"cmt">//--- Stores the date of the message.</span> <span class="keyword">class="type">class="kw">string</span> message_text; <span class="comment">class=class="str">"cmt">//--- Stores the text of the message.</span> }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Constructor to initialize class members |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> Class_Message::Class_Message(<span class="keyword">class="type">void</span>) { done = <span class="macro">false</span>; <span class="comment">class=class="str">"cmt">//--- Sets class="kw">default value indicating message not yet processed.</span> update_id = <span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">//--- Initializes update ID to zero.</span> message_id = <span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">//--- Initializes message ID to zero.</span> from_id = <span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">//--- Initializes sender ID to zero.</span> from_first_name = <span class="macro">NULL</span>; <span class="comment">class=class="str">"cmt">//--- Sets sender&class="macro">#x27;s first name to NULL (empty).</span>
◍ 把消息与回调节点封装成类
上面那段初始化把 Class_Message 结构体里的字段全部置空:发送者姓名、用户名给 NULL,chat_id 和 message_date 给 0,相当于每次新消息进来前先清场,避免上一条残留数据串台。 紧接着定义了两个对外暴露的类。Class_Chat 继承 CObject,内部挂了 member_id、member_state、member_time 三个基础字段,以及两个 Class_Message 实例 member_last 和 member_new_one,用来分别存上一条和刚收到的消息——这样在 EA 里轮询聊天记录时,能直接比对两端差异。 Class_CallbackQuery 则是给按钮回调用的。它记了回调 id、发送者 from_id 及姓名用户名、关联 message_id 与 message_text,还有最核心的 data 字段和回发用的 chat_id。实盘接 Telegram 信号时,点按钮触发的指令就全靠 data 字符串透传,MT5 解析后下单。外汇与贵金属波动剧烈,这类自动通道只适合小仓位验证。
from_last_name = NULL; class=class="str">"cmt">//--- Sets sender&class="macro">#x27;s last name to NULL (empty). from_username = NULL; class=class="str">"cmt">//--- Sets sender&class="macro">#x27;s username to NULL (empty). chat_id = class="num">0; class=class="str">"cmt">//--- Initializes chat ID to zero. chat_first_name = NULL; class=class="str">"cmt">//--- Sets chat’s first name to NULL (empty). chat_last_name = NULL; class=class="str">"cmt">//--- Sets chat’s last name to NULL (empty). chat_username = NULL; class=class="str">"cmt">//--- Sets chat’s username to NULL (empty). chat_type = NULL; class=class="str">"cmt">//--- Sets chat type to NULL (empty). message_date = class="num">0; class=class="str">"cmt">//--- Initializes message date to zero. message_text = NULL; class=class="str">"cmt">//--- Sets message text to NULL (empty). } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//| Class_Chat | class=class="str">"cmt">//+------------------------------------------------------------------+ class Class_Chat : class="kw">public CObject { class="kw">public: Class_Chat(){}; class=class="str">"cmt">//--- Declares an empty constructor. ~Class_Chat(){}; class=class="str">"cmt">//--- Declares an empty destructor. class="type">long member_id; class=class="str">"cmt">//--- Stores the chat ID. class="type">int member_state; class=class="str">"cmt">//--- Stores the state of the chat. class="type">class="kw">datetime member_time; class=class="str">"cmt">//--- Stores the time of chat activities. Class_Message member_last; class=class="str">"cmt">//--- Instance of Class_Message to store the last message. Class_Message member_new_one; class=class="str">"cmt">//--- Instance of Class_Message to store the new message. }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//| Class_CallbackQuery | class=class="str">"cmt">//+------------------------------------------------------------------+ class Class_CallbackQuery : class="kw">public CObject { class="kw">public: class="type">class="kw">string id; class=class="str">"cmt">//--- Stores the callback query ID. class="type">long from_id; class=class="str">"cmt">//--- Stores the sender’s ID. class="type">class="kw">string from_first_name; class=class="str">"cmt">//--- Stores the sender’s first name. class="type">class="kw">string from_last_name; class=class="str">"cmt">//--- Stores the sender’s last name. class="type">class="kw">string from_username; class=class="str">"cmt">//--- Stores the sender’s username. class="type">long message_id; class=class="str">"cmt">//--- Stores the message ID related to the callback. class="type">class="kw">string message_text; class=class="str">"cmt">//--- Stores the message text. class="type">class="kw">string data; class=class="str">"cmt">//--- Stores the callback data. class="type">long chat_id; class=class="str">"cmt">//--- Stores the chat ID to send responses. };