创建 MQL5-Telegram 集成 EA 交易(第 5 部分):从 Telegram 向 MQL5 发送命令并接收实时响应·进阶篇
📡

创建 MQL5-Telegram 集成 EA 交易(第 5 部分):从 Telegram 向 MQL5 发送命令并接收实时响应·进阶篇

(2/3)· 接上篇的消息外发,这回让 Telegram 命令反向穿透到 MQL5 内核并秒回

案例拆解新手友好 第 2/3 篇
不少交易者把 EA 接上 Telegram 就停在了单向播报,以为机器人会自己听话。真到了想远程关仓、改参数时,才发现指令根本进不去 MQL5。双向通道没打通前,所谓远程控制只是幻觉。

◍ EA 里的 Telegram 机器人骨架怎么搭

想把 MT5 EA 接上 Telegram 做信号推送或远程指令,第一步是把机器人抽象成一个类。下面这段 MQL5 定义了一个 Class_Bot_EA,把 token、名称、已处理消息 ID、用户白名单和聊天对象容器都收进成员变量,构造函数里统一初始化。 私有段里 member_update_id 初值为 0,意味着 EA 重启后会从最新轮询点之前漏掉的历史消息开始算;member_first_remove 置 true,用来在首次拉取时清掉 Telegram 测试连通性发的那条空回执。 构造函数从外部输入参数 InpToken 取 token,但不直接存,而是过一道 getTrimmedToken。这个函数先调用 getTrimmedString 做左右去空格,若结果为空串就在日志打 ERR: TOKEN EMPTY 并返回 "NULL",避免后面 HTTPS 请求带着空 token 白跑。 开 MT5 验证时,把 InpToken 故意留空编译跑一次,能在专家日志里看到 ERR: TOKEN EMPTY,证明过滤逻辑生效;正常填 token 后 member_token 不再为 NULL,就可以继续写 getChatUpdates 轮询。

MQL5 / C++
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">//--- An array to filter users.
    class="type">bool        member_first_remove; class=class="str">"cmt">//--- A boolean to indicate if the first message should be removed.

  class="kw">protected:
    CList       member_chats;       class=class="str">"cmt">//--- A list to store chat objects.
  class="kw">public:
    class="type">void Class_Bot_EA();   class=class="str">"cmt">//--- Declares the constructor.
    ~Class_Bot_EA(){};     class=class="str">"cmt">//--- Declares the destructor.
    class="type">int getChatUpdates();  class=class="str">"cmt">//--- Declares a function to get updates from Telegram.
    class="type">void ProcessMessages(); class=class="str">"cmt">//--- Declares a function to process incoming messages.
};

class="type">void Class_Bot_EA::Class_Bot_EA(class="type">void){ class=class="str">"cmt">//--- Constructor
  member_token=NULL; class=class="str">"cmt">//--- Initialize the bot&class="macro">#x27;s token as NULL.
  member_token=getTrimmedToken(InpToken); class=class="str">"cmt">//--- Assign the trimmed bot token from InpToken.
  member_name=NULL; class=class="str">"cmt">//--- Initialize the bot&class="macro">#x27;s name as NULL.
  member_update_id=class="num">0; class=class="str">"cmt">//--- Initialize the last update ID to class="num">0.
  member_first_remove=true; class=class="str">"cmt">//--- Set the flag to remove the first message 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="type">class="kw">string getTrimmedToken(const class="type">class="kw">string bot_token){
  class="type">class="kw">string token=getTrimmedString(bot_token); class=class="str">"cmt">//--- Trim the bot_token class="kw">using getTrimmedString function.
  if(token==""){ class=class="str">"cmt">//--- Check if the trimmed token is empty.
    Print("ERR: TOKEN EMPTY"); class=class="str">"cmt">//--- Print an error message if the token is empty.
    class="kw">return("NULL"); class=class="str">"cmt">//--- Return "NULL" if the token is empty.
  }
  class="kw">return(token); class=class="str">"cmt">//--- Return the trimmed token.
}

class="type">class="kw">string getTrimmedString(class="type">class="kw">string text){
  StringTrimLeft(text); class=class="str">"cmt">//--- Remove leading whitespace from the class="type">class="kw">string.

「用毫秒定时器拉起 Telegram 消息轮询」

EA 初始化时直接挂一个 3000 毫秒的毫秒级定时器,相当于每 3 秒强制触发一次 OnTimer,避免只靠 tick 更新导致消息延迟。OnInit 里还同步调用了一次 OnTimer,保证 EA 加载后立刻拉首轮聊天更新,不用干等一个周期。 Deinit 阶段必须 EventKillTimer 清掉定时器,否则 MT5 退出 EA 后后台计时仍可能残留引用;随后 ChartRedraw 刷一次图,把最后状态落盘。 OnTimer 本身只做两件事:getChatUpdates 拉 Telegram 数据,ProcessMessages 解析指令。整套机制让你在 MT5 里验证时,打开 Experts 日志每 3 秒应能看见一次轮询动作,外汇与贵金属品种波动快、杠杆高,实盘挂定时器需警惕断网重连后消息堆积的风险。

MQL5 / C++
  StringTrimRight(text); class=class="str">"cmt">//--- Remove trailing whitespace from the class="type">class="kw">string.
  class="kw">return(text); class=class="str">"cmt">//--- Return the trimmed class="type">class="kw">string.
}

Class_Bot_EA obj_bot; class=class="str">"cmt">//--- Create an instance of the Class_Bot_EA class

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

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

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

从 Telegram 拉更新并落库到 MT5 类对象

要让 EA 响应私聊指令,第一步是周期性调用 getUpdates 把聊天记录拉回 MT5。下面这段核心逻辑先校验令牌,再用 offset 避免重复处理:令牌为空直接打印 ERR: TOKEN EMPTY 并返 -1;否则拼出 /bot<token>/getUpdates 的 URL,把 member_update_id 作为 offset 参数发出 POST。

MQL5 / C++
class="type">int Class_Bot_EA::getChatUpdates(class="type">void){
class=class="str">"cmt">//--- ....
  if(member_token==NULL){
    Print("ERR: TOKEN EMPTY");
    class="kw">return(-class="num">1);
  }
  class="type">class="kw">string out;
  class="type">class="kw">string url=TELEGRAM_BASE_URL+"/bot"+member_token+"/getUpdates";
  class="type">class="kw">string params="offset="+IntegerToString(member_update_id);
  class="type">int res=postRequest(out, url, params, WEB_TIMEOUT);
}

class="type">int postRequest(class="type">class="kw">string &response, const class="type">class="kw">string url, const class="type">class="kw">string params,
postRequest 内部先用 StringToCharArray 把参数字符串转字符数组,定义两个数组接响应体和头,再走 WebRequest 发 POST。返回码 200 时先剥掉可能存在的 BOM 字节序标记,再把字符数组转回字符串返 0;非 200 则把 WebRequest 错误码或 HTTP 错误体打印出来,方便在 MT5 日志里定位。 拿到原始响应后别急着用,先解析 JSON。用反序列化函数把 out 转成 JSON 对象,done 标志位为 true 才继续;若 done 为 false 打 ERR:JSON PARSING 返 -1。接着读顶层 ok 字段,ok==false 打 ERR:JSON NOT OK 同样返 -1,这两道校验能挡掉九成以上的脏响应。 通过校验后循环取 update 数组:每次从 obj_item 抽 update_id、message_id、message_date(转 datetime 可读),文本走 decodeStringCharacters 解 HTML 实体。关键一步是 member_update_id = obj_msg.update_id + 1,把下一次拉取的偏移推到当前 ID 之后,否则同一条指令会被 EA 反复触发。 首条更新靠 member_first_remove 标志跳过,避免初始化时的历史消息搅局。用户名过滤靠 member_users_filter.Total() 判断:为 0 全收,大于 0 用 SearchLinear 查发件人是否在白名单。聊天 ID 不在 member_chats 列表里就 new 一个 Class_Chat 写入 chat_id、时间、文本、done=false;已存在则原地更新文本和时间。外汇与贵金属信号走这种通道时波动快、滑点多,实盘前务必在策略测试器里用模拟令牌跑通整套偏移逻辑。

MQL5 / C++
class="type">int Class_Bot_EA::getChatUpdates(class="type">void){
class=class="str">"cmt">//--- ....
  if(member_token==NULL){
    Print("ERR: TOKEN EMPTY");
    class="kw">return(-class="num">1);
  }
  class="type">class="kw">string out;
  class="type">class="kw">string url=TELEGRAM_BASE_URL+"/bot"+member_token+"/getUpdates";
  class="type">class="kw">string params="offset="+IntegerToString(member_update_id);
  class="type">int res=postRequest(out, url, params, WEB_TIMEOUT);
}

class="type">int postRequest(class="type">class="kw">string &response, const class="type">class="kw">string url, const class="type">class="kw">string params,

◍ POST 请求里剥掉 BOM 的实战写法

在 MT5 里用 WebRequest 拉外部模型或行情接口,返回体前面常带着 UTF-8 的 BOM(0xef 0xbb 0xbf 三个字节)。不处理掉,JSON 解析大概率直接报错,EA 日志里看着像接口挂了,其实是本地没清头。 下面这段把 POST 参数转 char 数组发出去,超时写死 5000 毫秒;响应码 200 时循环扫前 8 个字节,遇到 BOM 字节就把 start_index 往后推,最后用 CharArrayToString 从 start_index 起转成字符串。 HTTP 层错误也分了类:response_code 为 -1 直接吐 _LastError;100~511 之间算 HTTP 状态码错误,把响应体打印出来方便排错,返回 -1;其余情况原样返回响应码。外汇和贵金属行情接口在高波动时 WebRequest 超时概率会上升,建议先在策略测试器外手动跑一次确认网络白名单已加。

MQL5 / C++
const class="type">int timeout=class="num">5000){
  class="type">char data[]; class=class="str">"cmt">//--- Array to store the data to be sent in the request
  class="type">int data_size=StringLen(params); class=class="str">"cmt">//--- Get the length of the parameters
  StringToCharArray(params, data, class="num">0, data_size); class=class="str">"cmt">//--- Convert the parameters class="type">class="kw">string to a class="type">char array
  class="type">uchar result[]; class=class="str">"cmt">//--- Array to store the response data
  class="type">class="kw">string result_headers; class=class="str">"cmt">//--- Variable to store the response headers
  class=class="str">"cmt">//--- Send a POST request to the specified URL with the given parameters and timeout
  class="type">int response_code=WebRequest("POST", url, NULL, NULL, timeout, data, data_size, result, result_headers);
  if(response_code==class="num">200){ class=class="str">"cmt">//--- If the response code is class="num">200 (OK)
    class=class="str">"cmt">//--- Remove Byte Order Mark(BOM) if present
    class="type">int start_index=class="num">0; class=class="str">"cmt">//--- Initialize the starting index for the response
    class="type">int size=ArraySize(result); class=class="str">"cmt">//--- Get the size of the response data array
    class=class="str">"cmt">// Loop through the first class="num">8 bytes of the &class="macro">#x27;result&class="macro">#x27; array or the entire array if it&class="macro">#x27;s smaller
    for(class="type">int i=class="num">0; i<fmin(size,class="num">8); i++){
      class=class="str">"cmt">// Check if the current byte is part of the BOM
      if(result[i]==0xef || result[i]==0xbb || result[i]==0xbf){
        class=class="str">"cmt">// Set &class="macro">#x27;start_index&class="macro">#x27; to the byte after the BOM
        start_index=i+class="num">1;
      }
      else {class="kw">break;}
    }
    class=class="str">"cmt">//--- Convert the response data from class="type">char array to class="type">class="kw">string, skipping the BOM
    response=CharArrayToString(result, start_index, WHOLE_ARRAY, CP_UTF8);
    class=class="str">"cmt">//Print(response); //--- Optionally print the response for debugging
    class="kw">return(class="num">0); class=class="str">"cmt">//--- Return class="num">0 to indicate success
  }
  else{
    if(response_code==-class="num">1){ class=class="str">"cmt">//--- If there was an error with the WebRequest
      class="kw">return(_LastError); class=class="str">"cmt">//--- Return the last error code
    }
    else{
      class=class="str">"cmt">//--- Handle HTTP errors
      if(response_code>=class="num">100 && response_code<=class="num">511){
        response=CharArrayToString(result, class="num">0, WHOLE_ARRAY, CP_UTF8); class=class="str">"cmt">//--- Convert the result to class="type">class="kw">string
        Print(response); class=class="str">"cmt">//--- Print the response for debugging
        Print("ERR: HTTP"); class=class="str">"cmt">//--- Print an error message indicating an HTTP error
        class="kw">return(-class="num">1); class=class="str">"cmt">//--- Return -class="num">1 to indicate an HTTP error
      }
      class="kw">return(response_code); class=class="str">"cmt">//--- Return the response code for other errors
    }
  }

「解析回传 JSON 并抽取消息字段」

请求返回 res==0 只代表 HTTP 层成功,真正要落地的数据在 out 字符串里。先用 CJSONValue 做 Deserialize,done 为 false 时直接 Print("ERR: JSON PARSING") 并 return(-1),这一步能拦掉约九成接口格式突变的坑。 反序列化后必须核对顶层 "ok" 字段:obj_json["ok"].ToBool() 为 false 说明业务层报错,同样 return(-1)。很多新手只判断 res==0 就往下走,结果把错误响应当正常数据喂给后续逻辑。 正常时从 obj_json["result"] 数组取长度,ArraySize(obj_json["result"].m_elements) 给出本次推送的 update 条数。循环内逐条把 update_id、message_id、date(转 datetime)、text 以及发送方 id / first_name / last_name 塞进 Class_Message 结构,文本类字段都再过一遍 decodeStringCharacters 处理 HTML 实体。 开 MT5 把这段接在你自己的 WebRequest 回调后,故意让接口返回 {"ok":false} 验证分支是否真的截断,比盲信文档更管用。外汇与贵金属消息源延迟或格式异常属高风险,解析失败应有重拉机制而非单次退出。

MQL5 / C++
if(res==class="num">0){
  Print(out);
}
CJSONValue obj_json(NULL, jv_UNDEF);
class="type">bool done=obj_json.Deserialize(out);
Print(done);
if(!done){
  Print("ERR: JSON PARSING");
  class="kw">return(-class="num">1);
}
class="type">bool ok=obj_json["ok"].ToBool();
if(!ok){
  Print("ERR: JSON NOT OK");
  class="kw">return(-class="num">1);
}
Class_Message obj_msg;
class="type">int total=ArraySize(obj_json["result"].m_elements);
for(class="type">int i=class="num">0;i<total;i++){
  CJSONValue obj_item=obj_json["result"].m_elements[i];
  obj_msg.update_id=obj_item["update_id"].ToInt();
  obj_msg.message_id=obj_item["message"]["message_id"].ToInt();
  obj_msg.message_date=(class="type">class="kw">datetime)obj_item["message"]["date"].ToInt();
  obj_msg.message_text=obj_item["message"]["text"].ToStr();
  obj_msg.message_text=decodeStringCharacters(obj_msg.message_text);
  obj_msg.from_id=obj_item["message"]["from"]["id"].ToInt();
  obj_msg.from_first_name=obj_item["message"]["from"]["first_name"].ToStr();
  obj_msg.from_first_name=decodeStringCharacters(obj_msg.from_first_name);
  obj_msg.from_last_name=obj_item["message"]["from"]["last_name"].ToStr();
  obj_msg.from_last_name=decodeStringCharacters(obj_msg.from_last_name);
}

从 JSON 里抠出聊天身份并做白名单过滤

把 Telegram 回传的 JSON 拆包时,sender 和 chat 两块字段要分开取。username、first_name、last_name 这类字符串先 ToStr() 落库,再走一遍 decodeStringCharacters() 做转义还原,否则带 \u 编码的名字在 MT5 面板里会显示成乱码。 chat_id 用 ToInt() 直接转成长整型,这是后续定位会话的唯一键;chat_type 保留原始字符串,用来区分 private / group / channel。每次处理完把 member_update_id 置为 obj_msg.update_id+1,长轮询才不会重复拉同一条。 首条更新靠 member_first_remove 标志直接 continue 跳过,避免 bot 启动瞬间把历史消息当新信号。过滤逻辑是:member_users_filter 为空就全收,否则只在线性查找命中 from_username 时才往下走。 命中后拿 chat_id 去 member_chats 里遍历比对 member_id,找不到就 new 一个 Class_Chat 挂到链表尾,把 chat_id 写进去。这样外汇 / 贵金属喊单机器人只维护白名单用户的会话,行情误触达概率会低一些,但接口鉴权失效时仍有漏接风险。

MQL5 / C++
obj_msg.from_username=obj_item["message"]["from"]["username"].ToStr(); class=class="str">"cmt">//--- Get the sender&class="macro">#x27;s username
obj_msg.from_username=decodeStringCharacters(obj_msg.from_username); class=class="str">"cmt">//--- Decode the username

class=class="str">"cmt">//--- Extract chat details from the JSON object
obj_msg.chat_id=obj_item["message"]["chat"]["id"].ToInt(); class=class="str">"cmt">//--- Get the chat ID
obj_msg.chat_first_name=obj_item["message"]["chat"]["first_name"].ToStr(); class=class="str">"cmt">//--- Get the chat&class="macro">#x27;s first name
obj_msg.chat_first_name=decodeStringCharacters(obj_msg.chat_first_name); class=class="str">"cmt">//--- Decode the first name
obj_msg.chat_last_name=obj_item["message"]["chat"]["last_name"].ToStr(); class=class="str">"cmt">//--- Get the chat&class="macro">#x27;s last name
obj_msg.chat_last_name=decodeStringCharacters(obj_msg.chat_last_name); class=class="str">"cmt">//--- Decode the last name
obj_msg.chat_username=obj_item["message"]["chat"]["username"].ToStr(); class=class="str">"cmt">//--- Get the chat&class="macro">#x27;s username
obj_msg.chat_username=decodeStringCharacters(obj_msg.chat_username); class=class="str">"cmt">//--- Decode the username
obj_msg.chat_type=obj_item["message"]["chat"]["type"].ToStr(); class=class="str">"cmt">//--- Get the chat type
class=class="str">"cmt">//--- Update the ID for the next request
member_update_id=obj_msg.update_id+class="num">1;
class=class="str">"cmt">//--- If it&class="macro">#x27;s the first update, skip processing
if(member_first_remove){
   class="kw">continue;
}
class=class="str">"cmt">//--- Filter messages based on username
if(member_users_filter.Total()==class="num">0 || class=class="str">"cmt">//--- If no filter is applied, process all messages
   (member_users_filter.Total()>class="num">0 && class=class="str">"cmt">//--- If a filter is applied, check if the username is in the filter
   member_users_filter.SearchLinear(obj_msg.from_username)>=class="num">0)){
   class=class="str">"cmt">//--- Find the chat in the list of chats
   class="type">int index=-class="num">1;
   for(class="type">int j=class="num">0; j<member_chats.Total(); j++){
      Class_Chat *chat=member_chats.GetNodeAtIndex(j);
      if(chat.member_id==obj_msg.chat_id){ class=class="str">"cmt">//--- Check if the chat ID matches
         index=j;
         class="kw">break;
      }
   }
   class=class="str">"cmt">//--- If the chat is not found, add a new chat to the list
   if(index==-class="num">1){
      member_chats.Add(new Class_Chat); class=class="str">"cmt">//--- Add a new chat to the list
      Class_Chat *chat=member_chats.GetLastNode();
      chat.member_id=obj_msg.chat_id; class=class="str">"cmt">//--- Set the chat ID
把指令回环交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到命令往返延迟与解析失败提示,你只管在 Telegram 里发指令、专注决策。

常见问题

先确认交易终端到 MQL5 的 WebRequest 权限已开,且机器人 token 与 chat_id 匹配;多数失败卡在环境设置而非解码逻辑。
封装后 EA 只在更新事件触发时拉取,避免每 tick 轮询拖慢执行,也方便后续扩展命令类型。
text 与 callback_query 要分开处理,未带 username 的匿名群组消息需 fallback 到 chat.id 做路由。
可以,小布盯盘的品种页会显示 EA 与聊天端的握手状态和最近回执耗时,连不上时直接标红,省去你手动发测试指令。
外汇贵金属高风险,建议先在 demo 跑通命令回环,确认不会误触发下单再切实盘。