创建 MQL5-Telegram 集成 EA 交易 (第二部分):从 MQL5 发送信号到 Telegram(基础篇)
「把 MQL5 信号推到 Telegram 的底层通路」
要在 MT5 里把交易信号实时丢进 Telegram,核心不是写复杂指标,而是打通 MQL5 到 Telegram Bot API 的 HTTP 出口。MT5 本身不暴露浏览器环境,但 WebRequest 函数能直接发 HTTPS 请求,这就够了。
实际跑起来,先在 Telegram 找 @BotFather 建一个 bot,拿到 token;再建频道或群,把 bot 拉进去并记下 chat_id。这两个字符串是后续所有推送的寻址凭证,硬编码进 EA 或放输入参数都行,但别提交到公开仓库。
MQL5 的 WebRequest 默认被终端拦截,必须在 MT5「选项 → 智能交易系统」里把 https://api.telegram.org 加进允许地址列表,否则返回 -1。这是 2025 年 3 月实测仍生效的硬门槛,很多信号发不出去的 EA 都栽在这步。
发送动作建议包成独立函数,信号触发时异步调用,避免阻塞 OnTick。外汇与贵金属杠杆高、滑点突变频繁,信号延迟几秒可能已错过入场,推送逻辑越轻量越好。
◍ 从单向消息到实时信号推送
上一步我们已经跑通了 MQL5 与 Telegram 的基础链路:一个能收消息的 bot,加一段能发消息的脚本。那时程序只能单向通知,且单条消息若拼接多段文本就容易超长报错,所以一条消息只能发一个片段。 这一节把链路往前推了一步——EA 不再只是「会说话」,而是按预设条件自动开平仓,并把成交动作实时丢进 Telegram 群。我们基于经典的均线交叉系统产生信号,改写后的「Chatty Trader」在群聊通信上比旧版更清晰,速度持平或更快,成交或平仓时几乎能实时收到推送。 关键改进在消息结构:原先「已生成买入信号」和「开立买入订单」要么挤成一条易错长文、要么拆两条分开发;现在把多段文本和字符合并进单条消息,一次推完,不再受片段数量限制。 外汇与贵金属品种波动剧烈、滑点频发,这类实时信号只解决「知道发生了什么」,不替代执行风控。下一步我们会拆策略、贴 MQL5 实现、跑集成测试。
双均线交叉怎么变成下单指令
移动平均线交叉是被用得最滥也最直白的信号源之一:不叠任何指标,只看两条不同周期均线的相交。均线把价格锯齿烫平成一跟流线,长期线比短期线迟钝,两者必然在某根 K 线处叉开或金叉,这一刻就是原始的买卖触发点。 落地到 MT5,先定周期组合:想抓长趋势用 50 与 200,做短炒用 10 与 20。每来一个新分时或柱,重算两条均线并比对前值,把「上穿」判为买入、「下穿」判为卖出,输出成 0/1 二进制事件喂给 EA。 信号不落地就只是图上的线。我们把检测到的交叉封装进已有的 MQL5-Telegram 推送框架:消息体带品种名、方向、时间,叉形一出现就发到交易群。外汇与贵金属杠杆高、滑点突兀,这类信号只是概率倾向,收到不代表必动,但至少你能在叉发生的那一秒决定跟不跟。
「MT5 里把消息发去 Telegram 前先过一道编码」
直接把带换行和特殊符号的账户状态用 MT5 推到 Telegram,日志会报错且消息发不出去。实测一段含初始化语句、净值、可用保证金的多行文本,不加处理只能挤成一行乱序发送,结构完全丢失。 根因在 Telegram API 按 URL 解析消息,像空格、&、? 这类字符会被误读成指令或参数分隔符,导致两端通信中断。解决办法是本地先写个 UrlEncode 函数,把文本转成 URL 编码格式再发出去。 UrlEncode 的逻辑很直白:遍历每个字符,ASCII 48–57(数字)、65–90(大写)、97–122(小写)以及 - _ . ~ 等未保留字符原样保留;空格转成加号 +;其余复杂字符走 UTF-8 百分比编码(如 %E4%BD%A0)。下面这段是函数主干: [CODE]string UrlEncode(string text) { string encodedText = ""; int textLength = StringLen(text); for(int i = 0; i < textLength; i++) { short character = StringGetCharacter(text, i);
| if((character >= 48 && character <= 57) | (character >= 65 && character <= 90) | (character >= 97 && character <= 122) | character == '.' | character == '-' | character == '_' | character == '~') |
|---|
encodedText += ShortToString(character); else if(character == ' ') encodedText += "+"; else { uchar utf8Bytes[]; int bytes = ShortToUtf8(character, utf8Bytes); for(int j = 0; j < bytes; j++) encodedText += StringFormat("%%%02X", utf8Bytes[j]); } } return encodedText; }[/CODE] ShortToUtf8 才是真正处理多字节的地方。单字节(<0x80,十进制 0–127)直接拷贝;双字节(0x80–0x7FF)按位移和掩码拆成两字节并加 0xC0 / 0x80 前缀;三字节覆盖大部分汉字与符号;四字节专门接表情符号,比如钱袋符号 0x1F4B0 十进制是 128176,编码后占 4 字节。 外汇与贵金属信号推送涉及高风险,任何自动发信逻辑都先在模拟账户跑通再上实盘。编码漏掉代理半字符(0xD800–0xDFFF)会写进空格占位,遇到无法识别的字符统一替换成 U+FFFD,避免 API 直接拒收。
class="type">class="kw">string UrlEncode(class="type">class="kw">string text) { class="type">class="kw">string encodedText = ""; class="type">int textLength = StringLen(text); for(class="type">int i = class="num">0; i < textLength; i++) { class="type">short character = StringGetCharacter(text, i); if((character >= class="num">48 && character <= class="num">57) || (character >= class="num">65 && character <= class="num">90) || (character >= class="num">97 && character <= class="num">122) || character == &class="macro">#x27;.&class="macro">#x27; || character == &class="macro">#x27;-&class="macro">#x27; || character == &class="macro">#x27;_&class="macro">#x27; || character == &class="macro">#x27;~&class="macro">#x27;) encodedText += ShortToString(character); else if(character == &class="macro">#x27; &class="macro">#x27;) encodedText += "+"; else { class="type">uchar utf8Bytes[]; class="type">int bytes = ShortToUtf8(character, utf8Bytes); for(class="type">int j = class="num">0; j < bytes; j++) encodedText += StringFormat("%%%02X", utf8Bytes[j]); } } class="kw">return encodedText; }
◍ 用前一根柱方向快速起仓并转发 Telegram
策略测试器里 WebRequest 不可用,等均线交叉确认又太慢,所以初始化阶段先用一根柱的方向把单子开出去。OnInit 里读索引 1 的前一根柱:iOpen 取开盘、iClose 取收盘,开盘小于收盘判为看涨柱置 isBuySignal,否则 isSellSignal 置真,这根柱的涨跌就直接决定首单方向。 交易动作依赖 CTrade 类。全局用 #include <Trade/Trade.mqh> 把标准目录里的 Trade.mqh 展开进源码,再声明 CTrade obj_Trade 对象;手数写死 0.01,买入按 ask 开仓、止损止盈围绕 bid 算,卖出反向套同一套数值。外汇与贵金属杠杆高,0.01 手也须在实盘前自测保证金占用。 开仓后把信号和仓位拼成带表情符号的字符串发 Telegram。表情用 ushort 码点加 ShortToString 转串,不一定非写字符串字面量;信息里含交易类型、开仓价、时间、手数、SL、TP,再用 UrlEncode 把特殊字符和表情压成 URL 安全串覆盖原变量。 均线句柄在全局声明:handleFast/handleSlow 初值 INVALID_HANDLE(-1),bufferFast/bufferSlow 存 double,另设 long 幻数。OnInit 用 iMA(Symbol,Period,20,0,MODE_EMA,PRICE_CLOSE) 建快速 EMA,失败就 Print 错误并 return INIT_FAILED;慢线同理,打印句柄通常从 10 起跳、每多一个加 1。OnDeinit 用 IndicatorRelease 和 ArrayFree 回收资源。 OnTick 每报价触发,CopyBuffer(handleFast,0,0,3,bufferFast) 拉最近 3 根快慢值;用 iBars 与静态 prevBars 比大小,相等说明没新柱直接 return,不等才扫信号。快线现大于慢线且前一根快线小于等于慢线即看涨交叉,先循环平掉同品种同幻数卖单再开买;反之为卖交叉。最后 ChartIndicatorAdd 把两均线自动挂图上,完整信号链路就通了。
class="type">class="kw">double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY); class="type">class="kw">double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
把账户状态塞进推送消息的编码细节
EA 在图表初始化时,先把当前品种和账户权益、可用保证金拼成一段提示文本。第一段代码用单行字符串拼接,Equity 和 Free Margin 都靠 AccountInfoDouble 取 ACCOUNT_EQUITY 与 ACCOUNT_MARGIN_FREE,再用 DoubleToString 保留两位小数,例如 Equity: $12450.33、Free Margin: $9821.10。 第二段改成 \n 换行写法,消息在手机端推送里会折成三行显示,比单行更易读。注意 accountEquity 和 accountFreeMargin 的声明必须放在拼 msg 之前,否则编译会报『未声明标识符』。 要让这段消息走 HTTP 推送到外部接口,字符串得先做 URL 编码。UrlEncode 函数逐字符处理:数字 48–57、大写 65–90、小写 97–122,以及 ! ' ( ) * - . _ ~ 这些非保留字符原样保留;空格转成 '+'。外汇与贵金属杠杆高,权益和保证金随时剧烈波动,推送数值仅作监控参考,不构成任何方向建议。
class="type">class="kw">string msg = "🚀 EA INITIALIZED ON CHART " + _Symbol + " 🚀" +"📊 Account Status 📊; Equity: $" +DoubleToString(accountEquity,class="num">2) +"; Free Margin: $" +DoubleToString(accountFreeMargin,class="num">2); class="type">class="kw">double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY); class="type">class="kw">double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); class="type">class="kw">string msg = "🚀 EA INITIALIZED ON CHART " + _Symbol + " 🚀" +"\n📊 Account Status 📊" +"\nEquity: $" +DoubleToString(accountEquity,class="num">2) +"\nFree Margin: $" +DoubleToString(accountFreeMargin,class="num">2); class=class="str">"cmt">// FUNCTION TO ENCODE A STRING FOR USE IN URL class="type">class="kw">string UrlEncode(const class="type">class="kw">string text) { class="type">class="kw">string encodedText = ""; class=class="str">"cmt">// Initialize the encoded text as an empty class="type">class="kw">string class="type">int textLength = StringLen(text); class=class="str">"cmt">// Get the length of the input text class=class="str">"cmt">// Loop through each character in the input class="type">class="kw">string for (class="type">int i = class="num">0; i < textLength; i++) { class="type">class="kw">ushort character = StringGetCharacter(text, i); class=class="str">"cmt">// Get the character at the current position class=class="str">"cmt">// Check if the character is alphanumeric or one of the unreserved characters if ((character >= class="num">48 && character <= class="num">57) || class=class="str">"cmt">// Check if character is a digit(class="num">0-class="num">9) (character >= class="num">65 && character <= class="num">90) || class=class="str">"cmt">// Check if character is an uppercase letter(A-Z) (character >= class="num">97 && character <= class="num">122) || class=class="str">"cmt">// Check if character is a lowercase letter(a-z) character == &class="macro">#x27;!&class="macro">#x27; || character == &class="macro">#x27;\&class="macro">#x27;&class="macro">#x27; || character == &class="macro">#x27;(&class="macro">#x27; || character == &class="macro">#x27;)&class="macro">#x27; || character == &class="macro">#x27;*&class="macro">#x27; || character == &class="macro">#x27;-&class="macro">#x27; || character == &class="macro">#x27;.&class="macro">#x27; || character == &class="macro">#x27;_&class="macro">#x27; || character == &class="macro">#x27;~&class="macro">#x27;) { class=class="str">"cmt">// Append the character to the encoded class="type">class="kw">string without encoding encodedText += ShortToString(character); } class=class="str">"cmt">// Check if the character is a space else if (character == &class="macro">#x27; &class="macro">#x27;) { class=class="str">"cmt">// Encode space as &class="macro">#x27;+&class="macro">#x27; encodedText += ShortToString(&class="macro">#x27;+&class="macro">#x27;); }