English Русский Español Deutsch 日本語 Português
preview
创建 MQL5-Telegram 集成 EA 交易 (第二部分):从 MQL5 发送信号到 Telegram

创建 MQL5-Telegram 集成 EA 交易 (第二部分):从 MQL5 发送信号到 Telegram

MetaTrader 5交易系统 | 26 三月 2025, 10:15
503 0
Allan Munene Mutiiria
Allan Munene Mutiiria

概述

我们关于开发 MQL5 的 Telegram 集成 EA 交易平台系列文章的第一部分中,我们介绍了链接 MQL5 和 Telegram 所需的基本步骤。设置实际应用程序是第一步,之后,我们进入编码部分。希望在接下来的段落中,这种特殊事件顺序的原因会变得更加清晰。结果是,我们现在有了一个可以接收消息的机器人,以及一个可以发送消息的程序。我们还编写了一个简单的 MQL5 程序,演示如何通过机器人向应用程序发送消息。

在第一部分奠定基础后,我们现在可以继续下一步:使用 MQL5 将交易信号传输到 Telegram。我们最新增强的 EA 交易程序做了一些非常了不起的事情:它不仅可以根据预设条件开仓和平仓交易,还可以向 Telegram 群聊发送信号,让我们知道交易已执行,这同样令人印象深刻。交易信号本身已经过了一些改造,确保我们发送给 Telegram 的信息尽可能清晰简洁。我们的 “Chatty Trader” 在与 Telegram 中的群组交流方面比我们以前的版本做得更好,并且它的速度与我们的旧 “Chatty Trader” 相同或更快,这意味着我们可以期望在进行或关闭交易时几乎实时地接收到信号。

我们将根据著名的移动平均交叉系统生成信号,并传送生成的信号。此外,如果您还记得的话,在本系列的第一部分中,我们只有一条消息,这条消息可能很长,如果有人想在消息中添加片段,就会导致错误。因此,一次只能发送一条消息,如果有额外的片段,它们必须在不同的单独消息中发送。例如,发送“已生成买入信号。”和“开立买入订单。”可以是一条长信息,也可以是两条短信息。在本部分中,我们将把它们连接起来并修改消息,以便单个消息可以包含多个文本片段和字符。我们将在以下子主题中讨论整个过程:

  1. 策略概述
  2. MQL5 中的实现
  3. 测试集成
  4. 结论

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


策略概述

我们使用移动平均线交叉点产生交易信号,这是使用最广泛的技术分析工具之一。我们将描述我们认为最直接、最明确的方法,即使用移动平均线交叉来尝试识别潜在的买入或卖出机会。这是基于交叉本身的信号性质,不添加任何其他工具或指标。为了简单起见,我们将只考虑两个不同时期的移动平均线:短期移动平均线和长期移动平均线。

我们将探讨移动平均线交叉的功能以及它们如何产生可以采取行动的交易信号。移动平均线将价格数据平滑化,形成一条流动线,比实际价格图更适合识别趋势。这是因为,一般来说,平均线总是比锯齿线更流线型,更容易遵循。当你把两个不同时期的移动平均线加在一起时,它们会在某个时候相交,因此被称为“交叉”。 

为了使用 MQL5 将移动平均线交叉信号付诸实践,我们首先要确定最符合我们的交易策略的平均线的短期和长期周期数。为此,我们将使用标准周期数,例如,50 和 200 用于长期趋势,10 和 20 用于短期趋势。计算移动平均线后,我们将比较每个新分时报价或柱形处的交叉事件值,并将这些检测到的交叉信号转换为“买入”或“卖出”的二进制事件,供我们的 EA 交易系统采取行动。为了更容易理解我们的意思,让我们将这两个实例可视化。

向上交叉:

向上交叉

向下交叉:

向下交叉

这些生成的信号将与我们当前的 MQL5-Telegram 消息框架相结合。为了实现这一点,第一部分的代码将进行调整,以涵盖信号检测和格式化。一旦识别出交叉,将创建一条包含资产名称、交叉方向(买入/卖出)和信号时间的消息。及时将此消息传递到指定的 Telegram 聊天将确保我们的交易团队了解潜在的交易机会。除此之外,在交叉发生后立即收到消息的保证意味着我们将有机会根据相关信号发起交易,甚至开立市场头寸并传递头寸细节。


MQL5 中的实现

首先,我们将确保我们可以对消息进行分段并将其作为一个整体发送。在第一部分中,当我们发送一条包含换行符等特殊字符的复杂消息时,我们会收到一个错误,我们只能将其作为一条没有结构的消息发送。例如,我们有此代码片段,可获取初始化事件、账户净值以及可用的可用保证金:

   double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY);
   double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   string msg = "🚀 EA INITIALIZED ON CHART " + _Symbol + " 🚀"
                +"📊 Account Status 📊; Equity: $"
                +DoubleToString(accountEquity,2)
                +"; Free Margin: $"
                +DoubleToString(accountFreeMargin,2);

将其作为一个整体发送,我们得到的结果如下:

长消息

我们可以看到,虽然我们可以发送信息,但它的结构并不吸引人。第一行是初始化语句,第二行是账户状态,再下一行是净值,最后一行是可用保证金信息。为了实现这一点,需要按如下方式考虑新的换行符“\n”。

   double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY);
   double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   string msg = "🚀 EA INITIALIZED ON CHART " + _Symbol + " 🚀"
                +"\n📊 Account Status 📊"
                +"\nEquity: $"
                +DoubleToString(accountEquity,2)
                +"\nFree Margin: $"
                +DoubleToString(accountFreeMargin,2);

但是,当我们运行程序时,我们会在日志上收到一条错误消息,如图所示,并且该消息不会发送到 Telegram 聊天:

新消息发送错误

为了确保消息成功发送,我们必须对其进行编码。我们的集成需要对消息进行编码,以正确处理特殊字符。例如,如果我们的消息包含类似空间的内容或行为类似符号(“&”、“?”等),由于我们在集成过程中不够谨慎,Telegram 应用程序编程接口(API)可能会误解这些内容。我们正在认真对待这一点;这不是开玩笑。我们已经看到了字符编码的其他用途,例如在我们的计算机上打开某些类型的文档时,如图所示。

文档编码

编码是避免我们迄今为止遇到的问题类型的关键,因为 API 不了解我们试图发送什么,这样它就可以做我们希望它做的事情。

例如,发送到 API 的包含特殊字符的消息可能会干扰统一资源定位器(URL)结构,即计算机 “看到” URL 的方式,并可能导致解释错误。API 可能会将特殊字符解释为指令或代码的其他部分,而不是实际消息的一部分。这种通信中断可能发生在任何一端:当从程序发送消息时,或者当在编码的另一端接收消息时,没有执行其使消息中看不见的部分安全“可见”的主要功能。此外,在这种情况下,使用编码方案意味着我们具有与 Telegram API 接收端兼容的格式的消息。毕竟,这个问题涉及几个不同的系统,每个系统都对如何传递数据有特定的要求。因此,我们要做的第一件事是制作一个对我们的消息进行编码的函数。

// FUNCTION TO ENCODE A STRING FOR USE IN URL
string UrlEncode(const string text) {
    string encodedText = ""; // Initialize the encoded text as an empty string
    int textLength = StringLen(text); // Get the length of the input text

   ...

}

在这里,我们首先创建一个名为“UrlEncode”的 string 数据类型函数,该函数接受一个 string 类型的参数或文本,用于将提供的文本转换为 URL 编码格式。然后,我们初始化一个空字符串“encodedText”,它将用于在处理输入文本时构建 URL 编码的结果。接下来,我们使用 StringLen 函数确定输入字符串的长度,并将该长度存储在整型变量 “textLength” 中。这一步至关重要,因为它让我们知道需要处理多少个字符。通过存储长度,我们可以循环高效地遍历字符串的每个字符,确保所有字符都根据 URL 编码规则正确编码。对于迭代过程,我们将需要使用循环。

    // Loop through each character in the input string
    for (int i = 0; i < textLength; i++) {
        ushort character = StringGetCharacter(text, i); // Get the character at the current position
   
        ...

    }

在这里,我们启动一个 for 循环,从索引 0 处的第一个字符开始,迭代输入消息或文本中包含的所有字符。我们使用 StringGetCharacter 函数获取所选符号的值,该函数通常返回位于字符串指定位置的符号的值。该位置由索引 “i” 定义。我们将字符存储在名为 “character” 的短整型变量中。

        // Check if the character is alphanumeric or one of the unreserved characters
        if ((character >= 48 && character <= 57) ||  // Check if character is a digit (0-9)
            (character >= 65 && character <= 90) ||  // Check if character is an uppercase letter (A-Z)
            (character >= 97 && character <= 122) || // Check if character is a lowercase letter (a-z)
            character == '!' || character == '\'' || character == '(' ||
            character == ')' || character == '*' || character == '-' ||
            character == '.' || character == '_' || character == '~') {

            // Append the character to the encoded string without encoding
            encodedText += ShortToString(character);
        }

在这里,我们检查给定的字符是字母数字还是 URL 中常用的未保留字符之一。目标是确定字符是否需要编码,或者是否可以直接附加到编码字符串上。首先,我们通过验证该字符的 ASCII 值是否在 48 和 57 之间来检查该字符是否为数字。接下来,我们通过查看该字符的 ASCII 值是否在 65 到 90 之间来检查该字符是否为大写字母。类似地,我们通过确认该字符的 ASCII 值是否介于 97 和 122 之间来检查该字符是否为小写字母。这些值可以从“ASCII 表”中确认。

数字字符 - 48 至 57:

数字

大写字母字符 - 65 至 90:

大写字母

小写字母字符 - 97 至 122:

小写字母

除了这些字母数字字符外,我们还检查 URL 中使用的特定未保留字符。这些包括 ‘!’、‘’、‘(’、‘)’、‘*’、‘-’、‘.’、‘_’ 和 ‘~’。如果字符符合这些条件中的任何一个,则意味着该字符是字母数字或非保留字符之一。

当字符满足任何这些条件时,我们将其附加到“encodedText”字符串而不对其进行编码。这是通过使用 ShortToString 函数将字符转换为其字符串表示形式来实现的,这确保将字符以其原始形式添加到编码字符串中。如果这些条件都不满足,我们将继续检查空格字符。

        // Check if the character is a space
        else if (character == ' ') {
            // Encode space as '+'
            encodedText += ShortToString('+');
        }

在这里,我们使用 else if 语句通过将字符与空格字符进行比较来检查该字符是否为空格。如果该字符确实是空格,我们需要以适合 URL 的方式对其进行编码。我们没有像在计算机文档中那样使用典型的空格百分比编码(%20),而是选择将空格编码为加号“+”,这是在URL中表示空格的另一种常见方法,特别是在查询组件中。因此,我们使用 ShortToString 函数将加号“+”转换为其字符串表示形式,然后将其附加到 “encodedText” 字符串。

如果到目前为止,我们还有未编码的字符,这意味着我们的手上有一个令人头疼的地方,因为它是像表情符号这样的复杂字符。因此,我们需要通过使用 Unicode 转换格式-8(UTF-8)对所有非字母数字、非保留或空格的字符进行编码来处理它们,确保任何不属于之前检查的类别的字符都被安全编码以包含在 URL 中。

        // For all other characters, encode them using UTF-8
        else {
            uchar utf8Bytes[]; // Array to hold the UTF-8 bytes
            int utf8Length = ShortToUtf8(character, utf8Bytes); // Convert the character to UTF-8
            for (int j = 0; j < utf8Length; j++) {
                // Convert each byte to its hexadecimal representation prefixed with '%'
                encodedText += StringFormat("%%%02X", utf8Bytes[j]);
            }
        }

首先,我们声明一个数组 “utf8Bytes” 来保存字符的 Unicode 转换格式-8(UTF-8)字节表示。然后,我们调用 “ShortToUtf8” 函数,将 “character” 和 “utf8Bytes” 数组作为参数传递。我们将简要解释该函数,但现在只需知道该函数将字符转换为 UTF-8 表示形式,并返回转换中使用的字节数,将这些字节存储在 “utf8Bytes” 数组中。

接下来,我们使用 “for循环” 遍历 “utf8Bytes” 数组中的每个字节。对于每个字节,我们将其转换为以 “%” 字符为前缀的十六进制表示形式,这是在 URL 中对字符进行百分比编码的标准方式。我们使用 “StringFormat” 函数将每个字节格式化为带有 “%” 前缀的两位十六进制数。最后,我们将这个编码表示附加到 “encodedText” 字符串。最终,我们只需返回结果。

    return encodedText; // Return the URL-encoded string

函数的完整代码片段如下:

// FUNCTION TO ENCODE A STRING FOR USE IN URL
string UrlEncode(const string text) {
    string encodedText = ""; // Initialize the encoded text as an empty string
    int textLength = StringLen(text); // Get the length of the input text

    // Loop through each character in the input string
    for (int i = 0; i < textLength; i++) {
        ushort character = StringGetCharacter(text, i); // Get the character at the current position

        // Check if the character is alphanumeric or one of the unreserved characters
        if ((character >= 48 && character <= 57) ||  // Check if character is a digit (0-9)
            (character >= 65 && character <= 90) ||  // Check if character is an uppercase letter (A-Z)
            (character >= 97 && character <= 122) || // Check if character is a lowercase letter (a-z)
            character == '!' || character == '\'' || character == '(' ||
            character == ')' || character == '*' || character == '-' ||
            character == '.' || character == '_' || character == '~') {

            // Append the character to the encoded string without encoding
            encodedText += ShortToString(character);
        }
        // Check if the character is a space
        else if (character == ' ') {
            // Encode space as '+'
            encodedText += ShortToString('+');
        }
        // For all other characters, encode them using UTF-8
        else {
            uchar utf8Bytes[]; // Array to hold the UTF-8 bytes
            int utf8Length = ShortToUtf8(character, utf8Bytes); // Convert the character to UTF-8
            for (int j = 0; j < utf8Length; j++) {
                // Convert each byte to its hexadecimal representation prefixed with '%'
                encodedText += StringFormat("%%%02X", utf8Bytes[j]);
            }
        }
    }
    return encodedText; // Return the URL-encoded string
}

现在让我们看一下负责将字符转换为 UTF-8 表示形式的函数。

//+-----------------------------------------------------------------------+
//| Function to convert a ushort character to its UTF-8 representation    |
//+-----------------------------------------------------------------------+
int ShortToUtf8(const ushort character, uchar &utf8Output[]) {

   ...

}

该函数为整型数据类型,接受两个输入参数:字符值和输出数组。 

首先,我们转换单字节字符。

    // Handle single byte characters (0x00 to 0x7F)
    if (character < 0x80) {
        ArrayResize(utf8Output, 1); // Resize the array to hold one byte
        utf8Output[0] = (uchar)character; // Store the character in the array
        return 1; // Return the length of the UTF-8 representation
    }

值在 0x00 到 0x7F 范围内的单字节字符的转换很简单,因为它们直接以 UTF-8 表示在一个字节中。我们首先检查字符是否小于 0x80。如果是,我们使用 ArrayResize 函数将 “utf8Output” 数组的大小调整为仅一个字节。这使我们能够为输出 UTF-8 表示提供正确的大小。然后,我们将该字符转换为 uchar ,将其粘贴到数组的第一个元素中,这种做法称为类型转换。这与将字符的值复制到数组中相同。我们返回 1,表示以 UTF-8 表示形式的长度为一个字节。此过程将有效地处理任何单字节字符到 UTF-8 格式的转换,而不管操作系统如何。

它们的代表形式如下。

0x00,UTF-8:

0x00 UTF-8

0x7F,UTF-8:

0x7F UTF-8

您可以看到数字的十进制表示范围从 0 到 127。您可以再次注意到这些字符与初始 Unicode 字符相同。也许你想知道这是什么,让我们停下来,深入地看一下。在十六进制表示法中,0x80 和 0x7F 表示可以转换为十进制的特定值,以便更好地理解。十六进制数 0x80 相当于十进制的 128。这是因为十六进制是一个以 16 为底的数字系统,其中每个数字代表 16 的幂。在 0x80 中,“8” 代表 8 乘以 16^1(即 128),“0” 代表 0 乘以 16^0(即 0),总计 128。

另一方面,0x7F 相当于十进制的 127。在十六进制中,“7F” 表示 7 乘以 16^1 加上 15 乘以 16^0。计算后,我们得到 7 乘以 16(即 112)加上 F(即 15),总计 127。请参阅下面 A 到 F 的表示。十六进制的 F 十进制等于 15。

十六进制,A 到 F

因此,0x80 是十进制的 128,而 0x7F 是十进制的 127。这意味着 0x80 仅比 0x7F 多一,这使其成为 UTF-8 编码中单字节表示变为多字节表示的边界。

我们只是想确保这些解释是详细的,你不会想知道接下来的格式以及一切是如何理解的。现在你知道了。现在我们来讨论一下双字节字符。

    // Handle two-byte characters (0x80 to 0x7FF)
    if (character < 0x800) {
        ArrayResize(utf8Output, 2); // Resize the array to hold two bytes
        utf8Output[0] = (uchar)((character >> 6) | 0xC0); // Store the first byte
        utf8Output[1] = (uchar)((character & 0x3F) | 0x80); // Store the second byte
        return 2; // Return the length of the UTF-8 representation
    }


在这里,我们负责转换在 UTF-8 表示中需要两个字节的字符 - 具体来说,是指值介于 0x80 和 0x7FF 之间的字符。为此,我们首先检查所讨论的字符是否小于 0x800(十进制为 2048),这保证它确实在这个范围内。如果满足该条件,我们将调整 “utf8Output” 数组的大小以容纳两个字节(因为需要两个字节来表示 UTF-8 中的字符)。然后我们计算实际的 UTF-8 表示形式。

通过获取字符、将其右移 6 位,然后使用逻辑或运算将其与 0xC0 组合,可以得到第一个字节。此计算将第一个字节的最高有效位设置为双字节字符的 UTF-8 前缀。第二个字节的计算方法是用 0x3F 掩码处理字符以获取低 6 位,然后将其与 0x80 结合。此操作确保第二个字节具有正确的 UTF-8 前缀。

最后,我们将这两个字节放入 “utf8Output” 数组中,并将 2 报告给调用者,表明该字符在其 UTF-8 表示中需要两个字节。与单字节字符相比,这是使用双倍字节数的字符的必要和正确的编码。然后,我们就得到了 3 字节字符。

    // Handle three-byte characters (0x800 to 0xFFFF)
    if (character < 0xFFFF) {

        ...

    }

现在,您明白这意味着什么了。这里,十六进制数 “0xFFFF” 转换为十进制为 65535。我们认识到每个十六进制数字代表 16 的幂。对于 “0xFFFF”,每个数字都是 “F”,即十进制中的 15 - 我们已经看到过。为了计算其十进制值,我们根据其位置评估每个数字的贡献。我们从最高位值开始,即 (15 * 16^3),得到 (15 * 4096 = 61440)。接下来,我们计算 (15 * 16^2),它等于 (15 * 256 = 3,840)。那么,(15 * 16^1) 的结果为 (15 * 16 = 240)。最后,(15 * 16^0) 等于 (15 * 1 = 15)。将这些结果相加,我们得到 61440 + 3840 + 240 + 15,总计 65535。因此,“0xFFFF” 以十进制表示为 65535。考虑到这一点,可能有三个 3 字节字符的实例。我们先来看第一个例子。

        if (character >= 0xD800 && character <= 0xDFFF) { // Ill-formed characters
            ArrayResize(utf8Output, 1); // Resize the array to hold one byte
            utf8Output[0] = ' '; // Replace with a space character
            return 1; // Return the length of the UTF-8 representation
        }

在这里,我们处理 Unicode 范围 0xD800 到 0xDFFF 内的字符,这些字符被称为代理半字符,不能作为独立字符使用。我们首先检查字符是否在这个范围内。当我们遇到这种格式错误的字符时,我们首先调整 “utf8Output” 数组的大小以仅保存一个字节,确保我们的输出数组准备好仅存储一个字节。

接下来,我们通过将 “utf8Output” 数组的第一个元素设置为空格,将无效字符替换为空格字符。该选择是一个占位符,用于妥善处理无效输入。最后,我们返回 1,表示这个格式错误的字符的 UTF-8 表示形式长度为一个字节。接下来,我们检查表情符号。这意味着我们要处理 Unicode 范围 0xE000 到 0xF8FF 内的字符。这些字符包括表情符号和其他扩展符号。

        else if (character >= 0xE000 && character <= 0xF8FF) { // Emoji characters
            int extendedCharacter = 0x10000 | character; // Extend the character to four bytes
            ArrayResize(utf8Output, 4); // Resize the array to hold four bytes
            utf8Output[0] = (uchar)(0xF0 | (extendedCharacter >> 18)); // Store the first byte
            utf8Output[1] = (uchar)(0x80 | ((extendedCharacter >> 12) & 0x3F)); // Store the second byte
            utf8Output[2] = (uchar)(0x80 | ((extendedCharacter >> 6) & 0x3F)); // Store the third byte
            utf8Output[3] = (uchar)(0x80 | (extendedCharacter & 0x3F)); // Store the fourth byte
            return 4; // Return the length of the UTF-8 representation
        }

我们首先判断该字符是否属于这个表情符号范围。由于此范围内的字符在 UTF-8 中需要四字节表示,因此我们首先通过与 0x10000 进行按位或运算来扩展字符值。这个步骤使我们能够正确处理来自补充平面的字符。

随后,我们将 “utf8Output” 数组的大小调整为四个字节。这保证了我们有足够的空间在数组中存储整个 UTF-8 编码。因此,UTF-8 表示的计算是基于推导和组合四个部分(四个字节)。对于第一个字节,我们取 “extendedCharacter” 并将其右移 18 位。然后,我们将该值与 0xF0 进行逻辑组合(使用按位或运算,或 |)以获取第一个字节的对应“高”位。对于第二个字节,我们将 “extendedCharacter” 右移 12 位,并使用类似的技术来获取下一部分。

类似地,我们通过将扩展字符右移 6 位并掩蔽接下来的 6 位来计算第三个字节。我们将其与 0x80 结合起来得到第三个字节的第一部分。为了获得第二部分,我们用 0x3F(这给了我们扩展字符的最后 6 位)掩蔽扩展字符,并将其与 0x80 相结合。当我们计算并将这两个字节存储到 “utf8Output” 数组中时,我们返回 4,表示该字符在 UTF-8 中占用 4 个字节。例如,我们可能有一个表情符号字符 1F4B0。这是钱袋表情符号。

钱袋表情符号

为了计算它的十进制表示形式,我们首先将十六进制数字转换为十进制值。16^4位上的数字 1 贡献了 1×65536=65536。数字 F (十进制为 15)在 16^3 位上贡献了 15×4096=61440。16^2 位上的数字 4 贡献了 4×256=1024。数字 B 以十进制表示为 11,在 16^1 位上,贡献了 11×16=176。最后,16^0 位上的数字 0 贡献 0×1=0。

将这些贡献加在一起,我们得到 65536+61440+1024+176+0=128176。因此,0x1F4B0 转换为十进制的 128176。您可以在提供的图片中确认这一点。

最后,我们处理超出先前处理的特定范围并需要三字节 UTF-8 表示的字符。

        else {
            ArrayResize(utf8Output, 3); // Resize the array to hold three bytes
            utf8Output[0] = (uchar)((character >> 12) | 0xE0); // Store the first byte
            utf8Output[1] = (uchar)(((character >> 6) & 0x3F) | 0x80); // Store the second byte
            utf8Output[2] = (uchar)((character & 0x3F) | 0x80); // Store the third byte
            return 3; // Return the length of the UTF-8 representation
        }

我们首先调整 “utf8Output” 数组的大小,以便它可以包含必要的三个字节。每个字节的大小为 8,因此要保存三个字节,我们需要 24 位的空间。然后,我们按字节方式计算 UTF-8 编码的三个字节中的每一个。第一个字节是根据字符的顶部确定的。为了计算第二个字节,我们将字符向右移动 6 位,掩蔽结果值以获取接下来的 6 位,并将其与 0x80 结合起来设置连续位。获取第三个字节在概念上是相同的,只是我们不进行任何移位。相反,我们进行掩码处理以获取最后 6 位并将它们与 0x80 相结合。确定存储在 “utf8Output” 数组中的三个字节后,我们返回 3,表示表示跨越三个字节。

最后,我们必须处理字符无效或无法正确编码的情况,将其替换为 Unicode 替换字符 U+FFFD。

    // Handle invalid characters by replacing with the Unicode replacement character (U+FFFD)
    ArrayResize(utf8Output, 3); // Resize the array to hold three bytes
    utf8Output[0] = 0xEF; // Store the first byte
    utf8Output[1] = 0xBF; // Store the second byte
    utf8Output[2] = 0xBD; // Store the third byte
    return 3; // Return the length of the UTF-8 representation

我们首先将 “utf8Output” 数组的大小调整为三个字节,这保证我们有足够的空间来替换字符。接下来,我们将 “utf8Output” 数组的字节设置为 U+FFFD 的 UTF-8 表示形式。该字符在UTF-8中以字节序列 0xEF、0xBF 和 0xBD 出现,这些是直接分配给 “utf8Output”的字节中,其中 0xEF 为第一个字节,0xBF 为第二个字节,0xBD 为第三个字节。最后,我们返回 3,这表明替换字符的 UTF-8 表示占用三个字节。这就是确保我们可以将字符转换为 UTF-8 表示形式的完整函数。也可以使用先进的 UTF-16,但是由于它已经能完成网站内容工作,所以我们让一切保持简单。就这样,该函数的完整代码如下:

//+-----------------------------------------------------------------------+
//| Function to convert a ushort character to its UTF-8 representation    |
//+-----------------------------------------------------------------------+
int ShortToUtf8(const ushort character, uchar &utf8Output[]) {
    // Handle single byte characters (0x00 to 0x7F)
    if (character < 0x80) {
        ArrayResize(utf8Output, 1); // Resize the array to hold one byte
        utf8Output[0] = (uchar)character; // Store the character in the array
        return 1; // Return the length of the UTF-8 representation
    }
    // Handle two-byte characters (0x80 to 0x7FF)
    if (character < 0x800) {
        ArrayResize(utf8Output, 2); // Resize the array to hold two bytes
        utf8Output[0] = (uchar)((character >> 6) | 0xC0); // Store the first byte
        utf8Output[1] = (uchar)((character & 0x3F) | 0x80); // Store the second byte
        return 2; // Return the length of the UTF-8 representation
    }
    // Handle three-byte characters (0x800 to 0xFFFF)
    if (character < 0xFFFF) {
        if (character >= 0xD800 && character <= 0xDFFF) { // Ill-formed characters
            ArrayResize(utf8Output, 1); // Resize the array to hold one byte
            utf8Output[0] = ' '; // Replace with a space character
            return 1; // Return the length of the UTF-8 representation
        }
        else if (character >= 0xE000 && character <= 0xF8FF) { // Emoji characters
            int extendedCharacter = 0x10000 | character; // Extend the character to four bytes
            ArrayResize(utf8Output, 4); // Resize the array to hold four bytes
            utf8Output[0] = (uchar)(0xF0 | (extendedCharacter >> 18)); // Store the first byte
            utf8Output[1] = (uchar)(0x80 | ((extendedCharacter >> 12) & 0x3F)); // Store the second byte
            utf8Output[2] = (uchar)(0x80 | ((extendedCharacter >> 6) & 0x3F)); // Store the third byte
            utf8Output[3] = (uchar)(0x80 | (extendedCharacter & 0x3F)); // Store the fourth byte
            return 4; // Return the length of the UTF-8 representation
        }
        else {
            ArrayResize(utf8Output, 3); // Resize the array to hold three bytes
            utf8Output[0] = (uchar)((character >> 12) | 0xE0); // Store the first byte
            utf8Output[1] = (uchar)(((character >> 6) & 0x3F) | 0x80); // Store the second byte
            utf8Output[2] = (uchar)((character & 0x3F) | 0x80); // Store the third byte
            return 3; // Return the length of the UTF-8 representation
        }
    }
    // Handle invalid characters by replacing with the Unicode replacement character (U+FFFD)
    ArrayResize(utf8Output, 3); // Resize the array to hold three bytes
    utf8Output[0] = 0xEF; // Store the first byte
    utf8Output[1] = 0xBF; // Store the second byte
    utf8Output[2] = 0xBD; // Store the third byte
    return 3; // Return the length of the UTF-8 representation
}

有了编码函数,我们现在可以对消息进行编码并再次重新发送。

   double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY);
   double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   string msg = "🚀EA INITIALIZED ON CHART " + _Symbol + " 🚀"
                +"\n📊Account Status 📊"
                +"\nEquity: $"
                +DoubleToString(accountEquity,2)
                +"\nFree Margin: $"
                +DoubleToString(accountFreeMargin,2);
   
   string encloded_msg = UrlEncode(msg);
   msg = encloded_msg;

在这里,我们只需声明一个名为 “encoded_msg” 的字符串变量来存储我们的 URL 编码消息,最后将结果附加到初始消息中,从技术上讲,这会覆盖其内容,而不仅仅是声明另一个变量。当我们运行它时,我们得到的结果如下:

不带表情符号的消息

我们可以看到这是成功的。我们确实以结构化的形式收到了该消息。但是,消息中最初的表情符号会被丢弃。这是因为我们对它们进行了编码,现在为了让它们回来,我们必须输入它们各自的格式。如果你不需要删除它们,这意味着你要对它们进行硬编码,因此,你只需忽略函数中的表情符号片段。对我们来说,让我们以各自的格式对它们进行编码,以便它们可以自动编码。

   double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY);
   double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   string 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(msg);
   msg = encloded_msg;

这里,我们以 “\xF***” 格式表示该字符。如果在表示后面有一个单词,请务必使用空格或反斜杠 “\” 进行区分,即 “\xF123 ” 或 “\xF123\”。当运行该程序后,我们得到以下结果:

最终包含了表情符号

我们可以看到,我们现在有了正确的消息格式,所有字符都正确编码。这是一个成功!我们现在可以继续生成真实信号了。

由于 WebRequest 函数在策略测试器上不起作用,并且等待基于移动平均线交叉策略的信号生成将需要一些时间来等待确认,因此让我们制定一些其他快速策略,尽管我们稍后仍将使用移动平均线策略,以在程序初始化时使用。我们在初始化时评估前一个柱形,如果它是一个看涨柱形,我们就开立买入订单。否则,如果它是看跌或零方向柱,我们将开立卖单。如下图所示:

看涨和看跌烛形

所用逻辑的代码片段如下:

   double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   
   double Price_Open = iOpen(_Symbol,_Period,1);
   double Price_Close = iClose(_Symbol,_Period,1);
   
   bool isBuySignal = Price_Open < Price_Close;
   bool isSellSignal = Price_Open >= Price_Close;
   

在此我们定义报价,也就是要价和竞价。然后,我们使用 iOpen 函数获取索引 1 处前一个柱形的开盘价,该函数接受 3 个参数,即商品交易品种、周期和要获取值的柱形的索引。如果要获取收盘价,就使用 iClose 函数。然后我们定义布尔变量 “isBuySignal” 和 “isSellSignal”,比较开盘价和收盘价的值,如果开盘价小于收盘价或开盘价大于或等于收盘价,我们分别将买入和卖出信号标志存储在变量中。

要开立订单,我们还需要一个方法。

#include <Trade/Trade.mqh>
CTrade obj_Trade;

在全局范围内,最好是在代码的顶部,我们使用 #include 关键字包含交易类。这使我们能够访问 CTrade 类,我们将使用它来创建交易对象。这很关键,因为我们需要它来开启交易。

CTRADE 类

预处理器将用文件 Trade.mqh 的内容替换代码行 #include <Trade/Trade.mqh>。角括号表示 Trade.mqh 文件将从标准目录(通常是 terminal_installation_directory\MQL5\Include)中获取。搜索不包括当前目录。该行可以放在程序中的任何位置,但通常情况下,所有包含内容都放在源代码的开头,这样代码结构更合理,也更容易参考。通过声明 CTrade 类的 obj_Trade 对象,我们可以轻松访问该类中包含的方法,这要归功于 MQL5 开发人员。

有了这些,我们现在就可以开立仓位了。

   double lotSize = 0, openPrice = 0,stopLoss = 0,takeProfit = 0;
   
   if (isBuySignal == true){
      lotSize = 0.01;
      openPrice = Ask;
      stopLoss = Bid-1000*_Point;
      takeProfit = Bid+1000*_Point;
      obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
   }
   else if (isSellSignal == true){
      lotSize = 0.01;
      openPrice = Bid;
      stopLoss = Ask+1000*_Point;
      takeProfit = Ask-1000*_Point;
      obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
   }

我们定义 double 型变量来存储交易量、订单的开盘价、止损和止盈水平,并将它们初始化为零。为了开仓,我们首先检查 “isBuySignal ”是否包含 “true” 标志,这意味着前一个柱确实是看涨的,然后开立买入仓位。手数初始化为 0.01,开仓价为要价,止损和止盈水平根据出价计算,结果用于打开买入头寸。同样,要开立卖出头寸,需要计算这些值并在函数中使用。

一旦开仓,我们现在就可以在一条消息中收集有关生成的信号和开仓的信息,并将其转发给 Telegram。

   string position_type = isBuySignal ? "Buy" : "Sell";
   
   ushort MONEYBAG = 0xF4B0;
   string MONEYBAG_Emoji_code = ShortToString(MONEYBAG);
   string msg =  "\xF680 OPENED "+position_type+" 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;
   

在这里,我们创建了一个清晰准确的信息,其中包含与交易信号相关的信息。我们用表情符号和其他相关数据点格式化信息,我们认为这些数据点将使信息易于接收者消化。我们首先确定信号是“买入”还是“卖出”,这是通过使用三元运算符来实现的。然后,我们制作信息,包括一个表情符号表示的一叠钱,在我们看来,这适合于“买入”或“卖出”信号。我们使用了 “ushort” 格式的实际表情符号表示字符,然后使用 “ShortToString” 函数将字符代码转换为字符串变量,只是为了表明不一定总是使用字符串格式。但是,您可以看到转换过程需要一些时间和空间,但如果您想为各个字符命名,这是最好的方法。

然后,我们将未平仓交易头寸的信息组合成一个字符串。此字符串转换为消息后,包含交易的详细信息 - 交易类型、开盘价是多少、交易时间是多少、当前时间是多少、手数是多少、止损是多少、止盈是多少,等等。我们这样做的目的是使信息在视觉上更具吸引力并且易于理解。

在消息组成之后,我们调用 “UrlEncode” 函数对消息进行编码,以便安全地传输到 URL。我们特别确保所有特殊字符和表情符号都得到正确处理,并适合 网络传输。然后,我们将编码的消息存储在名为 “encloded_msg” 的变量中,并用初始消息覆盖编码的消息,或者通常进行交换。当我们运行它时,我们得到以下结果:

最终初始化信号消息

您可以看到我们已经成功对消息进行编码并将其以目标结构发送到 Telegram。负责发送此消息的完整源代码如下:

//+------------------------------------------------------------------+
//|                                  TELEGRAM_MQL5_SIGNALS_PART2.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"

#include <Trade/Trade.mqh>
CTrade obj_Trade;

// Define constants for Telegram API URL, bot token, and chat ID
const string TG_API_URL = "https://api.telegram.org";  // Base URL for Telegram API
const string botTkn = "7456439661:AAELUurPxI1jloZZl3Rt-zWHRDEvBk2venc";  // Telegram bot token
const string chatID = "-4273023945";  // Chat ID for the Telegram chat

// The following URL can be used to get updates from the bot and retrieve the chat ID
// CHAT ID = https://api.telegram.org/bot{BOT TOKEN}/getUpdates
// https://api.telegram.org/bot7456439661:AAELUurPxI1jloZZl3Rt-zWHRDEvBk2venc/getUpdates


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

   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
   ////--- Simple Notification with Emoji:
   //string msg = "🚀 EA INITIALIZED ON CHART " + _Symbol + " 🚀";
   ////--- Buy/Sell Signal with Emoji:
   //string msg = "📈 BUY SIGNAL GENERATED ON " + _Symbol + " 📈";
   //string msg = "📉 SELL SIGNAL GENERATED ON " + _Symbol + " 📉";
   ////--- Account Balance Notification:
   //double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   //string msg = "💰 Account Balance: $" + DoubleToString(accountBalance, 2) + " 💰";
   ////--- Trade Opened Notification:
   //string orderType = "BUY";  // or "SELL"
   //double lotSize = 0.1;  // Example lot size
   //double price = 1.12345;  // Example price
   //string msg = "🔔 " + orderType + " order opened on " + _Symbol + "; Lot size: " + DoubleToString(lotSize, 2) + "; Price: " + DoubleToString(price, 5) + " 🔔";
   ////--- Stop Loss and Take Profit Update:
   //double stopLoss = 1.12000;  // Example stop loss
   //double takeProfit = 1.13000;  // Example take profit
   //string msg = "🔄 Stop Loss and Take Profit Updated on " + _Symbol + "; Stop Loss: " + DoubleToString(stopLoss, 5) + "; Take Profit: " + DoubleToString(takeProfit, 5) + " 🔄";
   ////--- Daily Performance Summary:
   //double profitToday = 150.00;  // Example profit for the day
   //string msg = "📅 Daily Performance Summary 📅; Symbol: " + _Symbol + "; Profit Today: $" + DoubleToString(profitToday, 2);
   ////--- Trade Closed Notification:
   //string orderType = "BUY";  // or "SELL"
   //double profit = 50.00;  // Example profit
   //string msg = "❌ " + orderType + " trade closed on " + _Symbol + "; Profit: $" + DoubleToString(profit, 2) + " ❌";
   
//   ////--- Account Status Update:
//   double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY);
//   double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
//   string 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(msg);
//   msg = encloded_msg;

   double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   
   double Price_Open = iOpen(_Symbol,_Period,1);
   double Price_Close = iClose(_Symbol,_Period,1);
   
   bool isBuySignal = Price_Open < Price_Close;
   bool isSellSignal = Price_Open >= Price_Close;
   
   double lotSize = 0, openPrice = 0,stopLoss = 0,takeProfit = 0;
   
   if (isBuySignal == true){
      lotSize = 0.01;
      openPrice = Ask;
      stopLoss = Bid-1000*_Point;
      takeProfit = Bid+1000*_Point;
      obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
   }
   else if (isSellSignal == true){
      lotSize = 0.01;
      openPrice = Bid;
      stopLoss = Ask+1000*_Point;
      takeProfit = Ask-1000*_Point;
      obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
   }
   
   string position_type = isBuySignal ? "Buy" : "Sell";
   
   ushort MONEYBAG = 0xF4B0;
   string MONEYBAG_Emoji_code = ShortToString(MONEYBAG);
   string msg =  "\xF680 OPENED "+position_type+" 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;
   
   // Construct the URL for the Telegram API request to send a message
   // Format: https://api.telegram.org/bot{HTTP_API_TOKEN}/sendmessage?chat_id={CHAT_ID}&text={MESSAGE_TEXT}
   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, "", 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
}

我们现在需要加入基于移动平均线交叉的交易信号。首先,我们需要声明两个移动平均线指标句柄及其数据存储数组。

int handleFast = INVALID_HANDLE; // -1
int handleSlow = INVALID_HANDLE; // -1

double bufferFast[];
double bufferSlow[];

long magic_no = 1234567890;

首先,我们声明名为 “handleFast” 和 “handleSlow” 的整数数据类型变量,分别用于容纳快速和慢速移动平均指标。我们将句柄初始化为 “INVALID_HANDLE”,即 -1 值,表示它们当前没有引用任何有效的指标实例。然后,我们定义两个 double 型数组 “bufferFast” 和 “bufferSlow”,分别用于存储从快速和慢速指标中读取到的值。最后,我们声明一个 “long” 型变量来存储我们开设仓位的幻数。整个逻辑都放在全局范围内。

OnInit 函数中,我们初始化指标句柄并将存储数组设置为时间序列。 

   handleFast = iMA(Symbol(),Period(),20,0,MODE_EMA,PRICE_CLOSE);
   if (handleFast == INVALID_HANDLE){
      Print("UNABLE TO CREATE FAST MA INDICATOR HANDLE. REVERTING NOW!");
      return (INIT_FAILED);
   }

在这里,我们为快速移动平均指标创建一个句柄。这是使用 iMA 函数完成的,该函数使用 “Symbol”、“Period”、20、0、“MODE_EMA” 和 “PRICE_CLOSE” 参数进行调用。第一个参数 “Symbol” 是一个内置函数,它返回当前资产工具的名称。第二个参数 “Period” 返回当前时间框架。下一个参数 20 是移动平均线的周期数。第四个参数 0 表示我们希望将移动平均线应用于最近的价格柱。第五个参数 “MODE_EMA” 表示我们要计算指数移动平均线(EMA)。最后一个参数是 “PRICE_CLOSE”,表示我们根据收盘价计算移动平均线。此函数返回一个唯一标识此移动平均线指标实例的句柄,我们将其分配给 “handleFast”。

一旦我们尝试创建指标,我们就会验证句柄是否有效。“handleFast” 的结果为 “INVALID_HANDLE”,这表明我们无法为快速移动平均线指标创建句柄。在这种情况下,我们将严重级别为错误的消息打印到日志中。该消息发送给用户,指出程序 “无法创建快速 MA 指标句柄。立即恢复!”消息中明确指出,没有句柄意味着没有指标,这意味着我们无法创建指标句柄。因为没有这个指标,就没有交易系统,从而导致程序毫无用处,继续运行它也没有意义。由于我们遇到了失败,因此我们返回 “INIT_FAILED” 而不再进行任何进一步的操作。这将停止程序进一步运行并将其从图表中删除。

同样的逻辑也适用于慢速指标。

   handleSlow = iMA(Symbol(),Period(),50,0,MODE_SMA,PRICE_CLOSE);
   if (handleSlow == INVALID_HANDLE){
      Print("UNABLE TO CREATE FAST MA INDICATOR HANDLE. REVERTING NOW!");
      return (INIT_FAILED);
   }

如果您打印这些指标句柄,您将获得一个起始值 10,并且如果有更多的指标句柄,则它们的值将为每个句柄增加 1。让我们打印它们并看看会得到什么。我们通过以下代码实现这一点:

   Print("HANDLE FAST MA = ",handleFast);
   Print("HANDLE SLOW MA = ",handleSlow);

我们得到以下输出:

指标句柄打印输出

最后,我们将数据存储数组设置为时间序列,并设置幻数。

   ArraySetAsSeries(bufferFast,true);
   ArraySetAsSeries(bufferSlow,true);
   obj_Trade.SetExpertMagicNumber(magic_no);

通过使用 ArraySetAsSeries 函数可以将数组设置为时间序列。

OnDeinit 函数中,我们借助 IndicatorRelease 函数从计算机内存中释放指标句柄,并借助 ArrayFree 函数释放存储数组。这确保我们释放计算机上不必要的进程,从而节约其资源。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   // Code to execute when the expert is deinitialized
   
   IndicatorRelease(handleFast);
   IndicatorRelease(handleSlow);
   ArrayFree(bufferFast);
   ArrayFree(bufferSlow);
   
}

OnTick 事件处理函数中,我们执行将利用指标句柄并检查信号生成的代码。这是每次价格变动(即价格报价发生变化)时调用的函数,以获取最新价格。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   // Code to execute on every tick event

   ...

}

这是我们需要检索指标值的事件处理程序。

   if (CopyBuffer(handleFast,0,0,3,bufferFast) < 3){
      Print("UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS. REVERTING");
      return;
   }

首先,我们尝试使用 CopyBuffer 函数从快速移动平均线指标缓冲区中获取数据。我们用以下参数调用它:“handleFast”、0、0、3 和 “bufferFast”。第一个参数 “handleFast” 是我们从中获取指标值的目标指标。第二个参数是缓冲区编号,我们从中获取值,通常显示在数据窗口上,对于移动平均值,它始终为 0。第三个参数是我们从中获取值的柱形索引的起始位置,在本例中为 0 表示当前柱形。第四个参数是需要获取的值的数量,即柱形的数量。在这种情况下,3 表示从当前柱开始的前 3 个柱。最后一个参数是 “bufferFast”,它是我们存储 3 个读取到的值的目标数组。

现在,我们检查函数是否成功读取了请求的值,即 3。如果返回值小于 3,则表示该函数无法获取请求的数据。在这种情况下,我们会打印一条错误消息,指出“无法获取请求的数据以进行进一步分析。立即恢复!”这通知我们数据获取失败,并且由于我们没有足够的数据进行该过程,我们无法继续扫描信号。然后我们返回,停止该程序部分的进一步执行,并等待下一个分时报价。

读取慢速移动平均线的数据也采用相同的过程。

   if (CopyBuffer(handleSlow,0,0,3,bufferSlow) < 3){
      Print("UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS. REVERTING");
      return;
   }

由于 OnTick 函数在每个分时报价上运行,我们必须开发一种逻辑来确保每个柱形运行一次信号扫描代码。具体逻辑如下。

   int currBars = iBars(_Symbol,_Period);
   static int prevBars = currBars;
   if (prevBars == currBars) return;
   prevBars = currBars;

首先,我们声明一个整数变量 “currBars”,它存储了图表上根据指定的交易品种和周期(或者更确切地说是时间框架,正如您可能听说过的)计算出的当前柱形数量。这是通过使用 iBars 函数实现的,该函数只需两个参数,即交易品种和周期。 

然后,我们声明另一个静态整数变量 “prevBars”,以在生成新柱形时存储图表上先前柱形的总数,并在函数首次运行时使用图表上当前柱形的值对其进行初始化。我们将使用它来比较当前的柱形数量与之前的柱形数量,以确定图表上新柱形生成的实例。

最后,我们使用条件语句来检查当前的柱数是否等于之前的柱数。如果它们相等,则意味着没有形成新的柱,因此我们终止进一步执行并返回。否则,如果当前和前一个柱形计数不相等,则表明已形成新的柱形。在这种情况下,我们继续将前一个柱形变量更新为当前柱形变量,以便在下一个分时报价时,它将等于图表上的柱形数量,除非我们升级到新的柱形变量。

然后,我们定义变量,可以轻松地存储数据以供进一步分析,如下所示。

   double fastMA1 = bufferFast[1];
   double fastMA2 = bufferFast[2];
   
   double slowMA1 = bufferSlow[1];
   double slowMA2 = bufferSlow[2];

有了这些变量,我们现在可以检查交叉并采取必要的措施。

   if (fastMA1 > slowMA1 && fastMA2 <= slowMA2){
      for (int i = PositionsTotal()-1; i>= 0; i--){
         ulong ticket = PositionGetTicket(i);
         if (ticket > 0){
            if (PositionSelectByTicket(ticket)){
               if (PositionGetString(POSITION_SYMBOL) == _Symbol &&
                  PositionGetInteger(POSITION_MAGIC) == magic_no){
                  if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
                     obj_Trade.PositionClose(ticket);
                  }
               }
            }
         }
      }
      double lotSize = 0.01;
      double openPrice = Ask;
      double stopLoss = Bid-1000*_Point;
      double takeProfit = Bid+1000*_Point;
      obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
   }

在这里,我们寻找一个特定的交叉条件:如果最近的快速移动平均线(fastMA1)大于相应的慢速移动平均线(slowMA1),并且之前的快速移动平均线(fastMA2)小于或等于之前的慢速移动平均线(slowMA2),那么我们正在看一个看涨交叉,这表明潜在的买入信号。 

当发现看涨交叉时,我们会循环检查当前仓位,以检查是否有任何未平仓的卖出仓位以进行新的买入。如果需要,我们会在开设新的买入仓位之前关闭卖出仓位。我们从最近的仓位开始,一直到最远的仓位。

对于每个交易仓位,我们使用 PositionGetTicket 函数获取编号。如果编号大于 0,意味着我们确实有一个有效的编号,并且我们通过函数 PositionSelectByTicket 选择仓位,我们继续检查该仓位是否有效,并验证它是否属于当前交易品种和幻数。如果该仓位是卖出仓位,我们使用函数 “obj_Trade.PositionClose” 来平仓。在关闭所有现有的卖出仓位后,我们开立一个新的买入仓位,并设置交易参数:手数、开仓价、止损和止盈。一旦开仓,我们会通过向日志发送记录的方式,通知用户该情况。

      // BUY POSITION OPENED. GET READY TO SEND MESSAGE TO TELEGRAM
      Print("BUY POSITION OPENED. SEND MESSAGE TO TELEGRAM NOW.");

最后,我们发送一条与程序初始化部分相同的消息。

      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;

对于卖出交叉信号,相同的代码结构保留,但条件相反。

   else if (fastMA1 < slowMA1 && fastMA2 >= slowMA2){
      for (int i = PositionsTotal()-1; i>= 0; i--){
         ulong ticket = PositionGetTicket(i);
         if (ticket > 0){
            if (PositionSelectByTicket(ticket)){
               if (PositionGetString(POSITION_SYMBOL) == _Symbol &&
                  PositionGetInteger(POSITION_MAGIC) == magic_no){
                  if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
                     obj_Trade.PositionClose(ticket);
                  }
               }
            }
         }
      }
      double lotSize = 0.01;
      double openPrice = Bid;
      double stopLoss = Ask+1000*_Point;
      double takeProfit = Ask-1000*_Point;
      obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
      
      // SELL POSITION OPENED. GET READY TO SEND MESSAGE TO TELEGRAM
      Print("SELL POSITION OPENED. SEND MESSAGE TO TELEGRAM NOW.");

到此为止,代码结构已经基本完成。我们现在要做的就是在加载程序后自动将指标添加到图表中以进行可视化。因此,在初始化事件处理程序上,我们精心设计了自动添加指示符的逻辑,如下所示:

   //--- Add indicators to the chart automatically
   ChartIndicatorAdd(0,0,handleFast);
   ChartIndicatorAdd(0,0,handleSlow);

这里我们只是调用 ChartIndicatorAdd 函数来将指标添加到图表中,其中第一个和第二个参数分别指定图表窗口和子窗口。第三个参数是要添加的指标句柄。

因此,负责信号生成和通道化的完整 OnTick 事件处理函数代码如下:

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   // Code to execute on every tick event
   
   if (CopyBuffer(handleFast,0,0,3,bufferFast) < 3){
      Print("UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS. REVERTING");
      return;
   }
   if (CopyBuffer(handleSlow,0,0,3,bufferSlow) < 3){
      Print("UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS. REVERTING");
      return;
   }
   
   double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
   
   int currBars = iBars(_Symbol,_Period);
   static int prevBars = currBars;
   if (prevBars == currBars) return;
   prevBars = currBars;
   
   double fastMA1 = bufferFast[1];
   double fastMA2 = bufferFast[2];
   
   double slowMA1 = bufferSlow[1];
   double slowMA2 = bufferSlow[2];
   
   if (fastMA1 > slowMA1 && fastMA2 <= slowMA2){
      for (int i = PositionsTotal()-1; i>= 0; i--){
         ulong ticket = PositionGetTicket(i);
         if (ticket > 0){
            if (PositionSelectByTicket(ticket)){
               if (PositionGetString(POSITION_SYMBOL) == _Symbol &&
                  PositionGetInteger(POSITION_MAGIC) == magic_no){
                  if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
                     obj_Trade.PositionClose(ticket);
                  }
               }
            }
         }
      }
      double lotSize = 0.01;
      double openPrice = Ask;
      double stopLoss = Bid-1000*_Point;
      double takeProfit = Bid+1000*_Point;
      obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
      
      // 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;
   
      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, "", 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());
      }

      
   }
   else if (fastMA1 < slowMA1 && fastMA2 >= slowMA2){
      for (int i = PositionsTotal()-1; i>= 0; i--){
         ulong ticket = PositionGetTicket(i);
         if (ticket > 0){
            if (PositionSelectByTicket(ticket)){
               if (PositionGetString(POSITION_SYMBOL) == _Symbol &&
                  PositionGetInteger(POSITION_MAGIC) == magic_no){
                  if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
                     obj_Trade.PositionClose(ticket);
                  }
               }
            }
         }
      }
      double lotSize = 0.01;
      double openPrice = Bid;
      double stopLoss = Ask+1000*_Point;
      double takeProfit = Ask-1000*_Point;
      obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit);
      
      // 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;
   
      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, "", 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());
      }
      
   }
   
}
//+------------------------------------------------------------------+

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


测试集成

为了测试集成,我们通过注释掉初始化测试逻辑来禁用它,以防止打开许多信号,转移到较低的 1 分钟时段,并将指标周期更改为 5 和 10 以生成更快的信号。以下是我们取得的里程碑成果。

交易终端卖出信号确认:

MT5 卖出信号

Telegram 卖出信号确认:

Telegram 卖出信号

交易终端买入信号确认:

MT5 买入信号

Telegram 买入信号确认:

Telegram 买入信号

从图片中可以看出,集成工作已成功完成。有一个信号扫描,一旦确认,其详细信息将被编码在单个消息中并从交易终端发送到 Telegram 群聊。至此,我们成功实现了我们的目标。


结论

总之,本文在推动我们的集成 EA 交易 MQL5-Telegram 方面取得了长足的进步,实现了将交易信号直接从交易终端发送到 Telegram 聊天的主要目标。然而,我们并不局限于仅仅建立 MQL5 和 Telegram 之间的通信渠道,就像本系列第 1 部分中所做的那样。相反,我们专注于实际的交易信号本身,使用流行的移动平均线交叉技术分析工具。我们详细解释了这些信号的逻辑,以及目前通过 Telegram 发送这些信号的强大系统。其结果是我们的集成 EA 交易的整体设置有了显著的进步。

在本文中,我们仔细研究了如何生成和发送这些信号的技术工作原理。我们仔细研究了如何安全地编码和发送消息、如何管理指标句柄以及如何根据检测到的信号执行交易。我们编写了代码并将其与 Telegram 集成,以便我们在离开交易平台时可以立即通知自己交易信号。本文提供的实际示例和详细解释应该可以让您清楚地了解如何使用您的交易策略来设置类似的内容。

在本系列的第 3 部分中,我们将为 MQL5-Telegram 集成添加另一层。这次,我们将致力于研究将图表截图发送至 Telegram 的解决方案。直观分析市场和交易信号背景的能力将增强交易者的洞察力和理解力。文本信号与视觉数据相结合可以提供更有效的信号。这正是我们所追求的:不仅发送信号,还通过 Telegram 交易频道增强自动交易和态势的感知。请继续关注。


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

使用 SMA 和 EMA 自动优化止盈和指标参数的示例 使用 SMA 和 EMA 自动优化止盈和指标参数的示例
本文介绍了一种用于外汇交易的复杂 EA 交易,它能够将机器学习与技术分析相结合。它专注于交易苹果股票,具有自适应优化、风险管理和多策略的特点。回溯测试显示出良好的结果,盈利能力较高,但也有显著的回撤,表明还有进一步改进的潜力。
重构经典策略(第五部分):基于USDZAR的多品种分析 重构经典策略(第五部分):基于USDZAR的多品种分析
在本系列文章中,我们重新审视经典策略,看看是否可以使用人工智能来改进这些策略。在今天的文章中,我们将研究一种使用一篮子具有相关性的金融产品来进行多品种分析的流行策略,我们将重点关注货币对 USDZAR。
威廉·江恩(William Gann)方法(第二部分):创建江恩宫格指标 威廉·江恩(William Gann)方法(第二部分):创建江恩宫格指标
我们将基于“江恩九宫格”创建一个指标,该指标通过时间和价格方格构建而成。我们将提供指标代码,并在平台上针对不同的时间区间,对该指标进行测试。
重塑经典策略(第四部分):标普500指数与美国国债 重塑经典策略(第四部分):标普500指数与美国国债
在本系列文章中,我们使用现代算法分析经典交易策略,以确定是否可以利用人工智能改进这些策略。在今天的文章中,我们将重新审视一种利用标普500指数与美国国债之间关系的经典交易方法。