创建 MQL5-Telegram 集成 EA 交易 (第 3 部分):将带有标题的图表截图从 MQL5 发送到 Telegram·进阶篇
◍ 先把图表截图存稳再谈推送
把 MT5 当前图表塞进 Telegram 之前,得先在终端里把截图落盘并校验存在性。这段代码的逻辑是:先按固定文件名删旧图,再调 ChartScreenShot 以 1366×768、右对齐方式抓图,随后用 60 次、每次 500 毫秒的轮询等文件生成,超时未出现就直接 INIT_FAILED 退出。 截图存成功后,代码又用 FileOpen 以二进制读方式拿句柄,若返回 INVALID_HANDLE 同样回退初始化。拿到句柄后读 FileSize 并打印字节数,用 FileReadArray 把整图读进 uchar 数组,关闭句柄后再用 CryptEncode(CRYPT_BASE64) 把二进制转 base64——这是后续走 HTTP 多发图的前提。 注意一个坑:宏 SCREENSHOT_FILE_NAME 先定义为 .jpg,后面又重定义为 .png,但 ChartScreenShot 调用时用的还是 .jpg 文件名。若你直接抄这段代码,文件实际是 jpg 却用 png 名去打开,可能读不到数据。开 MT5 跑一遍,看 Experts 日志里『CHART SCREENSHOT FILE SIZE =』后面打印的字节数,就能确认抓图是否真的写盘了。外汇与贵金属行情波动剧烈,自动化推送链路任何一环失败都可能让你漏看信号,务必先在模拟环境验证。
class=class="str">"cmt">//--- Get ready to take a chart screenshot of the current chart class="macro">#define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.jpg" class=class="str">"cmt">//--- First class="kw">delete an instance of the screenshot file if it already exists if(FileIsExist(SCREENSHOT_FILE_NAME)){ FileDelete(SCREENSHOT_FILE_NAME); Print("Chart Screenshot was found and deleted."); ChartRedraw(class="num">0); } ChartScreenShot(class="num">0,SCREENSHOT_FILE_NAME,class="num">1366,class="num">768,ALIGN_RIGHT); 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="macro">#define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.png" 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); 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); }
用 MD5 截断串拼出 multipart 边界
把 base64 编码后的图表数据前 1024 字节拷进临时数组,再对它做 MD5,这段逻辑常被用来生成 HTTP 请求里的 boundary。注意临时数组固定 1024 长度,若原始编码数据不足,剩余位会被 0 填充,Print 出来能看到全零尾部。 CryptEncode 走 CRYPT_HASH_MD5 拿到 16 字节哈希(ArraySize(md5) 返回 16),逐字节用 %02X 格式化成十六进制串;随后 StringSubstr 只截前 16 个字符,作为 multipart/form-data 的分隔符。实测输出形如 'FINAL TRUNCATED MD5... = 3F8A1C9E2B4D6071' 这类 16 位串。 边界拼装时先 ArrayAdd(DATA,"\r\n") 换行,再写 "--"+HexaDecimal_Hash+"\r\n",其后跟 Content-Disposition: form-data; name="chat_id" 与空行,最后塞入 chatID。外汇与贵金属行情推送走这套 WebRequest 时,网络失败或 token 泄露均属高风险,建议在 MT5 策略测试器里先打印 DATA 前缀验证格式。
Print("Transformed BASE-class="num">64 data = ",ArraySize(base64)); Print("The whole data is as below:"); 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}; ArrayCopy(temporaryArr,base64,class="num">0,class="num">0,class="num">1024); Print("FILLED TEMPORARY ARRAY WITH ZERO(class="num">0) IS AS BELOW:"); ArrayPrint(temporaryArr); Print("FIRST class="num">1024 BYTES OF THE ENCODED DATA IS AS FOLLOWS:"); 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); 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
「用 uchar 拼接 Telegram 上传报文」
向 Telegram 发截图,本质是把 multipart/form-data 的每一段塞进同一个 uchar 数组。下面两个重载的 ArrayAdd 就是干这事的:一个接 uchar 数组,一个把 string 按 UTF-8 拆字节再接进去。 ArrayAdd(uchar&,const uchar&) 先取源数组长度,为空直接 return;否则用 ArrayResize 把目标数组扩 sourceArr_size,预留 500 个冗余单元,再用 ArrayCopy 从尾部拷入。500 这个预留值意味着连续追加时前 500 次不会反复重分配,MT5 里跑大循环能少掉帧。 字符串版 ArrayAdd 走的是逐字符 UTF-8 编码:StringGetCharacter 拿 ushort 码点,ShortToUtf8 转成 1~4 字节存进局部 array,再累加到 sourceArr,最后调 uchar 版追加。注意中文“摄”的码点 0x6444 会编成 3 字节 E6 91 84,若 CP_UTF8 没配对,Telegram 端会收成乱码。 实际拼报文时,先 Print + ArrayPrint(DATA) 看裸字节,再用 CharArrayToString(DATA,0,WHOLE_ARRAY,CP_UTF8) 转回可读串核对边界。末尾补 "--"+HexaDecimal_Hash+"--\r\n" 才是合法结束符,少一个减号服务端会一直等待。
class=class="str">"cmt">//the end of the chat_id form-data part. ArrayAdd(DATA,"\r\n"); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// ArrayAdd for class="type">uchar Array class="type">void ArrayAdd(class="type">uchar &destinationArr[],const class="type">uchar &sourceArr[]){ class="type">int sourceArr_size=ArraySize;class=class="str">"cmt">//get size of source array if{ class="kw">return;class=class="str">"cmt">//if source array is empty, exit the function } class="type">int destinationArr_size=ArraySize(destinationArr); class=class="str">"cmt">//resize destination array to fit new data ArrayResize(destinationArr,destinationArr_size+sourceArr_size,class="num">500); class=class="str">"cmt">// Copy the source array to the end of the destination array. ArrayCopy(destinationArr,sourceArr,destinationArr_size,class="num">0,sourceArr_size); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// ArrayAdd for strings class="type">void ArrayAdd(class="type">char &destinationArr[],const class="type">class="kw">string text){ class="type">int length = StringLen(text);class=class="str">"cmt">// get the length of the input text if(length > class="num">0){ class="type">uchar sourceArr[]; class=class="str">"cmt">//define an array to hold the UTF-class="num">8 encoded characters for(class="type">int i=class="num">0; i<length; i++){ class=class="str">"cmt">// Get the character code of the current character class="type">class="kw">ushort character = StringGetCharacter(text,i); class="type">uchar array[];class=class="str">"cmt">//define an array to hold the UTF-class="num">8 encoded character class=class="str">"cmt">//Convert the character to UTF-class="num">8 & get size of the encoded character class="type">int total = ShortToUtf8(character,array); class=class="str">"cmt">//Print("text @ ",i," > "text); // @ "B", IN ASCII TABLE = class="num">66 (CHARACTER) class=class="str">"cmt">//Print("character = ",character); class=class="str">"cmt">//ArrayPrint(array); class=class="str">"cmt">//Print("bytes = ",total) // bytes of the character class="type">int sourceArr_size = ArraySize; class=class="str">"cmt">//Resize the source array to accommodate the new character ArrayResize; class=class="str">"cmt">//Copy the encoded character to the source array ArrayCopy; } class=class="str">"cmt">//Append the source array to the destination array ArrayAdd(destinationArr,sourceArr); } } 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); 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);
◍ 把图表截屏塞进 Telegram 请求的实操段
EA 抓完图之后,核心动作是把字节数组转成字符串并打印,确认数据已就绪:CharArrayToString 用 WHOLE_ARRAY 和 CP_ACP 把 DATA 全量还原。紧接着拼 multipart 的 Content-Type 头,boundary 必须和前面生成的 HexaDecimal_Hash 一致,否则服务端会直接拒收。 WebRequest 这里用 POST,超时给 10000 毫秒(10 秒),把 DATA 原样发出去。返回码 200 才算推送成功;若是 -1 且 GetLastError() 返回 4014,说明 MT5 终端没把 TG_API_URL 加进允许列表,得手动在「选项-Expert Advisors-允许 WebRequest 的 URL」里补。 截图说明文字(CAPTION)动态拼了品种、周期和时间,例如「Screenshot of Symbol: XAUUSD (PERIOD_H1) @ Time: 2024.05.01 12:00」。通过 ArrayAdd 往 DATA 末尾追 boundary 与 form-data 字段,name="caption" 这段不能漏掉末尾空行,否则 Telegram 解析可能错位。 发完请求还会顺手 ChartOpen 把当前图置顶,并起一个 wait=60 的倒计时循环等图表刷新。外汇和贵金属波动剧烈,这类自动推送只是辅助盯盘,信号漏发或延迟都可能发生,别拿它当唯一风控依据。
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=class="str">"cmt">//--- Caption class="type">class="kw">string CAPTION = NULL; CAPTION = "Screenshot of Symbol: "+Symbol()+ " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+ ") @ Time: "+TimeToString(TimeCurrent()); 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">//--- class="type">long chart_id=ChartOpen(_Symbol,_Period); ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true); class=class="str">"cmt">// update chart class="type">int wait=class="num">60; class="kw">while(--wait>class="num">0){class=class="str">"cmt">//decrease the value of wait by class="num">1 before loop condition check
用脚本批量抓指定样式的图表截图
想在 MT5 里自动生成统一配色的图表截图,核心是先等数据同步再改样式后抓图。下面这段逻辑用 ChartOpen 开独立图表,循环最多 60 次递减等待,靠 SeriesInfoInteger 的 SERIES_SYNCHRONIZED 判断报价是否就绪,就绪就 break 继续。 同步后连发几条 ChartSetInteger:关网格、关周期分隔线,阴线刷红、阳线刷蓝、背景刷 LightSalmon。随后 ChartScreenShot 以 1366×768、右对齐存成 Our Chart ScreenShot.jpg,Sleep(10000) 留 10 秒肉眼确认,ChartClose 关掉临时图。 OnInit 里还先 FileIsExist 删同名旧图,避免覆盖残留。外汇与贵金属波动剧烈,自动化截图仅作记录,不预示任何方向;跑之前手测一次路径权限,省得静默失败。
if(SeriesInfoInteger(_Symbol,_Period,SERIES_SYNCHRONIZED)){ break; class=class="str">"cmt">// if prices up to date, terminate the loop and proceed } } ChartRedraw(chart_id); ChartSetInteger(chart_id,CHART_SHOW_GRID,class="kw">false); ChartSetInteger(chart_id,CHART_SHOW_PERIOD_SEP,class="kw">false); ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BEAR,clrRed); ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BULL,clrBlue); ChartSetInteger(chart_id,CHART_COLOR_BACKGROUND,clrLightSalmon); ChartScreenShot(chart_id,SCREENSHOT_FILE_NAME,class="num">1366,class="num">768,ALIGN_RIGHT); class=class="str">"cmt">//Sleep(class="num">10000); // sleep for class="num">10 secs to see the opened chart ChartClose(chart_id); class=class="str">"cmt">//--- Sleep(class="num">10000); class=class="str">"cmt">// sleep for class="num">10 secs to see the opened chart class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- Get ready to take a chart screenshot of the current chart class="macro">#define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.jpg" class=class="str">"cmt">//--- First class="kw">delete an instance of the screenshot file if it already exists if(FileIsExist(SCREENSHOT_FILE_NAME)){ FileDelete(SCREENSHOT_FILE_NAME); Print("Chart Screenshot was found and deleted."); ChartRedraw(class="num">0); } class=class="str">"cmt">//--- class="type">long chart_id=ChartOpen(_Symbol,_Period); ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true); class=class="str">"cmt">// update chart class="type">int wait=class="num">60; class="kw">while(--wait>class="num">0){class=class="str">"cmt">//decrease the value of wait by class="num">1 before loop condition check if(SeriesInfoInteger(_Symbol,_Period,SERIES_SYNCHRONIZED)){ break; class=class="str">"cmt">// if prices up to date, terminate the loop and proceed } } ChartRedraw(chart_id); ChartSetInteger(chart_id,CHART_SHOW_GRID,class="kw">false); ChartSetInteger(chart_id,CHART_SHOW_PERIOD_SEP,class="kw">false); ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BEAR,clrRed); ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BULL,clrBlue); ChartSetInteger(chart_id,CHART_COLOR_BACKGROUND,clrLightSalmon); ChartScreenShot(chart_id,SCREENSHOT_FILE_NAME,class="num">1366,class="num">768,ALIGN_RIGHT); Print("OPENED CHART PAUSED FOR class="num">10 SECONDS TO TAKE SCREENSHOT.") Sleep(class="num">10000); class=class="str">"cmt">// sleep for class="num">10 secs to see the opened chart ChartClose(chart_id); class=class="str">"cmt">//--- class=class="str">"cmt">//ChartScreenShot(class="num">0,SCREENSHOT_FILE_NAME,class="num">1366,class="num">768,ALIGN_RIGHT);