创建 MQL5-Telegram 集成 EA 交易(第 4 部分):模块化代码函数以增强可重用性
概述
在本系列的前一篇文章中,我们深入研究了从中 MetaTrader 5 向 Telegram 发送带有标题的图表快照的过程。我们的方法虽然有效,但相当简单,有点不灵活。我们将捕获屏幕截图所需的组件链接在一起,将其转换或编码为消息友好形式,然后将其发送至 Telegram 。虽然这种设置奏效了,但它导致了相当多的代码重复,而且不太容易管理。那么,我们能做些什么来改进这一实现呢?转向更加模块化的代码库!这是朝着更灵活、更可维护的系统迈出的第一步。
在本系列的第四部分中,我们将重点介绍如何通过代码模块化来增强程序的可重用性。我们将详细讨论代码模块化的原则,更具体地说,这些原则如何应用于我们的项目。接下来,我们将逐步介绍如何将现有的 mql5 脚本重组为单独的、定义良好的函数。最后,您可以选择使用旧的单片程序或具有相同输出的新的模块化 EA 交易系统。
在此之后,我们将有条不紊地更改当前的代码,使其在程序的实施例中占据新的空间。我们将把代码分解为离散的函数,每个函数都将执行一项任务:发送消息、截图和将数据编码为必要的传输形式。我们将演示每个部分在新结构中是如何组合在一起的,更重要的是,每个函数是如何在没有不必要的重复的情况下执行任务的,并且以一种允许我们轻松更新和扩展程序的方式。
最后,我们将讨论模块化策略代码的测试和验证过程。这将包括检查每个函数的正确操作和整体系统性能,并将结果与旧代码进行比较。虽然我们使用了术语模块化,但我们只是在谈论如何让我们的 EA 交易更易于理解和维护。在本文结束时,您将了解为什么这是开发中的重要一步,并清楚地了解我们是如何做到的以及我们希望获得什么好处。以下是我们创建 EA 交易时将遵循的主题:
最后,我们将制作出一个整洁高效的 MetaQuotes Language 5 (MQL5)-Telegram EA 交易。这种模块化和结构良好的代码易于集成,高度灵活,更新和添加新功能所需的工作量比以通常的方式编写代码要少得多。它不仅处理消息发送任务,还负责屏幕截图捕获和数据保存功能,以可管理和可扩展的方式进行,从而为未来的增强奠定了坚实的基础。那么让我们开始吧。
理解模块化的必要性
我们将研究代码模块化的思想,这是组织和管理大型代码库的基本软件工程技术。当一个程序被模块化时,它会被分解成更小、可管理的部分,这些部分几乎是自包含的。这些部分(或模块)中的每一个都执行特定的功能,并以定义良好的方式与其他模块交互。当然,程序的大部分仍然需要编写,因此我们还将研究一些基本原则和概念,这些原则和概念指导如何使程序模块化。
开发和维护阶段都显示了模块化的回报。当需要编写代码时,开发人员可以一次专注于一个模块。在进入下一个模块之前,每个模块中的代码都经过了实现、测试和调试。一旦系统开始运行,如果需要更改,通常可以在一个模块中进行更改。一个模块的更改对其他模块的影响很小。总体结果是一个更稳定的系统,更改或维修成本更低。举例来说,当某些事件发生时,我们的MQL5 EA 交易可以发送消息。我们的一个模块就是这样做的。另一个模块可以截图并保存。如果我们想扩展截图功能,我们可以在不弄乱发送消息的模块的情况下完成。
确保程序的可维护性看似简单;它看起来很简单,因为它实际上是关于保持使程序工作的一切处于干净、组织良好的秩序中。到目前为止,我们所说的几乎所有内容都是这一重要段落的前奏,因为没有什么比制作包含让各方满意所需的所有部分的模块更有利于可维护性的了 — 一部分给你,一部分给我,还有一部分给那边的那个功能。通过确保所有这些,我们还确保了可重用性,因为一旦一个模块完成了它应该做的事情,它就会尽可能好。
我们将通过将 MQL5 EA 交易划分为明确定义的函数和类来演示其代码模块化。我们将研究如何将这些模块拼接在一起,以完成指定的工作,比如发送短信、截图,以及重组我们输入系统的数据,使其按照我们的意愿运行。最后,我们希望了解这些变化如何使我们的 EA 交易更高效、更可维护、更可扩展。
重构消息发送代码
我们要做的第一件事是创建一个自定义函数或模块,在那里我们可以输入我们将封装和组织的逻辑,以实现最大的重用。第一个函数将负责发送一条简单的消息。
//+------------------------------------------------------------------+ //| FUNCTION TO SEND SIMPLE MESSAGE | //+------------------------------------------------------------------+ void sendSimpleMessage(){ //... }
在这里,我们将发送 Telegram 消息的过程封装在名为 “sendSimpleMessage” 的函数中。这种模块化结构简化了 EA 交易内代码的维护、管理和重用。我们使用 void 函数;即不返回值的函数。相反,它通过向 Telegram 发送消息来运行。该函数还能够 “在后台” 处理操作的成功和失败,因此代码不会因各种 if 语句而变得太混乱。这种封装允许主程序在想要发送消息时调用该函数,而不会陷入如何使用 Telegram API 的困境。
为了使消息发送操作更加灵活,我们需要包括可以自动重复使用的参数,以允许在需要时发送不同的文本,以及 API URL、机器人的令牌和聊天 ID。
void sendSimpleMessage(string custom_message,const string api_url, const string bot_token,const string chat_id, int timeout=10000){ //... }
这里我们定义了一个名为 “sendSimpleMessage” 的 void 函数,体现了它的用途:向 Telegram 发送一条普通的消息,不带任何复杂的附件或数据处理。然后填充四个必需的输入参数:“custom_message”、“api_url”、“bot_token”和“chat_id”,以及一个可选输入参数:“timeout”。让我们以结构化的方式分解参数,以便更容易理解。
- custom_message:这是一个字符串参数,包含我们想要发送到 Telegram 的实际文本消息。
- api_url:这是一个字符串参数,包含 Telegram Bot API 的基础 URL。此 URL 用于请求正确的 API 端点。
- bot_token:另一个字符串参数包含机器人的唯一令牌,这是对机器人进行身份验证和授权其发送消息所必需的。
- chat_id:此字符串参数指定将发送消息的 Telegram 聊天或频道的唯一标识符。
- timeout:这是一个可选的 int 参数,用于设置函数在将请求视为超时之前应等待 Telegram API 响应的时间(以毫秒为单位)。默认值设置为 10000 毫秒(10 秒),但用户可以根据需要提供自定义超时值。
您可能已经注意到我们在一些输入参数中使用了关键字 const 。这意味着传递的值是最终的,不能在函数体内更改、更改或替换,这确保了函数中没有覆盖错误。接下来,我们只需要将负责将简单消息从默认表单发送到函数的代码片段转移到函数中。
char data[]; // Array to hold data to be sent in the web request (empty in this case) char res[]; // Array to hold the response data from the web request string resHeaders; // String to hold the response headers from the web request string message = custom_message; const string url = api_url + "/bot" + bot_token + "/sendmessage?chat_id=" + chat_id + "&text=" + message; // Send the web request to the Telegram API int send_res = WebRequest("POST", url, "", timeout, data, res, resHeaders);
我们首先声明一对数组:“data” 和 “res”。“data” 数组为空,因为我们没有在 Web 请求中发送任何数据 — 我们仅将消息作为 URL 参数发送。我们发出请求后,“res” 数组将保存来自服务器的响应数据。此外,我们声明一个名为“resHeaders”的 字符串,它将保存 Telegram API 发回的任何响应头部。
接下来,我们从输入参数中取出 “custom_message” 并将其分配给 message 变量。这基本上使我们能够在需要时在函数中处理或传递消息。
我们通过将几个组件串联在一起来构建 API 请求 URL:基础 “api_url”、“/bot” 端点、身份验证 “bot_token” 和收件人聊天的 “chat_id”。为此,我们将消息文本添加为 URL 参数:“&text=”。结果是一个完整的 URL,其中包含 API 调用的所有必需数据。
最后,我们将 Web 请求逻辑传递给 WebRequest 函数。该函数负责向 Telegram API 发送 HTTP POST 请求。它会使用我们刚刚为 API 构建的 URL。请求的超时值默认为10秒(或用户指定的其他值),它决定了请求在放弃并继续其生命之前等待响应的时间。请求是用一个空数据数组发送的(也可以只是 JSON 格式的空对象),API 发送回我们的任何响应都存储在结果数组和结果头字符串中。
最后,我们只需添加 web 请求响应状态的检查逻辑。
// Check the response status of the web request if (send_res == 200) { // If the response status is 200 (OK), print a success message Print("TELEGRAM MESSAGE SENT SUCCESSFULLY"); } else if (send_res == -1) { // If the response status is -1 (error), check the specific error code if (GetLastError() == 4014) { // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal Print("PLEASE ADD THE ", api_url, " TO THE TERMINAL"); } // Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM MESSAGE"); } else if (send_res != 200) { // If the response status is not 200 or -1, print the unexpected response code and error code Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError()); }
从 MetaTrader 5 向 Telegram 发送简单消息的最终函数代码如下:
//+------------------------------------------------------------------+ //| FUNCTION TO SEND SIMPLE MESSAGE | //+------------------------------------------------------------------+ void sendSimpleMessage(string custom_message,const string api_url, const string bot_token,const string chat_id, int timeout=10000){ char data[]; // Array to hold data to be sent in the web request (empty in this case) char res[]; // Array to hold the response data from the web request string resHeaders; // String to hold the response headers from the web request string message = custom_message; const string url = api_url + "/bot" + bot_token + "/sendmessage?chat_id=" + chat_id + "&text=" + message; // Send the web request to the Telegram API int send_res = WebRequest("POST", url, "", timeout, data, res, resHeaders); // Check the response status of the web request if (send_res == 200) { // If the response status is 200 (OK), print a success message Print("TELEGRAM MESSAGE SENT SUCCESSFULLY"); } else if (send_res == -1) { // If the response status is -1 (error), check the specific error code if (GetLastError() == 4014) { // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal Print("PLEASE ADD THE ", api_url, " TO THE TERMINAL"); } // Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM MESSAGE"); } else if (send_res != 200) { // If the response status is not 200 or -1, print the unexpected response code and error code Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError()); } }
为了确定其是否完美运行,让我们转到 OnInit 事件处理函数,注释掉不必要的代码片段,然后调用该函数。调用函数将涉及键入其名称并提供必要的参数。
string msg = "EA INITIALIZED ON CHART " + _Symbol; // Message to send, including the chart symbol sendSimpleMessage(msg,TG_API_URL,botTkn,chatID,10000);
在这里,我们调用 “sendSimpleMessage” 函数向 Telegram 上的聊天发送消息。我们首先构建一个字符串 “msg”,它只是单词 “EA INITIALIZED ON CHART” 与当前图表的交易品种( _Symbol )的连接。此消息的下一个收件人将收到通知,表示 EA 交易已在某个特定图表上初始化。
一旦我们定义了想要发送的文本消息,我们就调用函数 “sendSimpleMessage”。我们传递函数的四个参数。第一个参数就是我们要发送的文本消息,所以它被命名为 “msg”。第二个参数是名为 “TG_API_URL” 的常量,它是 Telegram Bot API 的基础 URL。第三个参数是机器人的访问令牌(“botTkn”),第四个参数(“chatID”)是机器人将发送消息的 Telegram 中聊天或频道的 ID。最后,我们指定超时值为 10 秒(10000 毫秒)。如果 Telegram 服务器在那么长时间后没有响应,我们认为这是一个失败,函数将返回错误代码。测试时,我们收到以下消息:
成功了,现在您可以看到,我们不需要太长的代码片段来执行类似的操作。我们所需要的只是调用负责的函数并传递相应的参数。让我们再发送一条简单的消息,通知用户图表的时间框架或周期。
string new_msg = "THE CURRENT TIMEFRAME IS "+EnumToString(_Period); sendSimpleMessage(new_msg,TG_API_URL,botTkn,chatID,10000);
在这里,我们定义一个名为 “new_msg” 的新字符串变量。通过将文本 “THE CURRENT TIMEFRAME IS” 与 _Period 值的字符串版本(当前图表的时间框架)合并来建立新变量。这是使用 EnumToString 函数完成的,该函数将 _Period 的值转换为人类可读的形式。例如,如果图表设置为 1 小时时间范围,“new_msg” 将包含文本 “THE CURRENT TIMEFRAME IS PERIOD_H1”。之后,调用发送简单消息的相同函数,就可以了。您可以看到这有多么简单。运行测试后,我们得到以下输出:
我们可以看到发送代码是多么容易。我们只使用了两行代码来实现这一点。接下来,我们现在开始发送复杂的编码消息。函数内不会有太大的改变。它继承了相同的逻辑。然而,由于我们正在发送复杂的消息,我们希望处理可能导致的错误。因此,我们不仅仅声明一个 void 函数,还将拥有一个整数数据类型函数,该函数将返回特定的代码来说明调用该函数是失败还是成功。因此,在全局范围内,我们必须定义错误代码。
#define FAILED_CODE -1 #define SUCCEEDED_CODE +1
在这里,我们使用 #define 预处理器指令定义两个常量 “FAILED_CODE” 和 “SUCCEEDED_CODE”。我们为常量分配特定的整数型值来表示操作的结果:“FAILED_CODE” 设置为 -1 表示失败,而“SUCCEEDED_CODE” 设置为 +1 表示成功。这些常数可以是任何您认为合适的值。在它们的声明之后,我们接着构建我们的函数。
int sendEncodedMessage(string custom_message,const string api_url, const string bot_token,const string chat_id, int timeout=10000){ //... }
这里我们定义了一个名为 “sendEncodedMessage” 的整数型函数,表示该函数将返回整数值。web 请求数据将保持不变。但是,我们需要检查响应状态的成功或失败,并采取必要的行动。
// Check the response status of the web request if (send_res == 200) { // If the response status is 200 (OK), print a success message Print("TELEGRAM MESSAGE SENT SUCCESSFULLY"); return (SUCCEEDED_CODE); }
这里,如果响应状态为成功并且消息已成功发送,我们将返回 “SUCCEEDED_CODE”。否则,如果响应状态失败,我们将返回 “FAILED_CODE”。
else if (send_res == -1) { // If the response status is -1 (error), check the specific error code if (GetLastError() == 4014) { // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal Print("PLEASE ADD THE ", api_url, " TO THE TERMINAL"); } // Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM MESSAGE"); return (FAILED_CODE); } else if (send_res != 200) { // If the response status is not 200 or -1, print the unexpected response code and error code Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError()); return (FAILED_CODE); }
最后,如果一切顺利,我们需要返回成功的代码,如下所示:
return (SUCCEEDED_CODE);
最后返回成功非常重要,因为有可能没有检查任何子函数,函数需要返回一个整数。如果我们尝试编译没有返回代码的程序,我们会收到如下错误:
因此,负责发送复杂消息的完整功能代码如下:
//+------------------------------------------------------------------+ //| FUNCTION TO SEND ENCODED MESSAGE | //+------------------------------------------------------------------+ //#define FAILED_CODE -1 //#define SUCCEEDED_CODE +1 int sendEncodedMessage(string custom_message,const string api_url, const string bot_token,const string chat_id, int timeout=10000){ char data[]; // Array to hold data to be sent in the web request (empty in this case) char res[]; // Array to hold the response data from the web request string resHeaders; // String to hold the response headers from the web request string message = custom_message; const string url = api_url + "/bot" + bot_token + "/sendmessage?chat_id=" + chat_id + "&text=" + message; // Send the web request to the Telegram API int send_res = WebRequest("POST", url, "", timeout, data, res, resHeaders); // Check the response status of the web request if (send_res == 200) { // If the response status is 200 (OK), print a success message Print("TELEGRAM MESSAGE SENT SUCCESSFULLY"); return (SUCCEEDED_CODE); } else if (send_res == -1) { // If the response status is -1 (error), check the specific error code if (GetLastError() == 4014) { // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal Print("PLEASE ADD THE ", api_url, " TO THE TERMINAL"); } // Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM MESSAGE"); return (FAILED_CODE); } else if (send_res != 200) { // If the response status is not 200 or -1, print the unexpected response code and error code Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError()); return (FAILED_CODE); } return (SUCCEEDED_CODE); }
现在,让我们将与表情符号字符串联的分段帐户信息发送到 Telegram,并查看响应。将采用相同的代码结构,但我们将简要解释我们的具体操作。
////--- Account Status Update: double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY); double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); string complex_msg = "\xF680 EA INITIALIZED ON CHART " + _Symbol + "\xF680" +"\n\xF4CA Account Status \xF4CA" +"\nEquity: $" +DoubleToString(accountEquity,2) +"\nFree Margin: $" +DoubleToString(accountFreeMargin,2); string encloded_msg = UrlEncode(complex_msg); complex_msg = encloded_msg; sendEncodedMessage(complex_msg,TG_API_URL,botTkn,chatID,10000);
我们首先使用 AccountInfoDouble 并分别传递参数 ACCOUNT_EQUITY 和 ACCOUNT_MARGIN_FREE 来访问账户净值和可用保证金。这些参数可以实时获取账户净值和可用保证金。我们构建了一条详细消息 “complex_msg”,其中我们使用图表的交易品种以及账户净值和可用保证金,并使用表情符号对其进行格式化。我们使用 Telegram API 发送消息,但首先,我们必须确保它通过 HTTP 传输是安全的。我们通过使用 “UrlEncode” 函数对消息进行编码来实现这一点。发送消息后,我们在 Telegram 应用程序中得到的内容如下。
您可以看到我们已在 Telegram 中成功接收了复杂消息。这样我们就彻底完成了消息发送函数。很明显,通过将先前的功能封装在函数中,我们可以使代码更清晰,并允许在多个位置轻松地重用消息发送过程。当我们进一步模块化代码时,这将特别有用,例如添加发送图像和处理错误的功能时。这将在下一节中完成。
模块化截图函数
在这里,我们需要构造一个函数,该函数接受必要的参数并将图表图像发送到 Telegram。它的代码结构将与我们用于发送编码消息的代码结构相同。
int sendScreenShot(string screenshot_name,const string telegram_url, const string bot_token,const string chat_id, string caption=""){ //... }
我们声明一个名为 “sendScreenShot” 的整数函数,这意味着它将返回一个整数值。该函数接受多个参数,确保灵活性和模块化。
- “screenshot_name” 参数指的是将要发送的截图文件的名称,允许我们指定不同的截图。
- “telegram_url”、“bot_token” 和 “chat_id” 是与 Telegram API 通信所需的核心输入参数,使该函数能够适应各种机器人配置和 Telegram 帐户。
- 可选参数 “caption” 允许我们将描述性文字附加到屏幕截图,从而使我们能够在发送屏幕截图之前对其进行注释,从而增强功能。
由于保持了相同的代码结构,我们将只关注返回逻辑。第一个将是我们尝试打开和读取图像内容的实例。
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!"); return(FAILED_CODE); }
在这里,如果我们无法打开要发送到 Telegram 的屏幕截图数据,我们会从函数返回 “FAILED_CODE”,表示由于文件访问问题,发送屏幕截图的操作无法继续。接下来,我们只继承相同的逻辑来检查响应状态以及相应的消息日志和状态代码,如下所示:
// Send the web request to the Telegram API int send_res = WebRequest("POST",URL,HEADERS,10000, DATA, res, resHeaders); // Check the response status of the web request if (send_res == 200) { // If the response status is 200 (OK), print a success message Print("TELEGRAM SCREENSHOT FILE SENT SUCCESSFULLY"); return (SUCCEEDED_CODE); } else if (send_res == -1) { // If the response status is -1 (error), check the specific error code if (GetLastError() == 4014) { // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal Print("PLEASE ADD THE ", telegram_url, " TO THE TERMINAL"); } // Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM SCREENSHOT FILE"); return (FAILED_CODE); } else if (send_res != 200) { // If the response status is not 200 or -1, print the unexpected response code and error code Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError()); return (FAILED_CODE); } return (SUCCEEDED_CODE);
负责发送图表截图文件的完整函数如下:
//+------------------------------------------------------------------+ //| FUNCTION TO SEND CHART SCREENSHOT FILES | //+------------------------------------------------------------------+ int sendScreenShot(string screenshot_name,const string telegram_url, const string bot_token,const string chat_id, string caption=""){ 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!"); 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."); } int screenshot_Handle_Size = (int)FileSize(screenshot_Handle); if (screenshot_Handle_Size > 0){ Print("CHART SCREENSHOT FILE SIZE = ",screenshot_Handle_Size); } uchar photoArr_Data[]; ArrayResize(photoArr_Data,screenshot_Handle_Size); FileReadArray(screenshot_Handle,photoArr_Data,0,screenshot_Handle_Size); if (ArraySize(photoArr_Data) > 0){ Print("READ SCREENSHOT FILE DATA SIZE = ",ArraySize(photoArr_Data)); } FileClose(screenshot_Handle); //ArrayPrint(photoArr_Data); //--- create boundary: (data -> base64 -> 1024 bytes -> md5) //Encodes the photo data into base64 format //This is part of preparing the data for transmission over HTTP. uchar base64[]; uchar key[]; CryptEncode(CRYPT_BASE64,photoArr_Data,key,base64); if (ArraySize(base64) > 0){ Print("Transformed BASE-64 data = ",ArraySize(base64)); //Print("The whole data is as below:"); //ArrayPrint(base64); } //Copy the first 1024 bytes of the base64-encoded data into a temporary array uchar temporaryArr[1024]= {0}; //Print("FILLED TEMPORARY ARRAY WITH ZERO (0) IS AS BELOW:"); //ArrayPrint(temporaryArr); ArrayCopy(temporaryArr,base64,0,0,1024); //Print("FIRST 1024 BYTES OF THE ENCODED DATA IS AS FOLLOWS:"); //ArrayPrint(temporaryArr); //Create an MD5 hash of the temporary array //This hash will be used as part of the boundary in the multipart/form-data uchar md5[]; CryptEncode(CRYPT_HASH_MD5,temporaryArr,key,md5); if (ArraySize(md5) > 0){ Print("SIZE OF MD5 HASH OF TEMPORARY ARRAY = ",ArraySize(md5)); Print("MD5 HASH boundary in multipart/form-data is as follows:"); ArrayPrint(md5); } //Format MD5 hash as a hexadecimal string & //truncate it to 16 characters to create the boundary. string HexaDecimal_Hash=NULL;//Used to store the hexadecimal representation of MD5 hash int total=ArraySize(md5); for(int i=0; i<total; i++){ HexaDecimal_Hash+=StringFormat("%02X",md5[i]); } Print("Formatted MD5 Hash String is: \n",HexaDecimal_Hash); HexaDecimal_Hash=StringSubstr(HexaDecimal_Hash,0,16);//truncate HexaDecimal_Hash string to its first 16 characters //done to comply with a specific length requirement for the boundary //in the multipart/form-data of the HTTP request. Print("Final Truncated (16 characters) MD5 Hash String is: \n",HexaDecimal_Hash); //--- WebRequest char DATA[]; string URL = NULL; URL = telegram_url+"/bot"+bot_token+"/sendPhoto"; //--- add chart_id //Append a carriage return and newline character sequence to the DATA array. //In the context of HTTP, \r\n is used to denote the end of a line //and is often required to separate different parts of an HTTP request. ArrayAdd(DATA,"\r\n"); //Append a boundary marker to the DATA array. //Typically, the boundary marker is composed of two hyphens (--) //followed by a unique hash string and then a newline sequence. //In multipart/form-data requests, boundaries are used to separate //different pieces of data. ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n"); //Add a Content-Disposition header for a form-data part named chat_id. //The Content-Disposition header is used to indicate that the following data //is a form field with the name chat_id. ArrayAdd(DATA,"Content-Disposition: form-data; name=\"chat_id\"\r\n"); //Again, append a newline sequence to the DATA array to end the header section //before the value of the chat_id is added. ArrayAdd(DATA,"\r\n"); //Append the actual chat ID value to the DATA array. ArrayAdd(DATA,chat_id); //Finally, Append another newline sequence to the DATA array to signify //the end of the chat_id form-data part. ArrayAdd(DATA,"\r\n"); // EXAMPLE OF USING CONVERSIONS //uchar array[] = { 72, 101, 108, 108, 111, 0 }; // "Hello" in ASCII //string output = CharArrayToString(array,0,WHOLE_ARRAY,CP_ACP); //Print("EXAMPLE OUTPUT OF CONVERSION = ",output); // Hello Print("CHAT ID DATA:"); ArrayPrint(DATA); string chatID_Data = CharArrayToString(DATA,0,WHOLE_ARRAY,CP_UTF8); Print("SIMPLE CHAT ID DATA IS AS FOLLOWS:",chatID_Data); //--- Caption string CAPTION_STRING = NULL; CAPTION_STRING = caption; if(StringLen(CAPTION_STRING) > 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"); } //--- 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); string final_Simple_Data = CharArrayToString(DATA,0,WHOLE_ARRAY,CP_ACP); Print("FINAL FULL SIMPLE PHOTO DATA BEING SENT:",final_Simple_Data); string HEADERS = NULL; HEADERS = "Content-Type: multipart/form-data; boundary="+HexaDecimal_Hash+"\r\n"; Print("SCREENSHOT SENDING HAS BEEN INITIATED SUCCESSFULLY."); //char data[]; // Array to hold data to be sent in the web request (empty in this case) char res[]; // Array to hold the response data from the web request string resHeaders; // String to hold the response headers from the web request //string msg = "EA INITIALIZED ON CHART " + _Symbol; // Message to send, including the chart symbol //const string url = TG_API_URL + "/bot" + botTkn + "/sendmessage?chat_id=" + chatID + // "&text=" + msg; // Send the web request to the Telegram API int send_res = WebRequest("POST",URL,HEADERS,10000, DATA, res, resHeaders); // Check the response status of the web request if (send_res == 200) { // If the response status is 200 (OK), print a success message Print("TELEGRAM SCREENSHOT FILE SENT SUCCESSFULLY"); return (SUCCEEDED_CODE); } else if (send_res == -1) { // If the response status is -1 (error), check the specific error code if (GetLastError() == 4014) { // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal Print("PLEASE ADD THE ", telegram_url, " TO THE TERMINAL"); } // Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM SCREENSHOT FILE"); return (FAILED_CODE); } else if (send_res != 200) { // If the response status is not 200 or -1, print the unexpected response code and error code Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError()); return (FAILED_CODE); } return (SUCCEEDED_CODE); }
我们现在有发送截图的函数,但没有获取截图文件的函数。首先,我们将创建一个函数来获取程序所附图表的图像文件,因为它不需要太多参数。
//+------------------------------------------------------------------+ //| FUNCTION TO GET SCREENSHOT OF CURRENT CHART | //+------------------------------------------------------------------+ int getScreenshot_of_Current_Chart(string screenshot_name){ //--- First 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(0); } ChartScreenShot(0,screenshot_name,1366,768,ALIGN_RIGHT); // Wait for 30 secs to save screenshot if not yet saved int wait_loops = 60; while(!FileIsExist(screenshot_name) && --wait_loops > 0){ Sleep(500); } if(!FileIsExist(screenshot_name)){ Print("THE SPECIFIED SCREENSHOT DOES NOT EXIST (WAS NOT SAVED). REVERTING NOW!"); return (FAILED_CODE); } else if(FileIsExist(screenshot_name)){ Print("THE CHART SCREENSHOT WAS SAVED SUCCESSFULLY TO THE DATA-BASE."); return (SUCCEEDED_CODE); } return (SUCCEEDED_CODE); }
在这里,我们声明一个整数函数 “getScreenshot_of_Current_Chart”,来执行在 MetaTrader 5 中捕获和保存当前图表的屏幕截图的过程。该函数接受一个参数 “screenshot_name”,其中包含屏幕截图将保存到的文件的所需名称。我们通过检查是否已经存在名为 “screenshot_name” 的文件来开始该函数。如果是这样,我们会删除预先存在的文件。这一步至关重要,因为如果我们不删除预先存在的文件,我们将不可避免地出现覆盖问题 — 在这种情况下,保存的屏幕截图最终将与最近删除的文件同名。
在截屏之前,我们还调用函数 ChartRedraw 来刷新图表显示。然后,为了获取图表当前状态下的屏幕截图,我们调用函数 ChartScreenShot ,告诉它我们想要给文件的名称、所需的尺寸以及我们想要的对齐方式。在此之后,我们必须使用 while 循环来检查文件是否存在。我们等待了最多 30 秒让文件出现,然后再继续下一步,我们绝不想减慢下一步中屏幕截图出现的进程。
如果文件在此间隔后仍然不存在,我们会生成一条错误消息,通知屏幕截图未保存,并返回明确的 “FAILED_CODE”。但是,如果找到了文件,我们会发出一条成功消息并返回清晰的 “SUCCEEDED_CODE”。本质上,我们允许我们的操作出现两种可能的结果,并明确地标记它们。打开自定义图表并拍摄快照的函数继承了相同的逻辑。
//+------------------------------------------------------------------+ //| FUNCTION TO GET SCREENSHOT OF A NEW CHART | //+------------------------------------------------------------------+ int getScreenshot_of_New_Chart(string screenshot_name,string symbol_name, ENUM_TIMEFRAMES period_name){ //--- First 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(0); } long chart_id=ChartOpen(symbol_name,period_name); ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true); // update chart int wait=60; while(--wait>0){//decrease the value of wait by 1 before loop condition check if(SeriesInfoInteger(symbol_name,period_name,SERIES_SYNCHRONIZED)){ break; // 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); ChartScreenShot(chart_id,screenshot_name,1366,768,ALIGN_RIGHT); Print("OPENED CHART PAUSED FOR 10 SECONDS TO TAKE SCREENSHOT."); Sleep(10000); // sleep for 10 secs to see the opened chart ChartClose(chart_id); // Wait for 30 secs to save screenshot if not yet saved int wait_loops = 60; while(!FileIsExist(screenshot_name) && --wait_loops > 0){ Sleep(500); } if(!FileIsExist(screenshot_name)){ Print("THE SPECIFIED SCREENSHOT DOES NOT EXIST (WAS NOT SAVED). REVERTING NOW!"); return (FAILED_CODE); } else if(FileIsExist(screenshot_name)){ Print("THE CHART SCREENSHOT WAS SAVED SUCCESSFULLY TO THE DATA-BASE."); return (SUCCEEDED_CODE); } return (SUCCEEDED_CODE); }
在这里,我们声明一个整数函数,它接受 3 个参数:图像名称、交易品种名称和要打开的图表的周期。到目前为止,我们已经拥有了获取屏幕截图并将其发送到 Telegram 所需的函数。让我们继续获取并发送当前图表的屏幕截图,看看我们得到了什么。为了实现这一点,将应用以下代码片段。
getScreenshot_of_Current_Chart(SCREENSHOT_FILE_NAME);
sendScreenShot(SCREENSHOT_FILE_NAME,TG_API_URL,botTkn,chatID,NULL);
在这里,我们只调用分别负责获取和发送保存的屏幕截图并传递所需输入参数的两个函数。您现在可以看到逻辑是多么小,只需两行代码就足以创造奇迹。编译后,我们得到以下输出。
我们可以看到,我们可以在 Telegram 聊天中收到当前图表的屏幕截图文件。到目前为止,几乎涵盖了所有必要的内容,因此我们可以看到如何使用返回代码来应对函数的失败或成功。对于这种做法,我们将使用新的图表逻辑。
int get_screenshot_new_chart_result = getScreenshot_of_New_Chart(SCREENSHOT_FILE_NAME,_Symbol,_Period); if (get_screenshot_new_chart_result == FAILED_CODE){ string result_msg = "NEW CHART SCREENSHOT COULDN'T BE SAVED. REVERT NOW."; Print(result_msg); sendSimpleMessage(result_msg,TG_API_URL,botTkn,chatID,10000); return (INIT_FAILED); } else if (get_screenshot_new_chart_result == SUCCEEDED_CODE){ string result_msg = "SUCCESS. NEW CHART SCREENSHOT WAS SAVED. CONTINUE NOW."; Print(result_msg); sendSimpleMessage(result_msg,TG_API_URL,botTkn,chatID,10000); string sending_msg = "\x2705\SCREENSHOT SENDING HAS BEEN INITIATED SUCCESSFULLY."; sending_msg += "\n\x270C\YOU SHOULD RECEIVE THE IMAGE FILE WITHIN 10 SECONDS"; string encoded_sending_msg = UrlEncode(sending_msg); Print(encoded_sending_msg); sendEncodedMessage(encoded_sending_msg,TG_API_URL,botTkn,chatID,10000); }
在这里,我们管理来自函数 “getScreenshot_of_New_Chart” 的输出。该函数执行对新图表进行截图并保存的工作。我们使用明确的参数调用该函数:我们想要的屏幕截图文件的名称、图表的当前交易品种和时段。该函数的结果存储在我们称之为 “get_screenshot_new_chart_result” 的变量中。如果这个结果是成功的,我们继续进行我们计划的下一部分。如果失败了,我们会以合理的方式处理失败。
当我们收到 “FAILED_CODE” 的结果时,表示保存屏幕截图失败。在这种情况下,我们会生成一条错误消息,让用户清楚地知道屏幕截图保存过程中出现了问题。该消息被打印到终端并使用 “sendSimpleMessage” 函数发送到我们的 Telegram 聊天中。然后我们返回 “INIT_FAILED” 作为返回代码,让用户知道操作未成功并且不应尝试下一个操作。这将会终止初始化过程。
另一方面,如果结果是 “SUCCEEDED_CODE”,则表示屏幕截图已成功保存。我们准备并打印一条消息,说明终端在截图后发送了成功命令。然后,我们继续利用 “sendSimpleMessage” 函数通知用户他们的屏幕截图已保存,并且他们应该很快就会收到该文件。向用户发送消息的过程清晰、简洁且执行得当。向用户发送屏幕截图的命令已成功,用户应该会在大约 10 秒内收到该文件。通过日志,我们得到以下记录:
在 Telegram 聊天中,我们收到以下输出:
您现在可以看到,向 Telegram 发送多个消息实例以轻松聊天很容易。我们现在要做的就是调用截图发送逻辑的函数来传达图像文件。这是通过以下逻辑实现的:
sendScreenShot(SCREENSHOT_FILE_NAME,TG_API_URL,botTkn,chatID,NULL);
运行代码后,我们得到以下结果:
成功了,您可能会注意到,我们收到的图像没有标题。这是因为我们选择将标题字段设为 NULL ,这意味着不会考虑任何标题。要包含标题,我们只需定义标题字段并将其传递给函数。我们的默认标题将用于下图:
//--- Caption string CAPTION = NULL; CAPTION = "Screenshot of Symbol: "+Symbol()+ " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+ ") @ Time: "+TimeToString(TimeCurrent()); sendScreenShot(SCREENSHOT_FILE_NAME,TG_API_URL,botTkn,chatID,CAPTION);
编译后,我们会收到一个新打开的自定义图表的屏幕截图,其中包含与所述相同的默认参数,但更重要的是,还有一个标题,说明了屏幕截图的交易品种、时段以及捕获并传输到 Telegram 的时间。
成功了,我们现在有一个功能齐全的程序,利用函数与 Telegram 聊天进行通信。我们现在只需要对我们精心设计的模块化函数进行一些测试和实现,这些函数可以将基于移动平均线的交易信号从 MetaTrader 5 平台发送到 Telegram 聊天。这将在下一节中明确介绍。
测试和实现模块化函数
在本节中,我们将把重点从构建单个函数转移到在真实交易情况下应用这些函数,在这种情况下,信号确认会引发特定的反应。我们现在的目的是验证我们模块化的函数(例如用于捕获屏幕截图或发送消息的函数)是否可以在我们正在构建的更大的框架中协同工作。通过将我们的 EA 交易的逻辑放入可重复使用的函数中,我们可以更好地实现发送图表截图或更新帐户状态的目的,同时保持 EA 的逻辑 — EA 的工作原理是在满足特定条件时触发某些函数。
本主题将展示我们如何在确认交易信号时调用这些模块化函数,以确保所有组件在现实世界中高效工作。我们将严格检查这些函数的可靠性,通过反复执行的试验板来运行它们,看看它们是否能够经受住错误管理的严格考验,同时仍然完成向 Telegram 准确及时地发送信号确认消息的主要任务。在这样做时,我们不仅会验证代码的功能,而且还会将其作为创建几乎完全稳健且能够优雅地处理错误情况的交易信号管理系统的一步。所以现在我们只需要将初始化代码片段转移到信号生成部分。对于买入信号,其代码片段如下:
// BUY POSITION OPENED. GET READY TO SEND MESSAGE TO TELEGRAM Print("BUY POSITION OPENED. SEND MESSAGE TO TELEGRAM NOW."); //char data[]; // Array to hold data to be sent in the web request (empty in this case) //char res[]; // Array to hold the response data from the web request //string resHeaders; // String to hold the response headers from the web request ushort MONEYBAG = 0xF4B0; string MONEYBAG_Emoji_code = ShortToString(MONEYBAG); string msg = "\xF680 Opened Buy Position." +"\n====================" +"\n"+MONEYBAG_Emoji_code+"Price = "+DoubleToString(openPrice,_Digits) +"\n\xF412\Time = "+TimeToString(iTime(_Symbol,_Period,0),TIME_SECONDS) +"\n\xF551\Time Current = "+TimeToString(TimeCurrent(),TIME_SECONDS) +"\n\xF525 Lotsize = "+DoubleToString(lotSize,2) +"\n\x274E\Stop loss = "+DoubleToString(stopLoss,_Digits) +"\n\x2705\Take Profit = "+DoubleToString(takeProfit,_Digits) +"\n_________________________" +"\n\xF5FD\Time Local = "+TimeToString(TimeLocal(),TIME_DATE) +" @ "+TimeToString(TimeLocal(),TIME_SECONDS) ; string encloded_msg = UrlEncode(msg); msg = encloded_msg; sendEncodedMessage(msg,TG_API_URL,botTkn,chatID,10000); int get_screenshot_current_chart_result = getScreenshot_of_Current_Chart(SCREENSHOT_FILE_NAME); if (get_screenshot_current_chart_result == FAILED_CODE){ string result_msg = "CURRENT CHART SCREENSHOT COULDN'T BE SAVED. REVERT NOW."; Print(result_msg); sendSimpleMessage(result_msg,TG_API_URL,botTkn,chatID,10000); return; } else if (get_screenshot_current_chart_result == SUCCEEDED_CODE){ string result_msg = "SUCCESS. CURRENT CHART SCREENSHOT WAS SAVED. CONTINUE NOW."; Print(result_msg); sendSimpleMessage(result_msg,TG_API_URL,botTkn,chatID,10000); string sending_msg = "\x2705\SCREENSHOT SENDING HAS BEEN INITIATED SUCCESSFULLY."; sending_msg += "\n\x270C\YOU SHOULD RECEIVE THE IMAGE FILE WITHIN 10 SECONDS"; string encoded_sending_msg = UrlEncode(sending_msg); Print(encoded_sending_msg); sendEncodedMessage(encoded_sending_msg,TG_API_URL,botTkn,chatID,10000); } //--- Caption string CAPTION = NULL; CAPTION = "Screenshot of Symbol: "+Symbol()+ " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+ ") @ Time: "+TimeToString(TimeCurrent()); sendScreenShot(SCREENSHOT_FILE_NAME,TG_API_URL,botTkn,chatID,CAPTION);
在这里,我们只专注于通知生成的信号,并发送当前图表的屏幕截图,显示交易水平。对于卖出信号确认,类似的逻辑适用如下:
// SELL POSITION OPENED. GET READY TO SEND MESSAGE TO TELEGRAM Print("SELL POSITION OPENED. SEND MESSAGE TO TELEGRAM NOW."); //char data[]; // Array to hold data to be sent in the web request (empty in this case) //char res[]; // Array to hold the response data from the web request //string resHeaders; // String to hold the response headers from the web request ushort MONEYBAG = 0xF4B0; string MONEYBAG_Emoji_code = ShortToString(MONEYBAG); string msg = "\xF680 Opened Sell Position." +"\n====================" +"\n"+MONEYBAG_Emoji_code+"Price = "+DoubleToString(openPrice,_Digits) +"\n\xF412\Time = "+TimeToString(iTime(_Symbol,_Period,0),TIME_SECONDS) +"\n\xF551\Time Current = "+TimeToString(TimeCurrent(),TIME_SECONDS) +"\n\xF525 Lotsize = "+DoubleToString(lotSize,2) +"\n\x274E\Stop loss = "+DoubleToString(stopLoss,_Digits) +"\n\x2705\Take Profit = "+DoubleToString(takeProfit,_Digits) +"\n_________________________" +"\n\xF5FD\Time Local = "+TimeToString(TimeLocal(),TIME_DATE) +" @ "+TimeToString(TimeLocal(),TIME_SECONDS) ; string encloded_msg = UrlEncode(msg); msg = encloded_msg; sendEncodedMessage(msg,TG_API_URL,botTkn,chatID,10000); int get_screenshot_current_chart_result = getScreenshot_of_Current_Chart(SCREENSHOT_FILE_NAME); if (get_screenshot_current_chart_result == FAILED_CODE){ string result_msg = "CURRENT CHART SCREENSHOT COULDN'T BE SAVED. REVERT NOW."; Print(result_msg); sendSimpleMessage(result_msg,TG_API_URL,botTkn,chatID,10000); return; } else if (get_screenshot_current_chart_result == SUCCEEDED_CODE){ string result_msg = "SUCCESS. CURRENT CHART SCREENSHOT WAS SAVED. CONTINUE NOW."; Print(result_msg); sendSimpleMessage(result_msg,TG_API_URL,botTkn,chatID,10000); string sending_msg = "\x2705\SCREENSHOT SENDING HAS BEEN INITIATED SUCCESSFULLY."; sending_msg += "\n\x270C\YOU SHOULD RECEIVE THE IMAGE FILE WITHIN 10 SECONDS"; string encoded_sending_msg = UrlEncode(sending_msg); Print(encoded_sending_msg); sendEncodedMessage(encoded_sending_msg,TG_API_URL,botTkn,chatID,10000); } //--- Caption string CAPTION = NULL; CAPTION = "Screenshot of Symbol: "+Symbol()+ " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+ ") @ Time: "+TimeToString(TimeCurrent()); sendScreenShot(SCREENSHOT_FILE_NAME,TG_API_URL,botTkn,chatID,CAPTION);
为了确认是否产生了信号,让我们切换到 1 分钟的时段并等待信号响应。我们得到的第一个信号是卖出设置。它在交易终端中生成如下图所示:
MetaTrader 5 卖出信号日志:
Telegram 聊天栏中收到的卖出消息:
从上面提供的图像中,我们可以看到,卖出信号设置在交易终端中得到确认,重要消息记录到日志中,相应的持仓数据和屏幕截图已发送到 Telegram 聊天组。我们现在预计,当出现看涨交叉时,我们会关闭现有的卖出仓位,开设买入仓位,并将信息发送给 Telegram 群组。交易终端确认设置如下:
MetaTrader 5 买入信号日志:
Telegram聊天栏中收到的买入消息:
为了更有效地形象化地展示这一里程碑,这里有一个详细的视频,它基于 EA 交易的性能,从其编译开始,到其初始化和根据生成的信号开启交易,以及元数据如何从 MQL5 传输到 Telegram。
至此,我们可以看出函数代码的模块化是成功的。MQL5 和 Telegram 的集成现在可以顺利运行。通过使用模块化函数(我们用它来确认信号),我们成功地实现了消息传递和屏幕截图的自动化,这样每个关键事件或更新在发生时都会立即被 Dana White 推送到 Telegram 聊天中。我们已经对其进行了充分的测试,确信它有效。就充当 MQL5 和 Telegram 之间的桥梁而言,此实现既可靠又灵活 — 这是一种良好的模块化设计,我们可以在此基础上构建更复杂的集成。
结论
本文概述了 MetaQuotes Language 5 (MQL5) 与 Telegram 的集成,重点是创建用于发送消息和交易图表截图的模块化函数。模块化设计提高了 EA 交易的效率、可扩展性和可维护性,而没有不必要的复杂性。
下一篇文章将探讨双向通信系统,允许 Telegram 向 MetaTrader 5 发送命令并控制 EA 的操作。这种集成将实现更高级的交互,例如直接从 Telegram 请求实时交易数据或屏幕截图,从而突破 MQL5-Telegram 集成的界限。请继续关注进一步的发展。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/15706


