English Русский Español Deutsch 日本語 Português
preview
创建 MQL5-Telegram 集成 EA 交易 (第 3 部分):将带有标题的图表截图从 MQL5 发送到 Telegram

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

MetaTrader 5交易系统 |
453 18
Allan Munene Mutiiria
Allan Munene Mutiiria

概述

在上一篇文章,即我们系列的第二部分中,我们仔细研究了将 MetaQuotes Language 5 (MQL5) 与 Telegram 结合以生成和传递信号的过程。结果很明显;它允许我们向 Telegram 发送交易信号,当然,为了使整个事情有价值,有必要使用交易信号。那么,为什么我们必须在集成过程中采取下一步行动呢?在本系列的第三部分中,我们所做的很大程度上是“下一步”,旨在说明在发送交易信号方面将 MQL5 与 Telegram 合并的潜力。然而,我们发送的不是交易信号的文本部分,而是交易信号图表的屏幕截图。有时,更好的做法是不仅接收可以采取行动的信号,而且以可视化的方式在图表上看到信号设置,比如价格行为设置,在这种情况下,就是图表截图。

因此,在本文中,我们将重点介绍将图像数据转换为兼容格式以嵌入超文本传输协议安全(HTTPS) 请求的具体细节。如果我们要在 Telegram 机器人中包含图像,就必须进行这种转换。我们将详细研究该过程,从 MQL5 中的图表,通过 MetaTrader 5 交易终端,到精心安排的机器人消息,其中包含标题和图表图像,作为交易通知中令人印象深刻的部分。本文分为四个部分。

首先,我们将简要介绍图像编码和 HTTPS 传输的工作原理。在第一部分中,我们将解释所涉及的基本概念和用于完成此任务的技术。接下来,我们将深入研究 MQL5 中的实现过程,MQL5 是用于为 MetaTrader 5 平台编写交易程序的编程语言。我们将详细介绍如何使用图像编码和传输方法。之后,我们将测试已实现的程序,以验证其是否正常工作。然后,我们将对文章进行总结,再次强调要点,并描述在交易系统中进行这项工作的好处。以下是我们创建 EA 交易时将遵循的主题:

  1. HTTPS 图像编码与传输概述
  2. MQL5 中的实现
  3. 测试集成
  4. 结论

最后,我们将制作一个 EA 交易,将图表截图和图像以及交易信息(例如已生成的信号和从交易终端下达的订单)发送到指定的 Telegram 聊天。让我们开始吧。


HTTPS 图像编码与传输概述

通过互联网发送图像,特别是与应用程序编程接口(API)或消息传递平台集成,需要首先对图像数据进行编码,然后在没有不当延迟或影响效果或安全的情况下发送。直接图像文件发送的位和字节数太多,无法有效地通过让互联网用户访问特定网站、平台或服务的命令顺利工作。对于像 Telegram 这样的 API,它充当互联网用户和特定服务(如用于各种任务的基于 Web 的接口)之间的中介,发送图像需要先对图像文件进行编码,然后作为从用户到服务或反之亦然的命令有效负载的一部分发送,这尤其可以通过 HTTP 或 HTTPS 等协议实现。

转换图像以便发送的最常见方法是将图像转换为 Base64 编码的字符串。Base64 编码采用图像的二进制数据并创建文本表示。这是通过使用特定集合中的字符来完成的,这些字符使所谓的“编码图像”在通过文本协议发送时能够正常工作。为了创建 Base64 编码的图像,它的原始数据(在任何“读取”操作之前的实际文件)都是逐字节读取的。然后通过 Base64 字符表示读取的字节。一旦完成,文件就可以通过文本协议发送。

图像数据编码后,通过 HTTPS 发送,这是一种安全的 HTTP 形式。与以纯文本发送数据的 HTTP 不同,HTTPS 使用安全套接字层 (SSL)/传输层安全性 (TLS) 加密协议来确保传递到服务器和从服务器传出的数据保持私密和安全。HTTPS 对于保护交易信号和其他金融相关通信的重要性怎么强调都不为过。例如,一个获取交易信号的不道德的第三方可以利用这些信息进行交易并操纵市场,从而损害交易信号的无辜受害者,并使拦截信号的一方受益。过程可视化如下:

图像编码过程

总之,图片编码和发送方法将图片文件转换为适合网络通信的基于文本的格式。它还确保了通过 HTTPS 的安全传输。如果想要将图片数据集成到应用程序中,了解这两个概念至关重要。一个明显的例子是通过 Telegram 自动发送通知的交易系统 — 该平台在快速可靠地传递消息方面表现出色。


MQL5 中的实现

MQL5 中图像中继的实现将从在 MQL5 EA 交易中捕获当前交易图表的屏幕截图的过程开始。我们将对截图进行编码并通过 Telegram 发送。我们主要在 OnInit 事件处理程序中实现此功能,该处理程序在 EA 初始化时执行。正如我们所说, OnInit 事件处理程序的目的是准备和设置 EA 的必要组件,确保在执行交易操作的主要逻辑之前一切都配置正确。首先,我们定义屏幕截图图像文件名。

   //--- Get ready to take a chart screenshot of the current chart
   
   #define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.jpg"

在这里,我们采取工作流程中的第一步,即为屏幕截图文件的名称建立一个常量。我们通过 #define 指令实现这一点,它允许我们分配一个可以在整个代码中引用的常量值。在这里,我们创建一个名为“SCREENSHOT_FILE_NAME”的常量,它存储值“Our Chart ScreenShot.jpg”。我们这样做是有充分理由的:如果我们需要文件名来加载或保存某些内容,它可以使用这个常量。如果我们需要更改文件名或格式,我们只需要在这一个地方进行更改。您可能意识到我们选择的图像类型是 JPEG(JPG)。您可以选择任何您认为合适的,例如 PNG。但是,您应该记住,图像格式存在显著差异。例如,JPG 使用有损压缩算法,这意味着一些图像数据丢失,但图像大小会减小。您可以使用的格式示例如下:

图像格式

我们将屏幕截图功能集成到 OnInit 处理函数中。这确保系统在 EA 交易启动后立即捕获并保存图表的状态。我们声明了一个常量“SCREENSHOT_FILE_NAME”,它作为图表图像文件实际名称的替代。使用这个替代,我们(主要)避免了试图在大约相同的时间保存两个同名文件的陷阱。通过采取这一步骤,我们确保图表图像文件具有与我们此时主动编码和传输图像时所需的基本结构相同的基本结构。

这一步至关重要,因为它设置了文件命名约定,并保证我们能够在 EA 首次初始化时获取图表的图像。从现在开始,我们可以专注于从图表中获取数据,将其编码为适合人眼的形式,并将其发送到我们选择的 Telegram 频道。

接下来,为了防止我们试图覆盖并创建具有相似名称的文件,我们需要删除现有的文件(如果可用),并创建一个新的文件。这是通过以下代码片段实现的。

   //--- First 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(0);
   }

在这里,我们首先要确保在捕获新的截图文件之前,不存在任何截图文件的实例。这很重要,因为我们希望避免图表的当前状态与之前保存的屏幕截图之间出现任何混淆。为了实现这一点,我们检查系统中是否存在具有“SCREENSHOT_FILE_NAME”常量中所存储的名称的文件。我们使用 FileIsExist 函数执行此操作,该函数检查指定的目录,如果文件存在则返回 true。如果该文件确实存在,我们使用 FileDelete 函数将其删除。通过确保指定的目录中没有我们的旧屏幕截图,我们为稍后将创建的新目录腾出了空间。

删除之后,我们使用 Print 函数向终端发送一条消息,表明图表的屏幕截图已被找到并成功删除。这一点反馈对于调试来说非常方便,因为它可以确认系统正在正确处理先前屏幕截图的清除。毕竟,我们不想养成“删除”不存在的文件的习惯。我们还立即重新绘制图表(我们将此函数称为 ChartRedraw ),以确保我们使用的图表是最新的可视状态。经过这次清理,我们现在可以继续截图了。

   ChartScreenShot(0,SCREENSHOT_FILE_NAME,1366,768,ALIGN_RIGHT);

在这里,我们使用 ChartScreenShot 函数捕获图表截图,该函数对指定的图表进行快照并将其保存为图像文件。在我们的例子中,我们将参数“0”、“SCREENSHOT_FILE_NAME”、“1366”、“768”和“ALIGN_RIGHT”传递给函数来控制如何截取和保存屏幕截图。

  • 第一个参数“0”指定我们要从中截取屏幕截图的图表 ID。值 0 表示当前活动图表。如果我们想捕获不同的图表,我们需要传递特定的图表 ID。
  • 第二个参数,“SCREENSHOT_FILE_NAME”是保存屏幕截图的文件的名称。在我们的例子中,这是常量“Our Chart ScreenShot.jpg”。该文件将会被创建在终端的“Files”目录中,如果目录尚不存在,则会在截屏后生成。
  • 第三和第四个参数 1366 和 768 是以像素为单位定义屏幕截图的大小。这里的 1366 代表截图的宽度,768 代表高度。这些值可以根据用户的偏好或捕获的屏幕大小进行调整。
  • 最后一个参数 ALIGN_RIGHT 指定了如何在图表窗口中对齐屏幕截图。通过使用 ALIGN_RIGHT,我们将屏幕截图对齐到图表的右侧。根据所需的结果,可以使用其他对齐选项,如 ALIGN_LEFT 或 ALIGN_CENTER。

由于某些原因,无论多么微不足道,截图都可能延迟保存。因此,为了确保万无一失,我们需要启动一个迭代,等待几秒钟才能保存屏幕截图。

   // Wait for 30 secs to save screenshot if not yet saved
   int wait_loops = 60;
   while(!FileIsExist(SCREENSHOT_FILE_NAME) && --wait_loops > 0){
      Sleep(500);
   }

这里我们实现了一个 while 循环,等待截图文件成功保存,确保它已经被保存到正确的位置并且具有正确的名称,之后我们再继续。等待时间本身足够长,在正常情况下,屏幕截图文件应该很容易在文件系统上找到(如果它确实是在测试期间保存的)。我们从初始化为 60 的整数型变量“wait_loops”开始。如果循环的每次迭代都无法找到文件,则会引入半秒(500 毫秒 (ms))的等待时间 - 如果找不到文件,则从循环开始到结束总共需要 30 秒(60 次迭代 * 500 毫秒)。

在每次迭代中,我们还会减少“wait_loops”计数器,以防止如果文件未在指定时间内创建则循环无限期运行。此外,我们使用 Sleep 函数在每次检查之间创建 500 毫秒的延迟。这可以防止我们过于频繁地检查,并用太多的文件存在请求淹没我们的系统。

最后,我们需要在之后检查文件是否存在,如果它不存在,那么继续下去就没有意义了,因为我们的整个算法和逻辑都依赖于图像文件。如果确实存在,我们就可以继续下面的步骤。

   if(!FileIsExist(SCREENSHOT_FILE_NAME)){
      Print("THE SPECIFIED SCREENSHOT DOES NOT EXIST (WAS NOT SAVED). REVERTING NOW!");
      return (INIT_FAILED);
   }

在这里,我们定义了一种处理错误的机制,以检查屏幕截图文件是否已成功保存。等待文件创建一段时间后,我们使用函数 FileIsExist 检查文件是否存在。如果检查返回 false,表示文件不存在,我们将发出以下消息:“指定的屏幕截图不存在(未保存)。立即恢复!”此消息表示我们无法保存屏幕截图文件。发出此错误消息后,程序无法继续,因为我们完全需要该映像文件作为程序逻辑的基础。因此,我们以返回值“INIT_FAILED”退出,表示初始化无法成功完成。如果屏幕截图已保存,我们也会继续通知实例。

   else if(FileIsExist(SCREENSHOT_FILE_NAME)){
      Print("THE CHART SCREENSHOT WAS SAVED SUCCESSFULLY TO THE DATA-BASE.");
   }

运行代码后,结果如下:

保存屏幕截图

在这里,您可以看到我们能够成功删除图像文件的初始存在并保存另一个。要访问计算机上的图像,请右键单击文件名,选择“打开包含文件夹”,然后在“文件”文件夹中找到该文件。

文件选项 1

或者,您可以通过打开导航器、展开它、右键单击文件选项卡并选择“打开文件夹”来直接访问图像文件。

文件选项 2

这将打开注册图像文件的文件夹。

图像文件目录

在这里,您可以看到已注册了准确的图像名称。最后,让我们检查文件大小和类型,看看是否考虑了指定的正确信息。

文件类型和大小

我们可以看到文件类型为 JPG,并且截图的宽度和高度分别为 1366 x 768 像素,正如指定的那样。例如,如果想要不同的文件类型,比如 PNG,则只需要按如下所示更改文件类型:

   #define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.png"

当我们编译并运行此代码片段时,我们会以 GIF 图像格式创建另一个 PNG 类型的图像,如下所示:

PNG 和 JPG GIF

到目前为止,很明显我们成功地拍摄了图表快照并将其直接保存到文件中。因此,我们可以继续对图像文件进行编码,以便通过 HTTPS 传输。首先,我们需要打开图像文件并读取它。

   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!");
      return(INIT_FAILED);
   }

在上面的代码片段中,我们专注于 MQL5 中的文件操作,以操作我们之前保存的屏幕截图文件。我们声明一个名为“screenshot_Handle”的整型变量,并用值“INVALID_HANDLE”初始化它。“screenshot_Handle”作为对文件的引用,“INVALID_HANDLE”值作为占位符,让我们知道尚未打开有效文件。保存此值可确保我们可以通过文件句柄引用文件,并在出现问题时处理文件操作中出现的任何错误。

接下来,我们尝试使用 FileOpen 函数打开我们保存的截图。我们赋予它截图的名称,其中包含截图文件的路径。我们还赋予它两个标志:FILE_READFILE_BIN 。第一个标志告诉系统我们想要读取文件。第二个标志可能是这两个标志中更重要的一个,它告诉系统文件包含二进制数据(不应将其与屏幕截图中的一系列 1 和 0 混淆)。由于屏幕截图是一张图像,并且该图像具有某种标准格式(将该格式转换为真正“标准”、“简单”或“自然”的格式,图像就会变成一系列 1 和 0 — 没有格式,没有结构,只是纯粹的数学 — 一系列不同的 1 和 0,图像看起来完全不同,尽管这不是我们在这里关心的),我们希望找到一系列与图像相对应的字节。

当我们尝试打开文件后,“FileOpen” 函数要么返回有效句柄,要么返回 “INVALID_HANDLE”。我们使用 if 语句检查句柄的有效性。无效句柄意味着文件未成功打开。我们打印一条错误消息,指出屏幕截图句柄无效。所以,要么屏幕截图没有保存,要么无法访问,这向我们发出了程序遇到了障碍的信号。我们不再进一步推进,而是返回 “INIT_FAILED”,因为如果我们无法读取图像文件,继续下去就没有意义了。如果句柄 ID 确实有效,我们会通知用户成功。

   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.");
   }

在这里,我们添加了另一个验证步骤,以确保屏幕截图文件正确打开。检查“screenshot_Handle”是否有效(不等于“INVALID_HANDLE”)后,我们打印几条消息来表明文件已正确打开。这只是确认“screenshot_Handle”有效且我们已准备好继续前进的另一种方式。我们对第一条消息使用 Print 函数,它传达的信息与第二条消息相同:屏幕截图已成功保存并打开以供阅读。这两个语句的目的都是确认工作流程中当前步骤已成功完成。

然后,我们显示句柄 ID,该 ID 唯一标识文件,并允许对文件执行后续操作(读取、写入和编码)。句柄 ID 对于调试也很有用。它确认系统已获得并分配资源来管理此特定文件。我们以一个打印语句结束,该语句通知我们系统现在已准备好执行下一个操作,即对屏幕截图进行编码,以便使用 HTTPS 协议在网络上传输。

接下来,我们可以检查并验证句柄是否确实被记录和存储,以及它是否具有有效内容。

   int screenshot_Handle_Size = (int)FileSize(screenshot_Handle);
   if (screenshot_Handle_Size > 0){
      Print("CHART SCREENSHOT FILE SIZE = ",screenshot_Handle_Size);
   }

在这里,我们获取并验证之前用句柄打开的屏幕截图文件的大小。我们在屏幕截图句柄上调用 FileSize 函数,该函数以字节为单位返回文件的大小。然后我们将这个值分配给名为“screenshot_Handle_Size”的整数型变量。如果大小大于零,这表示文件包含某种数据,我们会将文件大小打印到日志中。这一步很有用,因为它让我们知道,在我们通过 HTTP 编码和发送文件之前,屏幕截图已正确保存并包含有效内容。

如果句柄确实有有效内容,则意味着我们打开了正确的文件,我们可以准备将屏幕截图文件的二进制数据读取到数组中进行解码。

   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));
   }

我们首先声明一个名为“photoArr_Data”的 uchar 数组,用于保存二进制数据。然后,我们通过调用 ArrayResize 函数来调整此数组的大小,以匹配屏幕截图文件的大小。接下来,我们使用 FileReadArray 函数将截图文件的内容读入“photoArr_Data”数组,从索引 0 开始,到文件末尾(screenshot_Handle_Size)。然后,我们在加载“photoArr_Data”数组后检查其大小,如果它大于 0,即它不为空,我们就记录它的大小。通常,这是处理屏幕截图文件的读取和处理的代码部分,以便它可用于编码和传输。

读取文件内容并存储后,我们现在需要关闭图像文件。这是通过其句柄完成的。

   FileClose(screenshot_Handle);

在这里,我们在成功将屏幕截图文件的数据读入存储数组后,最终关闭了该文件。我们调用函数 FileClose 来释放与截图文件关联的句柄。这释放了打开文件时分配的系统资源。在我们尝试对文件执行任何其他操作(如以任何方式访问、读取或写入)之前,确保文件已关闭至关重要。该函数表示我们已经完成了对文件的所有访问操作,我们现在正进入流程的下一阶段:对屏幕截图数据进行编码并准备传输。当运行该程序时,我们将得到以下结果:

读取文件

您可以看到,我们在存储数组中正确读取并存储了图像二进制数据。为了查看数据,我们可以使用 ArrayPrint 函数将其打印到日志中,如下所示:

   ArrayPrint(photoArr_Data);

打印后,我们得到以下数据:

图像二进制数据1

很明显,我们读取、复制和存储了完整的数据,即多达320894个字节。

接下来,我们需要通过将图片数据编码为 Base64 格式来准备通过 HTTP 传输的图片数据。由于图像等二进制数据不能直接通过 HTTP 传输,我们需要使用 Base64 编码将二进制数据转换为 ASCII 字符串格式。这确保了数据可以安全地包含在 HTTP 请求中。这是通过以下代码片段实现的。

   //--- 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);
   }

首先,我们建立两个数组。第一个是“base64”。它保存了编码数据。第二个数组是“key”。在这种情况下,我们从不使用“key”,但编码函数需要它。执行 Base64 编码工作的函数称为 CryptEncode 。这需要四个参数:编码类型(“CRYPT_BASE64”)、源二进制数据(“photoArr_Data”)、加密密钥(“key”)和输出数组(“base64”)。这个 CryptEncode 函数实际完成将二进制数据转换为 Base64 格式并将结果存储在“base64”数组中的工作。当我们用 ArraySize 函数检查“base64”的大小时,如果“base64”包含任何元素,也就是大于0,就表示编码成功。

为了将这些数据打印到日志中,我们使用 ArrayPrint 函数。

      Print("Transformed BASE-64 data = ",ArraySize(base64));
      Print("The whole data is as below:");
      ArrayPrint(base64);

我们得到以下结果:

数据打印

我们可以看到,原始数据二进制大小为 320894,和新转换的数据大小 427860 之间存在明显偏差。这种偏差是数据转换和编码的结果。

接下来,我们需要准备 Base64 编码数据的一个子集,以确保我们为流程的下一步处理其中可管理的部分。具体来说,我们需要专注于将编码数据的前 1024 个字节复制到临时数组中以供进一步使用。

   //Copy the first 1024 bytes of the base64-encoded data into a temporary array
   uchar temporaryArr[1024]= {0};
   ArrayCopy(temporaryArr,base64,0,0,1024);

首先,我们建立临时数组“temporaryArr”,其大小为 1024 字节。我们将其所有值初始化为零。我们使用这个数组来保存 Base64 编码数据的第一段。由于初始化值为零,我们避免了存储临时数组的内存中残留信息的任何潜在问题。

然后,我们使用 ArrayCopy 函数将前 1024 个字节从“base64”移动到“temporaryArr”。这可以干净高效地处理复制操作。原因和复制操作的细节是它们自己的故事,但我只会提到几件事。初始化的副作用是,如果您将 Base64 编码数据的第一部分视为某种随机乱码,则可以消除您对它的任何担忧。让我们记录空的临时数组。我们通过以下代码实现这一点。

   Print("FILLED TEMPORARY ARRAY WITH ZERO (0) IS AS BELOW:");
   ArrayPrint(temporaryArr);

经过编译,我们得到以下结果:

零填充数组

我们可以看到临时数组填充了零。然后,这些零被原始格式化数据的前 1024 个值替换。我们可以通过类似的逻辑再次查看这些数据。

   Print("FIRST 1024 BYTES OF THE ENCODED DATA IS AS FOLLOWS:");
   ArrayPrint(temporaryArr);

填充后的临时数据展示如下:

已填充临时数据

获取此临时数据后,我们需要从 Base64 编码数据的前 1024 个字节生成消息 MD5 哈希。此 MD5 哈希将用作多部分/表单数据结构中的边界的一部分,该结构通常用于 HTTP POST 请求中处理文件上传。

   //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);
   }


首先,我们声明一个名为“md5”的数组来存储 MD5 哈希的结果。MD5 算法(“MD”代表“Message Digest(消息摘要)”)是一种产生 128 位哈希值的加密哈希函数。哈希值通常表示为 32 个十六进制数字的字符串。

在本例中,我们使用 MQL5 内置函数 CryptEncodeCRYPT_HASH_MD5 参数来计算哈希值。我们向函数传递一个名为“temporaryArr”的临时数组,它保存了 Base64 编码数据的前 1024 个字节。“key”参数通常用于其他加密操作,但 MD5 不需要,在这种情况下设置为空数组。哈希运算的结果存储在“md5”数组中。

计算哈希值后,我们使用 ArraySize 函数验证数组中元素的数量,以检查“md5”数组是否非空。如果数组有任何元素,我们记录 MD5 哈希的大小,然后记录实际哈希值。该哈希值用于创建 multipart/form-data 格式的边界字符串,以帮助分隔正在传输的 HTTP 请求的不同部分。这里严格使用 MD 5算法是因为它的通用性和它产生的独特价值,而不是因为它是最好或最安全的算法。当运行该程序时,我们就会得到以下数据:

MD5 数据

在这里,您可以看到我们以数字形式获得 MD5 哈希数据。因此,我们需要将 MD5 哈希转换为十六进制字符串,然后截断它以满足在 multipart/form-data HTTP 请求中用作边界的特定长度要求,通常为16。

   //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);

首先,我们声明一个字符串变量“HexaDecimal_Hash”来保存 MD5 哈希的十六进制形式。此字符串将用作边界标记,以分隔 HTTP 请求有效负载的不同部分。

接下来,我们遍历存储在 md5 数组中的哈希的每个字节。我们使用格式说明符“%02X”将每个字节转换为两个字符的字符串。说明符的“%0”部分表示,必要时应使用前导零填充字符串,以确保每个字节由两个字符表示。“02”表示表示至少两个字符;“X”表示字符应为十六进制数字(必要时使用大写字母)。

再次,这些十六进制字符被附加到“HexaDecimal_Hash”字符串。最后,我们将字符串的内容输出到日志中,以验证其格式是否正确。运行该程序将产生以下信息:

最终字符串哈希

这是一次成功。接下来,我们需要构建并准备 multipart/form-data HTTP POST 请求的文件数据,该请求将用于通过 Telegram API 将图片发送到 Telegram 聊天。这将涉及准备请求正文,以服务器可以正确处理的格式包含表单字段和文件数据。我们通过以下代码片段实现这一点。

   //--- WebRequest
   char DATA[];
   string URL = NULL;
   URL = TG_API_URL+"/bot"+botTkn+"/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,chatID);
   //Finally, Append another newline sequence to the DATA array to signify
   //the end of the chat_id form-data part.
   ArrayAdd(DATA,"\r\n");

我们首先设置 HTTP 请求的“DATA”数组和“URL”。“URL”由三部分组成:API 的基本 URL(“TG_API_URL”);机器人的令牌,用于向 API 标识机器人(“botTkn”);以及向聊天发送照片的端点(“/sendPhoto”)。此 URL 指定了我们要向哪个“远程服务器”发送我们的“有效负载” — 我们要发送的图片以及我们想要附加到图片上的信息。端点 URL 不会改变;对于我们发出的每个请求它都是相同的。无论我们发送一张还是多张图片,无论我们是否将图片发送到不同的聊天等等,我们的请求都会到达同一个地方。

之后,我们在数据块的边缘添加一个边界标记。它由两个连字符(--)和我们独特的边界哈希(“HexaDecimal_Hash”)组成。总之,它看起来是这样的:“--HexaDecimal_Hash”。此边界标记出现在请求下一部分的数据块的开头,即“chart_id”表单字段。Content-Disposition 标头指定 multipart/form-data 请求的下一部分(下一个数据块)是表单字段,并给出该字段的名称(“chart_id”)。

我们添加此标题和换行符(“/r/n”)以指示标题部分的结束。在标题部分之后,我们将“chartID”值添加到 DATA 数组,后跟另一个换行符(“/r/n”)以指示“chart_id”表单数据部分的结束。此过程保证表单字段的格式正确并与请求的其他部分分离,以确保 Telegram 的 API 正确接收和处理数据。

您可能已经注意到我们在代码中使用了两个“重载函数”。下面我们来看看他们的代码片段。

//+------------------------------------------------------------------+
// ArrayAdd for uchar Array
void ArrayAdd(uchar &destinationArr[],const uchar &sourceArr[]){
   int sourceArr_size=ArraySize(sourceArr);//get size of source array
   if(sourceArr_size==0){
      return;//if source array is empty, exit the function
   }
   int destinationArr_size=ArraySize(destinationArr);
   //resize destination array to fit new data
   ArrayResize(destinationArr,destinationArr_size+sourceArr_size,500);
   // Copy the source array to the end of the destination array.
   ArrayCopy(destinationArr,sourceArr,destinationArr_size,0,sourceArr_size);
}

//+------------------------------------------------------------------+
// ArrayAdd for strings
void ArrayAdd(char &destinationArr[],const string text){
   int length = StringLen(text);// get the length of the input text
   if(length > 0){
      uchar sourceArr[]; //define an array to hold the UTF-8 encoded characters
      for(int i=0; i<length; i++){
         // Get the character code of the current character
         ushort character = StringGetCharacter(text,i);
         uchar array[];//define an array to hold the UTF-8 encoded character
         //Convert the character to UTF-8 & get size of the encoded character
         int total = ShortToUtf8(character,array);
         
         //Print("text @ ",i," > "text); // @ "B", IN ASCII TABLE = 66 (CHARACTER)
         //Print("character = ",character);
         //ArrayPrint(array);
         //Print("bytes = ",total) // bytes of the character
         
         int sourceArr_size = ArraySize(sourceArr);
         //Resize the source array to accommodate the new character
         ArrayResize(sourceArr,sourceArr_size+total);
         //Copy the encoded character to the source array
         ArrayCopy(sourceArr,array,sourceArr_size,0,total);
      }
      //Append the source array to the destination array
      ArrayAdd(destinationArr,sourceArr);
   }
}

这里,我们定义了两个自定义函数来处理 MQL5 中向数组添加数据,专门用于处理 ucharstring 类型。这些函数通过将各种信息附加到现有数组中来促进 HTTP 请求数据的构建,确保最终数据格式正确且适合传输。为了更容易理解,我们在函数中添加了注释,但让我们再次简要地浏览一下代码解释。

第一个函数 “ArrayAdd” 适用于无符号字符数组( uchar )。它被设置为将数据从源数组附加到目标数组。首先,它确定源数组中有多少个元素。这是通过调用源数组上的简单函数 ArraySize 来实现的。根据这条信息,我们检查源数组是否包含任何数据。如果没有,我们可以通过提前退出功能来避免继续的荒谬性。如果它确实包含数据,我们将继续下一步,即调整目标数组的大小以接受该数据。我们通过在目标数组上调用函数 ArrayResize 来实现这一点,现在我们可以放心地调用它,因为我们知道它会正常工作。

另一个函数将字符串添加到 char 数组,其工作原理如下:它计算输入字符串的长度。如果输入字符串不为空,它将获取字符串的每个字符,获取其代码,并将其附加到目标数组中,在此过程中将其转换为 UTF-8。为了转换字符串并将其附加到目标数组,此函数调整源数组的大小,以便在中间存储要添加的字符串数据。它确保字符串的 UTF-8 表示形式以及字符串本身正确存储在将用于构建 HTTP 请求体或其他类型数据结构的最终数据数组中。

为了查看我们所做的工作,让我们实现一个逻辑,该逻辑将打印与 HTTP 请求中发送的聊天 ID 相关的结果数据。

   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);   

首先,我们使用 ArrayPrint 函数来呈现原始数据数组。该函数为我们打印出数组的内容。然后,我们将“DATA”数组从字符数组转换为字符串格式。我们使用的函数是 CharArrayToString ,它将“DATA”中的原始字节数据转换为 UTF-8 编码的字符串。这里使用的参数指定我们要转换整个数组(“WHOLE_ARRAY”)并且字符编码是 UTF-8( CP_UTF8 )。这种转换是必要的,因为 HTTP 请求要求数据采用字符串格式。

总之,我们得到的是一个字符串“chatID_Data”,其最终格式将包含在 HTTP 请求中。通过使用 Print 函数,我们可以看到请求中的最终输出是什么样的。

聊天 ID 请求

我们可以看到,我们可以正确地将正确的聊天 ID 数据添加到数组中。通过相同的逻辑,我们也可以添加图像的数据来构建 multipart/form-data 请求体,以便通过 HTTP 将图片发送到 Telegram API。

   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");


首先,我们插入 multipart/form-data 的图片部分的边界标记。我们通过 ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n") 这一行来实现这一点。边界标记由两个连字符和“HexaDecimal_Hash”组成,用于分隔 multipart 请求的不同部分。“HexaDecimal_Hash” 是边界的唯一标识符,可以保证请求的每个部分都与下一个部分明确地分开。

然后我们将 Content-Disposition 标头包含在表单数据的图片部分中。我们使用 ArrayAdd 函数添加它,如下所示:ArrayAdd(DATA, "Content-Disposition: form-data; name=\" photo\"; filename=\"Upload_ScreenShot.jpg\"\r\n").这个标头表示后面的数据是一个文件,具体来说,就是名为“Upload_ScreenShot.jpg”的文件。因为我们已经通过标题的 name=\" photo\" 部分指定了当前正在处理的表单数据字段的名称为 \" photo\",所以服务器知道在处理传入请求时将文件“Upload_ScreenShot.jpg”作为该字段的一部分。该文件只是一个标识符,您可以将其更改为您喜欢的其他内容。

此后,我们使用 ArrayAdd(DATA, "\r\n") 将换行符序列附加到请求的标头。这表示标头部分的结束和实际文件数据的开始。然后我们使用 ArrayAdd(DATA, photoArr_Data) 将实际图片数据附加到 DATA 数组。这行代码将截图的二进制数据(之前经过 base64 编码)附加到请求正文。multipart/form-data 有效负载现在包含图片数据。

最后,我们使用 ArrayAdd(DATA, "\r\n") 添加另一个换行符序列,并使用 ArrayAdd(DATA, "--" + HexaDecimal_Hash + "--\r\n") 添加边界标记来关闭图片部分。边界标记末尾的 -- 表示 multipart 部分的结束。这个最终边界确保服务器正确识别请求中图片数据部分的结束。为了查看正在发送的数据,让我们再次通过与前一个类似的函数将其打印到日志部分。

   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);

以下是我们得到的结果:

最终文件数据上传

最后,我们构建向 Telegram API 发送 multipart/form-data 请求所需的 HTTP 请求标头。

   string HEADERS = NULL;
   HEADERS = "Content-Type: multipart/form-data; boundary="+HexaDecimal_Hash+"\r\n";

我们首先定义一个“HEADERS”字符串,将其初始化为“NULL”。此字符串包含我们需要为请求设置的 HTTP 标头。我们绝对必须设置的标题是 Content-Type。Content-Type 标头传达了要发送的数据类型及其格式。

我们为字符串分配正确的 Content-Type 值。这里的关键部分是“HEADERS”字符串本身。如果我们想理解为什么需要对“HEADERS”字符串进行特定的赋值,我们必须了解 HTTP 请求的“格式”。请求的格式表明请求是使用“Content-Type:multipart/form-data”标头发送的。完成所有这些后,我们现在可以发起 web 请求。首先,让我们通过发送下面的请求来通知用户。

   Print("SCREENSHOT SENDING HAS BEEN INITIATED SUCCESSFULLY.");

从初始代码中,我们注释掉不必要的 WebRequest 参数并切换到最新的参数。

   //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);

在这里,我们只向函数附加要发送的新 URL、标头和图像文件数据。响应逻辑保持不变,如下所示:

   // 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 ", TG_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 上:

MT5 确认

在 Telegram 上:

Telegram 确认

现在很明显,我们成功地将图像文件从 MetaTrader 5 交易终端发送到 Telegram 聊天。但是,我们只是发送了一个空的屏幕截图。为了向图像文件添加标题,我们实现以下逻辑,该逻辑向 multipart/form-data 请求添加可选标题,该请求将与图表截图一起发送到 Telegram API。

   //--- Caption
   string CAPTION = NULL;
   CAPTION = "Screenshot of Symbol: "+Symbol()+
             " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+
             ") @ Time: "+TimeToString(TimeCurrent());
   if(StringLen(CAPTION) > 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");
   }
   //---


我们首先将“CAPTION”字符串初始化为“NULL”,然后使用相关细节构建它。标题包括交易品种、图表的时间范围和当前时间,格式为字符串。然后我们检查“CAPTION”字符串的长度是否大于零。如果是,我们继续将标题添加到“DATA”数组,用于构建multipart 格式数据。这涉及附加一个边界标记,将表单数据部分指定为标题,并包括标题内容本身。运行该程序后,我们得到以下结果:

带标题的图像文件

成功了,我们可以看到,我们不仅收到了图像文件,还收到了一个描述性的标题,显示了所讨论图表的交易品种名称、周期和时间。

到目前为止,我们得到了附加程序的图表的屏幕截图。如果想要打开和修改不同的图表,我们需要为此实现不同的逻辑。 

   long chart_id=ChartOpen(_Symbol,_Period);
   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,_Period,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_FILE_NAME,1366,768,ALIGN_RIGHT);
   //Sleep(10000); // sleep for 10 secs to see the opened chart
   ChartClose(chart_id);
//---

这里,我们首先利用 ChartOpen 函数,使用预定义变量 _Symbol_Period ,为给定的交易品种和时间范围打开一个新图表。我们将新图表的 ID 赋值给变量“chart_id”。然后,我们使用“chart_id”来确保新图表在 MetaTrader 环境的前端可见,并且没有被任何以前的图表覆盖。

之后,我们开始一个最多可以运行 60 次迭代的循环。在该循环中,我们不断检查图表是否同步。为了测试同步,我们使用带有参数 _Symbol_PeriodSERIES_SYNCHRONIZED 的函数 SeriesInfoInteger 。如果我们发现图表已同步,我们就跳出循环。一旦我们确认图表同步,我们就使用带有参数“chart_id”的函数 ChartRedraw 来刷新图表。

我们自定义各种图表设置,以根据自己的喜好调整图表。我们使用函数 ChartSetInteger 来设置图表背景以及看跌和看涨烛形的颜色。我们设置的颜色提供了视觉清晰度,帮助我们轻松区分图表的不同元素。我们还通过禁用网格和句点分隔符,使图表在视觉上不那么杂乱。此时,您可以根据需要修改图表。最后,我们对图表进行了截图,以便在传输中使用。我们不希望图表不必要地打开,所以我们在截图后将其关闭。为此,我们使用 ChartClose 函数。当我们运行该程序时,我们得到以下结果:

修改后的图表

很明显,我们打开一个图表,根据自己的喜好进行修改,并在拍摄完快照后关闭它。为了可视化图表的打开和关闭过程,让我们延迟 10 秒才能看到图表。

   Sleep(10000); // sleep for 10 secs to see the opened chart

在这里,我们只需将图表打开 10 秒,让我们看到程序后台发生了什么。经过编译,我们得到以下结果:

图表打开关闭 GIF

负责截图、编码、加密并从交易终端发送到 Telegram 聊天的完整源代码如下:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {

   //--- Get ready to take a chart screenshot of the current chart
   
   #define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.jpg"
   
   //--- First 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(0);
   }
   
//---
   long chart_id=ChartOpen(_Symbol,_Period);
   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,_Period,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_FILE_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);
//---
   
   //ChartScreenShot(0,SCREENSHOT_FILE_NAME,1366,768,ALIGN_RIGHT);
   
   // Wait for 30 secs to save screenshot if not yet saved
   int wait_loops = 60;
   while(!FileIsExist(SCREENSHOT_FILE_NAME) && --wait_loops > 0){
      Sleep(500);
   }
   
   if(!FileIsExist(SCREENSHOT_FILE_NAME)){
      Print("THE SPECIFIED SCREENSHOT DOES NOT EXIST (WAS NOT SAVED). REVERTING NOW!");
      return (INIT_FAILED);
   }
   else if(FileIsExist(SCREENSHOT_FILE_NAME)){
      Print("THE CHART SCREENSHOT WAS SAVED SUCCESSFULLY TO THE DATA-BASE.");
   }
   
   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!");
      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.");
   }
   
   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 = TG_API_URL+"/bot"+botTkn+"/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,chatID);
   //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 = NULL;
   CAPTION = "Screenshot of Symbol: "+Symbol()+
             " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+
             ") @ Time: "+TimeToString(TimeCurrent());
   if(StringLen(CAPTION) > 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");
   }
   //---
   
   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 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 ", TG_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());
   }
   
   
   return(INIT_SUCCEEDED);  // Return initialization success status
}

现在很明显,我们已经实现了第三个目标,即从交易终端向 Telegram 聊天或群组发送图表截图图像文件和字幕。这是一次成功,让我们欢呼吧!我们现在需要做的是测试集成以确保它正常工作并查明出现的任何问题。这将在下一节中完成。


测试集成

为了确保我们的 EA 交易正确地将屏幕截图从 MetaTrader 5 交易终端发送到 Telegram,我们需要彻底测试集成。为了将事情结合起来,让我们采用 GIF 格式的测试逻辑。

测试 GIF

在上面提供的 GIF 中,我们演示了 MetaTrader 5 和 Telegram 之间的无缝交互,展示了发送图表截图的过程。GIF 首先显示 MetaTrader 5 平台,其中打开一个图表窗口,将其置于前台,然后暂停 10 秒,以便有时间进行最终调整。在此暂停期间,MetaTrader 5 中的日志选项卡会记录指示操作进度的消息,例如正在重新绘制的图表和正在捕获的屏幕截图。然后图表会自动关闭,截图会打包并发送至Telegram。在 Telegram 端,我们看到聊天中的屏幕截图,确认集成按预期工作。这张 GIF 以视觉方式强化了自动化系统的实时运行方式,从图表准备到在 Telegram 中成功传送图像。


结论

总而言之,本文逐步描述了如何将图表截图从 MetaTrader 5 交易平台发送到 Telegram 聊天中。我们首先使用 MetaTrader 5 生成图表截图。我们配置了图表的设置以确保其清晰,然后使用ChartScreenShot 函数捕获它以将图像获取到文件中。将文件保存到计算机后,我们打开文件并读取其二进制内容。然后,我们将以 Base64 格式编码的图表发送给 Telegram 的 API 可以理解的 HTTP 请求。通过这样做,我们可以实时将图像放入 Telegram 聊天中。

对图像进行编码以进行传输揭示了通过 HTTP 协议发送原始二进制数据所涉及的复杂性,特别是当目的地是 Telegram 等消息传递平台时。首先要理解的是,直接发送二进制数据是不可行的。相反, Telegram (和许多其他服务)要求以文本格式发送数据。我们使用广为人知的算法将原始二进制图像数据转换为 Base64,轻松满足了这一要求。之后,我们将 Base64 图像插入到 multipart/form-data HTTP 请求中。该演示不仅强调了 MetaTrader 5 平台作为创建自定义自动化手段的强大功能,还强调了如何将外部服务(在本例中为 Telegram)集成到交易策略中。

展望第 4 部分,我们将采用本文中的代码并将其转化为可重用的组件。我们将这样做,以便我们可以创建 Telegram 集成的多个实例,这将允许我们在本教程的下一部分中,根据我们的想法和喜好向 Telegram 发送不同的消息和截图 — 不仅是我们喜欢的,还有我们喜欢的时间和方式 — 而不必依赖于单个函数调用。通过将代码放入中,我们将使系统更加模块化和可扩展。我们还将这样做,以便将代码更轻松地集成到我们在第 1 部分中概述的不同交易场景中。这很重要,因为 Telegram 机制的集成应该与我们的 EA 交易动态灵活地协作,允许多种策略和账户场景在交易期间或交易日结束时的关键点发送各种消息和图像。请继续关注,我们将继续构建和完善这个集成系统。


本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/15616

最近评论 | 前往讨论 (18)
Aleksandr Slavskii
Aleksandr Slavskii | 9 6月 2025 在 02:38
Piotr Storozenko #:
      ::StringReplace(text, "\n", ShortToString(0x0A));
Piotr Storozenko
Piotr Storozenko | 9 6月 2025 在 11:42
Aleksandr Slavskii #:

谢谢。我找到了这个与普通 MQL5 名称相同的公共函数,并将其重命名以删除警告。现在编译很清楚了:)

Volker Mowy
Volker Mowy | 11 6月 2025 在 17:04
您好!感谢您提供的详细文档。不幸的是,我收到了两条错误信息。您能帮帮我吗?

致以最崇高的敬意,沃尔克

Volker Mowy
Volker Mowy | 5 7月 2025 在 13:36
Volker Mowy #:
您好!感谢您提供的详细文档。不幸的是,我收到了两条错误信息。您能帮帮我吗?

致以最崇高的敬意,沃尔克

谢谢,找到错误了!

666 // uchar 数组的 ArrayAdd
667 void ArrayAdd(char &destinationArr[], const uchar &sourceArr[]){
Volker Mowy
Volker Mowy | 6 7月 2025 在 06:33
小小的改变,换来的是更好的展示效果!
构建蜡烛图趋势约束模型(第8部分):EA开发(II) 构建蜡烛图趋势约束模型(第8部分):EA开发(II)
构思一个独立的EA。之前,我们讨论了一个基于指标的EA,它还与一个独立脚本配合,用于绘制风险与收益图形。今天,我们将讨论一个整合了所有功能的MQL5 EA的架构。
交易中的神经网络:状态空间模型 交易中的神经网络:状态空间模型
到目前为止,我们审阅的大量模型都是基于变换器架构。不过,在处理长序列时,它们或许效率低下。在本文中,我们将领略一种替代方向,即基于状态空间模型的时间序列预测。
开发回放系统(第 62 部分):玩转服务(三) 开发回放系统(第 62 部分):玩转服务(三)
在本文中,我们将开始解决在使用真实数据时可能影响应用程序性能的分时报价过量问题。这种过量通常会干扰在相应窗口构建一分钟柱形所需的正确时间。
如何在MQL5的EA中实现自优化 如何在MQL5的EA中实现自优化
MQL5中EA自优化的分步指南。我们将涵盖稳健的优化逻辑、参数选择的最佳实践,以及如何通过回测重构策略。此外,还将讨论诸如分步优化等高级方法,以增强您的交易方法。