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

MQL5-советник, интегрированный в Telegram (Часть 5): Отправка команд из Telegram в MQL5 и получение ответов в реальном времени

MetaTrader 5Торговые системы | 31 марта 2025, 11:42
610 5
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

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

Мы начнем с создания среды, необходимой для бесперебойного взаимодействия. Основная часть этой статьи будет посвящена созданию классов, которые автоматически извлекают обновления чата из JavaScript Object Notation (JSON), в данном случае - команды и запросы Telegram, которые были бы понятны советнику. Этот шаг имеет решающее значение для установления динамической двусторонней коммуникации, в которой бот не только отправляет сообщения, но и разумно реагирует на действия пользователя.

Кроме того, мы сосредоточимся на декодировании и интерпретации входящих данных, гарантируя, что советник сможет эффективно управлять различными типами команд из Telegram Application Programming Interface (API). Для демонстрации этого процесса мы предоставили подробное визуальное руководство, иллюстрирующее поток коммуникации между Telegram, MetaTrader 5 и редактором кода MQL5, что упрощает понимание того, как эти компоненты работают вместе.

Поток процесса интеграции

Представленная иллюстрация должна быть наглядной и демонстрировать компоненты интеграции. Таким образом, поток будет выглядеть следующим образом: Telegram отправляет команды в торговый терминал, к которому прикреплен советник, советник отправляет команды в MQL5, который декодирует, интерпретирует сообщения, готовит соответствующие ответы и отправляет их в торговый терминал и Telegram. Для простоты понимания мы разделим весь процесс на темы следующим образом:

  1. Настройка среды
  2. Создание классов для получения обновлений чата из JSON
  3. Декодирование и анализ данных из API Telegram
  4. Обработка ответов
  5. Тестирование реализации
  6. Заключение

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


Настройка среды

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

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

Здесь библиотека Trade/Trade.mqh предоставляет полный набор торговых функций. Библиотека позволяет советнику совершать сделки, управлять позициями и выполнять другие задачи, связанные с торговлей. Это важнейший компонент любого советника, нацеленного на взаимодействие с рынком. Библиотеки Arrays/List.mqh и Arrays/ArrayString.mqh включены для облегчения управления структурами данных. Первая из этих двух библиотек предназначена для управления динамическими списками. Вторая — для работы с массивами строк. Обе эти библиотеки особенно полезны при работе с торговыми сигналами, которые мы получаем от Telegram. В следующих главах мы более подробно объясним, что делают все эти компоненты. Чтобы получить доступ к библиотеке Arrays, откройте навигатор, разверните папку Include и отметьте один из двух пунктов, как показано ниже.

Библиотека массивов

Наконец, нам нужно определить базовый URL-адрес Telegram, время ожидания и токен бота, как показано ниже.

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

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


Создание классов для получения обновлений чата из JSON

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

Пустые данные

После загрузки мы возвращаем значение true, что означает, что процесс прошел успешно, но структура данных пуста. Это связано с тем, что за последние 24 часа из чата Telegram не было отправлено ни одного сообщения. То есть нам необходимо отправить сообщение, чтобы получить обновление. Для этого мы отправляем инициализирующее сообщение из чата Telegram, как показано ниже.

Первое сообщение инициализации в Telegram

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

Структура данных 1

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

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

Давайте сосредоточимся на прототипе класса, который мы объявили выше, чтобы в дальнейшем все шло гладко. Чтобы объявить класс, мы используем ключевое слово class, за которым следует имя класса. В нашем случае это Class_Message. Поскольку мы получим множество похожих структур данных, мы наследуем другой класс с именем CObject и делаем унаследованные члены внешнего класса общедоступными, используя ключевое слово public. Затем мы объявляем первых членов класса публичными (public). Прежде чем продолжить, давайте подробно рассмотрим, что все это значит. Ключевое слово является одним из 4 квалификаторов, обычно называемых спецификаторами доступа. Они определяют, как компилятор может получить доступ к переменным, членам структур или классам. Спецификаторов четыре: публичный (public), защищенный (protected), частный (private) и виртуальный (virtual).

Давайте разберем их по отдельности.

  • Public: Члены, объявленные с помощью спецификатора доступа public, доступны из любой части кода, где виден класс. Это означает, что функции, переменные или другие объекты за пределами класса могут напрямую обращаться и использовать их. Их часто используют для функций или переменных, к которым необходим доступ другим классам, функциям или скриптам.
  • Protected: Члены, объявленные с помощью спецификатора доступа protected, недоступны извне класса, но доступны внутри самого класса, производным классам (то есть подклассам, которые наследуют от этого класса) и дружественным классам/функциям. Это полезно для инкапсуляции данных, которые должны быть доступны подклассам, но не остальной части программы. Обычно они используются для того, чтобы позволить подклассам получать доступ к определенным переменным или функциям базового класса или изменять их, при этом скрывая эти члены от остальной части программы.
  • Private: Члены, объявленные с помощью спецификатора доступа private, доступны только внутри самого класса. Ни производные классы, ни какая-либо другая часть программы не могут напрямую обращаться к закрытым членам или изменять их. Это наиболее ограничительный уровень доступа, который обычно используется для переменных и вспомогательных функций, которые не должны быть доступны или изменяемы извне класса. Они обычно используются для сокрытия данных, гарантируя, что внутреннее состояние объекта может быть изменено только через четко определенные общедоступные интерфейсы (методы).
  • Virtual: Применяется только к методам класса (но не к методам структур) и сообщает компилятору, что этот метод следует поместить в таблицу виртуальных функций класса.

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

  • Объявление класса:

class Class_Message : public CObject{...};: Здесь мы объявляем новый класс с именем Class_Message. Класс является производным от CObject, который является базовым классом в MQL5 и используется для создания пользовательских объектов. Таким образом, Class_Message может использовать функции, предоставляемые средой MQL5, такие как управление памятью и другие преимущества объектно-ориентированного программирования, для удобного отображения сообщений в нашей программе.

  • Конструктор:

Class_Message();: Конструктор класса Class_Message объявляется здесь. Конструктор — это специальная функция, которая вызывается автоматически при создании экземпляра (или объекта) класса. Задача конструктора — инициализировать переменные-члены класса и выполнить все необходимые настройки при создании объекта. В случае Class_Message он инициализирует переменные-члены.

  • Деструктор:

~Class_Message(){};: класс Class_Message объявляет деструктор. Деструктор автоматически вызывается, когда экземпляр класса явно удаляется или выходит из области действия. Обычно деструктор определяется для выполнения очистки и концептуально является противоположностью конструктора, который вызывается при создании экземпляра класса. В этом случае деструктор класса Class_Message ничего не делает (не выполняет никаких задач по очистке), поскольку на данный момент это не нужно. 

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

Очистить детали сообщения

Из изображения выше видно, что в нашем классе должно быть не менее 14 членов. Мы определяем их следующим образом:

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

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

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

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

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

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

После определения класса сообщений нам необходимо инициализировать его члены, чтобы они были готовы к приему данных. Мы делаем это, вызывая конструктор класса.

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

Сначала мы вызываем базовый класс и определяем конструктор с помощью "оператора области действия" (::). Затем мы инициализируем переменные-члены значениями по умолчанию. Логическое значение done установлено на false, что означает, что сообщение еще не обработано. Оба параметра message_id и update_id инициализируются значением 0, что представляет собой идентификаторы по умолчанию для сообщения и обновления. Для информации, связанной с отправителем from_id устанавливается равным 0, а переменные from_first_name, from_last_name и from_username инициализируются NULL, что означает, что данные отправителя не установлены. Аналогично переменные, относящиеся к чату, то есть chat_id, chat_first_name, chat_last_name, chat_username и chat_type, также инициализируются значением 0 или NULL к их типам данных, что означает, что информация чата пока недоступна. Наконец, message_date устанавливается равным 0, а message_text инициализируется NULL, что означает, что содержание и дата сообщения еще не указаны. Технически мы инициализируем целочисленные (integer) переменные значением 0, а строки (strings) - NULL.

Аналогично нам необходимо определить еще один экземпляр класса, который будет использоваться для хранения отдельных чатов Telegram. Мы будем использовать эти данные для сравнения проанализированных данных и данных, полученных от Telegram. Например, когда мы отправляем команду "get Ask price" (получить цену Ask), мы анализируем данные, получаем обновления из JSON и проверяем, соответствуют ли какие-либо полученные данные, хранящиеся в JSON, нашей команде, и, если да, предпринимаем необходимые действия. Мы надеемся, что это прояснит некоторые вещи, но по мере продвижения вперед все станет еще яснее. Фрагмент кода класса выглядит следующим образом:

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

Мы определяем класс с именем Class_Chat для обработки и хранения информации отдельных Telegram-чатов. Класс содержит пустые конструктор и деструктор, а также несколько членов: member_id хранит уникальный идентификатор чата, member_state указывает состояние чата, а member_time содержит любую информацию, относящуюся ко времени чата. Класс имеет два экземпляра базового класса, который мы уже определили, Class_Message. Он содержит последнее и новое сообщения соответственно. Они нужны нам для хранения сообщений и индивидуальной обработки, когда пользователь отправляет несколько команд. Чтобы проиллюстрировать это, мы отправим сообщение инициализации, как показано ниже:

Второе сообщение инициализации

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

Структура данных второго сообщения

Из полученной структуры данных второго сообщения мы видим, что идентификаторы обновления и сообщения для первого сообщения составляют 794283239 и 664 соответственно, тогда как для второго сообщения — 794283240 и 665, что составляет разницу в 1. Мы надеемся, что это проясняет необходимость создания другого класса. Теперь мы можем приступить к созданию последнего класса по умолчанию, который мы будем использовать для беспрепятственного управления потоком взаимодействия. Его структура выглядит так.

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

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

Мы определяем класс Class_Bot_EA для управления взаимодействиями между Telegram-ботом и средой MQL5. Он имеет несколько закрытых членов, таких как member_token, который хранит токен аутентификации для бота, и member_name, который содержит имя бота. Другим элементом является member_update_id, который отслеживает последнее обработанное обновление. Несколько других участников управляют и сортируют взаимодействия пользователей. Класс имеет защищенный член member_chats, который поддерживает список объектов чата. Среди его открытых членов наиболее примечательными являются конструктор и деструктор, которые выполняют необходимую инициализацию и очистку экземпляров. Среди публичных членов есть также две примечательные функции: getChatUpdates, которая извлекает обновления из Telegram, и ProcessMessages, которая занимается обработкой входящих сообщений. Это самые важные функции, которые мы будем использовать для получения обновлений чата и обработки полученных команд. Мы инициализируем эти члены, используя тот же формат, что и для первого класса, как показано ниже.

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

Здесь мы вызываем конструктор для класса Class_Bot_EA и инициализируем переменные-члены для установки среды бота. Изначально member_token устанавливается на NULL в качестве заполнителя. Затем мы присваиваем ему обрезанную версию InpToken. Это значение очень важно, поскольку оно управляет аутентификацией бота. Если в коде оставить обрезанный заполнитель, бот просто не будет работать. member_name также инициализируется NULL, а member_update_id устанавливается на 0, что указывает на то, что обновления еще не были обработаны. Переменная member_first_remove равна true. Это означает, что бот настроен на удаление первого обработанного им сообщения. Наконец, оба фильтра member_chats и member_users_filter очищаются, чтобы гарантировать их запуск пустыми. Вы могли заметить, что мы использовали другую функцию для получения токена бота. Функция выглядит следующим образом.

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

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

Здесь мы определяем две функции, которые работают рука об руку, очищая и проверяя строку токена бота. Первая функция, getTrimmedToken, обращается к bot_token в качестве входного параметра. Затем она вызывает другую функцию, getTrimmedString, для удаления всех начальных и конечных пробелов из токена. После обрезки функция проверяет, является ли токен пустым. Если после обрезки токен оказывается пустым, выводится сообщение об ошибке, а функция возвращает NULL, что означает, что бот не может работать с этим токеном дальше. С другой стороны, если токен не пустой, он возвращается как действительный, обрезанный токен.

Вторая функция, getTrimmedString, выполняет фактическую работу по обрезке пробелов с обоих концов заданной строки. Она использует StringTrimLeft для удаления начальных пробелов и StringTrimRight для удаления конечных пробелов, затем возвращает обрезанную строку как токен, прошедший проверку на валидность.

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

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

После того, как мы объявим объект класса как obj_bot, мы сможем получить доступ к членам класса, используя оператор-точку. Нам потребуется проверять наличие обновлений и обрабатывать сообщения через заданный промежуток времени. Таким образом, вместо обработчика событий OnTick, который будет отнимать много времени на подсчет количества тиков и занимать ресурсы компьютера, мы используем функцию OnTimer, которая автоматически выполняет подсчет за нас. Чтобы использовать обработчик событий, нам нужно будет установить и инициализировать его на обработчик событий OnInit, как показано ниже.

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

Здесь мы инициализируем советника, настраивая событие таймера с помощью функции EventSetMillisecondTimer, срабатывающей каждые 3000 миллисекунд (3 секунды). Это гарантирует, что советник будет постоянно проверять наличие обновлений через регулярные промежутки времени. Затем мы немедленно вызываем обработчик событий OnTimer для получения первого обновления сразу после инициализации, гарантируя, что процесс начнется без задержки. Наконец, мы возвращаем INIT_SUCCEEDED, чтобы указать, что инициализация прошла успешно. Затем, поскольку мы установили таймер, после деинициализации программы нам необходимо удалить его, чтобы освободить ресурсы компьютера.

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

Здесь, когда советник удаляется или останавливается, первое, что мы делаем в обработчике событий OnDeinit, это останавливаем событие таймера. Это делается с помощью функции EventKillTimer, которая является логическим аналогом EventSetMillisecondTimer. Мы бы не хотели, чтобы таймер продолжал работать без советника. После остановки таймера мы вызываем функцию ChartRedraw. Вызов этой функции не является строго обязательным, но он может помочь в некоторых ситуациях, когда необходимо обновить график, чтобы внесенные изменения вступили в силу. Наконец, мы вызываем обработчик событий таймера, который позаботится о процессе подсчета.

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

Наконец, мы вызываем обработчик событий OnTimer. Внутри него мы вызываем две наши важные функции, необходимые для получения обновлений графика и обработки сообщений, используя созданный нами объект obj_bot. Для получения доступа к функциям класса используется "оператор-точка". Теперь мы можем сосредоточиться на функциях.


Декодирование и анализ данных из API Telegram

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

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

//--- ....

}

После вызова функции первое, что мы делаем, это убеждаемся, что у нас есть действительный токен, и если это не так, мы выводим сообщение об ошибке в журнал и возвращаем -1, указывая, что мы не можем продолжить работу без токена. Это показано ниже.

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

Если токен не пустой, мы можем приступить к подготовке запроса для отправки в API Telegram для получения обновлений из указанного чата.

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

Начнем с объявления переменной с именем out для хранения ответа, возвращаемого запросом API. Чтобы создать URL-адрес для запроса, мы объединяем базовый URL-адрес API (TELEGRAM_BASE_URL), токен бота (member_token) и метод, который мы хотим вызвать (/getUpdates). Этот метод извлекает обновления, отправленные боту пользователями, что позволяет нам увидеть, что произошло с момента последней проверки обновлений. Затем мы включаем в наш запрос один параметр. Параметр offset гарантирует, что мы получим только те обновления, которые произошли после последнего полученного обновления. Наконец, мы отправляем POST-запрос к API, при этом результат запроса сохраняется в переменной out и указывается в поле res в ответе. Мы использовали пользовательскую функцию postRequest. Ниже приведены фрагмент кода и его разбивка. Это похоже на то, что мы делали в предыдущих частях, но мы добавили комментарии, поясняющие используемые переменные.

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

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

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

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

Здесь мы отвечаем за отправку POST-запроса и обработку ответа. Начнем с того, что возьмем входные параметры и преобразуем их в форму, в которой их можно отправить, используя StringToCharArray для создания массива символов из строки параметров. Затем мы определяем два массива, которые будут собирать данные ответа и заголовки ответа. Наконец используем функцию WebRequest для отправки POST-запроса на URL, на который он должен перейти, с параметрами, которые он должен использовать, и настройкой времени ожидания.

Если наш запрос выполнен успешно (что мы определяем на основании получения кода ответа 200), мы убеждаемся, что в самом начале данных ответа нет ничего, что могло бы помешать обработке. В частности, мы проверяем наличие любых маркеров последовательности байтов (Byte Order Mark, BOM). Если мы его находим, мы относимся к нему как к подстроке, которой не должно быть, и принимаем меры, чтобы избежать его включения в данные, которые мы в конечном итоге используем. После этого мы преобразуем данные из массива символов в строку. Если мы пройдем все эти этапы без сбоев, мы вернем 0, что будет означать, что все прошло гладко.

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

Прежде чем продолжить, мы можем проверить отправляемые данные, проверив ответ и распечатав данные. Для этого используем следующую логику.

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

Здесь мы проверяем, равен ли результат запроса нулю, и если да, то выводим данные для отладки и проверки. После запуска мы получили следующие результаты.

Данные ответа

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

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

После создания объекта мы используем его для десериализации ответа, как показано ниже. 

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

Объявляем логическую переменную done для хранения результатов. Здесь мы сохраняем флаги успешности/неуспешности анализа ответа. Распечатаем его в целях отладки, как показано ниже.

      Print(done);

При распечатке получаем следующий ответ.

Ответ десериализации

Здесь мы видим, что мы правильно проанализировали ответ. Чтобы продолжить, нам нужно, чтобы ответ был равен true. В противном случае нам необходимо остановить процесс и вернуться, поскольку мы не получим доступ к остальным обновлениям сообщения. По этой причине нам нужно гарантировать, что в случае отрицательного ответа процесс будет остановлен.

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

Здесь мы проверяем, был ли анализ JSON успешным, оценивая логическую переменную done. Если анализ не удался (done равно false), мы выводим сообщение об ошибке "ERR: JSON PARSING", чтобы указать на проблему с интерпретацией ответа JSON. После этого мы возвращаем -1, чтобы сообщить о том, что в процессе анализа JSON произошла ошибка. Далее мы убеждаемся, что ответ обработан успешно, используя следующую логику.

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

Сначала мы проверяем значение поля "ok" в JSON, полученном из ответа. Это позволяет нам узнать, был ли запрос обработан успешно. Мы извлекаем это поле и сохраняем его в логическом значении с именем "ok". Если значение "ok" равно false, это указывает на то, что произошла ошибка или какая-то проблема с ответом, даже если сам запрос был успешным. В этом случае мы выводим "ERR: JSON NOT OK", чтобы сообщить о наличии какой-то проблемы, и возвращаем -1, чтобы указать на наличие какой-то проблемы при обработке ответа JSON. Если все прошло успешно, значит, у нас есть обновления сообщений, и мы можем приступить к их извлечению. Таким образом, нам необходимо будет объявить объект на основе класса messages следующим образом:

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

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

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

      }

На каждой итерации нам необходимо извлечь отдельный элемент обновления из ответа JSON, над которым мы работаем.

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

Затем мы можем перейти к получению обновлений отдельных чатов. Сначала давайте обновим сообщения.

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

Здесь мы берем сведения об отдельном сообщении из элемента обновления obj_item. Начнем с извлечения идентификатора обновления из объекта JSON и сохранения его в obj_msg.update_id. После этого мы извлекаем идентификатор сообщения и сохраняем его в obj_msg.message_id. Дата сообщения, которая представлена в не совсем понятном для человека формате, также включена в элемент, и мы сохраняем ее как объект datetime в obj_msg.message_date, который мы "преобразуем" в понятный для человека формат. Затем мы смотрим текст сообщения. В большинстве случаев мы можем просто взять текст и поместить его в obj_msg.message_text. Однако иногда его HTML-сущности закодированы. В других случаях он содержит специальные символы, которые также закодированы. В таких случаях мы используем функцию под названием decodeStringCharacters. Это функция, о которой мы уже говорили ранее. Затем мы извлекаем данные отправителя в аналогичном формате.

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

После извлечения данных отправителя мы аналогичным образом извлекаем данные чата.

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

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

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

Здесь мы обновляем member_update_id, чтобы убедиться, что следующий запрос обновлений от Telegram начинается в правильном месте. Присваивая значение obj_msg.update_id + 1, мы устанавливаем смещение таким образом, чтобы следующий запрос не включал текущее обновление и, по сути, получал только новые обновления, которые поступили после этого идентификатора. Это важно, поскольку мы не хотим обрабатывать одно и то же обновление более одного раза, а также хотим, чтобы бот был максимально отзывчивым. Далее мы проверяем наличие новых обновлений.

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

Здесь мы определяем, является ли текущее обновление первым обрабатываемым обновлением после инициализации, проверяя флаг member_first_remove. Если member_first_remove имеет значение true, это означает, что мы обрабатываем первое обновление — начальное обновление — после того, как все было инициализировано. Затем мы пропускаем обработку этого обновления и переходим к следующему. Наконец, мы сортируем и управляем сообщениями чата в зависимости от того, применяется ли фильтр по имени пользователя.

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

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

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

Сначала мы определяем, активен ли фильтр по имени пользователя, проверяя member_users_filter.Total(). Если фильтра нет (Total() == 0), мы обрабатываем все сообщения как обычно. Если есть фильтр (Total() > 0), мы проверяем, содержится ли в фильтре имя отправителя (obj_msg.from_username), используя member_users_filter.SearchLinear(). Если мы его находим, то продолжаем обработку сообщения.

Затем мы ищем чат в списке member_chats, перебирая его и сравнивая идентификатор чата (obj_msg.chat_id). Если чат не найден (index == -1), мы добавляем в список новый объект Class_Chat. Мы инициализируем объект с идентификатором чата, текущим временем, начальным состоянием 0 и текстом нового сообщения. Мы также помечаем новое сообщение как невыполненное (done = false).

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

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

Наконец, мы возвращаем результат post-запроса.

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

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


Обработка ответов

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

void Class_Bot_EA::ProcessMessages(void){

//---

}

Первое, что нам нужно сделать, это обработать отдельные чаты.

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

         //---

      }
   }

Здесь мы просматриваем коллекцию member_chats и извлекаем соответствующий объект чата для каждого чата, используя индексную переменную i из member_chats. Для каждого чата мы проверяем связанное с текущим чатом сообщение, чтобы узнать, было ли оно уже обработано, оценивая флаг done в структуре member_new_one. Если сообщение еще не обработано, мы устанавливаем этот флаг в значение true, отмечая сообщение как обработанное, чтобы предотвратить повторную обработку. Наконец, мы извлекаем текст сообщения из структуры member_new_one. Мы будем использовать текст, чтобы определить, какой ответ или действие (если таковые имеются) следует предпринять на основе содержания сообщения. Сначала давайте определим случай, когда пользователь отправляет приветственный текст "Hello" (привет) из Telegram.

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

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

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

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

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

Здесь мы определяем функцию sendMessageToTelegram, которая отправляет сообщение в указанный чат Telegram с помощью Telegram Bot API. Во-первых, мы создаем URL-адрес для запроса API, объединяя базовый URL-адрес для Telegram, токен бота (полученный с помощью getTrimmedToken) и конкретный метод отправки сообщений (sendMessage). Этот URL необходим для направления запроса API на правильную конечную точку. Далее мы формируем параметры запроса. Они включают в себя:

  • chat_id - идентификатор чата, в который будет отправлено сообщение.
  • text - содержимое сообщения, закодированное в URL-адресе для обеспечения его корректной передачи.

При предоставлении пользовательской разметки клавиатуры для ответа (reply_markup), она добавляется к параметрам. Это позволяет использовать интерактивные кнопки в сообщении. Дополнительные параметры включают в себя:

  • parse_mode=HTML - сообщение следует интерпретировать как HTML, допускающий форматированный текст.
  • disable_web_page_preview=true - в сообщении отключены все предварительные просмотры веб-страниц.

Наконец, функция отправляет запрос с помощью функции postRequest, которая обрабатывает фактическое взаимодействие с API Telegram. Код ответа на этот запрос возвращается, чтобы указать, было ли сообщение успешно отправлено или произошла ошибка.

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

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

Здесь мы сначала используем функцию sendMessageToTelegram для отправки ответного сообщения в соответствующий чат Telegram. Мы вызываем функцию с chat.member_id, которая нацеливает нужный чат на нужное сообщение с нужным содержанием. Параметр reply_markup имеет значение NULL, что означает, что отправленное сообщение не содержит клавиатурных или интерактивных элементов. После отправки сообщения мы используем оператор continue. Он пропускает весь оставшийся код в текущем обрабатываемом цикле и переходит к следующей итерации этого цикла. Логика здесь проста: мы обрабатываем и пересылаем ответ на текущее сообщение. После этого мы фактически движемся дальше, не обрабатывая никакой дополнительный код для текущего чата или сообщения в текущей итерации. После компиляции мы получаем следующее.

HELLO WORLD

Сообщение было получено и обработано в течение нескольких секунд. Теперь давайте перейдем к добавлению пользовательской клавиатуры ответа в нашу функцию.

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

Здесь мы определяем функцию customReplyKeyboardMarkup, которая создает пользовательскую клавиатуру ответа для Telegram. Эта функция принимает три параметра: keyboard, resize и one_time. Параметр keyboard определяет раскладку пользовательской клавиатуры в формате JSON. Параметр resize определяет, будет ли изменен размер клавиатуры в соответствии с экраном устройства пользователя. Если параметру resize присвоено значение true, размер клавиатуры будет изменен в соответствии с экраном устройства пользователя. Параметр one_time указывает, станет ли клавиатура "одноразовой" и исчезнет после взаимодействия с пользователем.

Внутри функции создается строка JSON, представляющая собой разметку клавиатуры для пользовательского ответа. Чтобы гарантировать, что параметр клавиатуры правильно отформатирован для запроса API, мы используем функцию UrlEncode для его кодирования. Далее мы используем функцию convertBoolToString для изменения логических значений resize и one_time (которые определяют, следует ли считать эти значения истинными (true) или ложными (false)) в их строковые представления. Наконец, сконструированная строка возвращается из функции и может использоваться в API-запросах к Telegram. Используемая нами пользовательская функция выглядит так.

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

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

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

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

Здесь функции hideCustomReplyKeyboard и forceReplyCustomKeyboard генерируют строки JSON, которые определяют конкретные действия, которые должна выполнить функция пользовательской клавиатуры Telegram.

Для функции hideCustomReplyKeyboard генерируемая ею строка JSON имеет вид: "{\"hide_keyboard\": true}". Эта конфигурация JSON сообщает Telegram, что необходимо скрыть клавиатуру ответа после того, как пользователь отправит сообщение. По сути, эта функция позволяет скрыть клавиатуру после отправки сообщения.

Для функции forceReplyCustomKeyboard генерируемая ею строка JSON имеет вид: "{\"force_reply\": true}". Эта строка сообщает Telegram, что необходимо потребовать ответ от пользователя, прежде чем он сможет взаимодействовать с любым другим элементом пользовательского интерфейса в чате. Эта строка позволяет пользователю взаимодействовать исключительно с только что отправленным сообщением.

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

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

При отправке сообщения в Telegram получаем следующий результат.

Пользовательская клавиатура ответа HELLO

Всё сработало как надо. Теперь мы можем отправить сообщение, просто нажав на кнопку. Однако она довольно большая. Теперь мы можем добавить несколько кнопок. Сначала добавим кнопки в формате строк.

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

Здесь мы определяем пользовательскую раскладку клавиатуры для ответа с помощью переменной buttons_rows. Строка "[[\"Hello 1\"],[\"Hello 2\"],[\"Hello 3\"]]" представляет собой клавиатуру с тремя кнопками - Hello 1, Hello 2 и Hello 3. Формат этой строки — JSON, который используется Telegram для визуализации клавиатуры. После запуска получаем следующие результаты.

Расположение строк

Для визуализации раскладки клавиатуры в формате столбцов реализуем следующую логику.

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

После запуска программы мы видим следующее.

Расположение столбцов

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

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

Здесь мы проверяем, входит ли входящее сообщение в число предопределенных команд /start, /help, Start и Help. Если это одна из этих команд, мы готовим приветственное сообщение, которое представляет бота пользователю и предоставляет список команд, которые можно отправить боту для взаимодействия с ним. Мы опускаем некоторые части этого списка и классифицируем другие его части, чтобы дать пользователю общее представление о том, что он может делать с помощью бота. Наконец, мы отправляем это сообщение вместе с пользовательской клавиатурой обратно пользователю. Клавиатура лучше подходит для взаимодействия с ботом, чем командная строка. Мы также определили пользовательскую клавиатуру следующим образом.

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

Используем макрос #define для определения двух элементов, которые будут использоваться в пользовательском интерфейсе Telegram-бота. Сначала мы определяем EMOJI_CANCEL как эмодзи-крестик, используя его представление в Unicode "\x274C". Мы будем использовать этот эмодзи в раскладке клавиатуры для обозначения опции Cancel. Представление эмодзи в формате Unicode показано ниже:

Крестик (Unicode)

Далее мы определяем KEYB_MAIN, который представляет собой основную раскладку клавиатуры для бота. Клавиатура структурирована как JSON-массив с рядами кнопок. Структура включает в себя параметры, содержащиеся в списке команд, а именно Name (имя), Account Info (информация о счете), Quotes (котировки), а также строку с More (дополнительно), Screenshot (скриншот) и кнопку Cancel (отмена), представленную EMOJI_CANCEL. Клавиатура будет отображаться пользователю, что позволит ему взаимодействовать с ботом, нажимая эти кнопки, а не вводить команды вручную. При запуске программы мы видим следующее.

TELEGRAM JSON UI 1

Теперь у нас есть пользовательская клавиатура в формате JSON и список команд, которые мы можем отправить боту. Теперь осталось только подготовить соответствующие ответы в соответствии с полученными командами от Telegram. Начнем с ответа на команду /name. 

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

Здесь мы проверяем, как выглядит полученное от пользователя сообщение - /name или Name. Если проверка дает положительный результат, мы приступаем к формированию ответа пользователю, содержащего имя файла советника, который в данный момент используется. Инициализируем строковую переменную с именем message, которая начинается с текста "The file name of the EA that I control is:\n" (имя файла управляемого мной советника:\n). За этим первоначальным объявлением следуют эмодзи в виде книги (код "\xF50B") и имя файла советника.

Для получения имени файла мы используем встроенный MQL5-макрос __FILE__. Макрос возвращает имя файла и путь. Затем мы создаем сообщение для отправки пользователю. Сообщение состоит из имени файла советника и пути к нему. Мы отправляем сформированное сообщение с помощью функции sendMessageToTelegram. Эта функция принимает три параметра: первый — идентификатор чата пользователя, которому мы хотим отправить сообщение; второй — само сообщение; а третий параметр, установленный на NULL, указывает, что мы не отправляем никаких пользовательских команд клавиатуры или кнопок вместе с нашим сообщением. Это важно, поскольку мы не хотим создавать дополнительную клавиатуру. При нажатии команды /name или ее кнопки мы получаем соответствующий ответ, как показано ниже.

Команда Name

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

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

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

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

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

Здесь мы обрабатываем сценарий, в котором пользователь отправляет сообщение /buy или Buy. Наш первый шаг — создать объект CTrade с именем obj_trade, который мы будем использовать для выполнения торговой операции. Затем мы получаем текущие цены ask и bid, вызвав функцию SymbolInfoDouble. Чтобы открыть позицию на покупку, мы используем функцию Buy объекта CTrade. Объем сделки устанавливаем на 0,01 лота. Для наших SL (стоп-лосс) и TP (тейк-профит) мы устанавливаем цену спроса минус 300 пунктов и цену спроса плюс 300 пунктов соответственно.

После открытия позиции мы определяем номер тикета нового ордера с помощью функции ResultOrder. Располагая тикетом, мы используем функцию PositionGetInteger, чтобы выбрать позицию по тикету. Затем мы извлекаем важные статистические данные, такие как цена входа, объем, стоп-лосс и тейк-профит. Используя эти числа, мы создаем сообщение, которое информирует пользователя об открытии позиции на покупку. Для обработки закрытия позиции и команды contact мы используем следующую похожую логику.

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

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

Очевидно, что мы можем реагировать на команды, отправленные из Telegram. До этого момента мы отправляли простые текстовые сообщения. Давайте станем немного изобретательнее и отформатируем наши текстовые сообщения, используя HTML или Markdown. Выбор за вами!

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

Здесь мы отвечаем пользователю, когда он отправляет сообщение /join или Join. Начнем с создания сообщения, приглашающего пользователя присоединиться к сообществу MQL5. Сообщение содержит гиперссылку, по которой пользователи могут кликнуть, чтобы присоединиться к сообществу, а также несколько примеров того, как можно форматировать текст с помощью HTML-тегов в Telegram:

  • Зачеркнутый текст: Мы используем тег <s>, чтобы зачеркнуть словосочетание Civil Engineering (гражданское строительство) и подчеркнуть, что мы специализируемся на Forex AlgoTrading (алгоритмический трейдинг на Форекс).
  • Предварительно отформатированный текст: Тег <pre> используется для отображения примера кода MQL5 в предварительно отформатированном текстовом блоке.
  • Курсив и подчеркнутый текст: Теги <u> и <i> объединены, чтобы подчеркнуть и выделить курсивом напоминание пользователям о необходимости следовать правилам сообщества, а для выразительности добавлен Unicode-эмодзи.
  • Полужирный текст: Тег <b> используется для выделения жирным шрифтом заключительной фразы "Happy Trading!" (Удачной торговли!)

Наконец, мы отправляем это отформатированное сообщение пользователю через Telegram с помощью функции sendMessageToTelegram, гарантируя, что пользователь получит хорошо оформленное и интересное приглашение присоединиться к MQL5-сообществу. После запуска получаем следующее.

HTML

Теперь, когда мы исчерпали списки команд, давайте продолжим изменять клавиатуру ответа и сгенерируем новую после нажатия кнопки more. Реализована следующая логика.

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

Когда мы получаем от пользователя сообщение more или More (дополнительно), мы воспринимаем это как сигнал к обновлению контекста текущей переписки. В мире чат-ботов сообщение указывает на то, что пользователь не удовлетворен текущим количеством вариантов или пока не нашел то, что искал. Поэтому наш ответ пользователю должен предоставлять различные варианты выбора. На практике это означает, что мы отправляем пользователю новое сообщение с новой раскладкой клавиатуры. KEYB_MORE выглядит так:

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

При запуске программы мы получаем следующий результат.

MORE GIF

Всё работает как надо. Аналогичным образом мы можем обрабатывать и другие команды.

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

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

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

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

Здесь мы имеем дело с разнообразными пользовательскими сообщениями для управления интерфейсом чата. Когда пользователь отправляет эмодзи "вверх", мы сбрасываем состояние чата до 0, предлагая пользователю снова выбрать пункт меню, сопровождаемый основной раскладкой клавиатуры. Когда пользователь отправляет next или Next (далее), мы обновляем состояние чата до 2 и предлагаем пользователю еще раз выбрать пункт меню, на этот раз из раскладки клавиатуры, которая предоставляет дополнительные параметры.

Для эмодзи в виде пистолета мы настраиваем состояние чата на основе его текущего значения: если состояние равно 2, мы переключаем его на 1 и отображаем клавиатуру с дополнительными опциями; если состояние отличается, мы переключаем его на 0 и отображаем клавиатуру главного меню. Для эмодзи отмены мы сбрасываем состояние чата на 0 и отправляем пользователю сообщение, в котором ему предлагается выбрать /start или /help для начала. Мы отправляем это сообщение со скрытой пользовательской клавиатурой ответа, чтобы очистить все активные пользовательские клавиатуры для пользователя. Ниже приведены дополнительные пользовательские макеты:

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

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

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

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

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

Здесь мы обрабатываем запросы пользователей на скриншоты графиков, управляя различными состояниями потока чата. Когда пользователь отправляет команду /screenshot или Screenshot, мы устанавливаем состояние чата на 10 и предлагаем пользователю ввести символ, отображая клавиатуру с доступными символами. Здесь важно отметить, что состояние чата может быть любым числом, даже 1000. Он просто действует как идентификатор или квантификатор для хранения состояния, которое мы помним во время обработки ответа. Если пользователь предоставляет символ, мы проверяем его правильность. Если он действителен, мы запрашиваем у пользователя период (допустимое "время" для графика), отображая клавиатуру с доступными вариантами периодов. Если пользователь предоставляет недопустимый символ, мы уведомляем его об этом и предлагаем предоставить нам допустимый символ.

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

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

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

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

SCREENSHOT GIF

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


Тестирование реализации

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

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

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

   //--- ...
   
}

После запуска мы получим следующий результат.

Предварительный просмотр веб-страницы включен

Теперь мы можем получать предварительные просмотры веб-страниц, как показано на рисунке. Всё работает как надо. Затем мы можем перейти к изменению форматирующего объекта или режима анализа с Hypertext Markup Language (HTML) на Markdown следующим образом:

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

   //--- ...

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

   //--- ...

}

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

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

Вот что мы изменили:

  • Ссылка: В Markdown ссылки создаются в виде [text](URL) вместо <a href="URL">text</a>.
  • Зачеркивание: Для зачеркивания используем ~text~ вместо <s>text</s>.
  • Предварительно отформатированный текст: Для работы с предварительно отформатированным текстом используем тройные обратные кавычки (```) вместо <pre>text</pre>.
  • Курсив и подчеркивание: Markdown в чистом виде не поддерживает подчеркивание. Ближайший вариант — курсив с помощью *text* или _text_. Эффект подчеркивания из HTML напрямую не поддерживается в Markdown, поэтому при необходимости он включается вместе с заполнителем.
  • Полужирное начертание: Используем двойные звездочки **text** вместо <b>text</b>.

При запуске программы мы видим следующее.

Вывод в Markdown

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

Успешное выполнение и проверка реализации, продемонстрированные в прилагаемом видео, подтверждают, что программа функционирует так, как задумано.


Заключение

Подводя итог, можно сказать, что созданный нами советник объединяет язык MetaQuotes Language 5 (MQL5) и торговую платформу MetaTrader 5 с мессенджером Telegram, позволяя пользователям буквально общаться со своими торговыми роботами. А почему бы и нет? Telegram превратился в мощный и удобный способ управления автоматизированными торговыми системами. Используя его, можно отправлять команды и получать ответы от системы в режиме реального времени.

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (5)
Extratimber Alpha
Extratimber Alpha | 2 окт. 2024 в 03:30

Очень впечатляющая работа!!!

Это позволяет реализовать следующие функции:

оповещение Tradingview в telegram

телеграмма на MQL5

СПАСИБО!

Allan Munene Mutiiria
Allan Munene Mutiiria | 2 окт. 2024 в 16:29
Extratimber Alpha #:

Очень впечатляющая работа!!!

Это позволяет реализовать следующие функции:

Оповещение Tradingview в telegram

телеграмма на MQL5

СПАСИБО!

@Extratimber Alpha большое спасибо за добрый отзыв. Мы рады, что он оказался для вас полезным.
Oluwatosin Michael Akinyemi
Oluwatosin Michael Akinyemi | 23 мар. 2025 в 08:35
obj_msg.update_id=obj_item["update_id"].ToInt(); //--- Получите идентификатор обновления
         obj_msg.message_id=obj_item["message"]["message_id"].ToInt(); //--- Получите идентификатор сообщения
         obj_msg.message_date=(datetime)obj_item["message"]["date"].ToInt(); //--- Получите дату сообщения

Здравствуйте, Аллан, спасибо за эту замечательную статью.

К сожалению, код кажется сломанным, начиная со строки 1384 при извлечении деталей сообщения из объекта JSON. Первый код в строке 1383

obj_msg.update_id=obj_item["update_id"].ToInt(); //--- Получите идентификатор обновления

работает хорошо при печати в журнал. update id возвращает правильный id. но message_id, message_date и все остальные in instances возвращают пустое значение. Из-за этих проблем в коде ничего не работает, как и следовало ожидать.

Не могли бы вы помочь решить эту проблему?

Еще раз спасибо за то, что уделили время этой статье.

Oluwatosin Michael Akinyemi
Oluwatosin Michael Akinyemi | 25 мар. 2025 в 11:38
Oluwatosin Michael Akinyemi #:

Здравствуйте, Аллан, спасибо за эту замечательную статью.

К сожалению, код кажется сломанным, начиная со строки 1384 при извлечении деталей сообщения из объекта JSON. Первый код в строке 1383

работает хорошо при печати в журнал. update id возвращает правильный id. но message_id, message_date и все остальные in instances возвращают пустое значение. Из-за этих проблем в коде ничего не работает, как и следовало ожидать.

Не могли бы вы помочь решить эту проблему?

Еще раз спасибо за то, что уделили время этой статье.

Здравствуйте, Аллан, я наконец-то нашел проблему с моей стороны. Спасибо за эту отличную статью!

Allan Munene Mutiiria
Allan Munene Mutiiria | 25 мар. 2025 в 20:24
Oluwatosin Michael Akinyemi #:

Здравствуйте, Аллан, я наконец-то обнаружил, что проблема была с моей стороны. Спасибо за эту прекрасную статью!

@Oluwatosin Michael Akinyemi спасибо, что обратили внимание. Добро пожаловать.
Нейросети в трейдинге: Выявление аномалий в частотной области (CATCH) Нейросети в трейдинге: Выявление аномалий в частотной области (CATCH)
Фреймворк CATCH сочетает преобразование Фурье и частотный патчинг для точного выявления рыночных аномалий, недоступных традиционным методам. В данной работе мы рассмотрим, как этот подход раскрывает скрытые закономерности в финансовых данных.
Переосмысливаем классические стратегии в MQL5 (Часть II): FTSE100 и Гилты Великобритании Переосмысливаем классические стратегии в MQL5 (Часть II): FTSE100 и Гилты Великобритании
В данной серии статей мы исследуем популярные торговые стратегии и попытаемся улучшить их с помощью ИИ. В сегодняшней статье мы вновь рассмотрим классическую торговую стратегию, построенную на взаимосвязи между фондовым рынком и рынком облигаций.
Пример стохастической оптимизации и оптимального управления Пример стохастической оптимизации и оптимального управления
Настоящий советник, получивший название SMOC (что, вероятно, означает оптимальное управление стохастической моделью (Stochastic Model Optimal Control), является простым примером передовой алгоритмической торговой системы для MetaTrader 5. Он использует комбинацию технических индикаторов, прогностического контроля моделей и динамического управления рисками для принятия торговых решений. Советник включает в себя адаптивные параметры, определение размера позиции на основе волатильности и анализ трендов для оптимизации его работы в изменяющихся рыночных условиях.
Треугольные и пилообразные волны: инструменты для трейдера Треугольные и пилообразные волны: инструменты для трейдера
Одним из методов технического анализа является волновой анализ. В этой статье мы рассмотрим волны несколько необычного вида — треугольные и пилообразные. На основе этих волн можно построить несколько технических индикаторов, с помощью которых можно анализировать движение цены на рынке.