创建 MQL5-Telegram 集成 EA 交易 (第 3 部分):将带有标题的图表截图从 MQL5 发送到 Telegram·综合运用
📘

创建 MQL5-Telegram 集成 EA 交易 (第 3 部分):将带有标题的图表截图从 MQL5 发送到 Telegram·综合运用

第 3/3 篇

截图落盘后的等待与编码边界生成

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 实测后改。

MQL5 / C++
  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 出包格式,不保证消息必达。

MQL5 / C++
  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 里验证。

MQL5 / C++
   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 字符串是不会换行的。

MQL5 / C++
    ::StringReplace(text, "\n", ShortToString(0x0A));

常见问题

建议落盘后延迟 100~300 毫秒再读取发送,避免文件句柄未释放导致空图或发送失败。
将截图文件的 MD5 哈希值作为 boundary 标识拼接进请求体头尾,确保前后一致,否则 Telegram 会拒收。
可以,小布支持绑定消息通道,触发条件后自动截图表并推送,你不用自己写封包代码。
先发一张本地生成的 GIF 做端到端测试,能收到且画面正常,再切真实图表截图。
常漏掉关闭文件流和释放 HTTP 句柄,应在发送完成后显式清理,防止内存泄漏和后续发送卡死。