创建 MQL5-Telegram 集成 EA 交易(第 5 部分):从 Telegram 向 MQL5 发送命令并接收实时响应·进阶篇
(2/3)· 接上篇的消息外发,这回让 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 轮询。
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 秒应能看见一次轮询动作,外汇与贵金属品种波动快、杠杆高,实盘挂定时器需警惕断网重连后消息堆积的风险。
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。
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,
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 超时概率会上升,建议先在策略测试器外手动跑一次确认网络白名单已加。
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} 验证分支是否真的截断,比盲信文档更管用。外汇与贵金属消息源延迟或格式异常属高风险,解析失败应有重拉机制而非单次退出。
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 写进去。这样外汇 / 贵金属喊单机器人只维护白名单用户的会话,行情误触达概率会低一些,但接口鉴权失效时仍有漏接风险。
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