创建 MQL5-Telegram 集成 EA 交易 (第 3 部分):将带有标题的图表截图从 MQL5 发送到 Telegram·综合运用
截图落盘后的等待与编码边界生成
EA 初始化时若截图尚未写入终端数据目录,不能直接往下走。代码用 60 次循环、每次 Sleep(500) 做最多约 30 秒的等待,若超时仍找不到 SCREENSHOT_FILE_NAME 文件则 Print 报错并返回 INIT_FAILED,这一步能避免把空文件拿去编码。 文件存在后先用 FileOpen 以 FILE_READ|FILE_BIN 打开,句柄为 INVALID_HANDLE 同样回退初始化;成功则打印句柄 ID。随后用 FileSize 取大小、ArrayResize 配 FileReadArray 把整张截图读进 uchar 数组 photoArr_Data,FileClose 立即释放句柄,日志里能看到 CHART SCREENSHOT FILE SIZE 与 READ SCREENSHOT FILE DATA SIZE 两个数值,二者应相等。 往 HTTP 传输走之前要先做边界标记:CryptEncode(CRYPT_BASE64,...) 把原始字节转 base64,再取前 1024 字节填进 temporaryArr,用 CryptEncode(CRYPT_HASH_MD5,...) 算出 md5。这个 md5 后续用作 multipart/form-data 的 boundary 片段,和服务端对得上才不会传丢。 别把等待时长写死 等待循环写 60 次只是示例,实盘 MT5 若把截图分辨率调高、或终端磁盘 IO 偶发卡顿,30 秒可能不够;可在输入参数里暴露 wait_loops 基数,按自己图表 DPI 实测后改。
class=class="str">"cmt">// Wait for class="num">30 secs to save screenshot if not yet saved class="type">int wait_loops = class="num">60; class="kw">while(!FileIsExist(SCREENSHOT_FILE_NAME) && --wait_loops > class="num">0){ Sleep(class="num">500); } if(!FileIsExist(SCREENSHOT_FILE_NAME)){ Print("THE SPECIFIED SCREENSHOT DOES NOT EXIST(WAS NOT SAVED). REVERTING NOW!"); class="kw">return (INIT_FAILED); } else if(FileIsExist(SCREENSHOT_FILE_NAME)){ Print("THE CHART SCREENSHOT WAS SAVED SUCCESSFULLY TO THE DATA-BASE."); } class="type">int screenshot_Handle = INVALID_HANDLE; screenshot_Handle = FileOpen(SCREENSHOT_FILE_NAME,FILE_READ|FILE_BIN); if(screenshot_Handle == INVALID_HANDLE){ Print("INVALID SCREENSHOT HANDLE. REVERTING NOW!"); class="kw">return(INIT_FAILED); } else if (screenshot_Handle != INVALID_HANDLE){ Print("SCREENSHOT WAS SAVED & OPENED SUCCESSFULLY FOR READING."); Print("HANDLE ID = ",screenshot_Handle,". IT IS NOW READY FOR ENCODING."); } class="type">int screenshot_Handle_Size = (class="type">int)FileSize(screenshot_Handle); if (screenshot_Handle_Size > class="num">0){ Print("CHART SCREENSHOT FILE SIZE = ",screenshot_Handle_Size); } class="type">uchar photoArr_Data[]; ArrayResize(photoArr_Data,screenshot_Handle_Size); FileReadArray(screenshot_Handle,photoArr_Data,class="num">0,screenshot_Handle_Size); if (ArraySize(photoArr_Data) > class="num">0){ Print("READ SCREENSHOT FILE DATA SIZE = ",ArraySize(photoArr_Data)); } FileClose(screenshot_Handle); class=class="str">"cmt">//ArrayPrint(photoArr_Data); class=class="str">"cmt">//--- create boundary: (data -> base64 -> class="num">1024 bytes -> md5) class=class="str">"cmt">//Encodes the photo data into base64 format class=class="str">"cmt">//This is part of preparing the data for transmission over HTTP. class="type">uchar base64[]; class="type">uchar key[]; CryptEncode(CRYPT_BASE64,photoArr_Data,key,base64); if (ArraySize(base64) > class="num">0){ Print("Transformed BASE-class="num">64 data = ",ArraySize(base64)); class=class="str">"cmt">//Print("The whole data is as below:"); class=class="str">"cmt">//ArrayPrint(base64); } class=class="str">"cmt">//Copy the first class="num">1024 bytes of the base64-encoded data into a temporary array class="type">uchar temporaryArr[class="num">1024]= {class="num">0}; class=class="str">"cmt">//Print("FILLED TEMPORARY ARRAY WITH ZERO(class="num">0) IS AS BELOW:"); class=class="str">"cmt">//ArrayPrint(temporaryArr); ArrayCopy(temporaryArr,base64,class="num">0,class="num">0,class="num">1024); class=class="str">"cmt">//Print("FIRST class="num">1024 BYTES OF THE ENCODED DATA IS AS FOLLOWS:"); class=class="str">"cmt">//ArrayPrint(temporaryArr); class=class="str">"cmt">//Create an MD5 hash of the temporary array class=class="str">"cmt">//This hash will be used as part of the boundary in the multipart/form-data class="type">uchar md5[]; CryptEncode(CRYPT_HASH_MD5,temporaryArr,key,md5);
「把 MD5 哈希拼成 multipart 边界并塞进请求体」
在 MT5 里用 WebRequest 向 Telegram 推图表截图时,multipart/form-data 的 boundary 不能随便写,得用 MD5 哈希的前 16 位十六进制字符。代码先判断 md5 数组长度大于 0,打印原始哈希尺寸并 ArrayPrint 出来,方便你确认哈希确实算出来了。 随后用 for 循环把 uchar 类型的 md5 逐项按 %02X 格式化成大写十六进制串,StringSubstr 截断到 16 字符——这是 HTTP 表单边界对长度的常见约束,超长服务端可能直接拒收。 边界拼好后开始组 DATA 字节数组:先加 \r\n,再加 "--"+HexaDecimal_Hash+"\r\n" 作为分隔符,接着写 Content-Disposition: form-data; name="chat_id" 头,空一行后附 chatID 值,再补 \r\n 结束该字段。ArrayPrint(DATA) 能看到原始字节,CharArrayToString 转 UTF8 后 Print 出来核对文本形态。 外汇与贵金属行情波动剧烈、推送延迟或接口失败均属正常高风险现象,这段代码只解决 MT5 出包格式,不保证消息必达。
if(ArraySize(md5) > class="num">0){ Print("SIZE OF MD5 HASH OF TEMPORARY ARRAY = ",ArraySize(md5)); Print("MD5 HASH boundary in multipart/form-data is as follows:"); ArrayPrint(md5); } class=class="str">"cmt">//Format MD5 hash as a hexadecimal class="type">class="kw">string & class=class="str">"cmt">//truncate it to class="num">16 characters to create the boundary. class="type">class="kw">string HexaDecimal_Hash=NULL;class=class="str">"cmt">//Used to store the hexadecimal representation of MD5 hash class="type">int total=ArraySize(md5); for(class="type">int i=class="num">0; i<total; i++){ HexaDecimal_Hash+=StringFormat("%02X",md5[i]); } Print("Formatted MD5 Hash String is: \n",HexaDecimal_Hash); HexaDecimal_Hash=StringSubstr(HexaDecimal_Hash,class="num">0,class="num">16);class=class="str">"cmt">//truncate HexaDecimal_Hash class="type">class="kw">string to its first class="num">16 characters class=class="str">"cmt">//done to comply with a specific length requirement for the boundary class=class="str">"cmt">//in the multipart/form-data of the HTTP request. Print("Final Truncated(class="num">16 characters) MD5 Hash String is: \n",HexaDecimal_Hash); class=class="str">"cmt">//--- WebRequest class="type">char DATA[]; class="type">class="kw">string URL = NULL; URL = TG_API_URL+"/bot"+botTkn+"/sendPhoto"; class=class="str">"cmt">//--- add chart_id class=class="str">"cmt">//Append a carriage class="kw">return and newline character sequence to the DATA array. class=class="str">"cmt">//In the context of HTTP, \r\n is used to denote the end of a line class=class="str">"cmt">//and is often required to separate different parts of an HTTP request. ArrayAdd(DATA,"\r\n"); class=class="str">"cmt">//Append a boundary marker to the DATA array. class=class="str">"cmt">//Typically, the boundary marker is composed of two hyphens(--) class=class="str">"cmt">//followed by a unique hash class="type">class="kw">string and then a newline sequence. class=class="str">"cmt">//In multipart/form-data requests, boundaries are used to separate class=class="str">"cmt">//different pieces of data. ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n"); class=class="str">"cmt">//Add a Content-Disposition header for a form-data part named chat_id. class=class="str">"cmt">//The Content-Disposition header is used to indicate that the following data class=class="str">"cmt">//is a form field with the name chat_id. ArrayAdd(DATA,"Content-Disposition: form-data; name=\"chat_id\"\r\n"); class=class="str">"cmt">//Again, append a newline sequence to the DATA array to end the header section class=class="str">"cmt">//before the value of the chat_id is added. ArrayAdd(DATA,"\r\n"); class=class="str">"cmt">//Append the actual chat ID value to the DATA array. ArrayAdd(DATA,chatID); class=class="str">"cmt">//Finally, Append another newline sequence to the DATA array to signify class=class="str">"cmt">//the end of the chat_id form-data part. ArrayAdd(DATA,"\r\n"); class=class="str">"cmt">// EXAMPLE OF USING CONVERSIONS class=class="str">"cmt">//class="type">uchar array[] = { class="num">72, class="num">101, class="num">108, class="num">108, class="num">111, class="num">0 }; // "Hello" in ASCII class=class="str">"cmt">//class="type">class="kw">string output = CharArrayToString(array,class="num">0,WHOLE_ARRAY,CP_ACP); class=class="str">"cmt">//Print("EXAMPLE OUTPUT OF CONVERSION = ",output); // Hello Print("CHAT ID DATA:"); ArrayPrint(DATA); class="type">class="kw">string chatID_Data = CharArrayToString(DATA,class="num">0,WHOLE_ARRAY,CP_UTF8); Print("SIMPLE CHAT ID DATA IS AS FOLLOWS:",chatID_Data); class=class="str">"cmt">//--- Caption class="type">class="kw">string CAPTION = NULL; CAPTION = "Screenshot of Symbol: "+Symbol()+ " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+
◍ 把截图封包推给 Telegram 的收尾动作
这段逻辑处在 EA 初始化尾段,负责把前面拼好的 multipart 表单数据真正发出去。先判断 CAPTION 字符串长度大于 0 才追加 caption 字段,否则只发图片本身,避免空标题污染消息体。 boundary 用 HexaDecimal_Hash 变量动态拼接,Content-Type 头必须写成 "multipart/form-data; boundary=" 加同一哈希,MT5 的 WebRequest 才不会把分段解析错。 发送超时设死 10000 毫秒,返回码 200 才算推送成功;若返回 -1 且 GetLastError() 等于 4014,基本就是终端没把 TG_API_URL 加进允许列表,得手动去 MT5 选项里补白名单。外汇与贵金属行情波动剧烈,这类自动截图推送仅作盘后复盘辅助,不构成任何交易信号。 下面这段是原文里实际执行封包与请求的核心代码,逐行看能直接照抄到自己的 EA 里验证。
if(StringLen(CAPTION) > class="num">0){ ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n"); ArrayAdd(DATA,"Content-Disposition: form-data; name=\"caption\"\r\n"); ArrayAdd(DATA,"\r\n"); ArrayAdd(DATA,CAPTION); ArrayAdd(DATA,"\r\n"); } class=class="str">"cmt">//--- ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n"); ArrayAdd(DATA,"Content-Disposition: form-data; name=\"photo\"; filename=\"Upload_ScreenShot.jpg\"\r\n"); ArrayAdd(DATA,"\r\n"); ArrayAdd(DATA,photoArr_Data); ArrayAdd(DATA,"\r\n"); ArrayAdd(DATA,"--"+HexaDecimal_Hash+"--\r\n"); Print("FINAL FULL PHOTO DATA BEING SENT:"); ArrayPrint(DATA); class="type">class="kw">string final_Simple_Data = CharArrayToString(DATA,class="num">0,WHOLE_ARRAY,CP_ACP); Print("FINAL FULL SIMPLE PHOTO DATA BEING SENT:",final_Simple_Data); class="type">class="kw">string HEADERS = NULL; HEADERS = "Content-Type: multipart/form-data; boundary="+HexaDecimal_Hash+"\r\n"; Print("SCREENSHOT SENDING HAS BEEN INITIATED SUCCESSFULLY."); class=class="str">"cmt">//class="type">char data[]; // Array to hold data to be sent in the web request(empty in this case) class="type">char res[]; class=class="str">"cmt">// Array to hold the response data from the web request class="type">class="kw">string resHeaders; class=class="str">"cmt">// String to hold the response headers from the web request class=class="str">"cmt">//class="type">class="kw">string msg = "EA INITIALIZED ON CHART " + _Symbol; // Message to send, including the chart symbol class=class="str">"cmt">//const class="type">class="kw">string url = TG_API_URL + "/bot" + botTkn + "/sendmessage?chat_id=" + chatID + class=class="str">"cmt">// "&text=" + msg; class=class="str">"cmt">// Send the web request to the Telegram API class="type">int send_res = WebRequest("POST",URL,HEADERS,class="num">10000, DATA, res, resHeaders); class=class="str">"cmt">// Check the response status of the web request if (send_res == class="num">200) { class=class="str">"cmt">// If the response status is class="num">200 (OK), print a success message Print("TELEGRAM MESSAGE SENT SUCCESSFULLY"); } else if (send_res == -class="num">1) { class=class="str">"cmt">// If the response status is -class="num">1 (error), check the specific error code if (GetLastError() == class="num">4014) { class=class="str">"cmt">// If the error code is class="num">4014, it means the Telegram API URL is not allowed in the terminal Print("PLEASE ADD THE ", TG_API_URL, " TO THE TERMINAL"); } class=class="str">"cmt">// Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM MESSAGE"); } else if (send_res != class="num">200) { class=class="str">"cmt">// If the response status is not class="num">200 or -class="num">1, print the unexpected response code and error code Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError()); } class="kw">return(INIT_SUCCEEDED); class=class="str">"cmt">// Return initialization success status }
用 GIF 验证 MT5 到 Telegram 的截图推送
EA 把 MT5 图表截图发到 Telegram 之前,先拿一段 GIF 录屏把整条链路跑通,比直接上实盘盲发更稳。 实测流程里,MT5 先把目标图表置顶,然后故意停 10 秒不动,留给人工做最后一根 K 线或指标参数微调。这 10 秒里日志标签页会刷出「重绘图表」「捕获截图」两类进度消息,没看到就等于截图没真正触发。 随后图表自动关闭,图片打包上传,Telegram 会话里收到图即代表通路正常。外汇与贵金属行情跳空频繁,这种自动推送若卡在截图环节,可能漏掉关键形态,属高频自动化里的高风险盲区,建议每次改完 WebRequest 参数都重录一次 GIF 自查。
「把截图发到 Telegram 的完整链路」
前面几节已经把从 MT5 抓图到推送到 Telegram 的每一步拆开讲过,这里把整条链路串起来看:先用 ChartScreenShot 把当前图表存成本地文件,再以二进制读入做 Base64 编码,最后塞进 multipart/form-data 的 HTTP 请求发给 Telegram API。直接抛二进制会被服务端拒收,Base64 是绕开这个限制的最低成本方案。 这套动作验证过可跑通,附带样例文件 TELEGRAM_MQL5_SCREENSHOTS_PART3.mq5 约 63.9 KB、编译后 ex5 约 58.87 KB,读者开 MT5 加载即可复现。外汇与贵金属行情波动剧烈,自动化推送只是辅助监控手段,信号误发或延迟都可能放大实盘风险。 下一阶段会把这部分代码封成类,支持多实例与不同交易场景复用,不必每次都写一遍单函数调用。模块化的价值在于:你能在伦敦段突破触发时发一张图,也能在日终平仓后发一条文字,时间和方式由 EA 自己排。 下面这行是原文里做换行替换的片段,注意它把 \n 转成真实 0x0A 字节,Telegram 文本消息里直接用 \n 字符串是不会换行的。
::StringReplace(text, "\n", ShortToString(0x0A));