
Creación de un asesor experto integrado de MQL5 y Telegram (Parte 3): Envío de señales de MQL5 a Telegram
Introducción
En el artículo anterior, la segunda parte de nuestra serie, examinamos detenidamente el proceso de fusión de MetaQuotes Language 5 (MQL5) con Telegram para la generación y retransmisión de señales. El resultado estaba claro; nos permitía enviar señales de trading a Telegram y, por supuesto, la necesidad de utilizar las señales de trading para que todo el asunto mereciera la pena. Entonces, ¿por qué debemos dar el siguiente paso en el proceso de integración? Lo que hacemos en esta tercera parte de la serie es en gran medida un "siguiente paso" para ilustrar el potencial de la fusión de MQL5 con Telegram en términos de envío de señales de trading. Sin embargo, en lugar de enviar solo la parte de texto de la señal comercial, enviamos una captura de pantalla del gráfico de la señal comercial. A veces es mejor no solo recibir una señal sobre la cual uno puede actuar, sino también ver la configuración de la señal, como las configuraciones de acción del precio en el gráfico en una representación visual, en este caso, la captura de pantalla del gráfico.
Así, en este artículo, nos centraremos en los aspectos específicos de la conversión de datos de imagen a un formato compatible para su incrustación en solicitudes Hypertext Transfer Protocol Secure (HTTP). Esta conversión debe ocurrir si queremos incluir imágenes en nuestro bot de Telegram. Vamos a trabajar a través de los detalles del proceso que nos lleva desde un gráfico en MQL5, a través del terminal de comercio MetaTrader 5 a un bot ingeniosamente arreglado con una leyenda y una imagen del gráfico como la parte visualmente impresionante de nuestra notificación de comercio. Este artículo se divide en cuatro partes.
Para empezar, haremos un resumen básico de cómo funcionan la codificación y la transmisión de imágenes a través de HTTPS. En esta primera sección, explicaremos los conceptos fundamentales y las técnicas utilizadas para llevar a cabo esta tarea. A continuación, nos sumergiremos en el proceso de implementación en MQL5, que es el lenguaje de programación utilizado para escribir programas de trading para la plataforma MetaTrader 5. Detallaremos cómo utilizar los métodos de codificación y transmisión de imágenes. Después, probaremos el programa implementado para comprobar que funciona correctamente. A continuación, concluiremos el artículo con un resumen para volver a tocar los puntos álgidos y describir las ventajas de realizar este trabajo en los sistemas de negociación. Estos son los temas que seguiremos para crear el Asesor Experto (Expert Advisor, EA):
- Descripción general de la codificación y transmisión de imágenes a través de HTTPS
- Implementación en MQL5
- Probando la integración
- Conclusión
Al final, habremos creado un Asesor Experto que envía capturas de pantalla de gráficos e imágenes con información comercial, como señales que se han generado y órdenes realizadas desde la terminal comercial al chat de Telegram especificado. Empecemos.
Descripción general de la codificación y transmisión de imágenes a través de HTTPS
El envío de imágenes a través de Internet y, en particular, la integración con interfaces de programación de aplicaciones (Application Programming Interfaces, API) o plataformas de mensajería requiere que los datos de la imagen primero se codifiquen y luego se envíen sin demoras indebidas ni comprometer el efecto o la seguridad. Un archivo de imagen directa envía demasiados bits y bytes para funcionar sin problemas a través de los tipos de comandos que permiten a un usuario de Internet acceder de manera efectiva a un sitio web, una plataforma o un servicio en particular. Para una API como Telegram, que actúa como intermediario entre un usuario de Internet y un servicio en particular (como una interfaz basada en web para varios tipos de tareas), enviar una imagen requiere que el archivo de imagen primero se codifique y luego se envíe como parte de la carga útil de un comando del usuario al servicio o viceversa, y esto se logra especialmente a través de protocolos como HTTP o HTTPS.
El método más frecuente para convertir imágenes para su envío es transformar la imagen en una cadena codificada en Base64. La codificación de Base64 toma los datos binarios de una imagen y crea una representación de texto. Esto se hace utilizando caracteres de un conjunto específico que hace que la llamada "imagen codificada" funcione correctamente cuando se envía mediante protocolos de texto. Para crear la imagen codificada en Base64, sus datos sin procesar (el archivo real, antes de cualquier operación de "lectura") se leen byte a byte. Luego, los bytes de lectura se representan mediante símbolos Base64. Una vez conseguido esto, el archivo puede proceder a ser enviado mediante un protocolo de texto.
Una vez codificados los datos de la imagen, se envían a través de HTTPS, que es una forma segura de HTTP. A diferencia de HTTP, que envía los datos en texto sin formato, HTTPS utiliza el protocolo de encriptación Secure Socket Layer (SSL)/ Transport Layer Security (TLS) para garantizar que los datos transmitidos desde y hacia un servidor permanecen privados y seguros. La importancia de HTTPS para la protección de las señales de negociación y otras comunicaciones relacionadas con las finanzas no puede exagerarse. Un tercero sin escrúpulos que se haga con señales comerciales, por ejemplo, puede utilizar esa información para realizar operaciones y manipular el mercado en detrimento de las víctimas inocentes de las señales comerciales y en beneficio de la parte que interceptó la señal. La visualización del proceso es la siguiente:
En resumen, el método de codificación y envío de imágenes convierte los archivos de imágenes en un formato basado en texto que es adecuado para las comunicaciones web. También garantiza una entrega segura a través de HTTPS. Es vital comprender estos dos conceptos si uno desea integrar datos de imágenes en las aplicaciones. Un ejemplo obvio son los sistemas comerciales que automatizan las notificaciones a través de Telegram—una plataforma que hace un excelente trabajo al entregar mensajes de manera rápida y confiable.
Implementación en MQL5
La implementación de la retransmisión de imágenes en MQL5 comenzará con el proceso de capturar una captura de pantalla del gráfico comercial actual dentro de un Asesor Experto (EA) MQL5. Codificaremos la captura de pantalla y la enviaremos a través de Telegram. Implementamos esta funcionalidad principalmente en el controlador de eventos OnInit, que se ejecuta cuando se inicializa el EA. Como hemos dicho, el objetivo del manejador de eventos OnInit es preparar y configurar los componentes necesarios del EA, asegurándose de que todo está configurado correctamente antes de que se ejecute la lógica principal de la operación de trading. En primer lugar, definimos el nombre de nuestro archivo de imagen de la captura de pantalla.
//--- Get ready to take a chart screenshot of the current chart #define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.jpg"
Aquí, damos el paso inicial en el flujo de trabajo que es establecer una constante para el nombre del archivo de captura de pantalla. Esto lo conseguimos con la directiva #define, que nos permite asignar un valor constante al que se puede hacer referencia en todo el código. Aquí, creamos una constante llamada "SCREENSHOT_FILE_NAME", que almacena el valor "Our Chart ScreenShot.jpg". Y lo hacemos por una muy buena razón: si alguna vez necesitamos el nombre del archivo para cargar o guardar algo, basta con utilizar esta constante. Si necesitamos cambiar el nombre o el formato del archivo, sólo tenemos que cambiarlo en este único lugar. Puedes darte cuenta de que hemos elegido que nuestro tipo de imagen sea un Joint Photographic Experts Group (JPEG). Puede elegir cualquiera que considere adecuado, como Portable Network Graphics (PNG). Sin embargo, debes tener en cuenta que existen diferencias significativas en los formatos de imagen. Por ejemplo, JPG utiliza un algoritmo de compresión con pérdidas, lo que significa que se pierde parte de los datos de la imagen pero se reduce su tamaño. Un ejemplo de los formatos que puede utilizar es el que se visualiza a continuación:
Integramos la función de captura de pantalla en el manejador OnInit. Esto garantiza que el sistema esté configurado para capturar y guardar el estado del gráfico tan pronto como se inicie el Asesor Experto. Hemos declarado una constante "SCREENSHOT_FILE_NAME", que sirve como sustituto del nombre real del archivo de imagen del gráfico. Al usar este marcador de posición, evitamos (en su mayoría) el problema de intentar guardar dos archivos con el mismo nombre aproximadamente al mismo tiempo. Al realizar este paso, nos aseguramos de que el archivo de imagen del gráfico tenga la misma estructura básica que necesitaría cuando codifiquemos y transmitamos activamente la imagen en este punto.
Este paso es esencial porque establece la convención de nombres de archivo y garantiza que podremos capturar la imagen del gráfico cuando se inicializa el EA por primera vez. A partir de este momento, podemos concentrarnos en capturar los datos del gráfico, codificarlos en un formato adecuado para los ojos humanos y enviarlos a nuestro canal de Telegram elegido.
A continuación, para evitar que intentemos sobrescribir y crear un archivo con un nombre similar, debemos eliminar el existente, si está disponible, y crear uno nuevo. Esto se logra mediante el siguiente fragmento de código.
//--- First delete an instance of the screenshot file if it already exists if(FileIsExist(SCREENSHOT_FILE_NAME)){ FileDelete(SCREENSHOT_FILE_NAME); Print("Chart Screenshot was found and deleted."); ChartRedraw(0); }
Aquí, comenzamos asegurándonos de que no existan instancias del archivo de captura de pantalla antes de capturar uno nuevo. Esto es importante porque queremos evitar cualquier confusión entre el estado actual del gráfico y las capturas de pantalla guardadas previamente. Para lograr esto, verificamos si existe en el sistema un archivo con el nombre almacenado en la constante "SCREENSHOT_FILE_NAME". Lo hacemos usando la función FileIsExist, que verifica el directorio especificado y devuelve verdadero si el archivo está presente. Si el archivo existe, lo borramos usando la función FileDelete. Al asegurarnos de que el directorio especificado está vacío de nuestra vieja captura de pantalla, hacemos espacio para la nueva que crearemos más adelante en el proceso.
Tras el borrado, enviamos un mensaje al terminal utilizando la función Print para indicar que la captura de pantalla del gráfico ha sido encontrada y erradicada con éxito. Esta pequeña información puede ser muy útil para la depuración, ya que sirve para confirmar que el sistema está gestionando correctamente la eliminación de capturas de pantalla anteriores. Después de todo, no querríamos adquirir el hábito de "borrar" archivos inexistentes. También redibujamos inmediatamente el gráfico (llamamos a esta función ChartRedraw) para asegurarnos de que estamos trabajando con un estado visual actualizado del gráfico. Tras esta limpieza, ya podemos proceder a realizar la captura de pantalla.
ChartScreenShot(0,SCREENSHOT_FILE_NAME,1366,768,ALIGN_RIGHT);
Aquí, capturamos la pantalla del gráfico utilizando la función ChartScreenShot, que toma una instantánea del gráfico especificado y la guarda como un archivo de imagen. En nuestro caso, pasamos los parámetros "0", "SCREENSHOT_FILE_NAME", "1366", "768" y "ALIGN_RIGHT" a la función para controlar cómo se toma y guarda la captura de pantalla.
- El primer parámetro, "0", especifica el ID del gráfico del que queremos tomar la captura de pantalla. Un valor de 0 se refiere al gráfico actualmente activo. Si quisiéramos capturar un gráfico diferente, necesitaríamos pasar el ID del gráfico específico.
- El segundo parámetro, "SCREENSHOT_FILE_NAME", es el nombre del archivo donde se guardará la captura de pantalla. En nuestro caso, esta es la constante "Our Chart ScreenShot.jpg". Este archivo se creará en el directorio "Files" del terminal y, si aún no existe, se generará después de tomar la captura de pantalla.
- Los parámetros tercero y cuarto, 1366 y 768, definen las dimensiones de la captura de pantalla en píxeles. Aquí, 1366 representa el ancho de la captura de pantalla y 768 representa la altura. Estos valores se pueden ajustar según las preferencias del usuario o el tamaño de la pantalla que se está capturando.
- El parámetro final, ALIGN_RIGHT, especifica cómo debe alinearse la captura de pantalla dentro de la ventana del gráfico. Al utilizar ALIGN_RIGHT, alineamos la captura de pantalla al lado derecho del gráfico. Se podrían utilizar otras opciones de alineación, como ALIGN_LEFT o ALIGN_CENTER, según el resultado deseado.
Por alguna razón, aunque sea muy insignificante, la captura de pantalla podría tardar en guardarse. Así, para dejar cada piedra girada, necesitamos iniciar una iteración donde podemos esperar unos segundos a que se guarde la captura de pantalla.
// Wait for 30 secs to save screenshot if not yet saved int wait_loops = 60; while(!FileIsExist(SCREENSHOT_FILE_NAME) && --wait_loops > 0){ Sleep(500); }
Aquí, implementamos un bucle while que espera a que el archivo de captura de pantalla se guarde correctamente, asegurándose de que se ha guardado en la ubicación correcta y con el nombre correcto antes de continuar. La espera en sí es lo suficientemente larga como para que, en circunstancias normales, el archivo de captura de pantalla se pueda encontrar fácilmente en el sistema de archivos (si de hecho estaba destinado a guardarse durante la prueba). Empezamos con la variable entera "wait_loops" inicializada a 60. Cada iteración del bucle, si procede sin encontrar el archivo, introduce una espera de medio segundo (500 milisegundos (ms)), que asciende a 30 segundos (60 iteraciones * 500 ms) desde el inicio del bucle hasta su final si no encuentra el archivo.
En cada iteración, también disminuimos el contador "wait_loops" para evitar que el bucle se ejecute indefinidamente si el archivo no se crea en el tiempo especificado. Además, utilizamos la función Sleep para crear un retraso de 500 milisegundos entre cada comprobación. Esto nos evita verificar con demasiada frecuencia y sobrecarguemos nuestro sistema con demasiadas solicitudes de existencia de archivos.
Por último, debemos verificar la existencia del archivo posteriormente, y si no existe, no tiene sentido continuar, ya que todo nuestro algoritmo y lógica dependen del archivo de imagen. Si existe, podemos proceder con los siguientes pasos.
if(!FileIsExist(SCREENSHOT_FILE_NAME)){ Print("THE SPECIFIED SCREENSHOT DOES NOT EXIST (WAS NOT SAVED). REVERTING NOW!"); return (INIT_FAILED); }
Aquí, definimos un mecanismo para manejar errores para verificar si el archivo de captura de pantalla se guardó correctamente. Después de esperar un tiempo hasta que se cree el archivo, verificamos la presencia del archivo mediante la función FileIsExist. Si la comprobación devuelve false, es decir, que el fichero no está presente, emitimos el siguiente mensaje: "THE SPECIFIED SCREENSHOT DOES NOT EXIST (WAS NOT SAVED). REVERTING NOW!" Este mensaje indica que no hemos podido guardar el archivo de captura de pantalla. Después de que aparece este mensaje de error, el programa no puede continuar porque necesitamos exclusivamente ese archivo de imagen como base para la lógica del programa. Por lo tanto, salimos con un valor de retorno de "INIT_FAILED", lo que indica que la inicialización no se pudo completar con éxito. Si se guardó la captura de pantalla, procedemos a informar la instancia también.
else if(FileIsExist(SCREENSHOT_FILE_NAME)){ Print("THE CHART SCREENSHOT WAS SAVED SUCCESSFULLY TO THE DATA-BASE."); }
Al ejecutar el código, estos son los resultados:
Aquí puedes ver que pudimos eliminar con éxito una existencia inicial del archivo de imagen y guardar otro. Para acceder a la imagen en su computadora, haga clic derecho en el nombre del archivo, seleccione abrir la carpeta contenedora y ubique el archivo en la carpeta de archivos.
Alternativamente, puede acceder directamente al archivo de imagen abriendo el navegador, expandiéndolo, haciendo clic derecho en la pestaña de archivos y eligiendo "Abrir carpeta".
Esto abre la carpeta de archivos donde se registró el archivo de imagen.
Aquí puedes ver que está registrado el nombre exacto de la imagen. Verifiquemos finalmente el tamaño y el tipo de archivo para ver si se tiene en cuenta la información correcta especificada.
Podemos ver que el tipo de archivo es JPG y el ancho y la altura de la captura de pantalla son 1366 por 768 píxeles respectivamente, tal como se especifica. Si, por ejemplo, uno quisiera tener un tipo de archivo diferente, digamos PNG, solo se debe cambiar el tipo de archivo como se muestra a continuación:
#define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.png"
Cuando compilamos y ejecutamos este fragmento de código, creamos otra imagen del tipo PNG como se visualiza a continuación en un formato de imagen de formato de intercambio de gráficos (GIF):
Hasta este punto, es evidente que tomamos con éxito la instantánea del gráfico y la guardamos directamente en los archivos. De esta forma tenemos vía libre para proceder a codificar el archivo de imagen para que pueda ser transmitido vía HTTPS. Primero necesitaremos abrir el archivo de imagen y leerlo.
int screenshot_Handle = INVALID_HANDLE; screenshot_Handle = FileOpen(SCREENSHOT_FILE_NAME,FILE_READ|FILE_BIN); if(screenshot_Handle == INVALID_HANDLE){ Print("INVALID SCREENSHOT HANDLE. REVERTING NOW!"); return(INIT_FAILED); }
En el fragmento de código anterior, nos centramos en las operaciones de archivos en MQL5 para manipular un archivo de captura de pantalla que guardamos anteriormente. Declaramos una variable entera llamada «screenshot_Handle» y la inicializamos con el valor «INVALID_HANDLE». El "screenshot_Handle" sirve como referencia al archivo, y el valor "INVALID_HANDLE" actúa como un marcador de posición que nos permite saber que aún no se ha abierto ningún archivo válido. Mantener este valor garantiza que podamos hacer referencia a un archivo a través de su identificador y que podamos manejar cualquier error que surja de las operaciones del archivo en caso de que algo salga mal.
A continuación, intentamos utilizar la función FileOpen para abrir nuestra captura de pantalla guardada. Le damos el nombre de la captura de pantalla, que contiene la ruta al archivo de captura de pantalla. También le damos dos banderas: FILE_READ y FILE_BIN. La primera bandera le dice al sistema que queremos leer el archivo. La segunda bandera, que probablemente es la más importante de las dos, le dice al sistema que el archivo contiene datos binarios (lo que no debe confundirse con la captura de pantalla que es una serie de unos y ceros). Dado que la captura de pantalla es una imagen, y la imagen tiene un formato más bien estándar (transforma ese formato en algo realmente "estándar", "fácil" o "natural" para trabajar, y la imagen se convierte en una serie de unos y ceros, sin formato, sin estructura, solo matemáticas puras; una serie diferente de unos y ceros, y la imagen se ve completamente diferente, aunque esa no es nuestra preocupación aquí), esperamos encontrar una serie de bytes que correspondan, de alguna manera, a la imagen.
La función «FileOpen» devuelve un «handle» válido o «INVALID_HANDLE» después de intentar abrir el archivo. Comprobamos la validez del identificador con una declaración if. Un identificador no válido significa que el archivo no se abrió correctamente. Imprimimos un mensaje de error que dice que el identificador de captura de pantalla no es válido. Entonces, o bien la captura de pantalla no se guardó o no se puede acceder a ella, lo que nos indica que el programa se topó con un muro. No continuamos y en su lugar devolvemos "INIT_FAILED" ya que no tiene sentido continuar si no podemos leer el archivo de imagen. Si el ID del identificador es realmente válido, informamos al usuario sobre el éxito.
else if (screenshot_Handle != INVALID_HANDLE){ Print("SCREENSHOT WAS SAVED & OPENED SUCCESSFULLY FOR READING."); Print("HANDLE ID = ",screenshot_Handle,". IT IS NOW READY FOR ENCODING."); }
Aquí, agregamos otro paso de verificación para garantizar que el archivo de captura de pantalla se haya abierto correctamente. Después de comprobar que "screenshot_Handle" es válido (no igual a "INVALID_HANDLE"), imprimimos un par de mensajes que indican que el archivo se ha abierto correctamente. Esta es simplemente otra forma de afirmar que el "screenshot_Handle" es bueno y que estamos listos para seguir adelante. Nosotros usamos la función Print para el primer mensaje, que dice lo mismo que el segundo mensaje: que la captura de pantalla se guardó correctamente y se abrió para leer. Ambas afirmaciones sirven para confirmar la finalización exitosa del paso actual en nuestro flujo de trabajo.
Luego mostramos el ID del identificador, que identifica de forma única el archivo y permite realizar operaciones posteriores (que serán lectura, escritura y codificación) en el archivo. El ID del identificador también es útil para la depuración. Confirma que el sistema ha obtenido y asignado recursos para administrar este archivo en particular. Concluimos con una declaración de impresión que nos informa que el sistema ahora está listo para realizar la siguiente operación, que es codificar la captura de pantalla para que pueda ser transmitida a través de la red utilizando el protocolo HTTPS.
A continuación, podemos comprobar y verificar que el identificador está efectivamente registrado y almacenado y que tiene un contenido válido.
int screenshot_Handle_Size = (int)FileSize(screenshot_Handle); if (screenshot_Handle_Size > 0){ Print("CHART SCREENSHOT FILE SIZE = ",screenshot_Handle_Size); }
Aquí, obtenemos y verificamos el tamaño del archivo de captura de pantalla abierto anteriormente con su identificador. Llamamos a la función FileSize en el manejador de la captura de pantalla, que devuelve el tamaño del archivo en bytes. A continuación, asignamos este valor a una variable entera llamada «screenshot_Handle_Size». Si el tamaño es mayor que cero, lo que indica que el archivo contiene algún tipo de datos, imprimimos el tamaño del archivo en el registro. Es útil tener este paso porque nos permite saber que, antes de codificar y enviar el archivo a través de HTTP, la captura de pantalla se guarda correctamente y tiene contenido válido.
Si el identificador realmente tiene contenido válido, significa que tenemos abierto el archivo correcto y podemos prepararnos para leer los datos binarios del archivo de captura de pantalla en una matriz para decodificarlos.
uchar photoArr_Data[]; ArrayResize(photoArr_Data,screenshot_Handle_Size); FileReadArray(screenshot_Handle,photoArr_Data,0,screenshot_Handle_Size); if (ArraySize(photoArr_Data) > 0){ Print("READ SCREENSHOT FILE DATA SIZE = ",ArraySize(photoArr_Data)); }
Empezamos declarando un array uchar llamado "photoArr_Data" que contendrá los datos binarios. A continuación, redimensionamos este array para que coincida con el tamaño del archivo de captura de pantalla llamando a la función ArrayResize. A continuación, leemos el contenido del archivo de capturas de pantalla en el «array photoArr_Data», empezando por el índice 0 y llegando hasta el final del archivo (el screenshot_Handle_Size) mediante el uso de la función FileReadArray. A continuación, comprobamos el tamaño de la matriz «photoArr_Data» después de cargarla y si es mayor que 0, lo que significa que no está vacía, registramos su tamaño. Normalmente, es la parte del código que se encarga de leer y procesar el archivo de captura de pantalla, de modo que pueda utilizarse para codificarlo y transmitirlo.
Después de leer el contenido del archivo y almacenarlo, ahora necesitamos cerrar el archivo de imagen. Esto se hace mediante su mango.
FileClose(screenshot_Handle);
Aquí, finalmente cerramos el archivo de captura de pantalla después de leer con éxito sus datos en la matriz de almacenamiento. Invocamos la función FileClose para liberar el manejador asociado al archivo de captura de pantalla. Esto libera recursos del sistema que fueron asignados cuando se abrió el archivo. Es fundamental asegurarse de que el archivo esté cerrado antes de intentar realizar cualquier otra operación en él, como acceder a él, leerlo o escribirlo de cualquier manera. La función señala que hemos completado todas las operaciones de acceso al archivo y ahora estamos pasando a la siguiente etapa del proceso: codificar los datos de la captura de pantalla y prepararlos para su transmisión. Una vez que ejecutamos esto, obtenemos el siguiente resultado:
Puede ver que leemos y almacenamos los datos binarios de la imagen correctamente en la matriz de almacenamiento. Para llegar a ver los datos, podemos imprimirlos en el registro mediante el uso de la función ArrayPrint de la siguiente manera:
ArrayPrint(photoArr_Data);
Al imprimir, estos son los datos que obtenemos:
Es evidente que leemos, copiamos y almacenamos los datos completos, es decir, hasta 320894.
A continuación, debemos preparar los datos de la fotografía para su transmisión a través de HTTP codificándolos en formato Base64. Dado que los datos binarios como las imágenes no se pueden transmitir directamente a través de HTTP, necesitamos usar la codificación Base64 para convertir los datos binarios en un ASCII formato de cadena. Esto garantiza que los datos se puedan incluir de forma segura en la solicitud HTTP. Esto se logra mediante el siguiente fragmento de código.
//--- create boundary: (data -> base64 -> 1024 bytes -> md5) //Encodes the photo data into base64 format //This is part of preparing the data for transmission over HTTP. uchar base64[]; uchar key[]; CryptEncode(CRYPT_BASE64,photoArr_Data,key,base64); if (ArraySize(base64) > 0){ Print("Transformed BASE-64 data = ",ArraySize(base64)); //Print("The whole data is as below:"); //ArrayPrint(base64); }
Para empezar, configuramos dos matrices. El primero es "base64". Aquí se almacenan los datos codificados. La segunda matriz es "key". Nunca utilizamos "key" en este contexto, pero la función de codificación lo requiere. La función que realiza el trabajo de codificación Base64 se llama CryptEncode. Toma cuatro parámetros: el tipo de codificación («CRYPT_BASE64»), los datos binarios de origen («photoArr_Data»), la clave de codificación («key») y la matriz de salida («base64»). Esta función CryptEncode realiza el trabajo real de convertir los datos binarios a formato Base64 y almacenar el resultado en la matriz «base64». Cuando comprobamos el tamaño de «base64» con la función ArraySize, si «base64» contiene algún elemento, es decir, si es mayor que 0, significa que la codificación se ha realizado correctamente.
Para imprimir estos datos en el diario, utilizamos la función ArrayPrint.
Print("Transformed BASE-64 data = ",ArraySize(base64)); Print("The whole data is as below:"); ArrayPrint(base64);
Obtenemos el siguiente resultado:
Podemos ver que hay una desviación significativa entre los datos binarios originales de tamaño 320894 y los datos recién convertidos de tamaño 427860. Esta desviación es el resultado de la transformación y codificación de los datos.
A continuación, tenemos que preparar un subconjunto de los datos codificados en Base64 para asegurarnos de que manejamos una parte manejable de los mismos para los siguientes pasos de nuestro proceso. En concreto, tenemos que centrarnos en copiar los primeros 1024 bytes de los datos codificados en una matriz temporal para su uso posterior.
//Copy the first 1024 bytes of the base64-encoded data into a temporary array uchar temporaryArr[1024]= {0}; ArrayCopy(temporaryArr,base64,0,0,1024);
Para empezar, creamos un array temporal, «temporaryArr», que tiene un tamaño de 1024 bytes. Inicializamos todos sus valores a cero. Utilizamos esta matriz para almacenar el primer segmento de los datos codificados en Base64. Como el valor de inicialización es cero, evitamos posibles problemas con la información residual en la memoria donde se almacena la matriz temporal.
A continuación, utilizamos la función ArrayCopy para mover los primeros 1024 bytes de «base64» a «temporaryArr». Que gestiona la operación de copia de forma limpia y eficaz. Las razones de ello y los detalles de la operación de copia son historia aparte, pero mencionaré sólo un par de cosas. El efecto secundario de la inicialización es la eliminación de cualquier preocupación que pueda tener sobre la primera parte de los datos codificados en Base64 si los visualiza como una especie de galimatías aleatorio. Registremos el array temporal vacío. Esto se consigue mediante el siguiente código.
Print("FILLED TEMPORARY ARRAY WITH ZERO (0) IS AS BELOW:"); ArrayPrint(temporaryArr);
Tras la compilación, esto es lo que tenemos:
Podemos ver que la matriz temporal está llena de ceros. Luego, estos ceros se reemplazan con los primeros 1024 valores de los datos formateados originalmente. Podemos ver estos datos nuevamente a través de una lógica similar.
Print("FIRST 1024 BYTES OF THE ENCODED DATA IS AS FOLLOWS:"); ArrayPrint(temporaryArr);
La presentación de datos temporales rellenados es la siguiente:
Después de obtener estos datos temporales, necesitamos generar un hash del algoritmo Message-Digest 5 (MD5) a partir de los primeros 1024 bytes de los datos codificados en Base64. Este hash MD5 se utilizará como parte del límite en una estructura "multipart/form-data", que a menudo se emplea en solicitudes HTTP POST para manejar cargas de archivos.
//Create an MD5 hash of the temporary array //This hash will be used as part of the boundary in the multipart/form-data uchar md5[]; CryptEncode(CRYPT_HASH_MD5,temporaryArr,key,md5); if (ArraySize(md5) > 0){ Print("SIZE OF MD5 HASH OF TEMPORARY ARRAY = ",ArraySize(md5)); Print("MD5 HASH boundary in multipart/form-data is as follows:"); ArrayPrint(md5); }
Para empezar, declaramos un array llamado «md5» para almacenar el resultado del hash MD5. El algoritmo MD5 (donde «MD» significa «Message Digest») es una función hash criptográfica que produce un valor hash de 128 bits. El hash se representa normalmente como una cadena de 32 dígitos hexadecimales.
En este caso, utilizamos la función incorporada de MQL5 CryptEncode con el parámetro CRYPT_HASH_MD5 para calcular el hash. Pasamos a la función un array temporal llamado «temporaryArr», que contiene los primeros 1024 bytes de los datos codificados en Base64. El parámetro «key» se utiliza normalmente para operaciones criptográficas adicionales, pero no es necesario para MD5 y se establece como una matriz vacía en este contexto. El resultado de la operación hash se almacena en el array «md5».
Tras calcular el hash, comprobamos que la matriz «md5» no está vacía verificando el número de elementos de la matriz mediante la función ArraySize. Si la matriz tiene elementos, registramos el tamaño del hash MD5 y luego el valor hash real. Ese valor hash se utiliza para crear una cadena de límite en el formato "multipart/form-data" que ayuda a separar las diferentes partes de la solicitud HTTP que se transmite. El algoritmo MD5 se utiliza aquí estrictamente por su carácter común y el valor único que produce, no porque sea el mejor algoritmo o el más seguro para usar. Una vez que ejecutamos esto, obtenemos los siguientes datos:
Aquí puedes ver que obtenemos los datos hash MD5 en forma numérica. Por lo tanto, necesitamos convertir el hash MD5 en una cadena hexadecimal y luego truncarlo para cumplir con un requisito de longitud específico para su uso como límite en solicitudes HTTP multipart/form-data, que normalmente es 16.
//Format MD5 hash as a hexadecimal string & //truncate it to 16 characters to create the boundary. string HexaDecimal_Hash=NULL;//Used to store the hexadecimal representation of MD5 hash int total=ArraySize(md5); for(int i=0; i<total; i++){ HexaDecimal_Hash+=StringFormat("%02X",md5[i]); } Print("Formatted MD5 Hash String is: \n",HexaDecimal_Hash); HexaDecimal_Hash=StringSubstr(HexaDecimal_Hash,0,16);//truncate HexaDecimal_Hash string to its first 16 characters //done to comply with a specific length requirement for the boundary //in the multipart/form-data of the HTTP request. Print("Final Truncated (16 characters) MD5 Hash String is: \n",HexaDecimal_Hash);
Para empezar, declaramos una variable string, «HexaDecimal_Hash», para contener la forma hexadecimal del hash MD5. Esta cadena servirá como marcador de límite para separar diferentes partes de nuestra carga útil de solicitud HTTP.
A continuación, recorremos cada byte del hash almacenado en la matriz md5. Convertimos cada byte en una cadena de dos caracteres, utilizando el especificador de formato "%02X". La parte "%0" del especificador indica que la cadena debe rellenarse con ceros iniciales si es necesario, para garantizar que cada byte esté representado con dos caracteres. El "02" indica dos caracteres (como mínimo) para la representación; la "X" indica que los caracteres deben ser números hexadecimales (con letras mayúsculas si es necesario).
Una vez más, estos caracteres hexadecimales se añaden a la cadena "HexaDecimal_Hash". Finalmente, enviamos el contenido de la cadena al registro para verificar que se haya formado correctamente. Una ejecución del programa da como resultado la siguiente información:
Esto fue un éxito. A continuación, debemos construir y preparar los datos del archivo para una solicitud HTTP POST multipart/form-data, que se utilizará para enviar la foto al chat de Telegram a través de la API de Telegram. Esto implicará preparar el cuerpo de la solicitud para incluir tanto los campos del formulario como los datos del archivo en un formato que el servidor pueda procesar correctamente. Logramos esto a través del siguiente fragmento de código.
//--- WebRequest char DATA[]; string URL = NULL; URL = TG_API_URL+"/bot"+botTkn+"/sendPhoto"; //--- add chart_id //Append a carriage return and newline character sequence to the DATA array. //In the context of HTTP, \r\n is used to denote the end of a line //and is often required to separate different parts of an HTTP request. ArrayAdd(DATA,"\r\n"); //Append a boundary marker to the DATA array. //Typically, the boundary marker is composed of two hyphens (--) //followed by a unique hash string and then a newline sequence. //In multipart/form-data requests, boundaries are used to separate //different pieces of data. ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n"); //Add a Content-Disposition header for a form-data part named chat_id. //The Content-Disposition header is used to indicate that the following data //is a form field with the name chat_id. ArrayAdd(DATA,"Content-Disposition: form-data; name=\"chat_id\"\r\n"); //Again, append a newline sequence to the DATA array to end the header section //before the value of the chat_id is added. ArrayAdd(DATA,"\r\n"); //Append the actual chat ID value to the DATA array. ArrayAdd(DATA,chatID); //Finally, Append another newline sequence to the DATA array to signify //the end of the chat_id form-data part. ArrayAdd(DATA,"\r\n");
Comenzamos configurando la matriz "DATA" y la "URL" para la solicitud HTTP. La «URL» se construye a partir de tres partes: la URL base para la API («TG_API_URL»); el token para el bot, que identifica al bot ante la API («botTkn»); y el endpoint para enviar una foto a un chat («/sendPhoto»). Esta URL especifica a qué «servidor remoto» estamos enviando nuestra «carga útil»: la foto que queremos enviar y la información que queremos adjuntar a la foto. La URL del punto final no cambia; es la misma para cada solicitud que hagamos. Nuestras solicitudes irán al mismo lugar tanto si enviamos una foto como varias, tanto si enviamos fotos a diferentes chats, etcétera.
Después de eso, agregamos un marcador de límite al borde de nuestro fragmento de datos. Se compone de dos guiones (--) y nuestro hash de límite único («HexaDecimal_Hash»). En total, aparece así: «--HexaDecimal_Hash». Este marcador de límite aparece al comienzo del fragmento de datos para la siguiente parte de la solicitud, que es un campo de formulario "chart_id". El encabezado Content-Disposition especifica que la siguiente parte (el siguiente fragmento de datos) de la solicitud multipart/form-data es un campo de formulario y proporciona el nombre de ese campo ("chart_id").
Agregamos este encabezado y un carácter de nueva línea ("/r/n") para indicar el final de la sección del encabezado. Después de la sección de encabezado, agregamos el valor "chartID" a la matriz DATA, seguido de otro carácter de nueva línea ("/r/n") para indicar el final de la parte de datos del formulario "chart_id". Este proceso garantiza que el campo del formulario esté correctamente formateado y separado de las otras partes de la solicitud para garantizar que la API de Telegram reciba y procese correctamente los datos.
Quizás hayas notado que usamos dos "funciones de sobrecarga" en el código. Veamos a continuación un fragmento de código.
//+------------------------------------------------------------------+ // ArrayAdd for uchar Array void ArrayAdd(uchar &destinationArr[],const uchar &sourceArr[]){ int sourceArr_size=ArraySize(sourceArr);//get size of source array if(sourceArr_size==0){ return;//if source array is empty, exit the function } int destinationArr_size=ArraySize(destinationArr); //resize destination array to fit new data ArrayResize(destinationArr,destinationArr_size+sourceArr_size,500); // Copy the source array to the end of the destination array. ArrayCopy(destinationArr,sourceArr,destinationArr_size,0,sourceArr_size); } //+------------------------------------------------------------------+ // ArrayAdd for strings void ArrayAdd(char &destinationArr[],const string text){ int length = StringLen(text);// get the length of the input text if(length > 0){ uchar sourceArr[]; //define an array to hold the UTF-8 encoded characters for(int i=0; i<length; i++){ // Get the character code of the current character ushort character = StringGetCharacter(text,i); uchar array[];//define an array to hold the UTF-8 encoded character //Convert the character to UTF-8 & get size of the encoded character int total = ShortToUtf8(character,array); //Print("text @ ",i," > "text); // @ "B", IN ASCII TABLE = 66 (CHARACTER) //Print("character = ",character); //ArrayPrint(array); //Print("bytes = ",total) // bytes of the character int sourceArr_size = ArraySize(sourceArr); //Resize the source array to accommodate the new character ArrayResize(sourceArr,sourceArr_size+total); //Copy the encoded character to the source array ArrayCopy(sourceArr,array,sourceArr_size,0,total); } //Append the source array to the destination array ArrayAdd(destinationArr,sourceArr); } }
Aquí definimos dos funciones personalizadas para manejar la adición de datos a arrays en MQL5, específicamente diseñadas para manejar tanto tipos uchar como string. Estas funciones facilitan la construcción de datos de solicitud HTTP agregando varios datos a una matriz existente, lo que garantiza que el formato de datos final sea correcto y adecuado para la transmisión. Agregamos comentarios a las funciones para facilitar su comprensión, pero repasemos brevemente nuevamente la explicación del código.
La primera función, «ArrayAdd», se aplica a matrices de caracteres sin signo (uchar). Está configurado para agregar datos de una matriz de origen a una matriz de destino. Primero, determina cuántos elementos hay en la matriz de origen. Esto se logra llamando a la función simple ArraySize en la matriz de origen. Con esa información, verificamos si la matriz de origen contiene algún dato. Si no es así, evitamos el ridículo de continuar saliendo de la función antes de tiempo. Si contiene datos, pasamos al siguiente paso, que es cambiar el tamaño de la matriz de destino para aceptar esos datos. Esto lo hacemos llamando a la función ArrayResize sobre el array de destino, que ahora, sin embargo, podemos llamar con confianza ya que sabemos que funcionará correctamente.
La otra función, que añade cadenas a una matriz char, funciona como sigue: Calcula la longitud de la cadena (string) de entrada. Si la cadena de entrada no está vacía, toma cada carácter de la cadena, obtiene su código y lo agrega a la matriz de destino, convirtiéndolo a UTF-8 en el proceso. Para convertir la cadena y agregarla a la matriz de destino, esta función redimensiona la matriz de origen para el almacenamiento intermedio de los datos de la cadena que se agregarán. Asegura que la representación UTF-8 de la cadena, así como la cadena misma, se almacenen correctamente en la matriz de datos final que se utilizará para construir cuerpos de solicitud HTTP u otros tipos de estructuras de datos.
Para ver lo que hicimos, implementemos una lógica que imprimirá los datos resultantes relacionados con el ID de chat que se enviará en la solicitud HTTP.
Print("CHAT ID DATA:"); ArrayPrint(DATA); string chatID_Data = CharArrayToString(DATA,0,WHOLE_ARRAY,CP_UTF8); Print("SIMPLE CHAT ID DATA IS AS FOLLOWS:",chatID_Data);
Para empezar, empleamos la función ArrayPrint para presentar la matriz de datos sin procesar. La función imprime el contenido de la matriz para nosotros. Luego realizamos la conversión de la matriz "DATA" de una matriz de caracteres a un formato de cadena. La función que utilizamos es CharArrayToString, que traduce los datos de bytes sin procesar en «DATA» a una cadena codificada en UTF-8. Los parámetros utilizados aquí especifican que queremos convertir todo el array («WHOLE_ARRAY») y que la codificación de caracteres es UTF-8 (CP_UTF8). Esta conversión es necesaria porque la petición HTTP requiere que los datos estén en formato de cadena.
En conclusión, lo que tenemos es una cadena, "chatID_Data", cuyo formato final es tal que será incluido en la petición HTTP. Usando la función Print, podemos ver cómo será la salida eventual en la petición.
Podemos ver que podemos agregar correctamente los datos de ID de chat correctos a la matriz. Mediante la misma lógica, también podemos agregar los datos de la imagen para construir el cuerpo de la solicitud multipart/form-data para enviar la foto a través de HTTP a la API de Telegram.
ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n"); ArrayAdd(DATA,"Content-Disposition: form-data; name=\"photo\"; filename=\"Upload_ScreenShot.jpg\"\r\n"); ArrayAdd(DATA,"\r\n"); ArrayAdd(DATA,photoArr_Data); ArrayAdd(DATA,"\r\n"); ArrayAdd(DATA,"--"+HexaDecimal_Hash+"--\r\n");
Para comenzar, insertamos el marcador de límite para la parte de fotografía de los datos multiparte/formulario. Lo hacemos con la línea ArrayAdd(DATA,«--»+HexaDecimal_Hash+«\r\n»). El marcador de límite, formado por dos guiones y el «HexaDecimal_Hash», sirve para separar las distintas partes de la solicitud multiparte. El «HexaDecimal_Hash», un identificador único para el límite, es una garantía de que cada parte de la solicitud está inequívocamente dividida de la siguiente.
A continuación, incluimos la cabecera Content-Disposition para la parte de la foto de los datos del formulario. Lo añadimos usando la función ArrayAdd, como sigue: ArrayAdd(DATA, «Content-Disposition: form-data; name=\» photo\«; filename=\»Upload_ScreenShot.jpg«\r\n»). Esta cabecera indica que los datos que siguen son un archivo, concretamente, el archivo llamado «Upload_ScreenShot.jpg». Como hemos especificado, a través de la porción "name=« photo\»" de la cabecera, que el campo de datos del formulario con el que estamos trabajando tiene el nombre \« photo,\» el servidor sabe que espera el archivo «Upload_ScreenShot.jpg» como parte de ese campo cuando procesa la petición entrante. El archivo es sólo un identificador y puedes cambiarlo por otro de tu agrado.
Después de esto, usamos ArrayAdd(DATA, «\r\n») para añadir una secuencia de nuevas líneas a las cabeceras de la petición. Indica el final de la sección de cabecera y el comienzo de los datos reales del archivo. A continuación, utilizamos ArrayAdd(DATA, photoArr_Data) para añadir los datos de la foto real a la matriz DATA. Esta línea de código añade los datos binarios de la captura de pantalla (previamente codificados en base64) al cuerpo de la solicitud. La carga útil multipart/form-data contiene ahora los datos de la foto.
Por último, añadimos otra secuencia de nueva línea con ArrayAdd(DATA, «\r\n») y el delimitador para cerrar la parte de la foto con ArrayAdd(DATA, «--» + HexaDecimal_Hash + «--\r\n»). El -- al final del marcador de límite indica el final de la sección de varias partes. Este límite final garantiza que el servidor identifique correctamente el final de la sección de datos de la foto dentro de la solicitud. Para ver los datos que se están enviando, imprimimos nuevamente en la sección de registro a través de una función similar a la anterior.
Print("FINAL FULL PHOTO DATA BEING SENT:"); ArrayPrint(DATA); string final_Simple_Data = CharArrayToString(DATA,0,WHOLE_ARRAY,CP_ACP); Print("FINAL FULL SIMPLE PHOTO DATA BEING SENT:",final_Simple_Data);
Estos son los resultados que obtenemos:
Finalmente, construimos los encabezados de solicitud HTTP necesarios para enviar una solicitud multipart/form-data a la API de Telegram.
string HEADERS = NULL; HEADERS = "Content-Type: multipart/form-data; boundary="+HexaDecimal_Hash+"\r\n";
Comenzamos definiendo una cadena "HEADERS", que se inicializa como "NULL". Esta cadena contiene los encabezados HTTP que necesitamos configurar para la solicitud. El encabezado que absolutamente debemos configurar es Content-Type. El encabezado Content-Type transmite el tipo de datos que se envían y cómo están formateados.
Asignamos a la cadena el valor Content-Type correcto. La parte crucial aquí es la cadena "HEADERS" en sí. Debemos entender el "formato" de la solicitud HTTP si queremos comprender por qué es necesaria esta asignación particular a la cadena "HEADERS". El formato de la solicitud indica que la solicitud se envía utilizando el encabezado "Content-Type: multipart/form-data". Después de hacer todo esto, ahora podemos iniciar la solicitud web. En primer lugar, informemos al usuario enviándole la solicitud a continuación.
Print("SCREENSHOT SENDING HAS BEEN INITIATED SUCCESSFULLY.");
Del código inicial, comentamos los parámetros WebRequest innecesarios y cambiamos a los últimos.
//char data[]; // Array to hold data to be sent in the web request (empty in this case) char res[]; // Array to hold the response data from the web request string resHeaders; // String to hold the response headers from the web request //string msg = "EA INITIALIZED ON CHART " + _Symbol; // Message to send, including the chart symbol //const string url = TG_API_URL + "/bot" + botTkn + "/sendmessage?chat_id=" + chatID + // "&text=" + msg; // Send the web request to the Telegram API int send_res = WebRequest("POST",URL,HEADERS,10000, DATA, res, resHeaders);
En este caso, sólo añadimos a la función la nueva URL, las cabeceras y los datos del archivo de imagen que se van a enviar. La lógica de la respuesta permanece intacta y sin cambios como se indica a continuación:
// Check the response status of the web request if (send_res == 200) { // If the response status is 200 (OK), print a success message Print("TELEGRAM MESSAGE SENT SUCCESSFULLY"); } else if (send_res == -1) { // If the response status is -1 (error), check the specific error code if (GetLastError() == 4014) { // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal Print("PLEASE ADD THE ", TG_API_URL, " TO THE TERMINAL"); } // Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM MESSAGE"); } else if (send_res != 200) { // If the response status is not 200 or -1, print the unexpected response code and error code Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError()); }
Cuando ejecutamos el programa, esto es lo que obtenemos:
En MetaTrader 5:
En Telegram:
Ahora es evidente que hemos enviado con éxito el archivo de imagen desde el terminal comercial MetaTrader 5 al chat de Telegram. Sin embargo, sólo enviamos una captura de pantalla vacía. Para agregar un título al archivo de imagen, implementamos la siguiente lógica que agrega un título opcional a la solicitud multipart/form-data, que se enviará junto con la captura de pantalla del gráfico a la API de Telegram.
//--- Caption string CAPTION = NULL; CAPTION = "Screenshot of Symbol: "+Symbol()+ " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+ ") @ Time: "+TimeToString(TimeCurrent()); if(StringLen(CAPTION) > 0){ ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n"); ArrayAdd(DATA,"Content-Disposition: form-data; name=\"caption\"\r\n"); ArrayAdd(DATA,"\r\n"); ArrayAdd(DATA,CAPTION); ArrayAdd(DATA,"\r\n"); } //---
Comenzamos inicializando la cadena "CAPTION" como "NULL" y luego construyéndola con detalles relevantes. El título incluye el símbolo de negociación, el marco temporal del gráfico y la hora actual, formateados como una cadena. Luego verificamos si la cadena "CAPTION" tiene una longitud mayor que cero. Si es así, procedemos a agregar el título a la matriz "DATA", que se utiliza para construir los datos del formulario de varias partes. Esto implica agregar un marcador de límite, especificar la parte de datos del formulario como un título e incluir el contenido del título en sí. Cuando ejecutamos esto, obtenemos los siguientes resultados:
Ha sido un éxito. Podemos ver que no sólo recibimos el archivo de imagen, sino también una leyenda descriptiva que muestra el nombre del símbolo, el periodo y la hora del gráfico en cuestión.
Hasta este punto, obtenemos la captura de pantalla del gráfico donde se adjunta el programa. En caso de que uno quiera abrir y modificar un gráfico diferente, necesitamos implementar una lógica diferente para ello.
long chart_id=ChartOpen(_Symbol,_Period); ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true); // update chart int wait=60; while(--wait>0){//decrease the value of wait by 1 before loop condition check if(SeriesInfoInteger(_Symbol,_Period,SERIES_SYNCHRONIZED)){ break; // if prices up to date, terminate the loop and proceed } } ChartRedraw(chart_id); ChartSetInteger(chart_id,CHART_SHOW_GRID,false); ChartSetInteger(chart_id,CHART_SHOW_PERIOD_SEP,false); ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BEAR,clrRed); ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BULL,clrBlue); ChartSetInteger(chart_id,CHART_COLOR_BACKGROUND,clrLightSalmon); ChartScreenShot(chart_id,SCREENSHOT_FILE_NAME,1366,768,ALIGN_RIGHT); //Sleep(10000); // sleep for 10 secs to see the opened chart ChartClose(chart_id); //---
Aquí, comenzamos abriendo un nuevo gráfico para el símbolo y el marco temporal dados mediante el uso de la función ChartOpen, utilizando la variable predefinida _Symbol, y _Period. Asignamos el ID del nuevo gráfico a la variable «chart_id». A continuación utilizamos «chart_id» para asegurarnos de que el nuevo gráfico es visible en la parte frontal del entorno MetaTrader y no queda tapado por ningún gráfico anterior.
A continuación, iniciamos un bucle que puede ejecutarse durante un máximo de 60 iteraciones. Dentro de ese bucle, seguimos comprobando si el gráfico está sincronizado. Para probar la sincronización, utilizamos la función SeriesInfoInteger con los parámetros _Symbol, _Period, y SERIES_SYNCHRONIZED. Si descubrimos que el gráfico está sincronizado, salimos del bucle. Una vez que confirmamos que el gráfico está sincronizado, utilizamos la función ChartRedraw con el parámetro «chart_id» para refrescar el gráfico.
Personalizamos varios parámetros del gráfico para ajustarlo a nuestro gusto. Utilizamos la función ChartSetInteger para establecer los colores del fondo del gráfico y de las velas bajistas y alcistas. Los colores que establecemos aportan claridad visual, ayudándonos a distinguir fácilmente entre los distintos elementos del gráfico. También hacemos que el gráfico sea menos recargado visualmente desactivando la cuadrícula y los separadores de puntos. En este punto puede modificar su gráfico como considere oportuno. Por último, hacemos una captura de pantalla del gráfico para utilizarla en la transmisión. No queremos que el gráfico quede abierto innecesariamente, así que lo cerramos después de hacer la captura de pantalla. Para ello, utilizamos la función ChartClose. Cuando ejecutamos el programa, obtenemos los siguientes resultados:
Está claro que abrimos un gráfico, lo modificamos a nuestro gusto y lo cerramos al final después de tomar una instantánea del mismo. Para visualizar el proceso de apertura y cierre del gráfico, esperemos 10 segundos para ver el gráfico.
Sleep(10000); // sleep for 10 secs to see the opened chart
Aquí, simplemente dejamos el gráfico abierto durante 10 segundos para permitirnos ver lo que está sucediendo en segundo plano en nuestro programa. Tras la compilación, esto es lo que tenemos:
El código fuente completo responsable de tomar capturas de pantalla, codificarlas, encriptarlas y enviarlas desde el terminal de comercio al chat de Telegram es el siguiente:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Get ready to take a chart screenshot of the current chart #define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.jpg" //--- First delete an instance of the screenshot file if it already exists if(FileIsExist(SCREENSHOT_FILE_NAME)){ FileDelete(SCREENSHOT_FILE_NAME); Print("Chart Screenshot was found and deleted."); ChartRedraw(0); } //--- long chart_id=ChartOpen(_Symbol,_Period); ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true); // update chart int wait=60; while(--wait>0){//decrease the value of wait by 1 before loop condition check if(SeriesInfoInteger(_Symbol,_Period,SERIES_SYNCHRONIZED)){ break; // if prices up to date, terminate the loop and proceed } } ChartRedraw(chart_id); ChartSetInteger(chart_id,CHART_SHOW_GRID,false); ChartSetInteger(chart_id,CHART_SHOW_PERIOD_SEP,false); ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BEAR,clrRed); ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BULL,clrBlue); ChartSetInteger(chart_id,CHART_COLOR_BACKGROUND,clrLightSalmon); ChartScreenShot(chart_id,SCREENSHOT_FILE_NAME,1366,768,ALIGN_RIGHT); Print("OPENED CHART PAUSED FOR 10 SECONDS TO TAKE SCREENSHOT.") Sleep(10000); // sleep for 10 secs to see the opened chart ChartClose(chart_id); //--- //ChartScreenShot(0,SCREENSHOT_FILE_NAME,1366,768,ALIGN_RIGHT); // Wait for 30 secs to save screenshot if not yet saved int wait_loops = 60; while(!FileIsExist(SCREENSHOT_FILE_NAME) && --wait_loops > 0){ Sleep(500); } if(!FileIsExist(SCREENSHOT_FILE_NAME)){ Print("THE SPECIFIED SCREENSHOT DOES NOT EXIST (WAS NOT SAVED). REVERTING NOW!"); return (INIT_FAILED); } else if(FileIsExist(SCREENSHOT_FILE_NAME)){ Print("THE CHART SCREENSHOT WAS SAVED SUCCESSFULLY TO THE DATA-BASE."); } int screenshot_Handle = INVALID_HANDLE; screenshot_Handle = FileOpen(SCREENSHOT_FILE_NAME,FILE_READ|FILE_BIN); if(screenshot_Handle == INVALID_HANDLE){ Print("INVALID SCREENSHOT HANDLE. REVERTING NOW!"); return(INIT_FAILED); } else if (screenshot_Handle != INVALID_HANDLE){ Print("SCREENSHOT WAS SAVED & OPENED SUCCESSFULLY FOR READING."); Print("HANDLE ID = ",screenshot_Handle,". IT IS NOW READY FOR ENCODING."); } int screenshot_Handle_Size = (int)FileSize(screenshot_Handle); if (screenshot_Handle_Size > 0){ Print("CHART SCREENSHOT FILE SIZE = ",screenshot_Handle_Size); } uchar photoArr_Data[]; ArrayResize(photoArr_Data,screenshot_Handle_Size); FileReadArray(screenshot_Handle,photoArr_Data,0,screenshot_Handle_Size); if (ArraySize(photoArr_Data) > 0){ Print("READ SCREENSHOT FILE DATA SIZE = ",ArraySize(photoArr_Data)); } FileClose(screenshot_Handle); //ArrayPrint(photoArr_Data); //--- create boundary: (data -> base64 -> 1024 bytes -> md5) //Encodes the photo data into base64 format //This is part of preparing the data for transmission over HTTP. uchar base64[]; uchar key[]; CryptEncode(CRYPT_BASE64,photoArr_Data,key,base64); if (ArraySize(base64) > 0){ Print("Transformed BASE-64 data = ",ArraySize(base64)); //Print("The whole data is as below:"); //ArrayPrint(base64); } //Copy the first 1024 bytes of the base64-encoded data into a temporary array uchar temporaryArr[1024]= {0}; //Print("FILLED TEMPORARY ARRAY WITH ZERO (0) IS AS BELOW:"); //ArrayPrint(temporaryArr); ArrayCopy(temporaryArr,base64,0,0,1024); //Print("FIRST 1024 BYTES OF THE ENCODED DATA IS AS FOLLOWS:"); //ArrayPrint(temporaryArr); //Create an MD5 hash of the temporary array //This hash will be used as part of the boundary in the multipart/form-data uchar md5[]; CryptEncode(CRYPT_HASH_MD5,temporaryArr,key,md5); if (ArraySize(md5) > 0){ Print("SIZE OF MD5 HASH OF TEMPORARY ARRAY = ",ArraySize(md5)); Print("MD5 HASH boundary in multipart/form-data is as follows:"); ArrayPrint(md5); } //Format MD5 hash as a hexadecimal string & //truncate it to 16 characters to create the boundary. string HexaDecimal_Hash=NULL;//Used to store the hexadecimal representation of MD5 hash int total=ArraySize(md5); for(int i=0; i<total; i++){ HexaDecimal_Hash+=StringFormat("%02X",md5[i]); } Print("Formatted MD5 Hash String is: \n",HexaDecimal_Hash); HexaDecimal_Hash=StringSubstr(HexaDecimal_Hash,0,16);//truncate HexaDecimal_Hash string to its first 16 characters //done to comply with a specific length requirement for the boundary //in the multipart/form-data of the HTTP request. Print("Final Truncated (16 characters) MD5 Hash String is: \n",HexaDecimal_Hash); //--- WebRequest char DATA[]; string URL = NULL; URL = TG_API_URL+"/bot"+botTkn+"/sendPhoto"; //--- add chart_id //Append a carriage return and newline character sequence to the DATA array. //In the context of HTTP, \r\n is used to denote the end of a line //and is often required to separate different parts of an HTTP request. ArrayAdd(DATA,"\r\n"); //Append a boundary marker to the DATA array. //Typically, the boundary marker is composed of two hyphens (--) //followed by a unique hash string and then a newline sequence. //In multipart/form-data requests, boundaries are used to separate //different pieces of data. ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n"); //Add a Content-Disposition header for a form-data part named chat_id. //The Content-Disposition header is used to indicate that the following data //is a form field with the name chat_id. ArrayAdd(DATA,"Content-Disposition: form-data; name=\"chat_id\"\r\n"); //Again, append a newline sequence to the DATA array to end the header section //before the value of the chat_id is added. ArrayAdd(DATA,"\r\n"); //Append the actual chat ID value to the DATA array. ArrayAdd(DATA,chatID); //Finally, Append another newline sequence to the DATA array to signify //the end of the chat_id form-data part. ArrayAdd(DATA,"\r\n"); // EXAMPLE OF USING CONVERSIONS //uchar array[] = { 72, 101, 108, 108, 111, 0 }; // "Hello" in ASCII //string output = CharArrayToString(array,0,WHOLE_ARRAY,CP_ACP); //Print("EXAMPLE OUTPUT OF CONVERSION = ",output); // Hello Print("CHAT ID DATA:"); ArrayPrint(DATA); string chatID_Data = CharArrayToString(DATA,0,WHOLE_ARRAY,CP_UTF8); Print("SIMPLE CHAT ID DATA IS AS FOLLOWS:",chatID_Data); //--- Caption string CAPTION = NULL; CAPTION = "Screenshot of Symbol: "+Symbol()+ " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+ ") @ Time: "+TimeToString(TimeCurrent()); if(StringLen(CAPTION) > 0){ ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n"); ArrayAdd(DATA,"Content-Disposition: form-data; name=\"caption\"\r\n"); ArrayAdd(DATA,"\r\n"); ArrayAdd(DATA,CAPTION); ArrayAdd(DATA,"\r\n"); } //--- ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n"); ArrayAdd(DATA,"Content-Disposition: form-data; name=\"photo\"; filename=\"Upload_ScreenShot.jpg\"\r\n"); ArrayAdd(DATA,"\r\n"); ArrayAdd(DATA,photoArr_Data); ArrayAdd(DATA,"\r\n"); ArrayAdd(DATA,"--"+HexaDecimal_Hash+"--\r\n"); Print("FINAL FULL PHOTO DATA BEING SENT:"); ArrayPrint(DATA); string final_Simple_Data = CharArrayToString(DATA,0,WHOLE_ARRAY,CP_ACP); Print("FINAL FULL SIMPLE PHOTO DATA BEING SENT:",final_Simple_Data); string HEADERS = NULL; HEADERS = "Content-Type: multipart/form-data; boundary="+HexaDecimal_Hash+"\r\n"; Print("SCREENSHOT SENDING HAS BEEN INITIATED SUCCESSFULLY."); //char data[]; // Array to hold data to be sent in the web request (empty in this case) char res[]; // Array to hold the response data from the web request string resHeaders; // String to hold the response headers from the web request //string msg = "EA INITIALIZED ON CHART " + _Symbol; // Message to send, including the chart symbol //const string url = TG_API_URL + "/bot" + botTkn + "/sendmessage?chat_id=" + chatID + // "&text=" + msg; // Send the web request to the Telegram API int send_res = WebRequest("POST",URL,HEADERS,10000, DATA, res, resHeaders); // Check the response status of the web request if (send_res == 200) { // If the response status is 200 (OK), print a success message Print("TELEGRAM MESSAGE SENT SUCCESSFULLY"); } else if (send_res == -1) { // If the response status is -1 (error), check the specific error code if (GetLastError() == 4014) { // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal Print("PLEASE ADD THE ", TG_API_URL, " TO THE TERMINAL"); } // Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM MESSAGE"); } else if (send_res != 200) { // If the response status is not 200 or -1, print the unexpected response code and error code Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError()); } return(INIT_SUCCEEDED); // Return initialization success status }
Ahora está claro que hemos conseguido nuestro tercer objetivo, es decir, enviar archivos de imágenes de capturas de pantalla de gráficos y pies de foto desde el terminal de operaciones al chat o grupo de Telegram. ¡Esto es un éxito y estamos felices! Lo que ahora debemos hacer es probar la integración para asegurarnos de que funciona correctamente y detectar cualquier problema que surja. Esto se hace en la siguiente sección.
Probando la integración
Para garantizar que nuestro Asesor Experto (EA) envíe correctamente las capturas de pantalla del terminal MetaTrader 5 a Telegram, necesitamos probar la integración a fondo. Para agrupar las cosas, tengamos la lógica de prueba en formato GIF.
En el GIF proporcionado arriba, demostramos la interacción perfecta entre MetaTrader 5 y Telegram, mostrando el proceso de envío de una captura de pantalla de un gráfico. El GIF comienza mostrando el MetaTrader 5 plataforma donde se abre una ventana de gráfico, se lleva al primer plano y luego se pausa durante 10 segundos, lo que da tiempo para realizar los ajustes finales. Durante esta pausa, la pestaña Diario en MetaTrader 5 registra mensajes que indican el progreso de la operación, como por ejemplo el redibujado del gráfico o la captura de pantalla. A continuación, el gráfico se cierra automáticamente y la captura de pantalla se empaqueta y se envía a Telegram. En el lado de Telegram, vemos que la captura de pantalla llega al chat, lo que confirma que la integración funciona como estaba previsto. Este GIF refuerza visualmente cómo funciona el sistema automatizado en tiempo real, desde la preparación del gráfico hasta la entrega satisfactoria de la imagen en Telegram.
Conclusión
Para concluir, este artículo ha descrito, paso a paso, cómo enviar una captura de pantalla de un gráfico desde la plataforma comercial MetaTrader 5 a un chat en Telegram. Primero generamos la captura de pantalla del gráfico utilizando MetaTrader 5. Configuramos los ajustes del gráfico para asegurarnos de que era claro y luego lo capturamos utilizando la función ChartScreenShot para obtener la imagen en un archivo. Tras guardar el archivo en nuestro ordenador, lo abrimos y leemos su contenido binario. Luego enviamos el gráfico, codificado en formato Base64, a una petición HTTP que la API de Telegram pudiera entender. De este modo, podríamos introducir la imagen en un chat de Telegram en tiempo real.
La codificación de la imagen para su transmisión reveló las complejidades que entraña el envío de datos binarios en bruto a través del protocolo HTTP, especialmente cuando el destino es una plataforma de mensajería como Telegram. Lo primero que hay que entender es que enviar datos binarios directamente no es factible. En cambio, Telegram (y muchos otros servicios) exigen que los datos se envíen en formato de texto. Cumplimos este requisito sin problemas utilizando un algoritmo ampliamente conocido para convertir los datos binarios brutos de la imagen a Base64. Después, insertamos la imagen Base64 en una petición HTTP multipart/form-data. Esta demostración no sólo subrayó la potencia de la plataforma MetaTrader 5 como medio para crear automatizaciones personalizadas, sino que también puso de relieve cómo integrar un servicio externo (Telegram en este caso) en una estrategia comercial.
De cara a la Parte 4, tomaremos el código de este artículo y le daremos forma de componentes reutilizables. Haremos esto para poder crear múltiples instancias de la integración con Telegram, lo que nos permitirá, en las siguientes partes del tutorial, enviar diferentes mensajes y capturas de pantalla a Telegram a nuestro antojo y capricho (no sólo como nos plazca sino también cuándo y cómo nos plazca), sin tener que depender de una única llamada a una función para hacerlo. Al poner el código en clases, haremos que el sistema sea más modular y escalable. También lo haremos para integrar el código más fácilmente en los distintos escenarios de negociación que esbozamos en la Parte 1. Esto es importante porque la integración del mecanismo de Telegram debe funcionar de forma dinámica y flexible con nuestros Asesores Expertos, permitiendo que múltiples estrategias y escenarios envíen una variedad de mensajes e imágenes en puntos críticos durante una operación o al final de un día de negociación. Permanezca atento mientras seguimos construyendo y perfeccionando este sistema integrado.
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/15616
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso
Gracias. He encontrado esta función pública con el mismo nombre que MQL5 regular y cambiar el nombre para eliminar la advertencia. Ahora compilar es claro :)
Saludos cordiales, Volker
Saludos cordiales, Volker