创建 MQL5-Telegram 集成 EA 交易(第 4 部分):模块化代码函数以增强可重用性·进阶篇
◍ EA 初始化时把账户状态推到 Telegram
在 MT5 里用 WebRequest 直连 Telegram Bot API,可以把 EA 初始化那一刻的账户权益与空闲保证金主动推送到手机。下面这段逻辑先拼出请求 URL,再以 POST 方式发出去,timeout 设为 10000 毫秒(10 秒)是比较稳妥的等待值,太短容易在行情跳动时漏发。 返回码 200 代表推送成功,终端会打印 TELEGRAM MESSAGE SENT SUCCESSFULLY;若是 -1 且 GetLastError() 返回 4014,说明目标域名没在终端「允许 WebRequest 的 URL 列表」里勾选,这是最常见的配置坑。 账户侧取数直接用 AccountInfoDouble(ACCOUNT_EQUITY) 和 ACCOUNT_MARGIN_FREE,拼进带 emoji 的字符串后必须过一遍 UrlEncode,否则换行符和特殊符号会让 Bot API 报 400。外汇与贵金属杠杆高,权益和空闲保证金瞬间波动大,这类推送只作监控辅助,不构成任何方向判断。 别把 4014 当成网络坏了 先开 MT5 的「工具→选项→EA 交易」,把你的 TG_API_URL 根域名加进允许列表,再跑回测,否则代码逻辑再对也发不出去。
class="type">char data[]; class=class="str">"cmt">// 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="type">class="kw">string message = custom_message; const class="type">class="kw">string url = api_url + "/bot" + bot_token + "/sendmessage?chat_id=" + chat_id + "&text=" + message; class=class="str">"cmt">// Send the web request to the Telegram API class="type">int send_res = WebRequest("POST", url, "", timeout, 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"); class="kw">return (SUCCEEDED_CODE); } 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 ", 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"); class="kw">return (FAILED_CODE); } 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 (FAILED_CODE); } class="kw">return (SUCCEEDED_CODE); } class=class="str">"cmt">////--- Account Status Update: class="type">class="kw">double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY); class="type">class="kw">double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); class="type">class="kw">string complex_msg = "\xF680 EA INITIALIZED ON CHART " + _Symbol + "\xF680" +"\n\xF4CA Account Status \xF4CA" +"\nEquity: $" +DoubleToString(accountEquity,class="num">2) +"\nFree Margin: $" +DoubleToString(accountFreeMargin,class="num">2); class="type">class="kw">string encloded_msg = UrlEncode(complex_msg); complex_msg = encloded_msg; sendEncodedMessage(complex_msg,TG_API_URL,botTkn,chatID,class="num">10000);
「把图表截图推到 Telegram 的模块化写法」
要让 MT5 把图表画面自动发到 Telegram,核心是先拆出两个独立函数:一个负责抓图存盘,一个负责读文件并发 POST 请求。这样主逻辑只需两行调用,后期接交易信号也方便。 sendScreenShot 用整数返回码标记成败:文件打不开直接回 FAILED_CODE,WebRequest 回 200 才回 SUCCEEDED_CODE,其他情况把错误码 4014(终端未放行 API 域名)单独提示。下面这段是发图函数的骨架与关键返回逻辑。 抓图侧也不能马虎。getScreenshot_of_Current_Chart 会先删同名旧文件避免覆盖,调 ChartRedraw 刷新后再用 ChartScreenShot 存盘,并用 while 循环最多等 30 秒确认文件落地;超时回 FAILED_CODE,成功回 SUCCEEDED_CODE。实测用户约 10 秒内能在 Telegram 收到图。 caption 参数留空传 NULL 时发过去的图没说明文字;想带上下文,就把品种、周期、时间拼成字符串传进去。编译后新开自定义图表的截图上就会带一行标题,排查哪张图来自哪段行情一目了然。
class="type">int sendScreenShot(class="type">class="kw">string screenshot_name,const class="type">class="kw">string telegram_url, const class="type">class="kw">string bot_token,const class="type">class="kw">string chat_id, class="type">class="kw">string caption=""){ class=class="str">"cmt">//... } class="type">int screenshot_Handle = INVALID_HANDLE; screenshot_Handle = FileOpen(screenshot_name,FILE_READ|FILE_BIN); if(screenshot_Handle == INVALID_HANDLE){ Print("INVALID SCREENSHOT HANDLE. REVERTING NOW!"); class="kw">return(FAILED_CODE); } 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 SCREENSHOT FILE SENT SUCCESSFULLY"); class="kw">return (SUCCEEDED_CODE); } 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 ", telegram_url, " TO THE TERMINAL"); } class=class="str">"cmt">// Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM SCREENSHOT FILE"); class="kw">return (FAILED_CODE); }
把图表截图读入并转成 Base64 待发
MT5 里要把图表截图推到外部接口,第一步是先以二进制方式把存在终端里的截图文件打开。FileOpen 拿到的句柄若是 INVALID_HANDLE,说明文件没落盘或路径错,直接返回失败码,这种异常在 EA 初次跑时命中率不低。 句柄有效后,FileSize 给出的字节数就是后续数组的基准。下面这段代码把截图读进 uchar 数组,并打印出实际读取长度,方便你在 Experts 日志里确认文件没读空: 读取成功后必须做 Base64 编码,因为 HTTP 表单无法直接吞二进制。CryptEncode 用 CRYPT_BASE64 把原始像素流变成可嵌字符串,编码后长度通常会比原文件大三分之一左右,日志里打印的 Transformed BASE-64 data 就是这个数值。 发图前还要截一段做边界校验。这里固定拷贝 base64 数组的前 1024 字节进临时数组,后面拼 multipart 边界时会用到,你可以先打开 MT5 日志看这 1024 字节是否随图表变化而变。
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 (FAILED_CODE); } class="kw">return (SUCCEEDED_CODE); class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| FUNCTION TO SEND CHART SCREENSHOT FILES | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int sendScreenShot(class="type">class="kw">string screenshot_name,const class="type">class="kw">string telegram_url, const class="type">class="kw">string bot_token,const class="type">class="kw">string chat_id, class="type">class="kw">string caption=""){ class="type">int screenshot_Handle = INVALID_HANDLE; screenshot_Handle = FileOpen(screenshot_name,FILE_READ|FILE_BIN); if(screenshot_Handle == INVALID_HANDLE){ Print("INVALID SCREENSHOT HANDLE. REVERTING NOW!"); class="kw">return(FAILED_CODE); } 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);
◍ 用 MD5 拼出 multipart 边界并塞入 chat_id
给 Telegram 发图走的是 multipart/form-data,边界字符串不能随便写。这段逻辑先对临时数组做 MD5,再用前 16 位十六进制当 boundary,既满足 HTTP 规范也对得上服务端解析长度要求。 CryptEncode 算出的 md5 数组长度固定为 16 字节(CRYPT_HASH_MD5 输出 128 位),循环里用 StringFormat("%02X",md5[i]) 转成大写十六进制,拼完再 StringSubstr 截前 16 字符。打印出来你能直接看到边界长什么样,复制到 Postman 里对照也行。 边界拼好后往 DATA 里逐段 ArrayAdd:先 \r\n,再 "--"+Hash+"\r\n",接着 Content-Disposition 头与 chat_id 值。注意每加完一个 form-data 段都要补 \r\n,否则服务端可能把字段吞掉。最后用 CharArrayToString(DATA,0,WHOLE_ARRAY,CP_UTF8) 转成可读串,Print 出来核对比直接发请求更安全。 外汇与贵金属行情波动剧烈、滑点频发,这类自动推送脚本只建议用于告警辅助,别拿它当下单依据。
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 = telegram_url+"/bot"+bot_token+"/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,chat_id); 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_STRING = NULL;
「把截图塞进 multipart 请求发到 TG」
这段逻辑干的事很直接:先把 caption 文本按 multipart/form-data 格式拼进 DATA 数组,边界符用的是 HexaDecimal_Hash。若 caption 长度大于 0,就追加一段 name="caption" 的表单字段;随后无条件追加 name="photo" 且 filename="Upload_ScreenShot.jpg" 的二进制段,结尾补上 "--"+Hash+"--" 表示表单结束。 HEADERS 只设了一行 Content-Type,明确 boundary 与 HexaDecimal_Hash 一致,否则服务端解析会直接失败。WebRequest 用 POST、超时 10000 毫秒(10 秒),把 DATA 作为请求体发出,响应写进 res 与 resHeaders。 返回码判断有三个分支:200 视为发送成功并返回 SUCCEEDED_CODE;-1 且 GetLastError()==4014 说明终端没把 TG 域名加进允许列表,此时提示去添加;其余非 200 一律打印 UNEXPECTED RESPONSE 及错误码并返回 FAILED_CODE。外汇与贵金属账户跑这类 EA 存在 API 泄露 bot token 的高风险,建议用只读权限的 bot 且限定 chat_id。
CAPTION_STRING = caption; if(StringLen(CAPTION_STRING) > 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_STRING); 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 SCREENSHOT FILE SENT SUCCESSFULLY"); class="kw">return (SUCCEEDED_CODE); } 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 ", telegram_url, " TO THE TERMINAL"); } class=class="str">"cmt">// Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM SCREENSHOT FILE"); class="kw">return (FAILED_CODE); } 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 (FAILED_CODE); } class="kw">return (SUCCEEDED_CODE); }
把图表截屏喂给视觉模型的两种取法
做价格行为复盘时,让小布这类 AIGC 工具读 MT5 图表,第一步是把当前画面或指定品种周期存成图片。下面两段函数分别解决“截当前图”和“开新图再截”的需求,可直接抄进 EA 或脚本。 当前图截图函数先判断文件是否已存在,存在就删掉并重绘,避免旧图干扰。随后以 1366×768、右对齐方式截屏,并用 60 次、每次 500 毫秒的循环等待文件落盘,最长约 30 秒;若超时未生成则返回失败码,否则返回成功码。 新图截图函数多一步 ChartOpen:传入 symbol_name 与 period_name 开一个独立图表,并等 SERIES_SYNCHRONIZED 为真确认行情同步。之后关掉网格和周期分隔线,把阴阳线强制染成红蓝、背景设为浅鲑鱼色,这样截出来的图对比度高,视觉模型辨识形态(如吞没、pin bar)的误读概率倾向更低。外汇与贵金属波动剧烈、杠杆风险高,截图仅作分析辅助,不构成方向建议。
class="type">int getScreenshot_of_Current_Chart(class="type">class="kw">string screenshot_name){ class=class="str">"cmt">//--- First class="kw">delete an instance of the screenshot file if it already exists if(FileIsExist(screenshot_name)){ FileDelete(screenshot_name); Print("Chart Screenshot was found and deleted."); ChartRedraw(class="num">0); } ChartScreenShot(class="num">0,screenshot_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; while(!FileIsExist(screenshot_name) && --wait_loops > class="num">0){ Sleep(class="num">500); } if(!FileIsExist(screenshot_name)){ Print("THE SPECIFIED SCREENSHOT DOES NOT EXIST(WAS NOT SAVED). REVERTING NOW!"); class="kw">return (FAILED_CODE); } else if(FileIsExist(screenshot_name)){ Print("THE CHART SCREENSHOT WAS SAVED SUCCESSFULLY TO THE DATA-BASE."); class="kw">return (SUCCEEDED_CODE); } class="kw">return (SUCCEEDED_CODE); } class="type">int getScreenshot_of_New_Chart(class="type">class="kw">string screenshot_name,class="type">class="kw">string symbol_name, ENUM_TIMEFRAMES period_name){ class=class="str">"cmt">//--- First class="kw">delete an instance of the screenshot file if it already exists if(FileIsExist(screenshot_name)){ FileDelete(screenshot_name); Print("Chart Screenshot was found and deleted."); ChartRedraw(class="num">0); } class="type">long chart_id=ChartOpen(symbol_name,period_name); ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true); class=class="str">"cmt">// update chart class="type">int wait=class="num">60; 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_name,period_name,SERIES_SYNCHRONIZED)){ class="kw">break; class=class="str">"cmt">// if prices up to date, terminate the loop and proceed } } ChartRedraw(chart_id); ChartSetInteger(chart_id,CHART_SHOW_GRID,false); ChartSetInteger(chart_id,CHART_SHOW_PERIOD_SEP,false); ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BEAR,clrRed); ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BULL,clrBlue); ChartSetInteger(chart_id,CHART_COLOR_BACKGROUND,clrLightSalmon);