English Русский Deutsch 日本語
preview
Creación de un asesor experto integrado de MQL5 y Telegram (Parte 2): Envío de señales de MQL5 a Telegram

Creación de un asesor experto integrado de MQL5 y Telegram (Parte 2): Envío de señales de MQL5 a Telegram

MetaTrader 5Sistemas comerciales | 13 febrero 2025, 08:39
345 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introducción

En la primera parte de nuestra serie sobre el desarrollo de un Asesor Experto integrado en Telegram para MQL5, cubrimos los pasos esenciales necesarios para vincular MQL5 y Telegram. El primer paso fue crear la aplicación propiamente dicha. Después de eso, pasamos a la parte de codificación. Esperamos que el motivo de este particular orden de acontecimientos quede más claro en los próximos párrafos. El resultado es que ahora tenemos un bot que puede recibir mensajes, así como un programa que puede enviarlos. También hemos escrito un programa MQL5 simple que demuestra cómo enviar un mensaje a través del bot a la aplicación.

Después de haber establecido las bases en la Parte 1, ahora podemos proceder al siguiente paso: transmitir señales comerciales a Telegram usando MQL5. Nuestro Asesor Experto recientemente mejorado hace algo bastante notable: no solo abre y cierra operaciones según condiciones preestablecidas, sino que también realiza la hazaña igualmente impresionante de transmitir una señal a un chat grupal de Telegram para informarnos que se ejecutó una operación. Las señales comerciales en sí mismas han pasado por un pequeño cambio de imagen, garantizando que la información que enviamos a Telegram sea lo más clara y concisa posible. Nuestro "Chatty Trader" hace un mejor trabajo hablando con el grupo en Telegram que nuestra versión anterior, y lo hace al mismo ritmo o más rápido que el que tenía nuestro antiguo "Chatty Trader", lo que significa que podemos esperar recibir señales casi en tiempo real a medida que se toman o se cierran operaciones.

Generaremos señales basadas en el famoso sistema de cruce de medias móviles y retransmitiremos las señales generadas. Además, si recuerdas, en la parte 1 de la serie, teníamos un solo mensaje que podía ser bastante largo y, si alguien quería agregar segmentos al mensaje, se producía un error. De esta forma, sólo se podría enviar un único mensaje a la vez y, si hubiera segmentos adicionales, tendrían que transmitirse en diferentes mensajes individuales. Por ejemplo, enviar "A buy signal has been generated" y "Open a buy order", sería un único mensaje largo o dos mensajes cortos. En esta parte, los concatenaremos y modificaremos el mensaje para que un solo mensaje pueda contener varios segmentos de texto y caracteres. Discutiremos todo el proceso en los siguientes subtemas:

  1. Visión general de la estrategia
  2. Implementación en MQL5
  3. Probando la integración
  4. Conclusión

Al final, habremos creado un asesor experto que envía información comercial, como las señales que se han generado y las órdenes realizadas desde la terminal comercial al chat de Telegram especificado. Empecemos.


Visión general de la estrategia

Producimos señales de trading con cruces de medias móviles, una de las herramientas de análisis técnico más utilizadas. Describiremos lo que consideramos el método más sencillo y claro para utilizar cruces de medias móviles para intentar identificar posibles oportunidades de compra o venta. Esto se basa en la naturaleza de señalización de los propios cruces, sin la adición de ninguna otra herramienta o indicador. Para simplificar, consideraremos solo dos promedios móviles de diferentes períodos: un promedio móvil de corto plazo y un promedio móvil de largo plazo.

Exploraremos la función de los cruces de medias móviles y cómo generan señales comerciales sobre las que podemos actuar. Los promedios móviles toman datos de precios y los suavizan, creando una especie de línea fluida que es mucho mejor para la identificación de tendencias que el gráfico de precios real. Esto se debe a que, en general, un promedio es siempre más preciso y más fácil de seguir que una línea irregular. Cuando se suman dos medias móviles de diferentes períodos, en algún momento se cruzarán, de ahí el término "cruce". 

Para poner en práctica las señales de cruce de medias móviles utilizando MQL5, comenzaremos por determinar los períodos de corto y largo plazo del promedio que más se alinean con nuestra estrategia comercial. Para este propósito, utilizaremos períodos estándar como 50 y 200 para las tendencias a largo plazo y 10 y 20 para las tendencias a corto plazo. Después de calcular los promedios móviles, compararemos los valores de los eventos de cruce en cada nuevo tick o barra y convertiremos estas señales de cruce detectadas en eventos binarios de "compra" o "venta" para que nuestro Asesor Experto actúe. Para entender fácilmente lo que queremos decir, visualicemos los dos casos.

Cruce ascendente:

CRUCE ASCENDENTE

Cruce descendente:

CRUCE DESCENDENTE

Estas señales generadas se combinarán con nuestro actual marco de mensajería MQL5-Telegram. Para lograr esto, se adaptará el código de la Parte 1 para abarcar la detección y el formato de señales. Al identificar un cruce, se creará un mensaje con el nombre del activo, la dirección del cruce (compra/venta) y el tiempo de la señal. La entrega oportuna de este mensaje a un chat de Telegram designado garantizará que nuestro grupo comercial se mantenga informado sobre posibles oportunidades comerciales. Aparte de cualquier otra cosa, la seguridad de recibir un mensaje justo después de que se haya producido el cruce significa que tendremos la oportunidad de iniciar una operación basada en la señal en cuestión, o incluso abrir una posición de mercado y transmitir los detalles de la posición.


Implementación en MQL5

Primero, nos aseguraremos de que podamos segmentar nuestro mensaje y enviarlo como un todo. En la primera parte, cuando enviamos un mensaje complejo que incluye caracteres especiales como saltos de línea, recibimos un error y solo podemos enviarlo como un mensaje único, sin estructura. Por ejemplo, teníamos este fragmento de código que obtiene el evento de inicialización, el capital de la cuenta y el margen libre disponible:

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

Enviando esto como un todo, esto es lo que obtenemos:

MENSAJE LARGO

Podemos ver que aunque podemos enviar el mensaje, su estructura no es atractiva. La oración de inicialización debe estar en la primera línea, luego el estado de la cuenta en la segunda línea, el capital en la línea siguiente y la información del margen libre en la última línea. Para lograr esto, es necesario considerar un nuevo carácter de salto de línea "\n" de la siguiente manera.

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

Sin embargo, cuando ejecutamos el programa, recibimos un mensaje de error en el diario como se muestra y el mensaje no se envía al chat de Telegram:

ERROR DE SALTO DE LÍNEA NUEVO

Para asegurarnos de que el mensaje se envíe correctamente, debemos codificarlo. Nuestra integración requiere la codificación de nuestros mensajes para manejar adecuadamente los caracteres especiales. Por ejemplo, si nuestro mensaje contiene algo parecido a un espacio o se comporta como un símbolo ("&", "?", etc.), estos podrían ser malinterpretados por la Interfaz de Programación de Aplicaciones (Application Programming Interface, API) de Telegram debido a una precaución insuficiente de nuestra parte durante la integración. Nos tomamos esto muy en serio, no es ninguna broma. Hemos visto otros usos de la codificación de caracteres, por ejemplo al abrir algunos tipos de documentos en nuestras computadoras, como se muestra.

CODIFICACIÓN DE DOCUMENTOS

La codificación es la clave para no tener el tipo de problemas que hemos encontrado hasta ahora, en que la API no entiende lo que intentamos enviarle para que pueda hacer lo que queremos que haga.

Por ejemplo, un mensaje enviado a la API que contenga un carácter especial podría interferir con la estructura del Localizador Uniforme de Recursos (Uniform Resource Locator, URL) -la forma en que la URL es "vista" por los ordenadores- y provocar errores de interpretación. La API podría interpretar el carácter especial como una instrucción o alguna otra parte del código en lugar de como parte del mensaje real. Este fallo de comunicación podría producirse en cualquiera de los dos extremos: al enviar el mensaje desde el programa o al recibirlo en el otro extremo de la codificación no está realizando su función principal de hacer que la parte no vista del mensaje sea segura para "ver". Además, usar el esquema de codificación significa que tenemos un mensaje en un formato compatible con el extremo receptor. La API de Telegram en este caso. Al fin y al cabo, en esta historia intervienen varios sistemas diferentes, y cada uno tiene requisitos específicos sobre cómo quiere que se le transmitan los datos. Por lo tanto, lo primero que haremos será crear una función que codifique nuestros mensajes.

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

   ...

}

Aquí, comenzamos creando una función de tipo de datos string llamada "UrlEncode" que toma un único parámetro o argumento, texto de tipo string, que está diseñada para convertir el texto proporcionado en un formato codificado como URL. A continuación, inicializamos una cadena string vacía, "encodedText", que se utilizará para construir el resultado codificado en URL a medida que procesamos el texto de entrada. A continuación, determinamos la longitud de la cadena de entrada mediante la función StringLen , almacenando esta longitud en la variable entera "textLength". Este paso es crucial ya que nos permite saber cuántos caracteres necesitamos procesar. Al almacenar la longitud, podemos iterar eficientemente a través de cada carácter de la cadena en un bucle, garantizando que todos los caracteres estén codificados correctamente de acuerdo con las reglas de codificación de URL. Para el proceso de iteración, necesitaremos utilizar un bucle.

    // 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
   
        ...

    }

Aquí, iniciamos un bucle for para iterar a través de todos los caracteres contenidos en el mensaje o texto de entrada, empezando por el primero en el índice 0 en adelante. Obtenemos el valor del símbolo seleccionado utilizando la función StringGetCharacter, que normalmente devuelve el valor de un símbolo, situado en la posición especificada de una cadena. La posición viene definida por el índice "i". Almacenamos el carácter en una variable ushort llamada "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);
        }

Aquí comprobamos si un carácter dado es alfanumérico o uno de los caracteres no reservados que se utilizan habitualmente en las URL. El objetivo es determinar si el carácter debe codificarse o si puede añadirse directamente a la cadena codificada. En primer lugar, comprobamos si el carácter es un dígito verificando si su valor ASCII se encuentra entre 48 y 57. A continuación, comprobamos si el carácter es una letra mayúscula viendo si su valor ASCII se encuentra entre 65 y 90. Del mismo modo, comprobamos si el carácter es una letra minúscula confirmando si su valor ASCII se encuentra entre 97 y 122. Estos valores pueden confirmarse a partir de la "tabla ASCII".

Caracteres numéricos; 48 a 57:

DÍGITOS

Caracteres en mayúsculas; 65 a 90:

LETRAS MAYÚSCULAS

Caracteres en minúsculas; 97 a 122:

LETRAS MINÚSCULAS

Además de estos caracteres alfanuméricos, también verificamos caracteres específicos no reservados utilizados en las URL. Entre ellos se incluyen '!', ''', '(', ')', '*', '-', '.', '_' y '~'. Si el carácter coincide con alguno de estos criterios, significa que se trata de un carácter alfanumérico o de uno de los caracteres no reservados.

Cuando el carácter cumple alguna de estas condiciones, lo añadimos a la cadena "encodedText" sin codificarlo. Esto se consigue convirtiendo el carácter a su representación en cadena mediante la función ShortToString, que garantiza que el carácter se añade a la cadena codificada en su forma original. Si no se cumple ninguna de estas condiciones, pasamos a comprobar si hay caracteres de espacio.

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

Aquí, utilizamos una sentencia else if para comprobar si el carácter es un espacio comparándolo con el carácter espacio. Si el carácter es efectivamente un espacio, tenemos que codificarlo de forma que sea apropiado para las URL. En lugar de utilizar la típica codificación porcentual para los espacios (%20), como hemos visto en el caso de los documentos informáticos, optamos por codificar los espacios como el signo más '+', que es otro método habitual para representar espacios en las URL, sobre todo en el componente de consulta. Así, convertimos el signo más '+' a su representación de cadena utilizando la función ShortToString y luego lo añadimos a la cadena "encodedText".

Si hasta aquí hemos llegado con caracteres sin codificar, significa que tenemos un quebradero de cabeza entre manos porque se trata de caracteres complejos como los emojis. Por lo tanto, tendremos que tratar todos los caracteres que no sean alfanuméricos, no reservados o espacios codificándolos mediante Unicode Transformation Format-8 (UTF-8), garantizando que cualquier carácter que no entre en las categorías comprobadas anteriormente se codifique de forma segura para su inclusión en una 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]);
            }
        }

En primer lugar, declaramos una matriz "utf8Bytes" para contener la representación en bytes del carácter en Formato de Transformación Unicode-8 (UTF-8). Luego llamamos a la función "ShortToUtf8", pasando el "carácter" y la matriz "utf8Bytes" como argumentos. Explicaremos la función en breve, pero por ahora, basta con saber que la función convierte el carácter a su representación UTF-8 y devuelve el número de bytes utilizados en la conversión, almacenando estos bytes en el array "utf8Bytes".

A continuación, utilizamos un bucle "for" para iterar sobre cada byte de la matriz "utf8Bytes". Para cada byte, lo convertimos a su representación hexadecimal prefijada con el carácter "%", que es la forma estándar de codificar porcentualmente los caracteres en las URL. Utilizamos la función "StringFormat" para formatear cada byte como un número hexadecimal de dos dígitos con un prefijo '%'. Por último, añadimos esta representación codificada a la cadena "encodedText". Al final, sólo devolvemos los resultados.

    return encodedText; // Return the URL-encoded string

El fragmento de código de la función completa es el siguiente:

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

Veamos ahora la función responsable de convertir los caracteres a su representación UTF-8.

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

   ...

}

La función es de tipo de datos integer y toma dos parámetros de entrada, el valor del carácter y la matriz de salida. 

En primer lugar, convertimos los caracteres de un solo byte.

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

La conversión de caracteres de un solo byte, que tienen valores comprendidos entre 0x00 y 0x7F, es sencilla, ya que se representan directamente en UTF-8 en un solo byte. Primero comprobamos si el carácter es inferior a 0x80. Si lo es, redimensionamos el array "utf8Output" a un solo byte utilizando la función ArrayResize. Esto nos permite tener el tamaño correcto para la representación UTF-8 de salida. A continuación, pegamos el carácter en el primer elemento de la matriz lanzando el carácter a un uchar, haciendo una Conversión de tipos. Esto equivaldría a copiar el valor del carácter en la matriz. Devolvemos 1, indicando que la representación UTF-8 tiene una longitud de un byte. Este proceso gestionará eficazmente la conversión de cualquier carácter de un byte a su forma UTF-8, independientemente del sistema operativo.

Su representación sería la siguiente.

0x00, UTF-8:

0x00 UTF-8

0x7F, UTF-8:

0x7F UTF-8

Puede ver que la representación decimal de los números va de 0 a 127. Puede observar de nuevo que estos caracteres son idénticos a los caracteres Unicode iniciales. Probablemente se pregunte qué es todo esto. Hagamos una pausa y profundicemos. En notación hexadecimal, 0x80 y 0x7F representan valores específicos que pueden convertirse a decimal para una mejor comprensión. El número hexadecimal 0x80 equivale a 128 en decimal. Esto se debe a que el hexadecimal es un sistema numérico de base-16, en el que cada dígito representa una potencia de 16. En 0x80, el "8" representa 8 veces 16^1 (que es 128) y el "0" representa 0 veces 16^0 (que es 0), lo que da un total de 128.

Por otro lado, 0x7F equivale a 127 en decimal. En hexadecimal, "7F" significa 7 veces 16^1 más 15 veces 16^0. Calculando esto, obtenemos 7 veces 16 (que es 112) más F (que es 15), resultando un total de 127. Véase la representación de A-F a continuación. El decimal bajo el hexadecimal F es igual a 15.

HEX, A-F

Así, 0x80 es 128 en decimal, y 0x7F es 127 en decimal. Esto significa que 0x80 es sólo uno más que 0x7F, por lo que es el límite donde la representación de un solo byte en la codificación UTF-8 cambia a una representación multibyte.

Sólo queríamos asegurarnos de que estas explicaciones son detalladas y de que no te preguntarás sobre los formatos de los procedimientos y cómo todo tiene sentido. Ahora ya lo sabes. Pasemos ahora a los caracteres de 2 bytes.

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


Aquí nos encargamos de convertir los caracteres que necesitan dos bytes en su representación UTF-8, es decir, los caracteres cuyos valores se sitúan entre 0x80 y 0x7FF. Para ello, primero comprobamos si el carácter en cuestión es inferior a 0x800 (2048 en decimal), lo que garantiza que efectivamente se encuentra en este rango. Si se cumple esta condición, redimensionamos la matriz "utf8Output" para que contenga dos bytes (ya que se necesitarán dos bytes para representar el carácter en UTF-8). A continuación, calculamos la representación UTF-8 real.

El primer byte se obtiene tomando el carácter, desplazándolo 6 bits a la derecha y combinándolo con 0xC0 mediante la operación OR lógica. Este cálculo establece los bits más significativos del primer byte en el prefijo UTF-8 para un carácter de dos bytes. El segundo byte se calcula enmascarando el carácter con 0x3F para obtener los 6 bits inferiores y combinándolo con 0x80. Esta operación garantiza que el segundo byte tenga el prefijo UTF-8 correcto.

Al final, colocamos estos dos bytes en la matriz "utf8Output" y devolvemos 2 a la persona que llama, indicando que el carácter requiere dos bytes en su representación UTF-8. Esta es la codificación necesaria y correcta para un carácter que utiliza el doble de bits que un carácter de un byte. A continuación, tenemos los caracteres de 3 bytes.

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

        ...

    }

A estas alturas, ya entiende lo que esto significa. Aquí, el número hexadecimal "0xFFFF" se convierte a 65.535 en decimal. Reconocemos que cada dígito hexadecimal representa una potencia de 16. Para "0xFFFF", cada dígito es "F", que es 15 en decimal (como ya lo habíamos visto). Para calcular su valor decimal, evaluamos la contribución de cada dígito en función de su posición. Comenzamos con el valor posicional más alto, que es (15 * 16^3), lo que nos da (15 * 4096 = 61.440). A continuación, calculamos (15 * 16^2), que es igual a (15 * 256 = 3.840). Entonces, (15 * 16^1) da como resultado (15 * 16 = 240). Finalmente, (15 * 16^0) es igual a (15 * 1 = 15). Sumando estos resultados, obtenemos 61.440 + 3.840 + 240 + 15, lo que da un total de 65.535. Por lo tanto, "0xFFFF" es 65.535 en decimal. Teniendo esto en mente, podría haber tres instancias de caracteres de 3 bytes. Veamos el primer caso.

        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
        }

Aquí, manejamos caracteres que se encuentran dentro del rango Unicode 0xD800 a 0xDFFF, que se conocen como mitades sustitutas y no son válidos como caracteres independientes. Comenzamos comprobando si el personaje está dentro de este rango. Cuando encontramos un carácter mal formado, primero redimensionamos la matriz "utf8Output" para que contenga solo un byte, lo que garantiza que nuestra matriz de salida esté preparada para almacenar solo un byte.

A continuación, reemplazamos el carácter no válido con un carácter de espacio estableciendo el primer elemento de la matriz "utf8Output" en un espacio. Esta opción es un marcador de posición para manejar la entrada no válida con elegancia. Finalmente, devolvemos 1, lo que indica que la representación UTF-8 de este carácter mal formado tiene una longitud de un byte. A continuación, comprobamos los caracteres emoji. Esto significa que tratamos con caracteres que se encuentran dentro del espectro Unicode de 0xE000 a 0xF8FF. Estos caracteres incluyen emojis y otros símbolos extendidos.

        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
        }

Comenzamos determinando si el personaje cae dentro de este rango de emoji. Dado que los caracteres que se encuentran dentro de este rango requieren una representación de cuatro bytes en UTF-8, primero ampliamos el valor del carácter realizando un OR bit a bit con 0x10000. Este paso nos permite procesar correctamente los caracteres de los planos suplementarios.

Posteriormente redimensionamos la matriz "utf8Output" a cuatro bytes. Esto garantiza que tengamos suficiente espacio para almacenar toda la codificación UTF-8 en la matriz. El cálculo para la representación UTF-8, entonces, se basa en derivar y combinar las cuatro partes (los cuatro bytes). Para el primer byte, tomamos el "carácter extendido" y lo desplazamos a la derecha 18 bits. Luego combinamos lógicamente (usando la operación OR bit a bit, o |) este valor con 0xF0 para obtener los bits "altos" apropiados para el primer byte. Para el segundo byte, desplazamos el "extendedCharacter" 12 bits a la derecha y utilizamos una técnica similar para obtener la siguiente parte.

De manera similar, calculamos el tercer byte desplazando a la derecha el carácter extendido 6 bits y enmascarando los siguientes 6 bits. Combinamos esto con 0x80 para obtener la primera parte del tercer byte. Para obtener la segunda parte, enmascaramos el carácter extendido con 0x3F (que nos da los últimos 6 bits del carácter extendido) y lo combinamos con 0x80. Después de calcular y almacenar estos dos bytes en la matriz "utf8Output", devolvemos 4, lo que indica que el carácter ocupa 4 bytes en UTF-8. Por ejemplo, podríamos tener un carácter emoji 1F4B0. Ese es el emoji de la bolsa de dinero.

EMOJIS DE DINERO

Para calcular su representación decimal, comenzamos convirtiendo los dígitos hexadecimales a valores decimales. El dígito 1 en el lugar 16^4 contribuye 1×65,536=65.536. El dígito F, que es 15 en decimal, en el lugar 16^3 contribuye 15×4,096=61.440. El dígito 4 en el lugar 16^2 contribuye 4×256=1.024. El dígito B, que es 11 en decimal, en el lugar 16^1, contribuye 11×16=176. Finalmente, el dígito 0 en el lugar 16^0 contribuye 0×1=0.

Sumando estas contribuciones, obtenemos 65.536+61.440+1.024+176+0=128.176. Por lo tanto, 0x1F4B0 se convierte a 128.176 en decimal. Puedes confirmarlo en la imagen proporcionada.

Por último, abordamos los caracteres que quedan fuera de los rangos específicos manejados anteriormente y necesitan una representación UTF-8 de tres bytes.

        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
        }

Comenzamos redimensionando la matriz "utf8Output" para que pueda contener los tres bytes necesarios. Cada byte tiene un tamaño de 8, por lo que para almacenar tres bytes, necesitamos espacio para 24 bits. Luego calculamos byte por byte cada uno de los tres bytes de la codificación UTF-8. El primer byte se determina desde la parte superior del carácter. Para calcular el segundo byte, desplazamos el carácter 6 bits hacia la derecha, enmascaramos el valor resultante para obtener los siguientes 6 bits y combinamos esto con 0x80 para establecer los bits de continuación. Obtener el tercer byte es conceptualmente lo mismo, excepto que no realizamos ningún desplazamiento. En lugar de ello, enmascaramos para obtener los últimos 6 bits y los combinamos con 0x80. Después de determinar los tres bytes, que están almacenados en la matriz "utf8Output", devolvemos 3, lo que indica que la representación abarca tres bytes.

Por último, tenemos que manejar los casos en los que el carácter no es válido o no se puede codificar correctamente reemplazándolo con el carácter de reemplazo 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

Comenzamos redimensionando la matriz "utf8Output" a tres bytes, lo que garantiza que tengamos suficiente espacio para reemplazar el carácter. A continuación, configuramos los bytes de la matriz "utf8Output" en la representación UTF-8 de U+FFFD. Este carácter aparece en UTF-8 como la secuencia de bytes 0xEF, 0xBF y 0xBD, que son los bytes rectos asignados directamente a "utf8Output", siendo 0xEF el primer byte, 0xBF el segundo byte y 0xBD el tercer byte. Finalmente, devolvemos 3, lo que indica que la representación UTF-8 del carácter de reemplazo ocupa tres bytes. Esa es la función completa que garantiza que podamos convertir un carácter a una representación UTF-8. También se podría utilizar UFT-16, que es avanzado, pero como esto hace el trabajo del sitio web, mantendremos todo simple. Así pues, el código completo de la función es el siguiente:

//+-----------------------------------------------------------------------+
//| 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
}

Armados con la función de codificación, ahora podemos codificar nuestro mensaje y reenviarlo nuevamente.

   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;

Aquí, simplemente declaramos una variable de cadena llamada "encoded_msg" que almacena nuestro mensaje codificado en URL, y finalmente agregamos el resultado al mensaje inicial, que técnicamente sobrescribe su contenido en lugar de simplemente declarar otra variable. Cuando ejecutamos esto, esto es lo que obtenemos:

MENSAJE SIN EMOJIS

Podemos ver que esto fue un éxito. Recibimos el mensaje de manera estructurada. Sin embargo, los caracteres emoji que aparecen inicialmente en el mensaje se descartan. Esto se debe a que los codificamos y ahora, para poder recuperarlos, tenemos que ingresar sus respectivos formatos. Si no necesitas eliminarlos, significa que los codificas de forma rígida y, por lo tanto, simplemente ignoras el fragmento de emoji en la función. Para nosotros, déjenoslos en su respectivo formato para que puedan ser codificados automáticamente.

   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;

Aquí representamos el carácter en formato "\xF***". Si tiene una palabra que sigue a la representación, asegúrese de utilizar un espacio o una barra invertida "\" para fines de distinción, es decir "\xF123 " o "\xF123\". Cuando ejecutamos esto, obtenemos el siguiente resultado:

INCLUSIÓN FINAL DE EMOJI

Podemos ver que ahora tenemos el formato de mensaje correcto con todos los caracteres codificados correctamente. ¡Esto es un éxito! Ahora podemos proceder a producir señales reales.

Puesto que la función WebRequest no funcionará en el probador de estrategias, y esperar a que se genere una señal basada en la estrategia de cruce de medias móviles requerirá algo de tiempo para esperar la confirmación, vamos a elaborar alguna otra estrategia rápida, aunque seguiremos utilizando la estrategia de medias móviles más adelante, para utilizarla en la inicialización del programa. Evaluamos la barra anterior en la inicialización y si es una barra alcista, abrimos una orden de compra. En caso contrario, si se trata de una barra bajista o de dirección cero, abrimos una orden de venta. Esto se ilustra a continuación:

VELAS DE TORO Y OSO

El fragmento de código utilizado para la lógica es el siguiente:

   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;
   

Aquí definimos las cotizaciones de precios, es decir, los precios de venta y de oferta. A continuación, obtenemos el precio de apertura de la barra anterior, en el índice 1, mediante la función iOpen, que toma 3 argumentos o parámetros, es decir, el símbolo de la materia prima, el periodo y el índice de la barra de la que obtener el valor. Para obtener el precio de cierre, se utiliza la función iClose. Luego definimos variables booleanas "isBuySignal" y "isSellSignal", que comparan los valores de los precios de apertura y cierre, y si el precio de apertura es menor que el precio de cierre o el precio de apertura es mayor o igual que el precio de cierre, almacenamos las banderas de señal de compra y venta en las variables respectivamente.

Para abrir las órdenes, necesitamos un método.

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

En el ámbito global, preferiblemente en la parte superior del código, incluimos la clase de comercio utilizando la palabra clave #include. Esto nos da acceso a la clase CTrade, que utilizaremos para crear un objeto comercial. Esto es crucial, ya que lo necesitamos para abrir operaciones.

CLASE CTRADE

El preprocesador sustituirá la línea #include <Trade/Trade.mqh> por el contenido del archivo Trade.mqh. Los corchetes indican que el archivo Trade.mqh se tomará del directorio estándar (normalmente es directorio_de_instalación_del_terminal\MQL5\Include). El directorio actual no se incluye en la búsqueda. La línea puede colocarse en cualquier parte del programa, pero normalmente, todas las inclusiones se colocan al principio del código fuente, para una mejor estructura del código y una referencia más fácil. La declaración del objeto obj_Trade de la clase CTrade nos dará acceso a los métodos contenidos en dicha clase fácilmente, gracias a los desarrolladores de MQL5.

Con esto, ya podemos abrir posiciones.

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

Definimos variables double para almacenar el volumen de negociación, el precio abierto de las órdenes, los niveles de stop loss y take profit, y las inicializamos a cero. Para abrir las posiciones, primero comprobamos si la "isBuySignal" contiene una bandera "true", lo que significa que la barra anterior fue efectivamente alcista, y luego abrimos la posición de compra. El tamaño del lote se inicializa en 0,01, el precio de apertura es la cotización de oferta, los niveles de stop loss y take profit se calculan a partir de la cotización de oferta, y los resultados se utilizan para abrir la posición de compra. Del mismo modo, para abrir la posición de venta, se calculan los valores y se utilizan en la función.

Una vez abiertas las posiciones, podemos reunir la información sobre la señal generada y la posición abierta en un único mensaje, y retransmitirlo a 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;
   

Aquí creamos un mensaje claro y preciso que contiene la información relacionada con la señal de negociación. Formateamos el mensaje con emojis y otros datos relevantes que creemos que harán que la información sea fácil de digerir para sus destinatarios. Empezamos por determinar si la señal está basada en "Compra" o "Venta", y esto se consigue mediante el uso de un operador ternario. Luego elaboramos el mensaje, incluyendo una representación emoji de una pila de dinero que, en nuestra opinión, es adecuada para una señal de "Comprar" o "Vender". Usamos los caracteres de representación emoji reales en su formato "ushort" y luego convertimos el código del carácter en una variable de cadena usando la función "ShortToString", simplemente para mostrar que no es obligatorio usar siempre los formatos de cadena. Sin embargo, puedes ver que el proceso de conversión toma algo de tiempo y espacio, aunque si quieres dar nombres a los respectivos personajes, es el mejor método.

Luego juntamos la información sobre la posición comercial abierta en una cadena. Esta cadena, cuando se convierte en un mensaje, contiene los detalles de la operación: qué tipo de operación es, cuál fue el precio de apertura, cuál fue la hora de la operación, cuál es la hora actual, cuál es el tamaño del lote, cuál es el stop loss, cuál es el take profit, etc. Lo hacemos de manera que el mensaje sea visualmente atractivo y fácil de interpretar.

Después de la composición del mensaje, llamamos a la función "UrlEncode" para codificar el mensaje para su transmisión segura a la URL. Nos aseguramos especialmente de que todos los caracteres especiales y emojis se manejen correctamente y sean adecuados para la web. Luego almacenamos el mensaje codificado en una variable llamada "encloded_msg" y sobrescribimos el mensaje codificado con el inicial, o normalmente lo intercambiamos. Cuando ejecutamos esto, obtenemos el siguiente resultado:

MENSAJE DE SEÑAL DE INICIALIZACIÓN FINAL

Puedes ver que hemos codificado exitosamente el mensaje y lo hemos enviado a Telegram en la estructura objetiva. El código fuente completo responsable de enviar esto es el siguiente:

//+------------------------------------------------------------------+
//|                                  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
}

Ahora necesitamos incluir las señales comerciales basadas en cruces de medias móviles. Primero, necesitaremos declarar los dos controladores de indicadores de promedio móvil y sus matrices de almacenamiento de datos.

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

double bufferFast[];
double bufferSlow[];

long magic_no = 1234567890;

Primero, declaramos variables de tipo de datos entero denominadas "handleFast" y "handleSlow" para albergar los indicadores de promedio de movimiento rápido y lento respectivamente. Inicializamos los identificadores con "INVALID_HANDLE", un valor -1, lo que significa que actualmente no hacen referencia a ninguna instancia de indicador válida. A continuación definimos dos arrays double; "bufferFast" y "bufferSlow", donde almacenamos el valor que recuperamos de los indicadores rápido y lento respectivamente. Por último, declaramos una variable "long" para almacenar el número mágico de las posiciones que abramos. Toda esta lógica se sitúa en el ámbito global.

En la función OnInit, inicializamos los manejadores del indicador y establecemos las matrices de almacenamiento como series temporales. 

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

Aquí, creamos un controlador para el indicador de promedio de movimiento rápido. Esto se hace utilizando la función iMAque se llama con los parámetros "Symbol", "Period", 20, 0, "MODE_EMA" y "PRICE_CLOSE". El primer parámetro, "Symbol", es una función incorporada que devuelve el nombre del instrumento actual. El segundo parámetro, "Period", devuelve el marco temporal actual. El siguiente parámetro, 20, es el número de periodos de la Media Móvil. El cuarto parámetro, 0, indica que queremos que la Media Móvil se aplique a las barras de precios más recientes. El quinto parámetro, "MODE_EMA", indica que queremos que se calcule la Media Móvil Exponencial (Exponential Moving Average, EMA). El último parámetro es "PRICE_CLOSE", que indica que calculamos la media móvil basándonos en los precios de cierre. Esta función devuelve un handle que identifica unívocamente esta instancia de indicador de media móvil y lo asignamos a "handleFast".

Una vez que hemos intentado crear el indicador, verificamos si el identificador es válido. Un resultado de "INVALID_HANDLE" para "handleFast" nos indica que no pudimos crear el identificador para el indicador de promedio de movimiento rápido. En este caso, imprimimos un mensaje en el registro con un nivel de gravedad de ERROR. El mensaje, dirigido al usuario, indica que el programa "UNABLE TO CREATE FAST MA INDICATOR HANDLE". REVERTING NOW!" En el mensaje se aclara que sin asa no hay indicador, lo que significa que no hemos podido crear el asa del indicador. Dado que sin este indicador no existe sistema de trading, lo que hace que el programa sea inútil, no tiene sentido seguir ejecutándolo. Devolvemos "INIT_FAILED" sin continuar porque hemos encontrado un fallo. Esto impide que el programa siga ejecutándose y lo elimina del gráfico.

La misma lógica se aplica al indicador lento.

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

Si imprime estos indicadores, obtendrá un valor inicial de 10 y, si hay más indicadores, su valor se incrementará en 1 para cada indicador. Vamos a imprimirlos y ver qué obtenemos. Logramos esto mediante el siguiente código:

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

Obtenemos el siguiente resultado:

INDICADOR MANEJA LA IMPRESIÓN

Finalmente, establecemos las matrices de almacenamiento de datos como series de tiempo y establecemos el número mágico.

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

La configuración de las matrices como series temporales se consigue mediante el uso de la función ArraySetAsSeries.

En la función OnDeinit, liberamos los manejadores de los indicadores de la memoria del ordenador con ayuda de la función IndicatorRelease y liberamos las matrices de almacenamiento con ayuda de la función ArrayFree. Así nos aseguramos de liberar al ordenador de procesos innecesarios, reservando sus recursos.

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

En el manejador de eventos OnTick, ejecutamos código que hará uso de los manejadores del indicador y comprobará la generación de señales. Se trata de una función que se llama en cada tick, es decir, cambio en las cotizaciones, para obtener los últimos precios.

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

   ...

}

Este es el manejador de eventos donde necesitamos recuperar los valores de los indicadores.

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

En primer lugar, intentamos obtener los datos del búfer del indicador de media rápida mediante la función CopyBuffer. Lo llamamos con los parámetros: "handleFast", 0, 0, 3, y "bufferFast". El primer parámetro, "handleFast", es el indicador de destino del que obtenemos los valores del indicador. El segundo parámetro es el número del buffer, de donde obtenemos los valores, normalmente tal y como se muestran en la ventana de datos, y para la media móvil es siempre 0. El tercer parámetro es la posición inicial del índice de la barra de donde obtenemos los valores, 0 en este caso significa la barra actual. El cuarto parámetro es el número de valores a recuperar, es decir, las barras. 3 en este caso significa las 3 primeras barras desde la barra actual. El parámetro final es el "bufferFast", que es el array de destino donde almacenamos nuestros 3 valores recuperados.

Ahora, comprobamos si la función ha recuperado con éxito los valores solicitados, es decir, 3. Si el valor devuelto es inferior a 3, indica que la función no ha podido recuperar los datos solicitados. En tal caso, se imprime un mensaje de error que dice: "UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS". REVERTING". Esto nos notifica que la recuperación de datos ha fallado, y no podemos seguir buscando señales ya que no tenemos suficientes datos para el proceso. A continuación, devolvemos, lo que detiene la ejecución de esta parte del programa, y esperamos al siguiente tick.

El mismo proceso se realiza para recuperar los datos de la media lenta.

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

Como la función OnTick se ejecuta en cada tick, tendremos que desarrollar una lógica para asegurarnos de ejecutar nuestro código de escaneo de señales una vez por barra. Esta es la lógica.

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

En primer lugar, declaramos una variable entera "currBars" que almacena el número calculado de barras actuales en el gráfico para el símbolo de negociación y el período especificados, o mejor dicho, el marco temporal, como lo habrá oído. Esto se consigue mediante el uso de la función iBars, que sólo toma dos argumentos, es decir, símbolo y punto. 

A continuación, declaramos otra variable estática entera "prevBars" para almacenar el número total de barras anteriores en el gráfico cuando se genera una nueva barra y la inicializamos con el valor de las barras actuales en el gráfico para la primera ejecución de la función. Lo utilizaremos para comparar el número actual de barras con el número anterior de barras, para determinar la instancia de una nueva generación de barras en el gráfico.

Por último, utilizamos una sentencia condicional para comprobar si el número de compases actual es igual al número de compases anterior. Si son iguales, significa que no se ha formado ninguna barra nueva, por lo que terminamos la ejecución y volvemos. En caso contrario, si el recuento de la barra actual y el de la anterior no son iguales, indica que se ha formado una nueva barra. En este caso, procedemos a actualizar la variable barras anteriores a las barras actuales, de forma que en el siguiente tick, será igual al número de barras del gráfico no a menos que nos graduemos a una nueva.

A continuación, definimos variables en las que podemos almacenar fácilmente nuestros datos para su posterior análisis, como se indica a continuación.

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

Con estas variables, ahora podemos comprobar si hay cruces y tomar las medidas necesarias.

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

Aquí, buscamos una condición de cruce particular: si la media de movimiento rápido más reciente (fastMA1) es mayor que la media de movimiento lento correspondiente (slowMA1), y la media de movimiento rápido anterior (fastMA2) fue menor o igual que la media de movimiento lento anterior (slowMA2), entonces estamos ante un cruce alcista, lo que indica una posible señal de compra. 

Cuando se identifica un cruce alcista, hacemos un bucle a través de las posiciones actuales para comprobar si hay alguna posición de venta abierta en el camino de una nueva compra. Si es necesario, cerramos las posiciones de venta antes de abrir las nuevas compras. Trabajamos de la posición más reciente a la menos reciente.

Para cada posición comercial, obtenemos el número de ticket utilizando la función PositionGetTicket. Si el número de ticket es mayor que 0, lo que significa que efectivamente tenemos un número de ticket válido, y seleccionamos la posición mediante la función PositionSelectByTicket, continuamos comprobando si la posición es válida y verificamos que pertenece al símbolo y número mágico actuales. Si la posición es de venta, utilizamos la función "obj_Trade.PositionClose" para cerrar la posición. Tras cerrar las posiciones de venta existentes, abrimos una nueva posición de compra, estableciendo los parámetros de la operación: tamaño del lote, precio de apertura, stop loss y take profit. Una vez abierta la posición, informamos al usuario de la instancia enviando un registro al diario.

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

Por último, enviamos un mensaje igual que hicimos en la sección de inicialización del programa.

      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;

Para la señal de cruce de venta, se mantiene la misma estructura de código con condiciones inversas.

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

Hasta este punto, la estructura del código está casi completa. Lo que tenemos que hacer ahora es añadir los indicadores al gráfico automáticamente una vez cargado el programa para su visualización. Así, en el manejador de eventos de inicialización, elaboramos la lógica para agregar los indicadores automáticamente de la siguiente manera:

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

Aquí, simplemente llamamos a la función ChartIndicatorAdd para añadir los indicadores al gráfico, donde el primer y segundo parámetro especifican la ventana del gráfico y la subventana respectivamente. El tercer parámetro es el indicador que se va a añadir.

Así, el código completo del manejador de eventos OnTick responsable de la generación y canalización de las señales es el siguiente:

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

Ahora está claro que hemos conseguido nuestro segundo objetivo, es decir, enviar señales desde el terminal de trading al chat o grupo de Telegram. ¡Esto es un éxito y estamos felices! Ahora tenemos que probar la integración para asegurarnos de que funciona correctamente y detectar cualquier problema que pueda surgir. Esto se hace en la siguiente sección.


Probando la integración

Para probar la integración, desactivamos la lógica de prueba de inicialización comentándola para evitar que se abran muchas señales, cambiamos a un periodo inferior, 1 minuto, y cambiamos los periodos de los indicadores a 5 y 10 para generar señales más rápidas. Estos son los resultados de los hitos que obtenemos.

Terminal de trading confirmación de señal de venta:

MT5 VENDER SEÑAL

Confirmación de la señal de venta de Telegram:

TELEGRAM SEÑAL DE VENTA

Confirmación de señal de compra en el terminal:

MT5 SEÑAL DE COMPRA

Confirmación de señal de compra en Telegram:

SEÑAL DE COMPRA DE TELEGRAM

De las imágenes se desprende que la integración funciona correctamente. Se escanea una señal y, una vez confirmada, sus detalles se codifican en un único mensaje y se envían desde el terminal de negociación al chat de grupo de Telegram. Así, hemos alcanzado con éxito nuestro objetivo.


Conclusión

En conclusión, este artículo ha supuesto un avance considerable en el impulso de nuestro Asesor Experto integrado, MQL5-Telegram, al conseguir el objetivo principal de enviar señales de trading directamente desde el terminal de trading a un chat de Telegram. Sin embargo, no nos hemos limitado a establecer un canal de comunicación entre MQL5 y Telegram, como se hizo en la primera parte de esta serie. InEn su lugar, nos centramos en las señales de negociación propiamente dichas, utilizando la popular herramienta de análisis técnico de los cruces de medias móviles. Hemos dado cuenta de la lógica de estas señales en detalle, así como del sólido sistema que existe ahora para enviarlas a través de Telegram. El resultado es un avance significativo en la configuración general de nuestro Asesor Experto integrado.

En este artículo, examinamos de cerca el funcionamiento técnico de cómo generar y enviar estas señales. Hemos estudiado detenidamente cómo codificar y enviar mensajes de forma segura, cómo gestionar las asas de los indicadores y cómo ejecutar operaciones basadas en señales detectadas. Elaboramos el código y lo integramos con Telegram para poder notificarnos al instante las señales de operaciones cuando estábamos lejos de nuestra plataforma de negociación. Los ejemplos prácticos y las explicaciones detalladas que se ofrecen en este artículo deberían darle una idea clara de cómo configurar algo similar utilizando sus estrategias de negociación.

En la Parte 3 de nuestra serie, añadiremos otra capa a nuestra integración MQL5-Telegram. Esta vez, trabajaremos en una solución para enviar capturas de pantalla de gráficos a Telegram. La capacidad de analizar visualmente el mercado y el contexto de la señal de negociación mejorará la percepción y la comprensión de los operadores. Las señales textuales combinadas con datos visuales proporcionan señales aún más potentes. Y eso es exactamente lo que buscamos aquí: no sólo enviar señales, sino mejorar la negociación automatizada y el conocimiento de la situación a través del canal de negociación de Telegram. ¡Nos vemos pronto!


Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/15495

Características del Wizard MQL5 que debe conocer (Parte 31): Selección de la función de pérdida Características del Wizard MQL5 que debe conocer (Parte 31): Selección de la función de pérdida
La función de pérdida es la métrica clave de los algoritmos de aprendizaje automático que proporciona información al proceso de formación cuantificando el rendimiento de un conjunto determinado de parámetros en comparación con el objetivo previsto. Exploramos los distintos formatos de esta función en una clase de asistente personalizada MQL5.
Creación de un asesor experto integrado de MQL5 y Telegram (Parte 1): Envío de mensajes desde MQL5 a Telegram Creación de un asesor experto integrado de MQL5 y Telegram (Parte 1): Envío de mensajes desde MQL5 a Telegram
En este artículo, creamos un Asesor Experto (EA) en MQL5 para enviar mensajes a Telegram usando un bot. Configuramos los parámetros necesarios, incluido el token de API del bot y el ID de chat, y luego realizamos una solicitud HTTP POST para entregar los mensajes. Posteriormente, gestionamos la respuesta para garantizar una entrega exitosa y solucionar cualquier problema que surja en caso de falla. Esto garantiza que enviemos mensajes desde MQL5 a Telegram a través del bot creado.
Desarrollo de un sistema de repetición (Parte 74): Un nuevo Chart Trade (I) Desarrollo de un sistema de repetición (Parte 74): Un nuevo Chart Trade (I)
En este artículo, modificaremos el último código visto en esta secuencia sobre Chart Trade. Estos cambios son necesarios para adaptar el código al modelo actual del sistema de repetición/simulador. El contenido expuesto aquí tiene como único propósito ser didáctico. En ningún caso debe considerarse una aplicación destinada a otros fines que no sean el aprendizaje y el estudio de los conceptos mostrados.
Aprendizaje automático y Data Science (Parte 29): Consejos esenciales para seleccionar los mejores datos de divisas para el entrenamiento de IA Aprendizaje automático y Data Science (Parte 29): Consejos esenciales para seleccionar los mejores datos de divisas para el entrenamiento de IA
En este artículo, profundizamos en los aspectos cruciales de la elección de los datos de Forex más relevantes y de alta calidad para mejorar el rendimiento de los modelos de IA.