English Русский 中文 Deutsch 日本語 Português
preview
Creación de un asesor experto integrado de MQL5 y Telegram (Parte 5): Envío de comandos desde Telegram a MQL5 y recepción de respuestas en tiempo real

Creación de un asesor experto integrado de MQL5 y Telegram (Parte 5): Envío de comandos desde Telegram a MQL5 y recepción de respuestas en tiempo real

MetaTrader 5Sistemas comerciales | 15 abril 2025, 08:22
341 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introducción

En este artículo, parte 5 de nuestra serie, continuamos integrando MetaQuotes Language 5 (MQL5) con Telegram, centrándonos en refinar la interacción entre MetaTrader 5 (MT5) y Telegram. Anteriormente, en la parte 4 de la serie, sentamos las bases para enviar mensajes complejos e imágenes de gráficos desde MQL5 a Telegram, estableciendo el puente de comunicación entre estas plataformas. Ahora, pretendemos ampliar esa base permitiendo que el Asesor Experto reciba e interprete órdenes directamente de los usuarios de Telegram. En lugar de que el Asesor Experto se controle a sí mismo generando señales, abriendo posiciones de mercado y enviando mensajes predefinidos a nuestro chat de Telegram, lo controlaremos desde el chat de Telegram transmitiendo comandos al Asesor que, a su vez, decodificará los comandos, los interpretará y enviará respuestas y respuestas de solicitudes intelectuales y apropiadas.

Comenzaremos por configurar el entorno necesario para facilitar esta comunicación, asegurándonos de que todo esté en su lugar para una interacción fluida. El núcleo de este artículo consistirá en crear clases que recuperen automáticamente las actualizaciones del chat a partir de datos JavaScript Object Notation (JSON), que son los comandos y peticiones de Telegram en este caso, lo que permitirá al Asesor Experto entender y procesar los comandos del usuario desde Telegram. Este paso es crucial para establecer una comunicación bidireccional dinámica en la que el bot no sólo envía mensajes, sino que también responde de forma inteligente a las entradas del usuario.

Además, nos centraremos en la decodificación e interpretación de los datos entrantes, asegurando que el Asesor Experto pueda gestionar eficazmente varios tipos de comandos desde la Interfaz de Programación de Aplicaciones (Application Programming Interface, API) de Telegram. Para demostrar este proceso, hemos proporcionado una guía visual detallada que ilustra el flujo de comunicación entre Telegram, MetaTrader 5 y el editor de código MQL5, facilitando la comprensión de cómo estos componentes trabajan juntos.

FLUJO DEL PROCESO DE INTEGRACIÓN

La ilustración proporcionada debe ser clara para mostrar los componentes de integración. Así, el flujo será el siguiente: Telegram envía órdenes al terminal de trading donde está conectado el Asesor Experto, el Asesor envía las órdenes a MQL5 que decodifica, interpreta los mensajes y prepara las respectivas respuestas, que a su vez las envía al terminal de trading y a Telegram como respuestas. Para una mayor comprensión, subdividiremos todo el proceso en temas de la siguiente manera:

  1. Configuración del entorno
  2. Creación de clases para obtener actualizaciones de chat desde JSON
  3. Decodificación y análisis de datos de la API de Telegram
  4. Manejo de respuestas
  5. Prueba de la implementación
  6. Conclusión

Al final del artículo, tendremos un Asesor Experto totalmente integrado que envía comandos y peticiones desde Telegram a MQL5 y obtiene respuestas supervisadas como respuestas en el chat de Telegram. Empecemos entonces.


Configuración del entorno

Es fundamental establecer un entorno que permita a nuestro Asesor Experto (Expert Advisor, EA) interactuar con Telegram antes de comenzar el trabajo real de creación de clases y funciones. Nuestro EA necesitará acceder a varias bibliotecas esenciales que facilitan la gestión de transacciones, matrices y cadenas en MQL5. Al poner a disposición estas bibliotecas esenciales, garantizamos que nuestro EA tenga acceso a una biblioteca de clases y funciones bien abastecida que facilita significativamente el camino para la implementación de nuestro EA. Esto es como se muestra a continuación:

#include <Trade/Trade.mqh>
#include <Arrays/List.mqh>
#include <Arrays/ArrayString.mqh>
    

Aquí, la biblioteca «<Trade/Trade.mqh>» proporciona un conjunto completo de funciones de negociación. Esta biblioteca permite al EA ejecutar operaciones, administrar posiciones y realizar otras tareas relacionadas con el trading. Es un componente crítico de cualquier EA que busque interactuar con el mercado. Las bibliotecas «<Arrays/List.mqh<>» y «<Arrays/ArrayString.mqh<>» que se incluyen a continuación facilitan la gestión de estructuras de datos. La primera de estas dos bibliotecas es para gestionar listas dinámicas. El segundo es para trabajar con matrices de cadenas (strings). Ambas bibliotecas son particularmente útiles cuando trabajamos con las señales comerciales que recibimos de Telegram. Eso fue mucha jerga, lo sabemos. En los siguientes capítulos profundizaremos un poco en esto e intentaremos explicar con más detalle qué hacen todos estos componentes. Para acceder a la biblioteca "Arrays", abra el navegador, expanda la carpeta de inclusiones y marque cualquiera de las dos como se ilustra a continuación.

BIBLIOTECA DE MATRICES

Por último, necesitamos definir la URL base de Telegram, el tiempo de espera y el token del bot como se muestra a continuación.

#define TELEGRAM_BASE_URL  "https://api.telegram.org"
#define WEB_TIMEOUT        5000
//#define InpToken "7456439661:AAELUurPxI1jloZZl3Rt-zWHRDEvBk2venc"
#define InpToken "7456439661:AAELUurPxI1jloZZl3Rt-zWHRDEvBk2venc"

Después de incluir las bibliotecas y compilar su programa, ya tiene todo listo con el entorno necesario para manejar estructuras de datos complejas recibidas de los comandos de Telegram y ahora podemos continuar con la implementación. 


Creación de clases para obtener actualizaciones de chat desde JSON

Esta es la sección donde nos centramos en desarrollar la funcionalidad principal que permite a nuestro Asesor Experto (EA) recibir actualizaciones de Telegram en tiempo real. En concreto, necesitaremos crear clases que analicen los datos JSON devueltos por la API de Telegram y extraigan la información necesaria, como las actualizaciones del chat y los comandos del usuario. Este paso es crucial para establecer un bucle de comunicación receptivo entre Telegram y MetaTrader 5. Primero, simulemos el proceso. Nuevamente cargaremos la función predeterminada para obtener actualizaciones de chat como se muestra a continuación en nuestro navegador para que obtengamos la estructura de datos que necesitamos para implementar las clases.

DATOS VACÍOS

Al cargar, devolvemos verdadero, lo que indica que el proceso fue exitoso pero la estructura de datos está vacía. Esto se debe a que no hay mensajes enviados desde el chat de Telegram en las últimas 24 horas. Por lo tanto, necesitamos enviar un mensaje para obtener una actualización. Para ello, enviamos un mensaje de inicialización desde el chat de Telegram como se muestra a continuación.

PRIMER MENSAJE DE INICIALIZACIÓN DE TELEGRAM

Una vez que enviamos el mensaje, tenemos una actualización y podemos recargar el enlace del navegador para obtener la estructura de los datos enviados.

ESTRUCTURA DE DATOS 1

En la imagen de arriba, podemos ver los detalles correctos en la estructura de datos que se construye a partir del mensaje que enviamos. Estos son exactamente los datos que necesitamos copiar en nuestras clases y recorrerlos cada vez que enviamos una nueva actualización de mensaje. Por tanto, construyamos una clase que contendrá todas las variables miembro. Construyamos primero el plano general de la clase.

//+------------------------------------------------------------------+
//|        Class_Message                                             |
//+------------------------------------------------------------------+
class Class_Message : public CObject{//Defines a class named Class_Message that inherits from CObject.
   public:
      Class_Message(); // constructor
      ~Class_Message(){}; // Declares a destructor for the class, which is empty.
};

Concentrémonos en el prototipo de clase que hemos declarado arriba para que todo fluya sin problemas más adelante. Para declarar una clase, usamos la palabra clave "class" seguida del nombre de la clase, en nuestro caso es "Class_Message". Dado que recibiremos muchas estructuras de datos similares, heredamos otra clase llamada "CObject" y hacemos públicos los miembros heredados de la clase extranjera utilizando la palabra clave "public". Luego declaramos que los primeros miembros de la clase son "public". Antes de continuar, expliquemos en detalle qué significa todo esto. La palabra clave es uno de los cuatro calificadores, comúnmente llamados especificadores de acceso, y definen cómo el compilador puede acceder a variables, miembros de estructuras o clases. Los cuatro son: público, protegido, privado y virtual.

Vamos a desglosarlos y explicar cada uno por separado.

  • Public: Los miembros declarados con el especificador de acceso «public» son accesibles desde cualquier parte del código donde la clase sea visible. Esto significa que las funciones, variables u otros objetos fuera de la clase pueden acceder y utilizar directamente a los miembros públicos. A menudo se utilizan para funciones o variables a las que necesitan acceder otras clases, funciones o scripts.
  • Protected: Los miembros declarados bajo el especificador de acceso «protected» no son accesibles desde fuera de la clase, pero son accesibles dentro de la propia clase, por clases derivadas (es decir, subclases que heredan de esta clase) y por clases/funciones amigas. Esto es útil para encapsular datos que deberían estar disponibles para las subclases pero no para el resto del programa. Generalmente se utilizan para permitir que las subclases accedan o modifiquen ciertas variables o funciones de la clase base mientras ocultan esos miembros del resto del programa.
  • Private: Los miembros declarados con el especificador de acceso «private» sólo son accesibles dentro de la propia clase. Ni las clases derivadas ni ninguna otra parte del programa pueden acceder o modificar directamente los miembros privados. Este es el nivel de acceso más restrictivo y normalmente se utiliza para variables y funciones auxiliares que no deben ser accesibles ni modificables desde fuera de la clase. Se utilizan comúnmente para implementar la ocultación de datos, garantizando que el estado interno de un objeto solo pueda modificarse a través de interfaces públicas bien definidas (métodos).
  • Virtual: Se aplica sólo a los métodos de clase (pero no a los métodos de estructuras) e indica al compilador que este método debe colocarse en la tabla de funciones virtuales de la clase.

De las sintaxis anteriores, sólo las tres primeras se utilizan habitualmente. Ahora, volviendo a nuestro prototipo de clase, analicemos lo que hace cada cosa.

  • Declaración de clase:

class Class_Message : public CObject{...};: Aquí, declaramos una nueva clase llamada «Class_Message». Esta clase deriva de "CObject", que es una clase base en MQL5 y se utiliza para crear objetos personalizados. De esta forma, "Class_Message" puede utilizar las características proporcionadas por el framework MQL5, como la gestión de memoria y otros beneficios de la programación orientada a objetos, para mostrar mensajes fácilmente en nuestro programa.

  • Constructor:

Class_Message();: Aquí se declara el constructor de la clase «Class_Message». Un constructor es una función especial que se llama automáticamente cuando se crea una instancia (u objeto) de la clase. El trabajo del constructor es inicializar las variables miembro de la clase y llevar a cabo cualquier configuración que deba realizarse cuando se crea el objeto. En el caso de Class_Message, inicializa las variables de los miembros.

  • Destructor:

~Class_Message(){};: La clase «Class_Message» declara un destructor. Se llama automáticamente a un destructor cuando una instancia de una clase se elimina explícitamente o queda fuera del ámbito. Generalmente, un destructor se define para realizar la limpieza y es conceptualmente lo opuesto a un constructor, que se llama cuando se crea una instancia de una clase. En este caso, el destructor de la clase "Class_Message" no hace nada (no realiza ninguna tarea de limpieza) porque eso no es necesario por ahora. 

Observe que tanto el constructor como el destructor contienen el mismo nombre que la clase base, sólo que el destructor tiene una tilde (~) como prefijo. Con esto, ahora podemos continuar definiendo los miembros de nuestra clase. Estos miembros son los mismos que recibimos en nuestra estructura de datos, por lo tanto, visualizaremos la estructura de datos y los miembros que necesitamos extraer de ella como se muestra a continuación.

DETALLES DEL MENSAJE INFORMATIVO

En la imagen de arriba, podemos ver que necesitamos un mínimo de 14 miembros en nuestra clase. Los definimos a continuación:

      bool              done; //A boolean member variable TO INDICATE if a message has been processed.
      long              update_id; //Store the update ID from Telegram.
      long              message_id;//Stores the message ID.
      //---
      long              from_id;//Stores the sender’s ID.
      string            from_first_name;
      string            from_last_name;
      string            from_username;
      //---
      long              chat_id;
      string            chat_first_name;
      string            chat_last_name;
      string            chat_username;
      string            chat_type;
      //---
      datetime          message_date;
      string            message_text;

Ahora tenemos todos los miembros de clase que necesitamos. La estructura de clase final se parece a la que se muestra a continuación. Hemos añadido comentarios para que todo se explique por sí solo en el proceso.

//+------------------------------------------------------------------+
//|        Class_Message                                             |
//+------------------------------------------------------------------+
class Class_Message : public CObject{//--- Defines a class named Class_Message that inherits from CObject.
   public:
      Class_Message(); //--- Constructor declaration.
      ~Class_Message(){}; //--- Declares an empty destructor for the class.
      
      //--- Member variables to track the status of the message.
      bool              done; //--- Indicates if a message has been processed.
      long              update_id; //--- Stores the update ID from Telegram.
      long              message_id; //--- Stores the message ID.

      //--- Member variables to store sender-related information.
      long              from_id; //--- Stores the sender’s ID.
      string            from_first_name; //--- Stores the sender’s first name.
      string            from_last_name; //--- Stores the sender’s last name.
      string            from_username; //--- Stores the sender’s username.

      //--- Member variables to store chat-related information.
      long              chat_id; //--- Stores the chat ID.
      string            chat_first_name; //--- Stores the chat first name.
      string            chat_last_name; //--- Stores the chat last name.
      string            chat_username; //--- Stores the chat username.
      string            chat_type; //--- Stores the chat type.

      //--- Member variables to store message-related information.
      datetime          message_date; //--- Stores the date of the message.
      string            message_text; //--- Stores the text of the message.
};

Después de definir la clase de mensajes, necesitamos inicializar sus miembros para que estén listos para recibir datos. Hacemos esto llamando al constructor de la clase.

//+------------------------------------------------------------------+
//|      Constructor to initialize class members                     |
//+------------------------------------------------------------------+
Class_Message::Class_Message(void){
   //--- Initialize the boolean 'done' to false, indicating the message is not processed.
   done = false;
   
   //--- Initialize message-related IDs to zero.
   update_id = 0;
   message_id = 0;
   
   //--- Initialize sender-related information.
   from_id = 0;
   from_first_name = NULL;
   from_last_name = NULL;
   from_username = NULL;
   
   //--- Initialize chat-related information.
   chat_id = 0;
   chat_first_name = NULL;
   chat_last_name = NULL;
   chat_username = NULL;
   chat_type = NULL;
   
   //--- Initialize the message date and text.
   message_date = 0;
   message_text = NULL;
}

Primero llamamos a la clase base y definimos el constructor utilizando el «operador de ámbito» (::). Luego inicializamos las variables miembro a sus valores predeterminados. El valor booleano "done" se establece en "false", lo que significa que el mensaje aún no se ha procesado. Tanto "message_id" como "update_id" se inicializan a 0, lo que representa los ID predeterminados para el mensaje y la actualización. Para la información relacionada con el remitente, «from_id» se establece en 0, y las variables «from_first_name», «from_last_name» y «from_username» se inicializan en NULL, lo que significa que no se establecen los detalles del remitente. Del mismo modo, las variables relacionadas con el chat, es decir, «chat_id», «chat_first_name», «chat_last_name», «chat_username» y «chat_type», también se inicializan a 0 o NULL en sus tipos de datos, lo que significa que la información del chat aún no está disponible. Por último, «message_date» se establece en 0, y «message_text» se inicializa en NULL, lo que significa que el contenido del mensaje y la fecha del mensaje aún no se han especificado. Técnicamente, inicializamos las variables de tipo de datos «integers» a 0, y las «strings» a NULL.

De manera similar, necesitamos definir otra instancia de clase que se utilizará para albergar chats individuales de Telegram. Utilizaremos estos datos para hacer una comparación entre los datos analizados y los datos recibidos de Telegram. Por ejemplo, cuando enviamos un comando «get Ask price», analizaremos los datos, obtendremos actualizaciones del JSON, y comprobaremos si alguno de los datos recibidos que están almacenados en el JSON coincide con nuestro comando, y si es así, tomaremos la acción necesaria. Esperamos que esto aclare algunas cosas, pero se aclararán más a medida que avancemos. El fragmento de código de la clase es el siguiente:

//+------------------------------------------------------------------+
//|        Class_Chat                                                |
//+------------------------------------------------------------------+
class Class_Chat : public CObject{
   public:
      Class_Chat(){}; //Declares an empty constructor.
      ~Class_Chat(){}; // deconstructor
      long              member_id;//Stores the chat ID.
      int               member_state;//Stores the state of the chat.
      datetime          member_time;//Stores the time related to the chat.
      Class_Message     member_last;//An instance of Class_Message to store the last message.
      Class_Message     member_new_one;//An instance of Class_Message to store the new message.
};

Definimos una clase llamada «Class_Chat» para manejar y guardar la información de los chats individuales de Telegram. Esta clase contiene un constructor y un destructor vacíos, y varios miembros: «member_id» almacena el ID único del chat; “member_state” indica el estado del chat; y “member_time” contiene cualquier información relacionada con el tiempo del chat. La clase tiene dos instancias de la clase base que ya hemos definido, «Class_Message», que contienen el último y el nuevo mensaje respectivamente. Los necesitamos para almacenar los mensajes y procesarlos individualmente cuando el usuario envía múltiples comandos. Para ilustrar esto, enviaremos un mensaje de inicialización como el siguiente:

SEGUNDO MENSAJE DE INICIALIZACIÓN

Al leer nuestras actualizaciones de chat, obtenemos la siguiente estructura de datos.

ESTRUCTURA DE DATOS DEL SEGUNDO MENSAJE

A partir de la estructura de datos del segundo mensaje recibido, podemos ver que los ID de actualización y mensaje del primer mensaje son 794283239 y 664 respectivamente, mientras que el segundo mensaje tiene 794283240 y 665, lo que supone una diferencia de 1. Esperamos que esto aclare la necesidad de una clase diferente. Ahora podemos proceder a crear la última clase predeterminada que usaremos para controlar el flujo de interacción sin problemas. Su estructura es la siguiente:

//+------------------------------------------------------------------+
//|   Class_Bot_EA                                                    |
//+------------------------------------------------------------------+
class Class_Bot_EA{
   private:
      string            member_token;         //--- Stores the bot’s token.
      string            member_name;          //--- Stores the bot’s name.
      long              member_update_id;     //--- Stores the last update ID processed by the bot.
      CArrayString      member_users_filter;  //--- An array to filter users.
      bool              member_first_remove;  //--- A boolean to indicate if the first message should be removed.
   
   protected:
      CList             member_chats;         //--- A list to store chat objects.

   public:
      void Class_Bot_EA();   //--- Declares the constructor.
      ~Class_Bot_EA(){};    //--- Declares the destructor.
      int getChatUpdates(); //--- Declares a function to get updates from Telegram.
      void ProcessMessages(); //--- Declares a function to process incoming messages.
};

Definimos una clase llamada «Class_Bot_EA» para gestionar las interacciones entre el bot de Telegram y el entorno MQL5. Tiene varios miembros privados como «member_token», que almacena el token de autenticación del bot, y «member_name», que contiene el nombre del bot. Otro miembro es el «member_update_id», que lleva la cuenta de la última actualización procesada. Varios otros miembros gestionan y filtran las interacciones de los usuarios. La clase tiene un miembro protegido, «member_chats», que mantiene una lista de objetos de chat. Entre sus miembros públicos, los más notables son el constructor y el destructor, que realizan la inicialización y limpieza necesarias de las instancias. También hay dos funciones notables entre los miembros públicos: "getChatUpdates", que recupera actualizaciones de Telegram, y "ProcessMessages", que maneja el procesamiento de los mensajes entrantes. Estas son las funciones más importantes que usaremos para obtener las actualizaciones del chat y procesar los comandos recibidos. Inicializaremos estos miembros usando un formato similar al que usamos con la primera clase, como se muestra a continuación.

void Class_Bot_EA::Class_Bot_EA(void){ //--- Constructor
   member_token=NULL; //--- Initialize the bot's token as NULL.
   member_token=getTrimmedToken(InpToken); //--- Assign the trimmed bot token from InpToken.
   member_name=NULL; //--- Initialize the bot's name as NULL.
   member_update_id=0; //--- Initialize the last update ID to 0.
   member_first_remove=true; //--- Set the flag to remove the first message to true.
   member_chats.Clear(); //--- Clear the list of chat objects.
   member_users_filter.Clear(); //--- Clear the user filter array.
}

Aquí, invocamos el constructor de la clase "Class_Bot_EA" e inicializamos las variables miembro para establecer el entorno del bot. Inicialmente, el «member_token» se establece en NULL como marcador de posición. Luego le asignamos la versión recortada de «InpToken». Este valor es muy importante ya que rige la autenticación del bot. Si el marcador de posición recortado se deja en el código, el bot simplemente no funcionará. El «member_name» también se inicializa a NULL, y el «member_update_id» se establece en 0, lo que indica que aún no se ha procesado ninguna actualización. La variable «member_first_remove» se establece en true. Esto significa que el bot está configurado para eliminar el primer mensaje que procesa. Por último, tanto «member_chats» como «member_users_filter» se borran para garantizar que se inician vacíos. Habrás notado que usamos una función diferente para obtener el token del bot. La función es la siguiente:

//+------------------------------------------------------------------+
//|        Function to get the Trimmed Bot's Token                   |
//+------------------------------------------------------------------+
string getTrimmedToken(const string bot_token){
   string token=getTrimmedString(bot_token); //--- Trim the bot_token using getTrimmedString function.
   if(token==""){ //--- Check if the trimmed token is empty.
      Print("ERR: TOKEN EMPTY"); //--- Print an error message if the token is empty.
      return("NULL"); //--- Return "NULL" if the token is empty.
   }
   return(token); //--- Return the trimmed token.
}

//+------------------------------------------------------------------+
//|        Function to get a Trimmed string                          |
//+------------------------------------------------------------------+
string getTrimmedString(string text){
   StringTrimLeft(text); //--- Remove leading whitespace from the string.
   StringTrimRight(text); //--- Remove trailing whitespace from the string.
   return(text); //--- Return the trimmed string.
}

Aquí, definimos dos funciones que trabajan mano a mano para limpiar y validar la cadena de tokens del bot. La primera función, «getTrimmedToken», accede al «bot_token» como entrada. A continuación, llama a otra función, «getTrimmedString», para eliminar cualquier espacio en blanco inicial o final del token. Después del recorte, la función verifica si el token está vacío. Si el token está vacío después del recorte, se imprime un mensaje de error, y la función devuelve «NULL» para indicar que el bot no puede ir más allá con este token. Por otro lado, si el token no está vacío, se devuelve como un token válido recortado.

La segunda función, «getTrimmedString», se encarga de recortar los espacios en blanco de ambos extremos de una cadena dada. Utiliza StringTrimLeft para eliminar los espacios en blanco iniciales y StringTrimRight para eliminar los espacios en blanco finales y, a continuación, devuelve la cadena recortada como un token que supera la prueba de validez.

Hasta aquí, ya tenemos las estructuras de datos necesarias para organizar los metadatos recibidos. A continuación, debemos proceder a obtener las actualizaciones del chat y procesarlas simultáneamente. Para garantizar una comunicación clara, primero llamaremos a las funciones de la clase. Al principio, para acceder a los miembros de la clase, tendremos que crear un objeto basado en la clase que nos dé el acceso necesario. Esto se consigue como se indica a continuación:

Class_Bot_EA obj_bot; //--- Create an instance of the Class_Bot_EA class

Después de declarar la clase objeto como «obj_bot», podemos acceder a los miembros de la clase utilizando el operador punto. Tendremos que comprobar si hay actualizaciones y procesar los mensajes en un intervalo de tiempo determinado. Así, en lugar de utilizar el manejador de eventos OnTick que consumirá mucho tiempo para contar el número de ticks que podría consumir recursos del ordenador, optamos por la función OnTimer que realiza automáticamente el conteo por nosotros. Para utilizar el manejador de eventos, tendremos que configurarlo e inicializarlo en el manejador de eventos OnInit como se indica a continuación.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){
   EventSetMillisecondTimer(3000); //--- Set a timer event to trigger every 3000 milliseconds (3 seconds)
   OnTimer(); //--- Call OnTimer() immediately to get the first update
   return(INIT_SUCCEEDED); //--- Return initialization success
}

Aquí, inicializamos el Asesor Experto configurando un evento de temporizador utilizando la función EventSetMillisecondTimer para que se active cada 3000 milisegundos (3 segundos). Esto garantiza que el Asesor Experto compruebe continuamente las actualizaciones a intervalos regulares. A continuación, llamamos inmediatamente al manejador de eventos OnTimer para obtener la primera actualización justo después de la inicialización, lo que garantiza que el proceso se inicie sin demora. Por último, devolvemos «INIT_SUCCEEDED» para indicar que la inicialización se ha realizado correctamente. Entonces, ya que configuramos el temporizador, una vez que el programa es desinicializado, necesitamos destruir el temporizador configurado para liberar también los recursos de la computadora.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
   EventKillTimer(); //--- Kill the timer event to stop further triggering
   ChartRedraw(); //--- Redraw the chart to reflect any changes
}

Aquí, cuando el Asesor Experto se elimina o se detiene, lo primero que hacemos en el manejador de eventos OnDeinit es detener el evento del temporizador. Para ello se utiliza la función EventKillTimer, que es la contrapartida lógica de EventSetMillisecondTimer. No queremos que el temporizador siga funcionando si el Asesor Experto deja de funcionar. Después de detener el temporizador, llamamos a la función ChartRedraw. Llamar a esta función no es estrictamente necesario, pero puede ayudar en algunas circunstancias en las que es necesario actualizar el gráfico para que se apliquen los cambios realizados. Por último, llamamos al controlador de eventos del temporizador para que se encargue del proceso de recuento.

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer(){
   obj_bot.getChatUpdates(); //--- Call the function to get chat updates from Telegram
   obj_bot.ProcessMessages(); //--- Call the function to process incoming messages
}

Por último, llamamos al manejador de eventos OnTimer. Dentro de él, llamamos a nuestras dos funciones cruciales que son necesarias para obtener las actualizaciones del gráfico y procesar los mensajes respectivamente utilizando el objeto «obj_bot» que hemos creado y utilizando el «operador de punto» para acceder a las funciones de la clase. Hasta aquí todo va bien y ahora podemos centrarnos en las funciones. Esto se hará en las siguientes secciones.


Decodificación y análisis de datos de la API de Telegram

Lo primero que debemos hacer es obtener las actualizaciones del chat que usaremos para hacer una comparación con el texto recibido de Telegram y si hay coincidencia, realizar la respuesta necesaria. Por lo tanto, haremos esto en la función responsable de obtener las actualizaciones.

//+------------------------------------------------------------------+
int Class_Bot_EA::getChatUpdates(void){

//--- ....

}

Después de llamar a la función, lo primero que hacemos es asegurarnos de que tenemos un token válido, y si no es así, imprimimos un mensaje de error en el log y devolvemos -1, indicando que no podemos seguir adelante sin el token. Esto es como se muestra a continuación.

   //--- Check if the bot token is NULL
   if(member_token==NULL){
      Print("ERR: TOKEN EMPTY"); //--- Print an error message if the token is empty
      return(-1); //--- Return with an error code
   }

Si el token no está vacío, podemos proceder a preparar una solicitud para enviar a la API de Telegram para recuperar actualizaciones de un chat específico.

   string out; //--- Variable to store the response from the request
   string url=TELEGRAM_BASE_URL+"/bot"+member_token+"/getUpdates"; //--- Construct the URL for the Telegram API request
   string params="offset="+IntegerToString(member_update_id); //--- Set the offset parameter to get updates after the last processed ID
   
   //--- Send a POST request to get updates from Telegram
   int res=postRequest(out, url, params, WEB_TIMEOUT);

Comenzamos declarando una variable llamada «out» para contener la respuesta devuelta por la solicitud de la API. Para construir la URL de la solicitud, combinamos la URL de la API base («TELEGRAM_BASE_URL»), el token del bot («member_token») y el método al que queremos llamar («/getUpdates»). Este método recupera las actualizaciones enviadas al bot por los usuarios, lo que nos permite ver qué ha ocurrido desde la última vez que comprobamos si había actualizaciones. A continuación, incluimos un único parámetro en nuestra solicitud. El parámetro «offset» garantiza que sólo obtengamos las actualizaciones que se hayan producido después de la última actualización recuperada. Por último, emitimos una solicitud POST a la API, con el resultado de la solicitud almacenado en la variable «out» e indicado por el campo «res» de la respuesta. Hemos utilizado una función personalizada «postRequest». Aquí está su fragmento de código y desglose. Es similar a lo que hemos venido haciendo en las partes anteriores, pero hemos añadido comentarios para explicar las variables utilizadas.

//+------------------------------------------------------------------+
//| Function to send a POST request and get the response             |
//+------------------------------------------------------------------+
int postRequest(string &response, const string url, const string params,
                const int timeout=5000){
   char data[]; //--- Array to store the data to be sent in the request
   int data_size=StringLen(params); //--- Get the length of the parameters
   StringToCharArray(params, data, 0, data_size); //--- Convert the parameters string to a char array

   uchar result[]; //--- Array to store the response data
   string result_headers; //--- Variable to store the response headers

   //--- Send a POST request to the specified URL with the given parameters and timeout
   int response_code=WebRequest("POST", url, NULL, NULL, timeout, data, data_size, result, result_headers);
   if(response_code==200){ //--- If the response code is 200 (OK)
      //--- Remove Byte Order Mark (BOM) if present
      int start_index=0; //--- Initialize the starting index for the response
      int size=ArraySize(result); //--- Get the size of the response data array
      // Loop through the first 8 bytes of the 'result' array or the entire array if it's smaller
      for(int i=0; i<fmin(size,8); i++){
         // Check if the current byte is part of the BOM
         if(result[i]==0xef || result[i]==0xbb || result[i]==0xbf){
            // Set 'start_index' to the byte after the BOM
            start_index=i+1;
         }
         else {break;}
      }
      //--- Convert the response data from char array to string, skipping the BOM
      response=CharArrayToString(result, start_index, WHOLE_ARRAY, CP_UTF8);
      //Print(response); //--- Optionally print the response for debugging

      return(0); //--- Return 0 to indicate success
   }
   else{
      if(response_code==-1){ //--- If there was an error with the WebRequest
         return(_LastError); //--- Return the last error code
      }
      else{
         //--- Handle HTTP errors
         if(response_code>=100 && response_code<=511){
            response=CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); //--- Convert the result to string
            Print(response); //--- Print the response for debugging
            Print("ERR: HTTP"); //--- Print an error message indicating an HTTP error
            return(-1); //--- Return -1 to indicate an HTTP error
         }
         return(response_code); //--- Return the response code for other errors
      }
   }
   return(0); //--- Return 0 in case of an unexpected error
}

Aquí nos encargamos de enviar una petición POST y de procesar la respuesta. Comenzamos tomando los parámetros de entrada y convirtiéndolos en una forma en la que puedan ser enviados-usando StringToCharArray para crear un array de caracteres a partir de la cadena de parámetros. A continuación, definimos dos matrices que capturarán los datos de respuesta y las cabeceras de respuesta. Por último, utilizamos la función WebRequest para enviar la petición POST a la URL a la que debe ir, con los parámetros que debe utilizar y un ajuste de tiempo de espera.

Cuando nuestra solicitud tiene éxito (lo que determinamos basándonos en la recepción de un código de respuesta 200), nos aseguramos de que no haya nada que pueda interferir con el procesamiento al principio de nuestros datos de respuesta. En concreto, comprobamos si hay alguna Byte Order Mark (BOM). Si encontramos una, la tratamos como una subcadena que no debería estar ahí, y tomamos medidas para evitar incluirla en los datos que finalmente utilicemos. A continuación, convertimos los datos de una matriz de caracteres a una cadena. Si superamos todos estos pasos sin encontrarnos con ningún problema, devolvemos un 0 para indicar que todo ha ido bien.

Cuando nuestra solicitud no tiene éxito, tratamos el error comprobando el código que aparece en la respuesta. Si el problema radica en la función WebRequest, le decimos al usuario qué código de error se estableció por última vez: sólo así podemos averiguar cuál es el problema. Si se trata de un error HTTP, hacemos todo lo posible por interpretar el mensaje de error que acompaña a la respuesta HTTP y le decimos al usuario lo que hemos encontrado. Por último, para cualquier otro código de respuesta que podamos obtener, simplemente devolvemos el código.

Antes de continuar, podemos verificar los datos que se envían verificando la respuesta e imprimiendo los datos. Logramos esto utilizando la siguiente lógica.

   //--- If the request was successful
   if(res==0){
      Print(out); //--- Optionally print the response
   }

Aquí, verificamos si el resultado de la publicación es igual a cero y, de ser así, imprimimos los datos para depuración y verificación. Al ejecutarlo, tenemos los siguientes resultados.

DATOS DE RESPUESTA

Aquí, podemos ver que la respuesta es verdadera, lo que significa que el proceso para obtener las actualizaciones fue exitoso. Ahora necesitamos obtener la respuesta de datos y, para recuperarla, necesitaremos utilizar un análisis JSON. No profundizaremos demasiado en el código responsable del análisis, pero lo incluiremos como un archivo y también lo agregaremos al alcance global de nuestro programa. Luego de agregarlo, procedemos a crear un objeto JSON como se muestra a continuación:

      //--- Create a JSON object to parse the response
      CJSONValue obj_json(NULL, jv_UNDEF);

Después de crear el objeto, lo usamos para deserializar la respuesta como se muestra a continuación: 

      //--- Deserialize the JSON response
      bool done=obj_json.Deserialize(out);

Declaramos una variable booleana "done" para almacenar los resultados. Aquí es donde almacenamos las banderas de si la respuesta la analizamos correctamente o no. Podemos imprimirlo para fines de depuración como se muestra a continuación.

      Print(done);

Al imprimir, obtenemos la siguiente respuesta.

RESPUESTA A LA DESERIALIZACIÓN

Aquí podemos ver que hemos analizado correctamente la respuesta. Necesitamos que la respuesta sea verdadera para que podamos continuar. En el caso donde la respuesta sea esta última, debemos detener el proceso y regresar ya que no accederemos al resto de actualizaciones del mensaje. Por esa razón entonces nos aseguramos de que si la respuesta es negativa, damos por finalizado el proceso.

      if(!done){
         Print("ERR: JSON PARSING"); //--- Print an error message if parsing fails
         return(-1); //--- Return with an error code
      }

Aquí, verificamos si el análisis de JSON fue exitoso evaluando la variable booleana "done". Si el análisis falla (es decir, "done" es falso), imprimimos un mensaje de error "ERR: JSON PARSING" para indicar que hubo un problema con la interpretación de la respuesta JSON. Luego de esto, devolvemos -1 para señalar que ocurrió un error durante el proceso de análisis de JSON. A continuación, nos aseguramos de que la respuesta se procese correctamente mediante la siguiente lógica.

      //--- Check if the 'ok' field in the JSON is true
      bool ok=obj_json["ok"].ToBool();
      //--- If 'ok' is false, there was an error in the response
      if(!ok){
         Print("ERR: JSON NOT OK"); //--- Print an error message if 'ok' is false
         return(-1); //--- Return with an error code
      }

Primero, verificamos el valor del campo 'ok' en el JSON que se recupera de la respuesta. Esto nos permite saber si la solicitud fue procesada exitosamente. Extraemos este campo y lo almacenamos en un booleano llamado "ok". Si el valor de "ok" es falso, indica que hubo un error o algún tipo de problema con la respuesta, aunque la solicitud en sí fue exitosa. En este caso, imprimimos "ERR: JSON NOT OK" para indicar que hubo algún tipo de problema y devolvemos -1 para indicar que también hubo algún tipo de problema al procesar la respuesta JSON. Si todo fue un éxito, significa que tenemos actualizaciones de mensajes y podemos proceder a recuperarlos. Por lo tanto, necesitaremos declarar un objeto basado en la clase de mensajes de la siguiente manera:

      //--- Create a message object to store message details
      Class_Message obj_msg;

Ahora podemos recorrer todas las actualizaciones de mensajes y almacenarlas en la clase usando el objeto creado. Primero, necesitamos obtener el número total de actualizaciones, lo que se logra mediante la siguiente lógica.

      //--- Get the total number of updates in the JSON array 'result'
      int total=ArraySize(obj_json["result"].m_elements);
      //--- Loop through each update
      for(int i=0; i<total; i++){

      }

En cada iteración, necesitamos recuperar un elemento de actualización individual de la respuesta JSON para la que trabajamos.

         //--- Get the individual update item as a JSON object
         CJSONValue obj_item=obj_json["result"].m_elements[i];

Luego podremos proceder a obtener las actualizaciones de chat individuales. Primero, veamos las actualizaciones del mensaje.

         //--- Extract message details from the JSON object
         obj_msg.update_id=obj_item["update_id"].ToInt(); //--- Get the update ID
         obj_msg.message_id=obj_item["message"]["message_id"].ToInt(); //--- Get the message ID
         obj_msg.message_date=(datetime)obj_item["message"]["date"].ToInt(); //--- Get the message date
         
         obj_msg.message_text=obj_item["message"]["text"].ToStr(); //--- Get the message text
         obj_msg.message_text=decodeStringCharacters(obj_msg.message_text); //--- Decode any HTML entities in the message text

Aquí, tomamos los detalles del mensaje individual del elemento de actualización indicado por "obj_item". Comenzamos extrayendo el ID de actualización del objeto JSON y guardándolo en "obj_msg.update_id". Después de eso, extraemos el ID del mensaje y lo almacenamos en "obj_msg.message_id". La fecha del mensaje, que viene en un formato no tan legible para los humanos, también se incluye en el elemento, y la almacenamos como un objeto «datetime» en «obj_msg.message_date», que «encasillamos» en un formato legible para los humanos. Luego miramos el texto del mensaje. En la mayoría de los casos, podemos simplemente tomar el texto y colocarlo en "obj_msg.message_text". Sin embargo, a veces, sus entidades HTML están codificadas; otras veces, tiene caracteres especiales que también están codificados. Para esos casos, los manejamos en una función llamada "decodeStringCharacters". Esta es una función que ya explicamos antes, simplemente la llamaremos para que haga su trabajo. Luego, en un formato similar, extraemos los datos del remitente.

         //--- Extract sender details from the JSON object
         obj_msg.from_id=obj_item["message"]["from"]["id"].ToInt(); //--- Get the sender's ID
         obj_msg.from_first_name=obj_item["message"]["from"]["first_name"].ToStr(); //--- Get the sender's first name
         obj_msg.from_first_name=decodeStringCharacters(obj_msg.from_first_name); //--- Decode the first name
         obj_msg.from_last_name=obj_item["message"]["from"]["last_name"].ToStr(); //--- Get the sender's last name
         obj_msg.from_last_name=decodeStringCharacters(obj_msg.from_last_name); //--- Decode the last name
         obj_msg.from_username=obj_item["message"]["from"]["username"].ToStr(); //--- Get the sender's username
         obj_msg.from_username=decodeStringCharacters(obj_msg.from_username); //--- Decode the username

Después de extraer los detalles del remitente, también extraemos los detalles del chat de manera similar.

         //--- Extract chat details from the JSON object
         obj_msg.chat_id=obj_item["message"]["chat"]["id"].ToInt(); //--- Get the chat ID
         obj_msg.chat_first_name=obj_item["message"]["chat"]["first_name"].ToStr(); //--- Get the chat's first name
         obj_msg.chat_first_name=decodeStringCharacters(obj_msg.chat_first_name); //--- Decode the first name
         obj_msg.chat_last_name=obj_item["message"]["chat"]["last_name"].ToStr(); //--- Get the chat's last name
         obj_msg.chat_last_name=decodeStringCharacters(obj_msg.chat_last_name); //--- Decode the last name
         obj_msg.chat_username=obj_item["message"]["chat"]["username"].ToStr(); //--- Get the chat's username
         obj_msg.chat_username=decodeStringCharacters(obj_msg.chat_username); //--- Decode the username
         obj_msg.chat_type=obj_item["message"]["chat"]["type"].ToStr(); //--- Get the chat type

Hasta este punto, deberías haber notado que la estructura es la misma que proporcionamos en la estructura de datos del navegador. Luego podemos proceder a actualizar el ID de actualización para asegurarnos de que la próxima solicitud de actualizaciones de Telegram comience en el punto correcto.

         //--- Update the ID for the next request
         member_update_id=obj_msg.update_id+1;

Aquí, actualizamos "member_update_id" para asegurarnos de que la próxima solicitud de actualizaciones de Telegram comience en el lugar correcto. Al asignar el valor "obj_msg.update_id + 1", establecemos el desplazamiento para que la próxima solicitud no incluya la actualización actual y, en efecto, solo obtenga nuevas actualizaciones que ocurran después de esta ID. Esto es importante porque no queremos manejar la misma actualización más de una vez y también queremos mantener el bot lo más receptivo posible. A continuación, comprobamos si hay nuevas actualizaciones.

         //--- If it's the first update, skip processing
         if(member_first_remove){
            continue;
         }

Aquí, determinamos si la actualización actual es la primera actualización posterior a la inicialización que se procesa marcando la bandera "member_first_remove". Si "member_first_remove" es verdadero, indica que estamos procesando la primera actualización (la actualización inicial) después de que todo se haya inicializado. Luego omitimos el procesamiento de esta actualización y simplemente continuamos con la siguiente. Por último, filtramos y gestionamos los mensajes de chat en función de si se aplica un filtro de nombre de usuario.

         //--- Filter messages based on username
         if(member_users_filter.Total()==0 || //--- If no filter is applied, process all messages
            (member_users_filter.Total()>0 && //--- If a filter is applied, check if the username is in the filter
            member_users_filter.SearchLinear(obj_msg.from_username)>=0)){

            //--- Find the chat in the list of chats
            int index=-1;
            for(int j=0; j<member_chats.Total(); j++){
               Class_Chat *chat=member_chats.GetNodeAtIndex(j);
               if(chat.member_id==obj_msg.chat_id){ //--- Check if the chat ID matches
                  index=j;
                  break;
               }
            }

            //--- If the chat is not found, add a new chat to the list
            if(index==-1){
               member_chats.Add(new Class_Chat); //--- Add a new chat to the list
               Class_Chat *chat=member_chats.GetLastNode();
               chat.member_id=obj_msg.chat_id; //--- Set the chat ID
               chat.member_time=TimeLocal(); //--- Set the current time for the chat
               chat.member_state=0; //--- Initialize the chat state
               chat.member_new_one.message_text=obj_msg.message_text; //--- Set the new message text
               chat.member_new_one.done=false; //--- Mark the new message as not processed
            }
            //--- If the chat is found, update the chat message
            else{
               Class_Chat *chat=member_chats.GetNodeAtIndex(index);
               chat.member_time=TimeLocal(); //--- Update the chat time
               chat.member_new_one.message_text=obj_msg.message_text; //--- Update the message text
               chat.member_new_one.done=false; //--- Mark the new message as not processed
            }
         }

Primero, determinamos si un filtro de nombre de usuario está activo verificando "member_users_filter.Total()". Si no hay ningún filtro ("Total() == 0"), manejamos todos los mensajes como de costumbre. Si hay un filtro ("Total() > 0"), verificamos si el nombre de usuario del remitente ("obj_msg.from_username") está en el filtro, utilizando "member_users_filter.SearchLinear()". Si encontramos el nombre de usuario, seguimos adelante y manejamos el mensaje.

Luego buscamos el chat en la lista "member_chats" iterándolo y comparando el ID del chat ("obj_msg.chat_id"). Si no se encuentra el chat (index == -1), añadimos un nuevo objeto «Class_Chat» a la lista. Inicializamos el objeto con el ID del chat, la hora actual, un estado inicial de 0 y el texto del nuevo mensaje. También marcamos el nuevo mensaje como no hecho (done = false).

Si el chat ya está en la lista, actualizamos el objeto de chat existente con el nuevo texto del mensaje y la hora actual, marcando el mensaje como no procesado. Esto garantiza que el último mensaje de cada chat se registre y actualice correctamente. Una vez hecho todo, establecemos el primer indicador de actualización como falso.

      //--- After the first update, set the flag to false
      member_first_remove=false;

Finalmente, devolvemos el resultado de la solicitud posterior.

   //--- Return the result of the POST request
   return(res);

Armados con esta función, podemos estar seguros de que recuperamos las actualizaciones del chat y las almacenamos en cada intervalo de tiempo establecido, y así podemos procesarlas cuando lo necesitemos. El procesamiento de los mensajes se realiza en la siguiente sección.


Manejo de respuestas

Tras obtener las actualizaciones del chat, podemos proceder a acceder a los mensajes recuperados, realizar comparaciones y enviar respuestas de vuelta a Telegram. Esto se consigue mediante el uso de la función «ProcessMessages» de la clase.

void Class_Bot_EA::ProcessMessages(void){

//---

}

Lo primero que tenemos que hacer es procesar los chats individuales.

   //--- Loop through all chats
   for(int i=0; i<member_chats.Total(); i++){
      Class_Chat *chat=member_chats.GetNodeAtIndex(i); //--- Get the current chat
      if(!chat.member_new_one.done){ //--- Check if the message has not been processed yet
         chat.member_new_one.done=true; //--- Mark the message as processed
         string text=chat.member_new_one.message_text; //--- Get the message text

         //---

      }
   }

Aquí, iteramos a través de la colección «member_chats» y recuperamos el objeto de chat correspondiente para cada chat utilizando la variable de índice, «i», de «member_chats». Para cada chat, comprobamos el mensaje asociado al chat actual para ver si ya se ha procesado evaluando la bandera done en la estructura «member_new_one». Si el mensaje aún no se ha procesado, establecemos este indicador en true, marcando el mensaje como gestionado para evitar el procesamiento duplicado. Por último, extraemos el texto del mensaje de la estructura «member_new_one». Utilizaremos el texto para determinar qué tipo de respuesta o acción, en su caso, debe adoptarse en función del contenido del mensaje. En primer lugar, vamos a definir una instancia en la que el usuario envía un texto de saludo «Hello» desde Telegram.

         //--- Process the command based on the message text
         
         //--- If the message is "Hello"
         if(text=="Hello"){
            string message="Hello world! You just sent a 'Hello' text to MQL5 and has been processed successfully.";
            
         }

Aquí verificamos si el texto del mensaje dice «Hello». En caso afirmativo, elaboramos una respuesta que permite al usuario saber que el sistema ha recibido y procesado el texto «Hello». Esta respuesta sirve como confirmación de que la entrada fue manejada correctamente por el código MQL5. Luego enviamos este acuse de recibo al usuario para informarle que su entrada se procesó correctamente. Para enviar la respuesta, necesitaremos crear otra función para manejar las respuestas.

//+------------------------------------------------------------------+
//| Send a message to Telegram                                      |
//+------------------------------------------------------------------+
int sendMessageToTelegram(const long chat_id,const string text,
                const string reply_markup=NULL){
   string output; //--- Variable to store the response from the request
   string url=TELEGRAM_BASE_URL+"/bot"+getTrimmedToken(InpToken)+"/sendMessage"; //--- Construct the URL for the Telegram API request

   //--- Construct parameters for the API request
   string params="chat_id="+IntegerToString(chat_id)+"&text="+UrlEncode(text); //--- Set chat ID and message text
   if(reply_markup!=NULL){ //--- If a reply markup is provided
      params+="&reply_markup="+reply_markup; //--- Add reply markup to parameters
   }
   params+="&parse_mode=HTML"; //--- Set parse mode to HTML (can also be Markdown)
   params+="&disable_web_page_preview=true"; //--- Disable web page preview in the message

   //--- Send a POST request to the Telegram API
   int res=postRequest(output,url,params,WEB_TIMEOUT); //--- Call postRequest to send the message
   return(res); //--- Return the response code from the request
}

Aquí, definimos la función «sendMessageToTelegram», que envía un mensaje a un chat de Telegram especificado usando la API de Telegram Bot. En primer lugar, construimos la URL para la petición API combinando la URL base de Telegram, el token del bot (recuperado usando «getTrimmedToken»), y el método específico para enviar mensajes («sendMessage»). Esta URL es esencial para dirigir la solicitud de API al punto final correcto. A continuación, construimos los parámetros de consulta para la solicitud. Estos parámetros incluyen:

  • chat_id: ID del chat al que se enviará el mensaje.
  • text: El contenido del mensaje, que está codificado con URL para garantizar que se transmite correctamente.

Si se proporciona una marca de teclado de respuesta personalizada («reply_markup»), se añade a los parámetros. Esto permite incluir botones interactivos en el mensaje. Los parámetros adicionales incluyen:

  • parse_mode=HTML: Especifica que el mensaje debe interpretarse como HTML, permitiendo texto formateado.
  • disable_web_page_preview=true: Asegura que cualquier vista previa de página web esté desactivada en el mensaje.

Por último, la función envía la solicitud utilizando la función «postRequest», que se encarga de la comunicación real con la API de Telegram. El código de respuesta de esta solicitud se devuelve para indicar si el mensaje se ha enviado correctamente o si se ha producido un error.

A continuación, podemos llamar a esta función con los parámetros respectivos como a continuación para enviar la respuesta.

            //--- Send the response message 
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;

Aquí, primero utilizamos la función «sendMessageToTelegram» para enviar el mensaje de respuesta al chat de Telegram apropiado. Llamamos a la función con el «chat.member_id» que se dirige al chat correcto para el mensaje de contenido correcto. El parámetro «reply_markup» se establece en NULL, lo que significa que el mensaje enviado no va acompañado de ningún elemento interactivo o de teclado. Después de enviar el mensaje, utilizamos la sentencia «continue». Omite cualquier código restante en el bucle que se está procesando actualmente y pasa a la siguiente iteración de ese bucle. La lógica aquí es sencilla: Manejamos y reenviamos la respuesta al mensaje actual. Después de eso, prácticamente seguimos adelante, sin procesar más código para el chat o mensaje actual en la iteración actual. Tras la compilación, esto es lo que obtenemos.

HELLO WORLD

Podemos ver que el mensaje se recibió y procesó en cuestión de segundos. Pasemos entonces a añadir un teclado de respuesta personalizado a nuestra función.

//+------------------------------------------------------------------+
//| Create a custom reply keyboard markup for Telegram               |
//+------------------------------------------------------------------+
string customReplyKeyboardMarkup(const string keyboard, const bool resize,
                           const bool one_time){
   // Construct the JSON string for the custom reply keyboard markup.
   // 'keyboard' specifies the layout of the custom keyboard.
   // 'resize' determines whether the keyboard should be resized to fit the screen.
   // 'one_time' specifies if the keyboard should disappear after being used once.
   
   // 'resize' > true: Resize the keyboard to fit the screen.
   // 'one_time' > true: The keyboard will disappear after the user has used it once.
   // 'selective' > false: The keyboard will be shown to all users, not just specific ones.
   
   string result = "{"
                   "\"keyboard\": " + UrlEncode(keyboard) + ", " //--- Encode and set the keyboard layout
                   "\"one_time_keyboard\": " + convertBoolToString(one_time) + ", " //--- Set whether the keyboard should disappear after use
                   "\"resize_keyboard\": " + convertBoolToString(resize) + ", " //--- Set whether the keyboard should be resized to fit the screen
                   "\"selective\": false" //--- Keyboard will be shown to all users
                   "}";
   
   return(result); //--- Return the JSON string for the custom reply keyboard
}

Aquí, definimos la función «customReplyKeyboardMarkup», que crea un teclado de respuesta personalizado para Telegram. Esta función toma tres parámetros: keyboard, resize y one_time. El parámetro keyboard especifica el diseño del teclado personalizado en formato JSON. El parámetro resize determina si el teclado se redimensionará para ajustarse a la pantalla del dispositivo del usuario. Si el parámetro de redimensionamiento se establece en true, el teclado se redimensionará para ajustarse a la pantalla del dispositivo del usuario. El parámetro one_time especifica si el teclado se convertirá en un teclado «de un solo uso», desapareciendo después de que el usuario haya interactuado con él.

Dentro de la función, se construye una cadena JSON que representa el marcado de teclado de la respuesta personalizada. Para garantizar que el parámetro de teclado tiene el formato correcto para la solicitud de API, utilizamos la función «UrlEncode» para codificarlo. A continuación, confiamos en la función «convertBoolToString» para cambiar los valores booleanos de resize y one_time (que determinan si estos valores deben considerarse verdaderos o falsos) en sus representaciones de cadena. Finalmente, la cadena construida es devuelta por la función y puede ser utilizada en peticiones API a Telegram. La función personalizada que utilizamos es la siguiente

//+------------------------------------------------------------------+
//| Convert boolean value to string                                 |
//+------------------------------------------------------------------+
string convertBoolToString(const bool _value){
   if(_value)
      return("true"); //--- Return "true" if the boolean value is true
   return("false"); //--- Return "false" if the boolean value is false
}

Por último, para ocultar y forzar la respuesta en los teclados personalizados, utilizamos las siguientes funciones.

//+------------------------------------------------------------------+
//| Create JSON for hiding custom reply keyboard                    |
//+------------------------------------------------------------------+
string hideCustomReplyKeyboard(){
   return("{\"hide_keyboard\": true}"); //--- JSON to hide the custom reply keyboard
}

//+------------------------------------------------------------------+
//| Create JSON for forcing a reply to a message                    |
//+------------------------------------------------------------------+
string forceReplyCustomKeyboard(){
   return("{\"force_reply\": true}"); //--- JSON to force a reply to the message
}

Aquí, las funciones «hideCustomReplyKeyboard» y «forceReplyCustomKeyboard» generan cadenas JSON que especifican acciones particulares a realizar por la función de teclado personalizado de Telegram.

Para la función «hideCustomReplyKeyboard», la cadena JSON que genera dice: «{\«hide_keyboard\»: true}». Esta configuración JSON le dice a Telegram que oculte el teclado de respuesta después de que el usuario envíe un mensaje. En esencia, esta función sirve para hacer desaparecer el teclado una vez que se ha enviado un mensaje.

Para la función «forceReplyCustomKeyboard», la cadena JSON que genera dice: «{\«force_reply\»: true}». Esta cadena indica a Telegram que requiera una respuesta del usuario antes de que pueda interactuar con cualquier otro elemento de la interfaz de usuario en el chat. Esta cadena sirve para mantener al usuario interactuando singularmente con el mensaje que se acaba de enviar.

Armados con la función de teclado de respuesta personalizada, llamemos a la función para construir el teclado de respuesta en Telegram.

         //--- If the message is "Hello"
         if(text=="Hello"){
            string message="Hello world! You just sent a 'Hello' text to MQL5 and has been processed successfully.";
            
            //--- Send the response message 
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup("[[\"Hello\"]]",false,false));
            continue;
         }

Cuando enviamos el mensaje en Telegram, obtenemos el siguiente resultado.

TECLADO DE RESPUESTA PERSONALIZADO HELLO

Podemos ver que esto fue un éxito. Ahora podemos enviar el mensaje simplemente haciendo clic en el botón. Sin embargo, es bastante grande. Ahora podemos agregar varios botones. Primero, agreguemos botones en formato de filas.

            string message="Hello world! You just sent a 'Hello' text to MQL5 and has been processed successfully.";
            string buttons_rows = "[[\"Hello 1\"],[\"Hello 2\"],[\"Hello 3\"]]";
            //--- Send the response message 
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(buttons_rows,false,false));
            continue;

Aquí, definimos un diseño de teclado de respuesta personalizado con la variable «buttons_rows». Esta cadena «[[\»Hello 1\«],[\»Hello 2\«],[\»Hello 3\«]]» representa un teclado con tres botones, cada uno etiquetado como «Hello 1», «Hello 2», y «Hello 3». El formato de esta cadena es JSON, que utiliza Telegram para representar el teclado. Al ejecutarlo, tenemos los siguientes resultados.

DISEÑO DE FILAS

Para visualizar la distribución del teclado en formato de columna, implementamos la siguiente lógica.

            string message="Hello world! You just sent a 'Hello' text to MQL5 and has been processed successfully.";
            string buttons_rows = "[[\"Hello 1\",\"Hello 2\",\"Hello 3\"]]";
            //--- Send the response message 
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(buttons_rows,false,false));

Al ejecutar el programa, recibimos la siguiente salida.

DISPOSICIÓN DE COLUMNAS

Podemos ver que el diseño que hemos recibido está en formato de columnas, lo que significa que el proceso ha sido un éxito. Ahora podemos seguir creando comandos más complejos. Al principio, tengamos una lista personalizada de comandos que el usuario pueda procesar rápidamente.

         //--- If the message is "/start", "/help", "Start", or "Help"
         if(text=="/start" || text=="/help" || text=="Start" || text=="Help"){
            //chat.member_state=0; //--- Reset the chat state
            string message="I am a BOT \xF680 and I work with your MT5 Forex trading account.\n";
            message+="You can control me by sending these commands \xF648 :\n";
            message+="\nInformation\n";
            message+="/name - get EA name\n";
            message+="/info - get account information\n";
            message+="/quotes - get quotes\n";
            message+="/screenshot - get chart screenshot\n";
            message+="\nTrading Operations\n";
            message+="/buy - open buy position\n";
            message+="/close - close a position\n";
            message+="\nMore Options\n";
            message+="/contact - contact developer\n";
            message+="/join - join our MQL5 community\n";
            
            //--- Send the response message with the main keyboard
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_MAIN,false,false));
            continue;
         }

Aquí, verificamos si el mensaje entrante está entre los comandos predeterminados de «/start», «/help», «Start» y «Help». Si es uno de estos comandos, preparamos una misiva de bienvenida que presenta el bot al usuario y proporciona una lista de comandos que se pueden enviar al bot para interactuar con él. Elimina partes de esta lista y categoriza otras para ofrecer al usuario una visión general de lo que puede hacer con el bot. Por último, enviamos este mensaje junto con un teclado personalizado al usuario, que es más adecuado para interactuar con el bot que la línea de comandos. También hemos definido el teclado personalizado de la siguiente manera.

   #define EMOJI_CANCEL "\x274C" //--- Cross mark emoji
   #define KEYB_MAIN    "[[\"Name\"],[\"Account Info\"],[\"Quotes\"],[\"More\",\"Screenshot\",\""+EMOJI_CANCEL+"\"]]" //--- Main keyboard layout

Usamos la macro #define para definir dos elementos que se usarán en la interfaz de usuario del bot de Telegram. En primer lugar, definimos «EMOJI_CANCEL» como un emoji de cruz utilizando su representación Unicode «\x274C». Utilizaremos este emoji en el teclado para indicar una opción «Cancel». La representación Unicode del emoji es la que se muestra a continuación:

MARCA DE CRUZ UNICODE

A continuación, definimos «KEYB_MAIN», que representa la distribución del teclado principal para el bot. El teclado está estructurado como una matriz JSON con filas de botones. El diseño incluye opciones que están contenidas en la lista de comandos que son «Name», «Account Info», «Quotes» y una fila con «More», «Screenshot» y el botón «Cancel» representado por el «EMOJI_CANCEL». Este teclado se mostrará al usuario, lo que le permitirá interactuar con el bot presionando estos botones en lugar de escribir comandos manualmente. Cuando ejecutamos el programa, obtenemos la siguiente salida.

Interfaz de usuario JSON de TELEGRAM 1

Ahora tenemos el teclado personalizado con formato JSON y la lista de comandos que podemos enviar al bot. Lo que ahora queda es elaborar las respuestas respectivas según los comandos recibidos de Telegram. Comenzaremos respondiendo al comando "/name". 

         //--- If the message is "/name" or "Name"
         if (text=="/name" || text=="Name"){
            string message = "The file name of the EA that I control is:\n";
            message += "\xF50B"+__FILE__+" Enjoy.\n";
            sendMessageToTelegram(chat.member_id,message,NULL);
         }

Aquí verificamos si el mensaje recibido del usuario es «/name» o «Name». En caso de que esta comprobación dé un resultado positivo, nos ponemos manos a la obra para construir una respuesta al usuario que contenga el nombre del archivo del Asesor Experto (EA) que se está utilizando en ese momento. Inicializamos una variable de cadena llamada «message», que comienza con el texto «The file name of the EA that I control is:\n». Seguimos esta declaración inicial con un emoji de libro (representado por el código «\xF50B») y el nombre del archivo EA.

Utilizamos la macro incorporada en MQL5 «__FILE__» para obtener el nombre del archivo. La macro devuelve el nombre y la ruta del archivo. A continuación, construimos un mensaje que se enviará al usuario. El mensaje consiste en el nombre del archivo EA y la ruta hacia él. Enviamos el mensaje construido utilizando la función «sendMessageToTelegram». Esta función toma tres parámetros: el primero es el ID de chat del usuario al que queremos enviar el mensaje; el segundo es el mensaje en sí; y el tercer parámetro, que se establece en «NULL», indica que no estamos enviando ningún comando de teclado o botón personalizado junto con nuestro mensaje. Esto es importante ya que no queremos crear un teclado adicional. Cuando hacemos clic en el comando «/name» o en su botón, recibimos la respuesta respectiva como se indica a continuación.

COMANDO NAME

Eso fue un éxito. De igual forma, elaboramos las respuestas respectivas a los comandos de información de cuenta y cotización de precios. Esto se logra mediante el siguiente fragmento de código.

         //--- If the message is "/info" or "Account Info"
         ushort MONEYBAG = 0xF4B0; //--- Define money bag emoji
         string MONEYBAGcode = ShortToString(MONEYBAG); //--- Convert emoji to string
         if(text=="/info" || text=="Account Info"){
            string currency=AccountInfoString(ACCOUNT_CURRENCY); //--- Get the account currency
            string message="\x2733\Account No: "+(string)AccountInfoInteger(ACCOUNT_LOGIN)+"\n";
            message+="\x23F0\Account Server: "+AccountInfoString(ACCOUNT_SERVER)+"\n";
            message+=MONEYBAGcode+"Balance: "+(string)AccountInfoDouble(ACCOUNT_BALANCE)+" "+currency+"\n";
            message+="\x2705\Profit: "+(string)AccountInfoDouble(ACCOUNT_PROFIT)+" "+currency+"\n";
            
            //--- Send the response message
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;
         }

         //--- If the message is "/quotes" or "Quotes"
         if(text=="/quotes" || text=="Quotes"){
            double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); //--- Get the current ask price
            double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); //--- Get the current bid price
            string message="\xF170 Ask: "+(string)Ask+"\n";
            message+="\xF171 Bid: "+(string)Bid+"\n";
            
            //--- Send the response message
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;
         }

Para los comandos de operación comercial, más particularmente la apertura de una posición de compra, utilizamos la siguiente lógica.

         //--- If the message is "/buy" or "Buy"
         if (text=="/buy" || text=="Buy"){
            CTrade obj_trade; //--- Create a trade object
            double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); //--- Get the current ask price
            double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); //--- Get the current bid price
            obj_trade.Buy(0.01,NULL,0,Bid-300*_Point,Bid+300*_Point); //--- Open a buy position
            double entry=0,sl=0,tp=0,vol=0;
            ulong ticket = obj_trade.ResultOrder(); //--- Get the ticket number of the new order
            if (ticket > 0){
               if (PositionSelectByTicket(ticket)){ //--- Select the position by ticket
                  entry=PositionGetDouble(POSITION_PRICE_OPEN); //--- Get the entry price
                  sl=PositionGetDouble(POSITION_SL); //--- Get the stop loss price
                  tp=PositionGetDouble(POSITION_TP); //--- Get the take profit price
                  vol=PositionGetDouble(POSITION_VOLUME); //--- Get the volume
               }
            }
            string message="\xF340\Opened BUY Position:\n";
            message+="Ticket: "+(string)ticket+"\n";
            message+="Open Price: "+(string)entry+"\n";
            message+="Lots: "+(string)vol+"\n";
            message+="SL: "+(string)sl+"\n";
            message+="TP: "+(string)tp+"\n";
            
            //--- Send the response message
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;
         }

Aquí, manejamos un escenario en el que el usuario envía el mensaje «/buy» o «Buy». Nuestro primer paso es crear un objeto CTrade llamado «obj_trade», que utilizaremos para realizar la operación de trading. A continuación, obtenemos los precios de compra y venta actuales llamando a la función SymbolInfoDouble. Para abrir nuestra posición de compra, utilizamos la función Buy del objeto CTrade. Fijamos el volumen de la operación en 0.01 lotes. Establecemos el stop loss (SL) y el take profit (TP) a 300 puntos por debajo y por encima del precio de demanda (bid), respectivamente.

Una vez abierta la posición, determinamos el número de entrada de la nueva orden mediante la función «ResultOrder». Con el ticket en la mano, utilizamos la función PositionGetInteger para seleccionar la posición por ticket. A continuación, recuperamos estadísticas vitales como el precio de entrada, el volumen, el stop loss y el take profit. Con estos números, construimos un mensaje que informará al usuario de que ha abierto una posición de compra. Para manejar el comando de cierre de posición y contacto, utilizamos la siguiente lógica similar.

         //--- If the message is "/close" or "Close"
         if (text=="/close" || text=="Close"){
            CTrade obj_trade; //--- Create a trade object
            int totalOpenBefore = PositionsTotal(); //--- Get the total number of open positions before closing
            obj_trade.PositionClose(_Symbol); //--- Close the position for the symbol
            int totalOpenAfter = PositionsTotal(); //--- Get the total number of open positions after closing
            string message="\xF62F\Closed Position:\n";
            message+="Total Positions (Before): "+(string)totalOpenBefore+"\n";
            message+="Total Positions (After): "+(string)totalOpenAfter+"\n";
            
            //--- Send the response message
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;
         }

         //--- If the message is "/contact" or "Contact"
         if (text=="/contact" || text=="Contact"){
            string message="Contact the developer via link below:\n";
            message+="https://t.me/Forex_Algo_Trader";
            
            //--- Send the contact message
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;
         }

Ahora está claro que podemos responder a comandos enviados desde Telegram. Hasta aquí, sólo enviamos mensajes de texto sin formato. Seamos un poco más extravagantes y demos formato a nuestros mensajes de texto utilizando la entidad Hypertext Markup Language (HTML), que también podría ser Markdown. Usted elige.

         //--- If the message is "/join" or "Join"
         if (text=="/join" || text=="Join"){
            string message="You want to be part of our MQL5 Community?\n";
            message+="Welcome! <a href=\"https://t.me/forexalgo_trading\">Click me</a> to join.\n";
            message+="<s>Civil Engineering</s> Forex AlgoTrading\n";//strikethrough
            message+="<pre>This is a sample of our MQL5 code</pre>\n";//preformat
            message+="<u><i>Remember to follow community guidelines!\xF64F\</i></u>\n";//italic, underline
            message+="<b>Happy Trading!</b>\n";//bold
            
            //--- Send the join message
            sendMessageToTelegram(chat.member_id,message,NULL);
            continue;
         }

Aquí respondemos al usuario cuando envía el mensaje «/join» o «Join». Empezaremos elaborando un mensaje que invite al usuario a unirse a la Comunidad MQL5. El mensaje incluye un hipervínculo en el que los usuarios pueden hacer clic para unirse a la comunidad, así como varios ejemplos de cómo se puede dar formato al texto utilizando etiquetas HTML en Telegram:

  • Texto tachado: Utilizamos la etiqueta <s> para tachar las palabras «Ingeniería Civil» y enfatizar que nos centramos en «Forex AlgoTrading».
  • Texto preformateado: La etiqueta <pre> se utiliza para mostrar una muestra de código MQL5 en un bloque de texto preformateado.
  • Texto en cursiva y subrayado: Las etiquetas <u> y <i> se combinan para subrayar y poner en cursiva un recordatorio para que los usuarios sigan las directrices de la comunidad, añadiendo un emoji Unicode para dar énfasis.
  • Texto en negrita: La etiqueta <b> se utiliza para poner en negrita la frase de cierre «Happy Trading!».

Por último, enviamos este mensaje formateado al usuario a través de Telegram utilizando la función «sendMessageToTelegram», asegurándonos de que el usuario recibe una invitación bien formateada y atractiva para unirse a la comunidad MQL5. Al ejecutarlo, obtenemos la siguiente salida.

ENTIDAD HTML

Ahora que hemos agotado las listas de comandos, sigamos modificando el teclado de respuesta y generemos uno nuevo una vez pulsado el botón «more». Se aplica la siguiente lógica.

         //--- If the message is "more" or "More"
         if (text=="more" || text=="More"){
            chat.member_state=1; //--- Update chat state to show more options
            string message="Choose More Options Below:";
            
            //--- Send the more options message with the more options keyboard
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_MORE,false,true));
            continue;
         }

Cuando recibimos el mensaje «more» o «More» del usuario, lo tomamos como una señal para actualizar el contexto de la conversación actual. En el mundo de los chatbots, el mensaje indica que el usuario no está satisfecho con la cantidad actual de opciones o no ha encontrado lo que busca hasta el momento. Por tanto, nuestra respuesta al usuario debe ofrecer una variedad diferente de selecciones. En términos prácticos, esto significa que enviamos al usuario un nuevo mensaje con una nueva distribución de teclado. La «KEYB_MORE» es como se muestra a continuación:

   #define EMOJI_UP    "\x2B06" //--- Upwards arrow emoji
   #define KEYB_MORE "[[\""+EMOJI_UP+"\"],[\"Buy\",\"Close\",\"Next\"]]" //--- More options keyboard layout

Cuando ejecutamos el programa obtenemos la siguiente salida.

MORE GIF

Eso fue un éxito. Podemos manejar de forma similar los otros comandos.

         //--- If the message is the up emoji
         if(text==EMOJI_UP){
            chat.member_state=0; //--- Reset chat state
            string message="Choose a menu item:";
            
            //--- Send the message with the main keyboard
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_MAIN,false,false));
            continue;
         }

         //--- If the message is "next" or "Next"
         if(text=="next" || text=="Next"){
            chat.member_state=2; //--- Update chat state to show next options
            string message="Choose Still More Options Below:";
            
            //--- Send the next options message with the next options keyboard
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_NEXT,false,true));
            continue;
         }

         //--- If the message is the pistol emoji
         if (text==EMOJI_PISTOL){
            if (chat.member_state==2){
               chat.member_state=1; //--- Change state to show more options
               string message="Choose More Options Below:";
               
               //--- Send the message with the more options keyboard
               sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_MORE,false,true));
            }
            else {
               chat.member_state=0; //--- Reset chat state
               string message="Choose a menu item:";
               
               //--- Send the message with the main keyboard
               sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_MAIN,false,false));
            }
            continue;
         }

         //--- If the message is the cancel emoji
         if (text==EMOJI_CANCEL){
            chat.member_state=0; //--- Reset chat state
            string message="Choose /start or /help to begin.";
            
            //--- Send the cancel message with hidden custom reply keyboard
            sendMessageToTelegram(chat.member_id,message,hideCustomReplyKeyboard());
            continue;
         }

Aquí tratamos diversos mensajes de usuario para controlar la interfaz del chat. Cuando un usuario envía el emoji de arriba, lo tomamos como una señal y reiniciamos el estado del chat a 0, pidiendo al usuario que elija de nuevo un elemento del menú, acompañado de la distribución principal del teclado. Cuando un usuario envía «siguiente» o «Next», actualizamos el estado del chat a 2 e indicamos al usuario que elija de nuevo un elemento del menú, esta vez desde una disposición del teclado que presenta opciones adicionales.

Para el emoji de la pistola, ajustamos el estado del chat en función de su valor actual: si el estado es 2, lo cambiamos a 1 y presentamos el teclado de más opciones; si el estado es distinto, lo cambiamos a 0 y presentamos el teclado del menú principal. Para el emoji de cancelación, reseteamos el estado del chat a 0 y enviamos al usuario un mensaje que le dice que elija «/start» o «/help» para empezar. Enviamos este mensaje con el teclado de respuesta personalizado oculto para borrar cualquier teclado personalizado activo para el usuario. Los diseños personalizados adicionales utilizados son los siguientes:

   #define EMOJI_PISTOL   "\xF52B" //--- Pistol emoji
   #define KEYB_NEXT "[[\""+EMOJI_UP+"\",\"Contact\",\"Join\",\""+EMOJI_PISTOL+"\"]]" //--- Next options keyboard layout

Hasta este punto todo está completo. Solo tenemos que manejar los comandos de captura de pantalla y eso será todo. La siguiente lógica se implementa para manejar el modo de recepción de las imágenes de los gráficos. Para este propósito se utilizará la distribución del teclado en lugar de tener que escribir manualmente.

         //--- If the message is "/screenshot" or "Screenshot"
         static string symbol = _Symbol; //--- Default symbol
         static ENUM_TIMEFRAMES period = _Period; //--- Default period
         if (text=="/screenshot" || text=="Screenshot"){
            chat.member_state = 10; //--- Set state to screenshot request
            string message="Provide a symbol like 'AUDUSDm'";
            
            //--- Send the message with the symbols keyboard
            sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_SYMBOLS,false,false));
            continue;
         }

         //--- Handle state 10 (symbol selection for screenshot)
         if (chat.member_state==10){
            string user_symbol = text; //--- Get the user-provided symbol
            if (SymbolSelect(user_symbol,true)){ //--- Check if the symbol is valid
               chat.member_state = 11; //--- Update state to period request
               string message = "CORRECT: Symbol is found\n";
               message += "Now provide a Period like 'H1'";
               symbol = user_symbol; //--- Update symbol
               
               //--- Send the message with the periods keyboard
               sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_PERIODS,false,false));
            }
            else {
               string message = "WRONG: Symbol is invalid\n";
               message += "Provide a correct symbol name like 'AUDUSDm' to proceed.";
               
               //--- Send the invalid symbol message with the symbols keyboard
               sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_SYMBOLS,false,false));
            }
            continue;
         }

         //--- Handle state 11 (period selection for screenshot)
         if (chat.member_state==11){
            bool found=false; //--- Flag to check if period is valid
            int total=ArraySize(periods); //--- Get the number of defined periods
            for(int k=0; k<total; k++){
               string str_tf=StringSubstr(EnumToString(periods[k]),7); //--- Convert period enum to string
               if(StringCompare(str_tf,text,false)==0){ //--- Check if period matches
                  ENUM_TIMEFRAMES user_period=periods[k]; //--- Set user-selected period
                  period = user_period; //--- Update period
                  found=true;
                  break;
               }
            }
            if (found){
               string message = "CORRECT: Period is valid\n";
               message += "Screenshot sending process initiated \xF60E";
               
               //--- Send the valid period message with the periods keyboard
               sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_PERIODS,false,false));
               string caption = "Screenshot of Symbol: "+symbol+
                                " ("+EnumToString(ENUM_TIMEFRAMES(period))+
                                ") @ Time: "+TimeToString(TimeCurrent());
               
               //--- Send the screenshot to Telegram
               sendScreenshotToTelegram(chat.member_id,symbol,period,caption);
            }
            else {
               string message = "WRONG: Period is invalid\n";
               message += "Provide a correct period like 'H1' to proceed.";
               
               //--- Send the invalid period message with the periods keyboard
               sendMessageToTelegram(chat.member_id,message,customReplyKeyboardMarkup(KEYB_PERIODS,false,false));
            }
            continue;
         }

Aquí, abordamos las solicitudes de los usuarios de una captura de pantalla de un gráfico administrando los diferentes estados del flujo de chat. Cuando el usuario envía el comando «/screenshot» o «Screenshot», establecemos el estado del chat en 10 y pedimos al usuario un símbolo mostrando un teclado con los símbolos disponibles. Es importante tener en cuenta aquí que el estado del chat puede ser cualquier número, incluso 1000. Simplemente actúa como un identificador o cuantificador para almacenar el estado que recordamos durante el procesamiento de la respuesta. Si el usuario proporciona un símbolo, verificamos su validez. Si es válido, le pedimos al usuario un período (un "tiempo" válido para el gráfico) mostrando un teclado con las opciones disponibles para los períodos. Si el usuario proporciona un símbolo no válido, se lo notificamos y le solicitamos que nos proporcione uno válido.

Cuando el usuario ingresa un período de tiempo, verificamos si es válido. Si la franja horaria es una de las opciones válidas predefinidas, pasamos a actualizar el estado del chat y reenviamos la solicitud del usuario de una captura de pantalla del símbolo indicado en la última leyenda válida, con los detalles de cumplimiento justo a tiempo necesarios para la declaración «si-entonces» implícita con la que empezamos, e iniciamos el proceso de captura de pantalla en nuestro backend. Si, por el contrario, el usuario proporciona un marco temporal que no coincide con una de las opciones predefinidas válidas, simplemente hacemos saber al usuario que la entrada era errónea, repitiendo las opciones válidas que mostramos de acuerdo con la solicitud de entrada inicial. A continuación se definen los teclados de respuesta personalizados para los símbolos y los puntos, así como la matriz de plazos que utilizamos.

   #define KEYB_SYMBOLS "[[\""+EMOJI_UP+"\",\"AUDUSDm\",\"AUDCADm\"],[\"EURJPYm\",\"EURCHFm\",\"EURUSDm\"],[\"USDCHFm\",\"USDCADm\",\""+EMOJI_PISTOL+"\"]]" //--- Symbol selection keyboard layout
   #define KEYB_PERIODS "[[\""+EMOJI_UP+"\",\"M1\",\"M15\",\"M30\"],[\""+EMOJI_CANCEL+"\",\"H1\",\"H4\",\"D1\"]]" //--- Period selection keyboard layout

   //--- Define timeframes array for screenshot requests
   const ENUM_TIMEFRAMES periods[] = {PERIOD_M1,PERIOD_M15,PERIOD_M30,PERIOD_H1,PERIOD_H4,PERIOD_D1};

Hasta aquí, ya tenemos todo listo con nuestro teclado y respuestas totalmente personalizados. Para comprobarlo, ejecutamos el programa. Estos son los resultados que obtenemos.

GIF DE CAPTURA DE PANTALLA

Aquí podemos ver que el proceso de envío de captura de pantalla se ha iniciado y completado. Cualquier comando o entrada no válidos se maneja de una manera que garantiza que el usuario solo envíe comandos válidos. Para garantizar que todo funcione como está previsto y detectar cualquier limitación resultante, necesitaremos probar la implementación exhaustivamente. Esto se hace en la siguiente sección.


Prueba de la implementación

La prueba es una fase crucial para validar que el programa que creamos funciona según lo previsto. Entonces tendremos que comprobar si funciona correctamente. Lo primero que hacemos es habilitar la vista previa de la página web en nuestras respuestas de enlace. Permitir vistas previas de páginas web en los enlaces permite a los usuarios echar un vistazo al contenido antes de hacer clic. Ven un título y una imagen que a menudo transmiten una buena idea de lo que trata la página vinculada. Esto es fantástico desde el punto de vista de la experiencia del usuario, especialmente si tenemos en cuenta que a menudo es difícil juzgar la calidad de un enlace sólo a partir del texto del enlace en sí. De esta forma, activaremos la vista previa deshabilitada en falso de la siguiente manera.

//+------------------------------------------------------------------+
//| Send a message to Telegram                                      |
//+------------------------------------------------------------------+
int sendMessageToTelegram( ... ){
   
   //--- ...

   params+="&disable_web_page_preview=false"; //--- Enable web page preview in the message

   //--- ...
   
}

Una vez que ejecutamos esto, obtenemos el siguiente resultado.

VISTA PREVIA DE PÁGINA WEB HABILITADA

Ahora podemos recibir las vistas previas de la página web como se muestra. Eso fue un éxito. A continuación, podemos pasar a cambiar la entidad de formato o modo de análisis sintáctico de Hypertext Markup Language (HTML) a Markdown de la siguiente manera:

//+------------------------------------------------------------------+
//| Send a message to Telegram                                      |
//+------------------------------------------------------------------+
int sendMessageToTelegram( ... ){

   //--- ...

   params+="&parse_mode=Markdown"; //--- Set parse mode to Markdown (can also be HTML)

   //--- ...

}

En el modo de análisis de Markdown, necesitaremos cambiar toda la estructura de formato de nuestro código inicial con entidades de Markdown. La forma correcta será la siguiente. 

      //--- If the message is "/join" or "Join"
      if (text=="/join" || text=="Join"){
         string message = "You want to be part of our MQL5 Community?\n";
         message += "Welcome! [Click me](https://t.me/forexalgo_trading) to join.\n"; // Link
         message += "~Civil Engineering~ Forex AlgoTrading\n"; // Strikethrough
         message += "```\nThis is a sample of our MQL5 code\n```"; // Preformatted text
         message += "*_Remember to follow community guidelines! \xF64F_*"; // Italic and underline
         message += "**Happy Trading!**\n"; // Bold
      
         //--- Send the join message
         sendMessageToTelegram(chat.member_id, message, NULL);
         continue;
      }

Esto es lo que cambiamos:

  • Enlace: En Markdown, los enlaces se crean con [texto](URL) en lugar de <a href="URL">texto</a> .
  • Tachado: Utilice ~texto~ para el tachado en lugar de <s>texto</s>.
  • Texto preformateado: Utilice tres puntos suspensivos (```) para formatear texto preformateado en lugar de <pre>texto</pre>.
  • Italico y subrayado: Markdown no admite el subrayado de forma nativa. Lo más parecido es la cursiva con *text* o _text_. El efecto de subrayado de HTML no se admite directamente en Markdown, por lo que se incluye con un marcador de posición si es necesario.
  • Negrita: Utilice asteriscos dobles **text** para negrita en lugar de <b>text</b>.

Cuando ejecutamos el programa, recibimos la siguiente salida.

SALIDA DE MARKDOWN

Para demostrar el proceso de prueba, hemos preparado un vídeo que muestra el programa en acción. Este vídeo ilustra los diferentes casos de prueba que ejecutamos y destaca cómo el programa respondió a diversas entradas y qué tan bien realizó sus tareas necesarias. Al ver este vídeo, obtendrás una imagen muy clara del proceso de pruebas y podrás ver, sin ninguna duda, que la implementación cumple con los requisitos esperados. El vídeo se presenta a continuación.

En resumen, la ejecución exitosa y la verificación de la implementación, como se demuestra en el video adjunto, afirman que el programa está funcionando según lo previsto.


Conclusión

En resumen, el Asesor Experto que hemos creado integra el lenguaje MetaQuotes Language 5 (MQL5) -junto con la plataforma de trading MetaTrader 5- con la app de mensajería Telegram, permitiendo a los usuarios hablar literalmente con sus robots de trading. ¿Y por qué no? Telegram ha surgido como una forma poderosa y fácil de usar para controlar sistemas de comercio automatizado. Utilizándolo se pueden enviar comandos y recibir respuestas del sistema en tiempo real.

En nuestro caso, nos hemos asegurado de que en lugar de esperar a que el Expert Advisor se comunique con el bot Telegram que retransmite la comunicación al usuario, conectemos los dos bots y nos comuniquemos con el Expert Advisor cuando queramos sin tener que esperar a que se genere una señal. Establecemos una serie de conversaciones entre el usuario y el bot. Nos aseguramos de que los comandos MQL5 que el usuario enviaba a través de Telegram se interpretaban correctamente. Después de muchas pruebas, podemos decir con confianza que nuestro Asesor Experto es fiable y robusto.

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

Archivos adjuntos |
Redes neuronales en el trading: Análisis de la situación del mercado usando el Transformador de patrones Redes neuronales en el trading: Análisis de la situación del mercado usando el Transformador de patrones
A la hora de analizar la situación del mercado con nuestros modelos, el elemento clave es la vela. No obstante, sabemos desde hace tiempo que las velas pueden ayudar a predecir los movimientos futuros de los precios. Y en este artículo aprenderemos un método que nos permitirá integrar ambos enfoques.
Redes neuronales en el trading: Transformador con codificación relativa Redes neuronales en el trading: Transformador con codificación relativa
El aprendizaje autosupervisado puede ser una forma eficaz de analizar grandes cantidades de datos no segmentados. El principal factor de éxito es la adaptación de los modelos a las particularidades de los mercados financieros, lo cual contribuye a mejorar el rendimiento de los métodos tradicionales. Este artículo le presentará un mecanismo alternativo de atención que permitirá considerar las dependencias y relaciones relativas entre los datos de origen.
Implementación de un algoritmo de trading de negociación rápida utilizando SAR Parabólico (Stop and Reverse, SAR) y Media Móvil Simple (Simple Moving Average, SMA) en MQL5 Implementación de un algoritmo de trading de negociación rápida utilizando SAR Parabólico (Stop and Reverse, SAR) y Media Móvil Simple (Simple Moving Average, SMA) en MQL5
En este artículo, desarrollamos un Asesor Experto de trading de ejecución rápida en MQL5, aprovechando los indicadores SAR Parabólico (Stop and Reverse, SAR) y Media Móvil Simple (Simple Moving Average, SMA) para crear una estrategia de trading reactiva y eficiente. Detallamos la implementación de la estrategia, incluyendo el uso de los indicadores, la generación de señales y el proceso de prueba y optimización.
Asesores Expertos Auto-Optimizables con MQL5 y Python (Parte III): Descifrando el algoritmo del Boom 1000 Asesores Expertos Auto-Optimizables con MQL5 y Python (Parte III): Descifrando el algoritmo del Boom 1000
En esta serie de artículos, analizamos cómo podemos construir Asesores Expertos capaces de adaptarse de forma autónoma a las condiciones dinámicas del mercado. En el artículo de hoy, intentaremos sintonizar una red neuronal profunda con los mercados sintéticos de Deriv.