English 中文 Español Deutsch 日本語 Português
preview
MQL5-советник, интегрированный в Telegram (Часть 3): Отправка скриншотов графиков с подписями из MQL5 в Telegram

MQL5-советник, интегрированный в Telegram (Часть 3): Отправка скриншотов графиков с подписями из MQL5 в Telegram

MetaTrader 5Торговые системы |
795 18
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В предыдущей статье, которая является второй частью нашей серии, мы подробно рассмотрели объединение языка MetaQuotes Language 5 (MQL5) с Telegram для генерации и отправке сигналов. Результат был очевиден: это позволило нам отправлять торговые сигналы в Telegram и, конечно же, необходимость использования торговых сигналов для того, чтобы все это имело смысл. Итак, что из себя будет представлять следующий шаг интеграции? Вместо отправки только текстовой части торгового сигнала мы будем отправлять скриншоты графика с торговым сигналом. Иногда лучше не только получить сигнал, на основании которого можно действовать, но и увидеть его на скриншоте.

В статье мы сосредоточимся на особенностях преобразования данных изображения в совместимый формат для встраивания в HTTP-запросы. Это преобразование необходимо, если мы хотим включить изображения в нашего Telegram-бота. Мы рассмотрим детали процесса, который позволяет нам превратить график в торговом терминале MetaTrader 5 в искусно оформленное торговое уведомление с подписью и изображением. Статья будет состоять из четырех частей.

Для начала мы дадим общее представление о том, как работает кодирование и передача изображений по протоколу HTTPS. В первом разделе мы объясним основные концепции и методы, используемые для выполнения этой задачи. Далее мы погрузимся в реализацию на MQL5 - языке программирования для написания торговых программ для платформы MetaTrader 5. Мы подробно расскажем, как использовать методы кодирования и передачи изображений. После этого мы протестируем реализованную программу, чтобы убедиться в ее корректной работе. В заключение мы еще раз раз коснемся основных моментов и опишем преимущества подхода. Темы, рассматриваемые в статье:

  1. Кодирование и передача изображений по HTTPS
  2. Реализация средствами MQL5
  3. Тестирование интеграции
  4. Заключение

В итоге мы создадим советник, который будет отправлять торговую информацию, включая изображения графиков и сигналов, а также размещенные ордера из торгового терминала в указанный чат Telegram. Давайте начнем.


Кодирование и передача изображений по HTTPS

Отправка изображений через Интернет и, в частности, интеграция с API или платформами обмена сообщениями требует, чтобы данные изображения сначала были закодированы, а затем отправлены без неоправданной задержки или ущерба для эффективности или безопасности. Файл прямого изображения отправляет слишком много битов и байтов для бесперебойной работы с командами, которые фактически позволяют интернет-пользователю получить доступ к определенному сайту, платформе или сервису. Для API такого сервиса как Telegram, который выступает в качестве посредника между пользователем и определенной службой (например, веб-интерфейсом для различных видов задач), отправка изображения требует, чтобы файл изображения был сначала закодирован, а затем отправлен как часть полезной нагрузки команды от пользователя к службе или наоборот. Это достигается, в частности, с помощью таких протоколов, как HTTP или HTTPS.

Наиболее распространенный подход к конвертации изображений для отправки - преобразование изображения в строку Base64. Кодировка Base64 берет двоичные данные изображения и создает текстовое представление. Это делается с использованием символов из определенного набора, что позволяет так называемому "закодированному изображению" правильно функционировать при отправке по текстовым протоколам. Для создания изображения в кодировке Base64 его необработанные данные (фактический файл до любых операций "чтения") считываются побайтно. Затем считанные байты представляются с помощью символов Base64. После этого файл можно отправлять по текстовому протоколу.

После того, как данные изображения закодированы, они отправляются с помощью HTTPS - безопасной формы HTTP. В отличие от HTTP, который отправляет данные в виде обычного текста, HTTPS использует протокол шифрования Secure Socket Layer (SSL)/Transport Layer Security (TLS), чтобы гарантировать конфиденциальность и безопасность данных, передаваемых на сервер и отправляемых с него. Трудно переоценить важность HTTPS для защиты торговых сигналов и других финансовых коммуникаций. Недобросовестная третья сторона, получившая в свои руки торговые сигналы, может использовать эту информацию для совершения сделок и манипулирования рынком в ущерб пользователям торговых сигналов. Процесс выглядит так:

Кодирование изображения

Подводя итог, можно сказать, что метод кодирования и отправки изображений преобразует файлы изображений в текстовый формат, подходящий для веб-коммуникаций. Он также обеспечивает безопасную доставку по протоколу HTTPS. Крайне важно понимать эти две концепции, если вы хотите интегрировать данные изображений в приложения. Очевидным примером являются торговые системы, которые автоматизируют уведомления через Telegram — платформу, которая отлично справляется с быстрой и надежной доставкой сообщений.


Реализация средствами MQL5

Начнем реализацию отправки изображений в MQL5 с создания скриншота текущего торгового графика в MQL5-советнике. Закодируем скриншот и отправим его через Telegram. Мы реализуем эту функциональность в первую очередь в обработчике событий OnInit, который выполняется при инициализации советника. Как уже отмечалось, цель обработчика событий OnInit - подготовить и настроить необходимые компоненты советника, гарантируя, что все настроено правильно, прежде чем будет выполнена основная логика торговой операции. Сначала мы определяем имя файла нашего скриншота.

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

Здесь мы делаем первый шаг в рабочем процессе, а именно устанавливаем константу для имени файла скриншота. Это достигается с помощью директивы #define, которая позволяет нам назначить постоянное значение, на которое можно ссылаться по всему коду. Здесь мы создаем константу с именем SCREENSHOT_FILE_NAME, в которой хранится значение Our Chart ScreenShot.jpg. Если нам когда-нибудь понадобится имя файла для загрузки или сохранения чего-либо, мы можем просто использовать эту константу. Если нам нужно изменить имя или формат файла, нам нужно внести изменения только здесь. Вы можете заметить, что мы выбрали формат изображения Joint Photographic Experts Group (JPEG). Вы можете выбрать любой подходящий вам формат, например Portable Network Graphics (PNG). Однако следует иметь в виду, что существуют существенные различия в форматах изображений. Например, JPG использует алгоритм сжатия с потерями, что означает, что часть данных изображения теряется, но размер изображения уменьшается. Пример форматов, которые вы можете использовать, показан ниже:

Форматы изображений

Мы интегрируем функцию скриншота в обработчик OnInit. Это гарантирует, что система будет настроена на захват и сохранение состояния графика сразу после запуска советника. Мы объявили константу SCREENSHOT_FILE_NAME, которая заменяет фактическое имя файла изображения графика. Используя этот заполнитель, мы (в основном) избегаем ловушки, связанной с попыткой сохранить два файла с одинаковым именем примерно в одно и то же время. Выполняя этот шаг, мы гарантируем, что файл изображения графика будет иметь ту же базовую структуру, которая ему понадобится при активном кодировании и передаче изображения на этом этапе.

Этот шаг важен, поскольку он устанавливает соглашение об именовании файлов и гарантирует, что мы сможем получить изображение графика при первой инициализации советника. С этого момента мы можем сосредоточиться на извлечении данных из графика, кодировании их в форму, удобную для человеческого восприятия, и отправке их на выбранный нами канал Telegram.

Далее, чтобы предотвратить попытку перезаписать и создать файл с похожим именем, нам нужно удалить существующий файл, если он есть, и создать новый. Это достигается с помощью следующего фрагмента кода.

   //--- First delete an instance of the screenshot file if it already exists
   if(FileIsExist(SCREENSHOT_FILE_NAME)){
      FileDelete(SCREENSHOT_FILE_NAME);
      Print("Chart Screenshot was found and deleted.");
      ChartRedraw(0);
   }

Здесь мы начинаем с того, что убеждаемся, что не существует ни одного экземпляра файла скриншота, прежде чем сделать новый скриншот. Это важно, поскольку мы хотим избежать путаницы между текущим состоянием графика и ранее сохраненными скриншотами. Для этого мы проверяем, существует ли в системе файл с именем, сохраненным в константе SCREENSHOT_FILE_NAME. Мы делаем это с помощью функции FileIsExist, которая проверяет указанный каталог и возвращает true, если файл присутствует. Если файл существует, мы удаляем его с помощью функции FileDelete. Убедившись, что в указанном каталоге нет нашего старого скриншота, мы освобождаем место для нового, который создадим позже.

После удаления мы отправляем сообщение в терминал с помощью функции Print, указывающий на то, что скриншот графика найден и успешно удален. Эта небольшая обратная связь может быть очень полезна для отладки, поскольку она также является подтверждением того, что система правильно обрабатывает очистку предыдущих скриншотов. В конце концов, мы бы не хотели, чтобы "удаление" несуществующих файлов вошло у нас в привычку. Мы также немедленно перерисовываем график (функция ChartRedraw), чтобы гарантировать, что мы работаем с актуальным визуальным состоянием графика. После очистки мы можем приступить к созданию скриншота.

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

Здесь мы делаем скриншот с помощью функции ChartScreenShot, которая делает снимок указанного графика и сохраняет его как файл изображения. В нашем случае мы передаем параметры 0, SCREENSHOT_FILE_NAME, 1366, 768 и ALIGN_RIGHT в функцию, чтобы управлять созданием и сохранением скриншотов.

  • Первый параметр, 0, указывает идентификатор графика, скриншот которого мы хотим сделать. Значение 0 относится к текущему активному графику. Если бы мы хотели снять другой график, нам пришлось бы передать конкретный идентификатор графика.
  • Второй параметр, SCREENSHOT_FILE_NAME, - это имя файла, в котором будет сохранен скриншот. В нашем случае это константа Our Chart ScreenShot.jpg. Файл будет создан в каталоге "Файлы" терминала. Если он еще не существует, то будет создан после создания скриншота.
  • Третий и четвертый параметры, 1366 и 768, определяют размеры скриншота в пикселях. Здесь 1366 представляет ширину скриншота, а 768 — высоту. Эти значения можно настроить в зависимости от предпочтений пользователя или размера захватываемого экрана.
  • Последний параметр, ALIGN_RIGHT, указывает, как скриншот должен быть выровнен в окне графика. Используя ALIGN_RIGHT, мы выравниваем скриншот по правой стороне графика. В зависимости от желаемого результата можно использовать и другие параметры выравнивания, такие как ALIGN_LEFT или ALIGN_CENTER.

По некоторым причинам, пусть даже и незначительным, скриншот может сохраняться с задержкой. Нам нужно инициировать итерацию, в которой мы можем подождать несколько секунд, пока скриншот не будет сохранен.

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

Здесь мы используем цикл while, который ждет успешного сохранения файла скриншота, гарантируя, что он сохранен в правильном месте и с правильным именем, прежде чем продолжить. Само ожидание достаточно долгое, поэтому при нормальных обстоятельствах файл скриншота должен быть легко найден в файловой системе (если он действительно должен был быть сохранен во время теста). Начнем с целочисленной переменной wait_loops, инициализирована значением 60. Каждая итерация цикла, если она продолжается без обнаружения файла, вводит полусекундное (500 миллисекунд (мс)) ожидание — что составляет 30 секунд (60 итераций * 500 мс) от начала цикла до его конца, если файл не найден.

На каждой итерации мы также уменьшаем счетчик wait_loops, чтобы предотвратить бесконечное выполнение цикла, если файл не будет создан за указанное время. Кроме того, мы используем функцию Sleep для создания задержки в 500 миллисекунд между каждой проверкой. Это позволяет нам избежать частых проверок и не перегружать систему слишком большим количеством запросов на наличие файлов.

Наконец, нам необходимо проверить существование файла, и если его не существует, то нет смысла продолжать дальше, поскольку весь наш алгоритм и логика зависят от файла изображения. Если он существует, мы можем перейти к следующим шагам.

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

Здесь мы определяем механизм обработки ошибок, позволяющий проверить, был ли успешно сохранен файл скриншота. Подождав некоторое время, пока файл будет создан, мы проверяем наличие файла с помощью функции FileIsExist. Если проверка возвращает false, что означает отсутствие файла, выдается следующее сообщение: "THE SPECIFIED SCREENSHOT DOES NOT EXIST (WAS NOT SAVED). REVERTING NOW!" (Указанный скриншот не существует (не был сохранен). Выполняется откат!). Это сообщение означает, что нам не удалось сохранить файл скриншота. После появления этого сообщения об ошибке программа не может продолжать работу, поскольку нам нужен именно этот файл изображения как основа для логики программы. Поэтому мы выходим со значением возврата INIT_FAILED, указывающим на то, что инициализация не может быть успешно завершена. Если скриншот был сохранен, мы также информируем об этом.

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

После запуска кода получены следующие результаты:

Сохраняем скриншот

Здесь вы можете видеть, что нам удалось успешно удалить первоначальный файл изображения и сохранить другой. Чтобы получить доступ к изображению на вашем компьютере, щелкните правой кнопкой мыши по имени файла, выберите "Открыть папку с файлом" и найдите нужный файл.

Находим файл 1

Вы также можете напрямую получить доступ к файлу изображения, открыв "Навигатор", развернув его, щелкнув правой кнопкой мыши на вкладке с файлами и выбрав "Открыть папку".

Находим файл 2

Откроется папка с файлами, в которой был зарегистрирован файл изображения.

Папка файлов с изображениями

Здесь вы можете увидеть, что зарегистрировано точное название изображения. Давайте наконец проверим размер и тип файла, чтобы увидеть, учтены ли правильные данные.

Тип и размер файла

Мы видим, что тип файла — JPG, а ширина и высота скриншота составляют 1366 на 768 пикселей соответственно, как и было указано. Если, например, требуется другой формат, скажем, PNG, то нужно изменить только тип файла, как показано ниже:

   #define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.png"

При компиляции и запуске этого фрагмента кода мы создаем такое же изображение в формате Graphics Interchange Format (GIF):

PNG и JPG

До этого момента очевидно, что мы успешно сделали скриншот и сохранили его напрямую в файлы. Таким образом, мы можем приступить к кодированию файла изображения, чтобы его можно было передавать по протоколу HTTPS. Сначала нам нужно открыть файл изображения и прочитать его.

   int screenshot_Handle = INVALID_HANDLE;
   screenshot_Handle = FileOpen(SCREENSHOT_FILE_NAME,FILE_READ|FILE_BIN);
   if(screenshot_Handle == INVALID_HANDLE){
      Print("INVALID SCREENSHOT HANDLE. REVERTING NOW!");
      return(INIT_FAILED);
   }

В приведенном выше фрагменте кода мы фокусируемся на файловых операциях в MQL5 для управления файлом скриншота, который мы сохранили ранее. Объявляем целочисленную переменную screenshot_Handle и инициализируем ее значением INVALID_HANDLE. screenshot_Handle служит ссылкой на файл, а значение INVALID_HANDLE выступает в качестве заполнителя, который сообщает нам, что ни один допустимый файл еще не был открыт. Удержание этого значения гарантирует, что мы сможем ссылаться на файл через его хэндл и обрабатывать любые ошибки, возникающие при работе с файлами.

Далее попробуем использовать функцию FileOpen, чтобы открыть сохраненный скриншот. Мы даем ему имя скриншота, которое содержит путь к нужному файлу. Мы также даем ему два флага: FILE_READ и FILE_BIN. Первый флаг сообщает системе, что мы хотим прочитать файл. Второй флаг, который, вероятно, является более важным из двух, сообщает системе, что файл содержит двоичные данные (которые не следует путать со скриншотом, представляющим собой последовательность единиц и нулей). Поскольку скриншот представляет собой изображение относительно стандартного формата (преобразуйте этот формат во что-то действительно "стандартное", "простое" или "естественное" для работы, и изображение станет серией единиц и нулей — никакого форматирования, никакой структуры, просто голая математика — другая серия единиц и нулей, и изображение будет выглядеть совершенно иначе), мы ожидаем найти серию байтов, которые каким-то образом соответствуют изображению.

После попытки открыть файл функция FileOpen возвращает либо действительный хэндл, либо INVALID_HANDLE. Проверяем правильность хэндла с помощью оператора if. Неверный хэндл означает, что файл не был успешно открыт. Выводим сообщение об ошибке, в котором говорится, что хэндл скриншота неверен. Либо скриншот не был сохранен, либо к нему нет доступа, что является для нас сигналом о том, что программа уперлась в стену. Мы возвращаем 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.");
   }

Здесь мы добавляем еще один шаг проверки, чтобы убедиться, что файл скриншота открылся правильно. Убедившись, что screenshot_Handle верен (не равен INVALID_HANDLE), выводим несколько сообщений, указывающих на то, что файл открылся правильно. Это просто еще один способ подтвердить, что screenshot_Handle хорош и мы готовы двигаться дальше. Мы используем функцию Print для первого сообщения, которая сообщает то же самое, что и второе: скриншот успешно сохранен и открыт для чтения. Оба эти сообщения служат для подтверждения успешного завершения текущего этапа нашего рабочего процесса.

Затем отображаем идентификатор хэндла, который уникально идентифицирует файл и позволяет выполнять последующие операции (чтение, запись и кодирование) с файлом. Идентификатор хэндла также полезен для отладки. Он подтверждает, что система получила и выделила ресурсы для управления этим конкретным файлом. В заключение мы выводим оператор print о том, что система готова выполнить следующую операцию — закодировать скриншот для передачи по сети с использованием протокола HTTPS.

Далее мы можем проверить и убедиться, что хэндл действительно записан и сохранен, а также имеет допустимое содержимое.

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

Здесь мы получаем и проверяем размер открытого ранее файла скриншота с помощью его хэндла. Вызываем функцию FileSize для хэндла скриншота, которая возвращает размер файла в байтах. Затем мы присваиваем это значение целочисленной переменной screenshot_Handle_Size. Если размер больше нуля, что указывает на то, что файл содержит какие-то данные, мы выводим размер файла в журнал. Этот шаг полезен, поскольку он позволяет нам быть уверенными в том, что скриншот сохранен правильно и имеет допустимое содержимое перед кодированием и отправкой файла по HTTP.

Если хэндл действительно имеет допустимое содержимое, это означает, что у нас открыт правильный файл, и мы можем подготовиться к считыванию двоичных данных файла скриншота в массив для декодирования.

   uchar photoArr_Data[];
   ArrayResize(photoArr_Data,screenshot_Handle_Size);
   FileReadArray(screenshot_Handle,photoArr_Data,0,screenshot_Handle_Size);
   if (ArraySize(photoArr_Data) > 0){
      Print("READ SCREENSHOT FILE DATA SIZE = ",ArraySize(photoArr_Data));
   }

Начнем с объявления массива uchar с именем photoArr_Data, который будет содержать двоичные данные. Затем мы изменяем размер этого массива, чтобы он соответствовал размеру файла скриншота, с помощью функции ArrayResize. Далее мы считываем содержимое файла скриншота в массив photoArr_Data array, начиная с индекса 0 и до конца файла (screenshot_Handle_Size) с помощью функции FileReadArray. Затем проверяем размер массива photoArr_Data после его загрузки, и если он больше 0, что означает, что он не пустой, записываем его размер. Обычно это часть кода, которая отвечает за чтение и обработку файла скриншота, чтобы его можно было использовать для кодирования и передачи.

После считывания и сохранения содержимого файла нам необходимо закрыть файл изображения. Это делается с помощью его хэндла.

   FileClose(screenshot_Handle);

Здесь мы наконец закрываем файл скриншота после успешного считывания его данных в массив хранения. Вызываем функцию FileClose, чтобы освободить хэндл, связанный с файлом скриншота. Это освобождает системные ресурсы, выделенные при открытии файла. Крайне важно убедиться, что файл закрыт, прежде чем пытаться выполнить какие-либо другие операции с файлом, такие как доступ к нему, чтение или запись. Функция сигнализирует о том, что мы завершили все операции доступа к файлу, и теперь переходим к следующему этапу: кодированию данных скриншота и подготовке их к передаче. После запуска мы получим следующий результат:

READ FILE

Как видим, мы правильно считываем и сохраняем двоичные данные изображения в массиве хранения. Чтобы увидеть данные, мы можем распечатать их в журнале с помощью функции ArrayPrint:

   ArrayPrint(photoArr_Data);

Вот какие данные мы получаем после отображения:

Двоичные данные изображения 1

Мы считываем, копируем и сохраняем полные данные, то есть до 320 894.

Далее нам необходимо подготовить данные фотографии для передачи по протоколу HTTP, закодировав их в формате Base64. Поскольку двоичные данные, такие как изображения, не могут быть напрямую переданы по протоколу HTTP, нам необходимо использовать кодировку для преобразования двоичных данных в формат строки ASCII. Это гарантирует, что данные могут быть безопасно включены в HTTP-запрос. Это достигается с помощью следующего фрагмента кода.

   //--- create boundary: (data -> base64 -> 1024 bytes -> md5)
   //Encodes the photo data into base64 format
   //This is part of preparing the data for transmission over HTTP.
   uchar base64[];
   uchar key[];
   CryptEncode(CRYPT_BASE64,photoArr_Data,key,base64);
   if (ArraySize(base64) > 0){
      Print("Transformed BASE-64 data = ",ArraySize(base64));
      //Print("The whole data is as below:");
      //ArrayPrint(base64);
   }

Для начала создадим два массива. Первый — "base64". Здесь хранятся закодированные данные. Второй массив — "key" (ключ). Мы никогда не используем "key" в этом контексте, но этого требует функция кодирования. Функция, которая выполняет кодирование Base64, называется CryptEncode. Для этого требуются четыре параметра: тип кодирования (CRYPT_BASE64), the исходные двоичные данные (photoArr_Data), ключ шифрования (key) и выходной массив (base64). Функция CryptEncode выполняет фактическую работу по преобразованию двоичных данных в формат Base64 и сохранению результата в массиве base64. При проверке размера base64 с помощью функции ArraySize, если base64 содержит хоть какие-то элементы (то есть он больше нуля), это означает, что кодирование прошло успешно.

Используем функцию ArrayPrint, чтобы распечатать эти данные в журнале.

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

Получаем следующие результаты:

Отображение данных

Мы видим значительное отклонение между исходным двоичным файлом данных размером 320 894 и вновь преобразованными данными размером 427 860. Это отклонение является результатом преобразования и кодирования данных.

Далее нам необходимо подготовить подмножество данных, закодированных в Base64, чтобы гарантировать обработку их управляемой части для следующих этапов нашего процесса. В частности, нам необходимо сосредоточиться на копировании первых 1024 байт закодированных данных во временный массив для дальнейшего использования.

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

Для начала мы создаем временный массив temporaryArr размером 1024 байта. Инициализируем все его значения нулем. Используем этот массив для хранения первого сегмента данных, закодированных в Base64. Поскольку инициализирующее значение равно нулю, мы избегаем любых потенциальных проблем с остаточной информацией в памяти, где хранится временный массив.

Затем используем функцию ArrayCopy для перемещения первых 1024 байтов из base64 в temporaryArr. Это позволяет эффективно выполнять операцию копирования. Мы не будем углубляться в подробности операции копирования. Я упомяну лишь пару вещей. Побочным эффектом инициализации является устранение любых возможных опасений по поводу первой части данных, закодированных в Base64, если вы визуализируете их как какую-то случайную тарабарщину. Давайте запишем пустой временный массив. Для этого используем следующий код.

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

Вот что мы получаем после компиляции:

Массив, заполненный нулями

Мы видим, что временный массив заполнен нулями. Затем эти нули заменяются первыми 1024 значениями изначально отформатированных данных. Мы можем просмотреть эти данные снова, используя аналогичную логику.

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

Заполненное временное представление данных выглядит так:

Заполненные временные данные

Получив эти временные данные, нам необходимо сгенерировать хэш алгоритма выборки сообщений (Message-Digest algorithm 5, MD5) из первых 1024 байт данных, закодированных в Base64. Этот хеш MD5 будет использоваться как часть границы в структуре multipart/form-data, которая часто применяется в запросах HTTP POST для обработки загрузки файлов.

   //Create an MD5 hash of the temporary array
   //This hash will be used as part of the boundary in the multipart/form-data
   uchar md5[];
   CryptEncode(CRYPT_HASH_MD5,temporaryArr,key,md5);
   if (ArraySize(md5) > 0){
      Print("SIZE OF MD5 HASH OF TEMPORARY ARRAY = ",ArraySize(md5));
      Print("MD5 HASH boundary in multipart/form-data is as follows:");
      ArrayPrint(md5);
   }


Для начала мы объявляем массив с именем md5 для хранения результата хэша MD5. Алгоритм MD5 — это криптографическая хеш-функция, которая выдает 128-битное хеш-значение. Чаще всего хеш представляется в виде строки из 32 шестнадцатеричных цифр.

В этом случае мы используем встроенную функцию MQL5 CryptEncode с параметром CRYPT_HASH_MD5 для вычисления хеша. Мы передаем функции временный массив temporaryArr, который содержит первые 1024 байта данных, закодированных в Base64. Параметр key обычно используется для дополнительных криптографических операций, но не требуется для MD5 и в этом контексте устанавливается в пустой массив. Результат операции хеширования сохраняется в массиве md5.

После вычисления хеша мы убеждаемся в том, что массив md5 не пуст, проверяя количество элементов в массиве с помощью функции ArraySize. Если в массиве есть какие-либо элементы, мы регистрируем размер MD5-хеша, а затем его фактическое значение. Это хеш-значение используется для создания граничной строки в формате multipart/form-data, которая помогает разделять различные части передаваемого HTTP-запроса. Алгоритм MD5 используется здесь исключительно из-за его универсальности и уникальности получаемого им значения, а не потому, что это лучший или самый безопасный алгоритм. После запуска мы получим следующие данные:

Данные MD5

Как видите, здесь мы получаем данные хэша MD5 в числовой форме. Таким образом, нам необходимо преобразовать хеш MD5 в шестнадцатеричную строку, а затем обрезать ее, чтобы она соответствовала определенному требованию к длине для использования в качестве границы в HTTP-запросах multipart/form-data, которая обычно составляет 16.

   //Format MD5 hash as a hexadecimal string &
   //truncate it to 16 characters to create the boundary.
   string HexaDecimal_Hash=NULL;//Used to store the hexadecimal representation of MD5 hash
   int total=ArraySize(md5);
   for(int i=0; i<total; i++){
      HexaDecimal_Hash+=StringFormat("%02X",md5[i]);
   }
   Print("Formatted MD5 Hash String is: \n",HexaDecimal_Hash);
   HexaDecimal_Hash=StringSubstr(HexaDecimal_Hash,0,16);//truncate HexaDecimal_Hash string to its first 16 characters
   //done to comply with a specific length requirement for the boundary
   //in the multipart/form-data of the HTTP request.
   Print("Final Truncated (16 characters) MD5 Hash String is: \n",HexaDecimal_Hash);

Для начала мы объявляем строковую переменную HexaDecimal_Hash для хранения шестнадцатеричной формы хэша MD5. Эта строка будет служить граничным маркером для разделения различных частей полезной нагрузки нашего HTTP-запроса.

Далее мы просматриваем каждый байт хеша, сохраненного в массиве md5. Мы преобразуем каждый байт в двухсимвольную строку, используя спецификатор формата "%02X". Часть спецификатора "%0" указывает, что при необходимости строка должна быть дополнена начальными нулями, чтобы гарантировать, что каждый байт представлен двумя символами. "02" указывает на два символа (минимум) для представления; "X" указывает на то, что символы должны быть шестнадцатеричными цифрами (при необходимости с заглавными буквами).

Как уже говорилось, эти шестнадцатеричные символы добавляются к строке HexaDecimal_Hash. Наконец, мы выводим содержимое строки в журнал, чтобы убедиться, что она сформирована правильно. В результате выполнения программы выводится следующая информация:

Окончательная хеш-строка

Это успех. Далее нам необходимо создать и подготовить данные файла для HTTP POST-запроса multipart/form-data, который будет использоваться для отправки изображения в чат Telegram через API Telegram. Это потребует подготовки тела запроса, включающего как поля формы, так и данные файла в формате, который сервер сможет правильно обработать. Этого можно добиться с помощью следующего фрагмента кода.

   //--- WebRequest
   char DATA[];
   string URL = NULL;
   URL = TG_API_URL+"/bot"+botTkn+"/sendPhoto";
   //--- add chart_id
   //Append a carriage return and newline character sequence to the DATA array.
   //In the context of HTTP, \r\n is used to denote the end of a line
   //and is often required to separate different parts of an HTTP request.
   ArrayAdd(DATA,"\r\n");
   //Append a boundary marker to the DATA array.
   //Typically, the boundary marker is composed of two hyphens (--)
   //followed by a unique hash string and then a newline sequence.
   //In multipart/form-data requests, boundaries are used to separate
   //different pieces of data.
   ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n");
   //Add a Content-Disposition header for a form-data part named chat_id.
   //The Content-Disposition header is used to indicate that the following data
   //is a form field with the name chat_id.
   ArrayAdd(DATA,"Content-Disposition: form-data; name=\"chat_id\"\r\n");
   //Again, append a newline sequence to the DATA array to end the header section
   //before the value of the chat_id is added.
   ArrayAdd(DATA,"\r\n");
   //Append the actual chat ID value to the DATA array.
   ArrayAdd(DATA,chatID);
   //Finally, Append another newline sequence to the DATA array to signify
   //the end of the chat_id form-data part.
   ArrayAdd(DATA,"\r\n");

Начнем с настройки массива DATA и URL для HTTP-запроса. URL состоит из трех частей: базовый URL для API (TG_API_URL); токен для бота, который идентифицирует бота для API (botTkn); и конечная точка для отправки изображения в чат (/sendPhoto). URL-адрес указывает, на какой "удалённый сервер" мы отправляем нашу "полезную нагрузку" — изображение и сопутствующую информацию. URL-адрес конечной точки не меняется, он одинаков для каждого сделанного нами запроса. Наши запросы будут попадать в одно и то же место независимо от того, отправляем ли мы одно изображение или несколько, отправляем ли мы изображения в разные чаты и так далее.

После этого мы добавляем граничный маркер к краю нашего фрагмента данных. Он состоит из двух дефисов (--) и нашего уникального граничного хеша (HexaDecimal_Hash). В целом это выглядит так: "--HexaDecimal_Hash". Этот маркер границы появляется в начале фрагмента данных для следующей части запроса, которая представляет собой поле формы chart_id. Заголовок Content-Disposition указывает, что следующая часть (следующий фрагмент данных) запроса multipart/form-data является полем формы, и дает имя этого поля (chart_id).

Добавляем этот заголовок и символ новой строки (/r/n), чтобы обозначить конец раздела заголовка. После раздела заголовка добавляем значение chartID в массив DATA, за которым следует еще один символ новой строки (/r/n), чтобы обозначить конец части form-data chart_id. Это гарантирует, что поле формы правильно отформатировано и отделено от других частей запроса, чтобы API Telegram правильно получал и обрабатывал данные.

Вы, возможно, заметили, что мы использовали в коде две "перегружаемые функции". Ниже приведен фрагмент их кода.

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

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

Здесь мы определяем две пользовательские функции для обработки добавления данных в массивы в MQL5, специально разработанные для обработки типов uchar и string. Эти функции облегчают создание данных HTTP-запроса путем добавления различных фрагментов информации к существующему массиву, гарантируя, что окончательный формат данных является правильным и пригодным для передачи. Мы добавили комментарии к функциям для облегчения понимания, но давайте еще раз кратко поясним код.

Первая функция, ArrayAdd, применяется к массивам беззнаковых символов (uchar). Она настроена на добавление данных из исходного массива в целевой массив. Сначала она определяет количество элементов в исходном массиве. Это достигается путем вызова простой функции ArraySize на исходном массиве. Используя эту информацию, мы проверяем, содержит ли исходный массив какие-либо данные. Если нет, мы избегаем продолжения, выходя из функции раньше времени. Если данные есть, мы переходим к следующему шагу — изменению размера целевого массива для их приема. Мы делаем это, вызывая функцию ArrayResize в массиве назначения. Теперь мы можем вызывать его с уверенностью, что он будет работать правильно.

Другая функция, которая добавляет строки в массив char, работает следующим образом: она вычисляет длину входной строки. Если входная строка не пуста, она берет каждый символ строки, получает его код и добавляет его в целевой массив, одновременно преобразуя его в UTF-8. Чтобы преобразовать строку и добавить ее в целевой массив, эта функция изменяет размер исходного массива для промежуточного хранения добавляемых строковых данных. Она гарантирует, что представление строки в формате UTF-8, а также сама строка будут правильно сохранены в конечном массиве данных, который будет использоваться для построения тел HTTP-запросов или других видов структур данных.

Чтобы увидеть результат, давайте реализуем логику, которая будет выводить результирующие данные, связанные с идентификатором чата, который будет отправлен в 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);   

Для начала мы используем функцию ArrayPrint для представления массива необработанных данных. Функция выводит содержимое массива. Затем мы преобразуем массив DATA из символьного формата в строковый. Функция, которую мы используем, - CharArrayToString, который преобразует необработанные байтовые данные DATA в строку в кодировке UTF-8. Используемые здесь параметры указывают, что мы хотим преобразовать весь массив (WHOLE_ARRAY) и что кодировка символов - UTF-8 (CP_UTF8). Это преобразование необходимо, поскольку HTTP-запрос требует, чтобы данные были в строковом формате.

В заключение, у нас есть строка chatID_Data, окончательный формат которой таков, что она будет включена в HTTP-запрос. Используя функцию Print, мы можем увидеть, как будет выглядеть конечный вывод в запросе.

CHAT ID REQUEST

Мы видим, что можем правильно добавить данные идентификатора чата в массив. Используя ту же логику, мы можем также добавить данные изображения для построения тела запроса multipart/form-data для отправки изображения по HTTP в API 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");


Для начала мы добавляем маркер границы для раздела изображений в данных multipart/form. Делается это с помощью строки ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n"). Граничный маркер, состоящий из двух дефисов и HexaDecimal_Hash, служит для разделения различных частей составного запроса. HexaDecimal_Hash, уникальный идентификатор границы, является гарантией того, что каждая часть запроса безошибочно отделена от следующей.

Затем мы включаем заголовок Content-Disposition для раздела изображений данных формы. Мы добавляем его с помощью функции ArrayAdd следующим образом: ArrayAdd(DATA, "Content-Disposition: form-data; name=\" photo\"; filename=\"Upload_ScreenShot.jpg\"\r\n"). Заголовок указывает, что следующие данные представляют собой файл с именем Upload_ScreenShot.jpg. Поскольку мы указали с помощью части заголовка name=\"photo\", что поле данных формы, с которым мы в данный момент работаем, имеет имя \"photo\", сервер при обработке входящего запроса ожидает файл Upload_ScreenShot.jpg как часть этого поля. Файл — это всего лишь идентификатор, и вы можете изменить его на любой другой по своему усмотрению.

После этого мы используем ArrayAdd(DATA, "\r\n") для добавления последовательности новой строки к заголовкам запроса. Это указывает на конец раздела заголовка и начало фактических данных файла. Затем мы используем ArrayAdd(DATA, photoArr_Data) для добавления фактических данных изображения в массив DATA. Эта строка кода добавляет двоичные данные скриншота (ранее закодированные в base64) к телу запроса. Полезная нагрузка multipart/form-data теперь содержит данные изображения.

Наконец, мы добавляем еще одну последовательность новой строки с помощью ArrayAdd(DATA, "\r\n") и маркер границы, чтобы закрыть раздел изображения с помощью ArrayAdd(DATA, "--" + HexaDecimal_Hash + "--\r\n"). "--" в конце граничного маркера обозначает конец составного (multipart) раздела. Эта конечная граница гарантирует, что сервер правильно идентифицирует конец раздела данных изображения в запросе. Чтобы просмотреть отправляемые данные, давайте снова выведем их в раздел журнала с помощью функции, аналогичной предыдущей.

   Print("FINAL FULL PHOTO DATA BEING SENT:");
   ArrayPrint(DATA);
   string final_Simple_Data = CharArrayToString(DATA,0,WHOLE_ARRAY,CP_ACP);
   Print("FINAL FULL SIMPLE PHOTO DATA BEING SENT:",final_Simple_Data);

Вот какие результаты мы получаем:

Загрузка окончательных данных файла

Наконец, мы создаем заголовки HTTP-запроса, необходимые для отправки запроса multipart/form-data в API Telegram.

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

Начнем с определения строки HEADERS, которая инициализируется как NULL. Эта строка содержит HTTP-заголовки, которые нам необходимо установить для запроса. Заголовок, который мы обязательно должны задать, — Content-Type. Заголовок Content-Type передает тип отправляемых данных и способ их форматирования.

Мы присваиваем строке правильное значение Content-Type. Важнейшей частью здесь является сама строка HEADERS. Мы должны знать "формат" HTTP-запроса, если хотим понять, почему необходимо это конкретное назначение строки HEADERS. Формат запроса говорит о том, что запрос отправляется с использованием заголовка "Content-Type: multipart/form-data". Теперь мы можем инициировать веб-запрос. Для начала проинформируем пользователя, отправив запрос ниже.

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

Из исходного кода закомментируем ненужные параметры WebRequest и переключимся на последние.

   //char data[];  // Array to hold data to be sent in the web request (empty in this case)
   char res[];  // Array to hold the response data from the web request
   string resHeaders;  // String to hold the response headers from the web request
   //string msg = "EA INITIALIZED ON CHART " + _Symbol;  // Message to send, including the chart symbol
   
   //const string url = TG_API_URL + "/bot" + botTkn + "/sendmessage?chat_id=" + chatID +
   //   "&text=" + msg;
      
   // Send the web request to the Telegram API
   int send_res = WebRequest("POST",URL,HEADERS,10000, DATA, res, resHeaders);

Здесь мы только добавляем к функции новый URL, заголовки и данные файла изображения для отправки. Логика ответа не меняется:

   // Check the response status of the web request
   if (send_res == 200) {
      // If the response status is 200 (OK), print a success message
      Print("TELEGRAM MESSAGE SENT SUCCESSFULLY");
   } else if (send_res == -1) {
      // If the response status is -1 (error), check the specific error code
      if (GetLastError() == 4014) {
         // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal
         Print("PLEASE ADD THE ", TG_API_URL, " TO THE TERMINAL");
      }
      // Print a general error message if the request fails
      Print("UNABLE TO SEND THE TELEGRAM MESSAGE");
   } else if (send_res != 200) {
      // If the response status is not 200 or -1, print the unexpected response code and error code
      Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError());
   }

Запустив программу, мы получим следующее:

В MetaTrader 5:

Подтверждение MetaTrader 5

В Telegram:

Подтверждение TELEGRAM

Теперь видно, что мы успешно отправили файл изображения из торгового терминала MetaTrader 5 в чат Telegram. Однако мы просто отправили пустой скриншот. Чтобы добавить подпись к изображению, мы реализуем следующую логику, добавляющую необязательную подпись к запросу multipart/form-data, который будет отправлен вместе со скриншотом графика в API 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");
   }
   //---


Начнем с инициализации строки CAPTION как NULL, а затем составим ее с соответствующими данными. Подпись включает торговый символ, временные рамки графика и текущее время, отформатированные в виде строки. Затем мы проверяем, имеет ли строка CAPTION длину больше нуля. Если это так, переходим к добавлению заголовка в массив DATA, который используется для построения данных составной формы. Это включает добавление маркера границы, указание части form-data в качестве заголовка и включение самого содержимого заголовка. При запуске мы получаем следующие результаты:

Файл изображения с подписью

Это успех. Мы получили не только файл изображения, но и подпись с названием символа, периодом и временем рассматриваемого графика.

Пока мы получаем скриншот графика, на котором запущена программа. Если нужно открыть и изменить другой график, необходимо реализовать другую логику. 

   long chart_id=ChartOpen(_Symbol,_Period);
   ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true);
   // update chart
   int wait=60;
   while(--wait>0){//decrease the value of wait by 1 before loop condition check
      if(SeriesInfoInteger(_Symbol,_Period,SERIES_SYNCHRONIZED)){
         break; // if prices up to date, terminate the loop and proceed
      }
   }

   ChartRedraw(chart_id);
   ChartSetInteger(chart_id,CHART_SHOW_GRID,false);
   ChartSetInteger(chart_id,CHART_SHOW_PERIOD_SEP,false);
   ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BEAR,clrRed);
   ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BULL,clrBlue);
   ChartSetInteger(chart_id,CHART_COLOR_BACKGROUND,clrLightSalmon);

   ChartScreenShot(chart_id,SCREENSHOT_FILE_NAME,1366,768,ALIGN_RIGHT);
   //Sleep(10000); // sleep for 10 secs to see the opened chart
   ChartClose(chart_id);
//---

Здесь мы начинаем с открытия нового графика для заданного символа и таймфрейма с помощью функции ChartOpen, использующей предопределенные переменные _Symbol и _Period. Присваиваем идентификатор нового графика переменной chart_id. Затем мы используем chart_id, чтобы убедиться, что новый график отображается на переднем плане среды MetaTrader и не перекрывается предыдущими графиками.

После этого мы запускаем цикл, который может выполняться максимум 60 итераций. В этом цикле мы постоянно проверяем, синхронизирован ли график. Для проверки синхронизации мы используем функцию SeriesInfoInteger с параметрами _Symbol, _Period и SERIES_SYNCHRONIZED. Если мы обнаруживаем, что график синхронизирован, мы выходим из цикла. Убедившись, что график синхронизирован, мы используем функцию ChartRedraw с параметром chart_id для обновления графика.

Мы можем настроить график по своему вкусу. Используем функцию ChartSetInteger для установки цветов фона графика, а также медвежьих и бычьих свечей. Заданные цвета помогают нам легко различать разные элементы графика. Мы также делаем график визуально менее загроможденным, отключив сетку и разделители периодов. На этом этапе вы можете изменить график по своему усмотрению. Наконец, мы делаем скриншот графика для использования при передаче. Мы не хотим оставлять график открытым без необходимости, поэтому закрываем его после создания скриншота. Для этого используем функцию ChartClose. При запуске программы мы получаем следующие результаты:

Измененный график

Мы открываем график, изменяем его по своему вкусу и закрываем, сделав скриншот. Чтобы визуализировать процесс открытия и закрытия графика, сделаем задержку в 10 секунд.

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

Здесь мы просто оставляем график открытым на 10 секунд, чтобы увидеть работу нашей программы. Вот что мы получаем после компиляции:

Открытие и закрытие графика (GIF)

Полный исходный код, отвечающий за создание скриншотов, их кодирование, шифрование и отправку из торгового терминала в чат Telegram, выглядит следующим образом:

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

   //--- Get ready to take a chart screenshot of the current chart
   
   #define SCREENSHOT_FILE_NAME "Our Chart ScreenShot.jpg"
   
   //--- First delete an instance of the screenshot file if it already exists
   if(FileIsExist(SCREENSHOT_FILE_NAME)){
      FileDelete(SCREENSHOT_FILE_NAME);
      Print("Chart Screenshot was found and deleted.");
      ChartRedraw(0);
   }
   
//---
   long chart_id=ChartOpen(_Symbol,_Period);
   ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true);
   // update chart
   int wait=60;
   while(--wait>0){//decrease the value of wait by 1 before loop condition check
      if(SeriesInfoInteger(_Symbol,_Period,SERIES_SYNCHRONIZED)){
         break; // if prices up to date, terminate the loop and proceed
      }
   }

   ChartRedraw(chart_id);
   ChartSetInteger(chart_id,CHART_SHOW_GRID,false);
   ChartSetInteger(chart_id,CHART_SHOW_PERIOD_SEP,false);
   ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BEAR,clrRed);
   ChartSetInteger(chart_id,CHART_COLOR_CANDLE_BULL,clrBlue);
   ChartSetInteger(chart_id,CHART_COLOR_BACKGROUND,clrLightSalmon);

   ChartScreenShot(chart_id,SCREENSHOT_FILE_NAME,1366,768,ALIGN_RIGHT);
   Print("OPENED CHART PAUSED FOR 10 SECONDS TO TAKE SCREENSHOT.")
   Sleep(10000); // sleep for 10 secs to see the opened chart
   ChartClose(chart_id);
//---
   
   //ChartScreenShot(0,SCREENSHOT_FILE_NAME,1366,768,ALIGN_RIGHT);
   
   // Wait for 30 secs to save screenshot if not yet saved
   int wait_loops = 60;
   while(!FileIsExist(SCREENSHOT_FILE_NAME) && --wait_loops > 0){
      Sleep(500);
   }
   
   if(!FileIsExist(SCREENSHOT_FILE_NAME)){
      Print("THE SPECIFIED SCREENSHOT DOES NOT EXIST (WAS NOT SAVED). REVERTING NOW!");
      return (INIT_FAILED);
   }
   else if(FileIsExist(SCREENSHOT_FILE_NAME)){
      Print("THE CHART SCREENSHOT WAS SAVED SUCCESSFULLY TO THE DATA-BASE.");
   }
   
   int screenshot_Handle = INVALID_HANDLE;
   screenshot_Handle = FileOpen(SCREENSHOT_FILE_NAME,FILE_READ|FILE_BIN);
   if(screenshot_Handle == INVALID_HANDLE){
      Print("INVALID SCREENSHOT HANDLE. REVERTING NOW!");
      return(INIT_FAILED);
   }
   
   else if (screenshot_Handle != INVALID_HANDLE){
      Print("SCREENSHOT WAS SAVED & OPENED SUCCESSFULLY FOR READING.");
      Print("HANDLE ID = ",screenshot_Handle,". IT IS NOW READY FOR ENCODING.");
   }
   
   int screenshot_Handle_Size = (int)FileSize(screenshot_Handle);
   if (screenshot_Handle_Size > 0){
      Print("CHART SCREENSHOT FILE SIZE = ",screenshot_Handle_Size);
   }
   uchar photoArr_Data[];
   ArrayResize(photoArr_Data,screenshot_Handle_Size);
   FileReadArray(screenshot_Handle,photoArr_Data,0,screenshot_Handle_Size);
   if (ArraySize(photoArr_Data) > 0){
      Print("READ SCREENSHOT FILE DATA SIZE = ",ArraySize(photoArr_Data));
   }
   FileClose(screenshot_Handle);
   
   //ArrayPrint(photoArr_Data);
   
   //--- create boundary: (data -> base64 -> 1024 bytes -> md5)
   //Encodes the photo data into base64 format
   //This is part of preparing the data for transmission over HTTP.
   uchar base64[];
   uchar key[];
   CryptEncode(CRYPT_BASE64,photoArr_Data,key,base64);
   if (ArraySize(base64) > 0){
      Print("Transformed BASE-64 data = ",ArraySize(base64));
      //Print("The whole data is as below:");
      //ArrayPrint(base64);
   }
   
   //Copy the first 1024 bytes of the base64-encoded data into a temporary array
   uchar temporaryArr[1024]= {0};
   //Print("FILLED TEMPORARY ARRAY WITH ZERO (0) IS AS BELOW:");
   //ArrayPrint(temporaryArr);
   ArrayCopy(temporaryArr,base64,0,0,1024);
   //Print("FIRST 1024 BYTES OF THE ENCODED DATA IS AS FOLLOWS:");
   //ArrayPrint(temporaryArr);
      
   //Create an MD5 hash of the temporary array
   //This hash will be used as part of the boundary in the multipart/form-data
   uchar md5[];
   CryptEncode(CRYPT_HASH_MD5,temporaryArr,key,md5);
   if (ArraySize(md5) > 0){
      Print("SIZE OF MD5 HASH OF TEMPORARY ARRAY = ",ArraySize(md5));
      Print("MD5 HASH boundary in multipart/form-data is as follows:");
      ArrayPrint(md5);
   }

   //Format MD5 hash as a hexadecimal string &
   //truncate it to 16 characters to create the boundary.
   string HexaDecimal_Hash=NULL;//Used to store the hexadecimal representation of MD5 hash
   int total=ArraySize(md5);
   for(int i=0; i<total; i++){
      HexaDecimal_Hash+=StringFormat("%02X",md5[i]);
   }
   Print("Formatted MD5 Hash String is: \n",HexaDecimal_Hash);
   HexaDecimal_Hash=StringSubstr(HexaDecimal_Hash,0,16);//truncate HexaDecimal_Hash string to its first 16 characters
   //done to comply with a specific length requirement for the boundary
   //in the multipart/form-data of the HTTP request.
   Print("Final Truncated (16 characters) MD5 Hash String is: \n",HexaDecimal_Hash);
   
   //--- WebRequest
   char DATA[];
   string URL = NULL;
   URL = TG_API_URL+"/bot"+botTkn+"/sendPhoto";
   //--- add chart_id
   //Append a carriage return and newline character sequence to the DATA array.
   //In the context of HTTP, \r\n is used to denote the end of a line
   //and is often required to separate different parts of an HTTP request.
   ArrayAdd(DATA,"\r\n");
   //Append a boundary marker to the DATA array.
   //Typically, the boundary marker is composed of two hyphens (--)
   //followed by a unique hash string and then a newline sequence.
   //In multipart/form-data requests, boundaries are used to separate
   //different pieces of data.
   ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n");
   //Add a Content-Disposition header for a form-data part named chat_id.
   //The Content-Disposition header is used to indicate that the following data
   //is a form field with the name chat_id.
   ArrayAdd(DATA,"Content-Disposition: form-data; name=\"chat_id\"\r\n");
   //Again, append a newline sequence to the DATA array to end the header section
   //before the value of the chat_id is added.
   ArrayAdd(DATA,"\r\n");
   //Append the actual chat ID value to the DATA array.
   ArrayAdd(DATA,chatID);
   //Finally, Append another newline sequence to the DATA array to signify
   //the end of the chat_id form-data part.
   ArrayAdd(DATA,"\r\n");

   // EXAMPLE OF USING CONVERSIONS
   //uchar array[] = { 72, 101, 108, 108, 111, 0 }; // "Hello" in ASCII
   //string output = CharArrayToString(array,0,WHOLE_ARRAY,CP_ACP);
   //Print("EXAMPLE OUTPUT OF CONVERSION = ",output); // Hello
   
   Print("CHAT ID DATA:");
   ArrayPrint(DATA);
   string chatID_Data = CharArrayToString(DATA,0,WHOLE_ARRAY,CP_UTF8);
   Print("SIMPLE CHAT ID DATA IS AS FOLLOWS:",chatID_Data);   


   //--- Caption
   string CAPTION = NULL;
   CAPTION = "Screenshot of Symbol: "+Symbol()+
             " ("+EnumToString(ENUM_TIMEFRAMES(_Period))+
             ") @ Time: "+TimeToString(TimeCurrent());
   if(StringLen(CAPTION) > 0){
      ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n");
      ArrayAdd(DATA,"Content-Disposition: form-data; name=\"caption\"\r\n");
      ArrayAdd(DATA,"\r\n");
      ArrayAdd(DATA,CAPTION);
      ArrayAdd(DATA,"\r\n");
   }
   //---
   
   ArrayAdd(DATA,"--"+HexaDecimal_Hash+"\r\n");
   ArrayAdd(DATA,"Content-Disposition: form-data; name=\"photo\"; filename=\"Upload_ScreenShot.jpg\"\r\n");
   ArrayAdd(DATA,"\r\n");
   ArrayAdd(DATA,photoArr_Data);
   ArrayAdd(DATA,"\r\n");
   ArrayAdd(DATA,"--"+HexaDecimal_Hash+"--\r\n");
   
   Print("FINAL FULL PHOTO DATA BEING SENT:");
   ArrayPrint(DATA);
   string final_Simple_Data = CharArrayToString(DATA,0,WHOLE_ARRAY,CP_ACP);
   Print("FINAL FULL SIMPLE PHOTO DATA BEING SENT:",final_Simple_Data);

   string HEADERS = NULL;
   HEADERS = "Content-Type: multipart/form-data; boundary="+HexaDecimal_Hash+"\r\n";
   
   Print("SCREENSHOT SENDING HAS BEEN INITIATED SUCCESSFULLY.");
   
   //char data[];  // Array to hold data to be sent in the web request (empty in this case)
   char res[];  // Array to hold the response data from the web request
   string resHeaders;  // String to hold the response headers from the web request
   //string msg = "EA INITIALIZED ON CHART " + _Symbol;  // Message to send, including the chart symbol
   
   //const string url = TG_API_URL + "/bot" + botTkn + "/sendmessage?chat_id=" + chatID +
   //   "&text=" + msg;
      
   // Send the web request to the Telegram API
   int send_res = WebRequest("POST",URL,HEADERS,10000, DATA, res, resHeaders);

   // Check the response status of the web request
   if (send_res == 200) {
      // If the response status is 200 (OK), print a success message
      Print("TELEGRAM MESSAGE SENT SUCCESSFULLY");
   } else if (send_res == -1) {
      // If the response status is -1 (error), check the specific error code
      if (GetLastError() == 4014) {
         // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal
         Print("PLEASE ADD THE ", TG_API_URL, " TO THE TERMINAL");
      }
      // Print a general error message if the request fails
      Print("UNABLE TO SEND THE TELEGRAM MESSAGE");
   } else if (send_res != 200) {
      // If the response status is not 200 or -1, print the unexpected response code and error code
      Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError());
   }
   
   
   return(INIT_SUCCEEDED);  // Return initialization success status
}

Мы достигли нашей третьей цели, а именно, отправки файлов скриншотов графиков с подписями из торгового терминала в чат или группу Telegram. Теперь нам нужно протестировать интеграцию, чтобы убедиться в ее корректной работе и выявить любые возникающие проблемы.


Тестирование интеграции

Чтобы убедиться, что наш советник правильно отправляет скриншоты из MetaTrader 5 в Telegram, нам необходимо тщательно протестировать интеграцию. Давайте сохраним логику тестирования в формате GIF.

Тестирование (GIF)

В представленном выше GIF-изображении показывается бесшовное взаимодействие между MetaTrader 5 и Telegram при отправке скриншота графика. GIF-анимация начинается с показа платформы MetaTrader 5, в которой окно графика открывается, выводится на передний план, а затем приостанавливается на 10 секунд для окончательной корректировки. Во время этой паузы на вкладке "Журнал" в MetaTrader 5 регистрируются сообщения, демонстрирующие ход выполнения операции, например, перерисовку графика и создание скриншотов. Затем график автоматически закрывается, а скриншот упаковывается и отправляется в Telegram. Со стороны Telegram мы видим, что в чате появился скриншот, подтверждающий, что интеграция работает так, как задумано. GIF-анимация наглядно демонстрирует, как автоматизированная система работает в режиме реального времени - от подготовки графика до успешной доставки изображения в Telegram.


Заключение

В статье пошагово описывается, как отправить скриншот графика с торговой платформы MetaTrader 5 в Telegram-чат. Сначала мы сгенерировали скриншот графика в MetaTrader 5. Мы настроили необходимые параметры графика и зафиксировали его с помощью функции ChartScreenShot для получения изображения в виде файла. После сохранения файла на компьютере, мы открыли его и прочитали его двоичное содержимое. Затем мы отправили график, закодированный в формате Base64, в HTTP-запрос, который мог понять Telegram API. Таким образом мы опубликовали изображение в чате Telegram в реальном времени.

Кодирование изображения для передачи выявило сложности, связанные с отправкой необработанных двоичных данных по протоколу HTTP, особенно когда местом назначения является платформа обмена сообщениями, такая как Telegram. Первое, что нужно понять, — отправка двоичных данных напрямую просто невозможна. Telegram (и многие другие сервисы) требует, чтобы данные отправлялись в текстовом формате. Мы выполнили это требование, применив широко известный алгоритм для преобразования необработанных двоичных данных изображения в Base64. После этого мы вставили изображение Base64 в HTTP-запрос multipart/form-data. Эта демонстрация не только подчеркнула мощь платформы MetaTrader 5 как средства создания пользовательской автоматизации, но и показала, как интегрировать внешний сервис — в данном случае Telegram — в торговую стратегию.

Заглядывая вперед в Часть 4, мы возьмем код из этой статьи и превратим его в повторно используемые компоненты. Мы сделаем это для того, чтобы иметь возможность создать несколько экземпляров интеграции Telegram, что позволит нам в следующих частях серии отправлять различные сообщения и скриншоты в Telegram тогда и в таком виде, в каком мы захотим — без необходимости полагаться для этого на один вызов функции. Разделив код на классы, мы сделаем систему более модульной и масштабируемой. Мы также сделаем это для того, чтобы упростить интеграцию кода в различные торговые сценарии, описанные в Части 1. Это важно, поскольку интеграция механизма Telegram должна работать динамично и гибко с нашими советниками, позволяя нескольким стратегиям и сценариям счетов отправлять различные сообщения и изображения в наиболее важные моменты во время торговли или в конце торгового дня. В следующей статье мы продолжаем разрабатывать и совершенствовать интегрированную систему.


Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/15616

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (18)
Aleksandr Slavskii
Aleksandr Slavskii | 9 июн. 2025 в 02:38
Piotr Storozenko #:
      ::StringReplace(text, "\n", ShortToString(0x0A));
Piotr Storozenko
Piotr Storozenko | 9 июн. 2025 в 11:42
Aleksandr Slavskii #:

Спасибо. Я нашел эту публичную функцию с тем же именем, что и в обычном MQL5, и переименовал, чтобы убрать предупреждение. Теперь компиляция проходит без проблем :)

Volker Mowy
Volker Mowy | 11 июн. 2025 в 17:04
Здравствуйте! Спасибо за такую подробную документацию. К сожалению, я получаю два сообщения об ошибках. Можете ли вы мне помочь?

С наилучшими пожеланиями, Фолькер

Volker Mowy
Volker Mowy | 5 июл. 2025 в 13:36
Volker Mowy #:
Здравствуйте! Спасибо за такую подробную документацию. К сожалению, я получаю два сообщения об ошибках. Можете ли вы мне помочь?

С наилучшими пожеланиями, Фолькер

Спасибо, ошибка найдена!

666 // ArrayAdd для массива uchar
667 void ArrayAdd(char &destinationArr[], const uchar &sourceArr[]){
Volker Mowy
Volker Mowy | 6 июл. 2025 в 06:33
Небольшое изменение для лучшего отображения!
Формулировка динамического советника на нескольких парах (Часть 1): Корреляция и обратная корреляция валютных пар Формулировка динамического советника на нескольких парах (Часть 1): Корреляция и обратная корреляция валютных пар
Динамический советник на нескольких парах использует как корреляционные, так и обратные корреляционные стратегии для оптимизации эффективности торговли. Анализируя рыночные данные в режиме реального времени, он определяет и использует взаимосвязь между валютными парами.
Бильярдный алгоритм оптимизации — Billiards Optimization Algorithm (BOA) Бильярдный алгоритм оптимизации — Billiards Optimization Algorithm (BOA)
Метод BOA, вдохновленный классической игрой в бильярд, моделирует процесс поиска оптимальных решений, как игру с шарами, стремящимися попасть в лузы, олицетворяющие наилучшие результаты. В данной статье мы рассмотрим основы работы BOA, его математическую модель и эффективность в решении различных оптимизационных задач.
Cоздание стратегии возврата к среднему на основе машинного обучения Cоздание стратегии возврата к среднему на основе машинного обучения
В данной статье предлагается очередной оригинальный подход к созданию торговых систем на основе машинного обучения, с использованием кластеризации и разметки сделок для стратегий возврата к среднему.
Нейросети в трейдинге: Интеграция теории хаоса в прогнозирование временных рядов (Окончание) Нейросети в трейдинге: Интеграция теории хаоса в прогнозирование временных рядов (Окончание)
Продолжаем интеграцию методов, предложенных авторами фреймворка Attraos, в торговые модели. Напомню, что данный фреймворк использует концепции теории хаоса для решения задач прогнозирования временных рядов, интерпретируя их как проекции многомерных хаотических динамических систем.