Создание бота для Telegram на языке MQL5

Andriy Voitenko | 11 мая, 2016

Введение

12 апреля 2016 года на конференции F8 в Сан-Франциско компания Facebook анонсировала внедрение API для ботов в свой мессенджер. В этот же день вышло большое обновление для платформы Telegram Bot Platform. Версия 2.0 порадовала новыми функциями. Похоже, что к хорошо забытому старому, а именно ботам, которые были популярны еще в эпоху ICQ, возвращается интерес. На новом витке спирали развития ботам дают более продуманную функциональность, открытый интерфейс для программирования, мультимедийную поддержку. В общем, теперь у них есть всё, чтобы они стали незаменимы, когда вы хотите что-то найти, посмотреть или купить.

Эта статья задумана как пошаговое руководство для создания ботов для Telegram на языке MQL5. Что же такое бот? Бот (сокращение от «робот») – это специальный аккаунт в Telegram, для которого предусмотрена возможность программно обмениваться сообщениями. Бот работает на вашей (клиентской) стороне, и взаимодействует с сервером Telegram посредством специального набора команд, входящих в Bot API. Прежде чем мы приступим к созданию непосредственно самого бота, пожалуйста, установите Telegram и авторизуйтесь в этом сервисе. Регистрация привязана к номеру телефона, но помимо этого, у вас может быть ник @username, по которому вас можно будет найти через поиск. Теперь пришло время познакомиться с папкой всех ботов.


Регистрация нового бота

За регистрацию и настройку ботов отвечает специальный бот @BotFather. Найдем его через поиск. После добавления в список контактов начнем общение с ним командой /start. В ответ он пришлет список всех доступных команд, как показано на рисунке 1.


Список команд @BotFather.

Рис.1. Список команд @BotFather.

Командой /newbot начнем регистрацию нового бота. Необходимо придумать два названия. Первое – имя (name) бота, которое можно задать на вашем родном языке. Второе - имя пользователя бота (username) на латинице, заканчивающееся на приставку “bot”. В результате мы получим токен – ключ доступа для работы с ботом через API. Пример регистрации приведен на рисунке 2.


Регистрация нового бота

Рис.2.  Регистрация нового бота.

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

Итак, бот зарегистрирован. Поговорим о том, в каких режимах его можно использовать.


Режимы работы ботов

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


бот и приватные чаты

Рис.3.  Бот и приватные чаты.


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

Второй режим — это групповые чаты. Здесь сообщение, отосланное любым из участников группы, видят все (рисунок 4).

бот и групповой чат

Рис.4. Бот в групповом чате.

Что касается ботов, то им через команду /setjoingroups можно разрешить вступать в группы. Если бот добавлен в группу, то ему командой /setprivacy можно задать возможность либо получать все сообщения, либо только те, которые начинаются с признака команды символа “/”. Честно говоря, мне удалось придумать только одно назначение для бота в этом режиме – это сбор статистики сообщений для последующего анализа.

Третий режим – это работа на канале. Каналы в Telegram – это аккаунты для трансляции сообщений для широкой аудитории, которые поддерживают неограниченное количество подписчиков. Важной особенностью каналов является невозможность пользователей оставлять комментарии и лайки в ленте сообщений (связь только односторонняя). Создавать сообщения на ленте могут только администраторы канала (рисунок 5).


Бот в качестве администратора канала

Рис.5. Бот в качестве администратора канала.

В список администраторов можно также добавлять и ботов. Таким образом это делает канал идеальным инструментом для раздачи торговых сигналов. Чуть позже мы напишем простого бота, публикующего сигналы от стандартного индикатора MACD. Создать новый публичный канал можно через меню “New Channel” мессенджера. Не забудьте внести вашего бота в список администраторов канала. Это делается через окно свойств самого канала. На этом подготовительные работы завершены, и мы можем приступить к программированию.


Обработка потока сообщений

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

Общение с сервером происходит посредством POST запросов с использованием функции WebRequst. Для каждой команды предусмотрен свой URL:

https://api.telegram.org/bot< TOKEN >/ METHOD_NAME

где, TOKEN – токен зарегистрированного бота; METHOD_NAME — список поддерживаемых методов.

Ответы от сервера приходят в JSON формате, поэтому понадобился хороший JSON парсер. Я применил нативный парсер JSON Serialization and Deserialization. Хочу поблагодарить Алексея (sergeev) за проделанную им работу. Также применена панель для отображения некоторых параметров. Класс CComment, взятый из Codebase подошел для этой задачи. Для универсальности наименования публичных методов класса были заимствованы из документации по Bot API. Список методов, которые удалось реализовать в классе, приведен ниже:

Углубимся в программирование, чтобы понять, как пользоваться этими функциями.


GetMe

Так как токен отсылается в каждом запросе, прежде всего реализована функция GetMe, которая проверяет его на достоверность. Эту проверку желательно делать при старте эксперта и в случае неудачи информировать пользователя об этом.

int GetMe()
 Возвращаемое значение  код ошибки

В случае успеха GetMe возвращает 0, и через метод Name() можно узнать имя (username) бота. Это имя в работе не участвует. Однако оно будет выведено на панель в качестве информации. Такой адрес как telegram.me/<botname> позволяет воспользоваться Web-версией мессенджера и будет служить ссылкой для рекламы вашего бота. Эксперт с проверкой токена в OnInit может иметь следующий вид:

//+------------------------------------------------------------------+
//|                                               Telegram_GetMe.mq5 |
//|                        Copyright 2014, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

#include <Telegram.mqh>

input string InpToken="177791741:AAH0yB3YV7ywm80af_-AGqb7hzTR_Ud9DhQ";//Token

CCustomBot bot;
int getme_result;
//+------------------------------------------------------------------+
//|   OnInit                                                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- set token
   bot.Token(InpToken);
//--- check token
   getme_result=bot.GetMe();
//--- run timer
   EventSetTimer(3);
   OnTimer();
//--- done
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//|   OnDeinit                                                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Comment("");
  }
//+------------------------------------------------------------------+
//|   OnTimer                                                        |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- show error message end exit
   if(getme_result!=0)
     {
      Comment("Error: ",GetErrorDescription(getme_result));
      return;
     }
//--- show bot name
   Comment("Bot name: ",bot.Name());

//---{ insert your code here }
  }
//+------------------------------------------------------------------+


GetUpdates

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

int GetUpdate()
 Возвращаемое значение  код ошибки

Рассмотрим подробнее внутреннее содержание этой функции. При её вызове происходит чтение и парсинг всех непрочитанных сообщений от пользователей. Пример одного из таких сообщений приведен ниже:

{ 
   "ok":true,
   "result":[ 
      { 
         "update_id":349778698,
         "message":{ 
            "message_id":2,
            "from":{ 
               "id":198289825,
               "first_name":"Andriy",
               "last_name":"Voitenko",
               "username":"avaticks"
            },
            "chat":{ 
               "id":198289825,
               "first_name":"Andriy",
               "last_name":"Voitenko",
               "username":"avaticks",
               "type":"private"
            },
            "date":1459775817,
            "text":"\/start"
         }
      }
   ]
}

Пользователь с ником avaticks прислал боту команду /start. Задача состоит в том, чтобы сохранять такие сообщения и в дальнейшем отвечать на них. При этом уникальным идентификатором является номер чата chat[id]. Один и тот же пользователь, общающийся с ботом через разные устройства, имеет разные идентификаторы чатов. Этот параметр подходит в качестве уникального ключа для построения списка чатов. В процессе работы бот будет накапливать массив чатов и обновлять в каждом из них последнее присланное сообщение. Если мы ответили на него, то такое сообщение считается обработанным и для него устанавливается флаг done. Тип чата также известен. Он может быть либо приватным (private), либо групповым (group).

Для написания своего бота нам необходимо все лишь наследоваться от CCustomBot и переопределить в своем классе виртуальную функцию ProcessMessage, которая предусмотрена для того, чтобы реализовать в ней свою логику работы. Полноценный бот, согласно документации Telegram, должен уметь отвечать на две команды "/start" и "/help". Давайте же напишем первого бота, который будет отвечать на эти команды.

//+------------------------------------------------------------------+
//|                                          Telegram_GetUpdates.mq5 |
//|                        Copyright 2014, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

#include <Telegram.mqh>
//+------------------------------------------------------------------+
//|   CMyBot                                                         |
//+------------------------------------------------------------------+
class CMyBot: public CCustomBot
  {
public:
   void ProcessMessages(void)
     {
      for(int i=0; i<m_chats.Total(); i++)
        {
         CCustomChat *chat=m_chats.GetNodeAtIndex(i);
         //--- if the message is not processed
         if(!chat.m_new_one.done)
           {
            chat.m_new_one.done=true;
            string text=chat.m_new_one.message_text;

            //--- start
            if(text=="/start")
               SendMessage(chat.m_id,"Hello, world! I am bot. \xF680");

            //--- help
            if(text=="/help")
               SendMessage(chat.m_id,"My commands list: \n/start-start chatting with me \n/help-get help");
           }
        }
     }
  };

//---
input string InpToken="177791741:AAH0yB3YV7ywm80af_-AGqb7hzTR_Ud9DhQ";//Token
//---
CMyBot bot;
int getme_result;
//+------------------------------------------------------------------+
//|   OnInit                                                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- set token
   bot.Token(InpToken);
//--- check token
   getme_result=bot.GetMe();
//--- run timer
   EventSetTimer(3);
   OnTimer();
//--- done
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//|   OnDeinit                                                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Comment("");
  }
//+------------------------------------------------------------------+
//|   OnTimer                                                        |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- show error message end exit
   if(getme_result!=0)
     {
      Comment("Error: ",GetErrorDescription(getme_result));
      return;
     }
//--- show bot name
   Comment("Bot name: ",bot.Name());
//--- reading messages
   bot.GetUpdates();
//--- processing messages
   bot.ProcessMessages();
  }
//+------------------------------------------------------------------+
Результат его работы представлен на рисунке 6.


Бот с минимальным набором команд

Рис.6. Бот с минимальным набором команд.


Работа с клавиатурой

Для интерактивного общения с пользователем для ботов придумали “клавиатуры”. При отправке сообщения для каждого чата можно отображать “клавиатуру” с заранее заданным набором клавиш. Нажимая на клавишу пользователь отправляет сообщение с текстом, указанным на ней. Таким образом заметно упрощается взаимодействие бота с пользователем.

В классе имеются три функции для работы с клавиатурой. Первая из них создает объект клавиатуры.

string ReplyKeyboardMarkup(const string keyboard,
                           const bool resize,
                           const bool one_time)
 keyboard
 строка, задающая расположение клавиш
 resize  разрешение на изменение размеров клавиатуры
 one_time  показывать клавиатуру только один раз. После нажатия на клавишу клавиатура исчезает.
 Возвращаемое значение строка (JSON объект), которую необходимо передать в качестве параметра reply_markup при отправке сообщения с помощью SendMessage

Вторая функция прячет клавиатуру.

string ReplyKeyboardHide()
 Возвращаемое значение строка (JSON объект), которую необходимо передать в качестве параметра reply_markup при отправке сообщения с помощью SendMessage

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

string ForceReply()
 Возвращаемое значение строка (JSON объект), которую необходимо передать в качестве параметра reply_markup при отправке сообщения с помощью SendMessage

Теперь перейдем к рассмотрению того, как пользоваться этими функциями.


SendMessage

Клавиатура не может быть отображена или спрятана сама по себе. Действие присылается вместе с сообщением. Функция SendMessage для отправки сообщений в чат имеет следующий вид:

int SendMessage(const long chat_id,
                const string text,
                const string reply_markup=NULL)
 chat_id
 номер чата
 text  текст сообщения
 reply markup  клавиатура (JSON объект)
 Возвращаемое значение  код ошибки

В данном случае клавиатура является опциональной. Мы можем отправлять простые текстовые сообщения из наших MQL-программ. На мой взгляд, эта функция более интересна, чем нативная SendNotification. Во-первых, мы можем отправлять сообщения чаще (примерно раз в секунду). Во вторых, поддерживается формат HTML. Также немаловажным бонусом является возможность отправлять смайлики.

Тelegram поддерживает большое число смайликов (Emoji) таблицу которых можно посмотреть здесь. Как вы можете заметить, подавляющее большинство кодов смайликов находится в диапазоне (1F300 – 1F700). Их разрядность выходит за пределы двухбайтовой кодировки строк, принятой в MQL5. Если же убрать старшие разряды так, чтобы осталось двухбайтовое число, то полученный диапазон (F300 – F700) попадает в область (E000— F8FF), которая в таблице Unicode зарезервирована для частного использования. Таким образом, нам ничто не мешает для отправки смайлика использовать два младших байта. Строка сообщения с классическим улыбающимся смайликом с кодом U+1F642 может иметь следующий вид:

string text="Have a nice day.\xF642";//текст сообщения со смайликом U+1F642

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

//+------------------------------------------------------------------+
//|                                         Telegram_SendMessage.mq5 |
//|                        Copyright 2014, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

#include <Telegram.mqh>
//+------------------------------------------------------------------+
//|   CMyBot                                                         |
//+------------------------------------------------------------------+
class CMyBot: public CCustomBot
  {
private:
   string            m_button[3];
public:
   //+------------------------------------------------------------------+
   void CMyBot::CMyBot(void)
     {
      m_button[0]="Button #1";
      m_button[1]="Button #2";
      m_button[2]="Button #3";
     }

   //+------------------------------------------------------------------+
   string GetKeyboard()
     {
      return("[[\""+m_button[0]+"\"],[\""+m_button[1]+"\"],[\""+m_button[2]+"\"]]");
     }

   //+------------------------------------------------------------------+
   void ProcessMessages(void)
     {
      for(int i=0;i<m_chats.Total();i++)
        {
         CCustomChat *chat=m_chats.GetNodeAtIndex(i);
         if(!chat.m_new_one.done)
           {
            chat.m_new_one.done=true;
            string text=chat.m_new_one.message_text;

            //--- start or help commands
            if(text=="/start" || text=="/help")
               bot.SendMessage(chat.m_id,"Click on the buttons",bot.ReplyKeyboardMarkup(GetKeyboard(),false,false));

            //--- on click event
            int total=ArraySize(m_button);
            for(int k=0;k<total;k++)
              {
               if(text==m_button[k])
                  bot.SendMessage(chat.m_id,m_button[k],bot.ReplyKeyboardMarkup(GetKeyboard(),false,false));
              }
           }
        }
     }
  };

input string InpToken="177791741:AAH0yB3YV7ywm80af_-AGqb7hzTR_Ud9DhQ";//Token

CMyBot bot;
int getme_result;
//+------------------------------------------------------------------+
//|   OnInit                                                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- set token
   bot.Token(InpToken);
//--- check token
   getme_result=bot.GetMe();
//--- run timer
   EventSetTimer(1);
   OnTimer();
//--- done
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//|   OnDeinit                                                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Comment("");
  }
//+------------------------------------------------------------------+
//|   OnTimer                                                        |
//+------------------------------------------------------------------+
void OnTimer()
  {
//--- show error message end exit
   if(getme_result!=0)
     {
      Comment("Error: ",GetErrorDescription(getme_result));
      return;
     }
//--- show bot name
   Comment("Bot name: ",bot.Name());
//--- reading messages
   bot.GetUpdates();
//--- processing messages
   bot.ProcessMessages();
  }
//+------------------------------------------------------------------+

В результате мы получим сообщение с клавиатурой, показанное на рисунке 7.

сообщение с клавиатурой

Рис.7. Сообщение с клавиатурой.

Теперь попробуем реализовать аналог элементов управления RadioButton и CheckBox. Например, нам нужно выбрать одну из трех опций, а также включить или выключить определенную опцию. Изменения коснутся только нашего класса, поэтому остальной код эксперта остается неизменным из предыдущего примера.
//+------------------------------------------------------------------+
//|                                         Telegram_SendMessage.mq5 |
//|                        Copyright 2014, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

#include <Telegram.mqh>

#define MUTE_TEXT       "Mute"
#define UNMUTE_TEXT     "Unmute"

#define LOCK_TEXT       "Lock"
#define UNLOCK_TEXT     "Unlock"

#define RADIO_SELECT    "\xF518"
#define RADIO_EMPTY     "\x26AA"

#define MUTE_CODE       "\xF515"
#define UNMUTE_CODE     "\xF514"

#define LOCK_CODE       "\xF512"
#define UNLOCK_CODE     "\xF513"
//+------------------------------------------------------------------+
//|   CMyBot                                                         |
//+------------------------------------------------------------------+
class CMyBot: public CCustomBot
  {
private:
   string            m_radio_button[3];
   int               m_radio_index;
   bool              m_lock_state;
   bool              m_mute_state;

public:
   //+------------------------------------------------------------------+
   void CMyBot::CMyBot(void)
     {
      m_radio_button[0]="Radio Button #1";
      m_radio_button[1]="Radio Button #2";
      m_radio_button[2]="Radio Button #3";
      m_radio_index=0;
      m_lock_state=false;
      m_mute_state=true;
     }

   //+------------------------------------------------------------------+
   string GetKeyboard()
     {
      //---
      string radio_code[3]={RADIO_EMPTY,RADIO_EMPTY,RADIO_EMPTY};
      if(m_radio_index>=0 && m_radio_index<=2)
         radio_code[m_radio_index]=RADIO_SELECT;
      //---
      string mute_text=UNMUTE_TEXT;
      string mute_code=UNMUTE_CODE;
      if(m_mute_state)
        {
         mute_text=MUTE_TEXT;
         mute_code=MUTE_CODE;
        }
      //---
      string lock_text=UNLOCK_TEXT;
      string lock_code=UNLOCK_CODE;
      if(m_lock_state)
        {
         lock_text=LOCK_TEXT;
         lock_code=LOCK_CODE;
        }
      //---
      //Print(m_lock.GetKey());
      return(StringFormat("[[\"%s %s\"],[\"%s %s\"],[\"%s %s\"],[\"%s %s\",\"%s %s\"]]",
             radio_code[0],m_radio_button[0],
             radio_code[1],m_radio_button[1],
             radio_code[2],m_radio_button[2],
             lock_code,lock_text,
             mute_code,mute_text));
     }

   //+------------------------------------------------------------------+
   void ProcessMessages(void)
     {
      for(int i=0;i<m_chats.Total();i++)
        {
         CCustomChat *chat=m_chats.GetNodeAtIndex(i);
         if(!chat.m_new_one.done)
           {
            chat.m_new_one.done=true;
            string text=chat.m_new_one.message_text;

            //--- start
            if(text=="/start" || text=="/help")
              {
               bot.SendMessage(chat.m_id,"Click on the buttons",bot.ReplyKeyboardMarkup(GetKeyboard(),false,false));
              }

            //--- Click on a RadioButton
            int total=ArraySize(m_radio_button);
            for(int k=0;k<total;k++)
              {
               if(text==RADIO_EMPTY+" "+m_radio_button[k])
                 {
                  m_radio_index=k;
                  bot.SendMessage(chat.m_id,m_radio_button[k],bot.ReplyKeyboardMarkup(GetKeyboard(),false,false));
                 }
              }

            //--- Unlock
            if(text==LOCK_CODE+" "+LOCK_TEXT)
              {
               m_lock_state=false;
               bot.SendMessage(chat.m_id,UNLOCK_TEXT,bot.ReplyKeyboardMarkup(GetKeyboard(),false,false));
              }

            //--- Lock
            if(text==UNLOCK_CODE+" "+UNLOCK_TEXT)
              {
               m_lock_state=true;
               bot.SendMessage(chat.m_id,LOCK_TEXT,bot.ReplyKeyboardMarkup(GetKeyboard(),false,false));
              }

            //--- Unmute
            if(text==MUTE_CODE+" "+MUTE_TEXT)
              {
               m_mute_state=false;
               bot.SendMessage(chat.m_id,UNMUTE_TEXT,bot.ReplyKeyboardMarkup(GetKeyboard(),false,false));
              }

            //--- Mute
            if(text==UNMUTE_CODE+" "+UNMUTE_TEXT)
              {
               m_mute_state=true;
               bot.SendMessage(chat.m_id,MUTE_TEXT,bot.ReplyKeyboardMarkup(GetKeyboard(),false,false));
              }
           }
        }
     }
  };

В результате мы получим окно вот такого вида (рисунок 8).

Рис.8. Элементы управления RadioButton и CheckBox

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

Стоит отметить, что в этих примерах в качестве получателя сообщения мы передаем номер чата chat_id. Мы не можем знать его заранее, чтобы отослать сообщение пользователю, пока он первым не начнет общение с ботом. Обратиться к пользователю по его номеру мобильного телефона или @username также нет возможности. Это сделано, очевидно, в целях безопасности, чтобы боты не рассылали спам. А вот публиковать сообщения на канале боты могут, обращаясь по его имени используя второй вариант SendMessage.

int SendMessage(const string channel_name,
                const string text)
 channel_name
 имя канала в виде @name
 text  текст сообщения. Поддерживаются теги HTML.
 Возвращаемое значение  код ошибки

Результат работы этой функции можно увидеть ниже, на рисунке 9.


Работа с мультимедиа

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

SendPhoto

В классе реализована возможность отправки фото с двумя вариантами использования.

int SendPhoto(const long   chat_id,
              const string local_path,
              string       &photo_id,
              const string caption=NULL,
              const bool   common_flag=false,
              const int    timeout=10000)
 chat_id  номер чата
 local_path  локальный путь к файлу в папке <каталог данных>\MQL5\Files
 photo_id  идентификатор загруженной на сервер фотографии
 caption  текст подписи под фото
 common_flag  флаг расположения файла в общей папке всех клиентских терминалов \Terminal\Common\Files
 timeout  таймаут операции в миллисекундах

Пример кода, отправляющего фото:

CCustomBot bot;

string token = "208375865:AAFnuOjlZ3Wsdan6PAjeqqUtBybe0Di1or8";

bot.Token(token);

string photo_id;
int result=bot.SendPhoto(198289825,"EURUSD1.gif",photo_id,"screenshot");
if(result==0)
   Print("Photo ID: ",photo_id);
else
   Print("Error: ",GetErrorDescription(result));

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

int SendPhoto(const long chat_id,
              const string photo_id,
              const string caption=NULL)
 chat_id  номер чата
 photo_id  идентификатор загруженной на сервер фотографии
 caption  текст подписи под фото


SendChartAction

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

int SendChatAction(const long chat_id,
                   const ENUM_CHAT_ACTION actiona)
 chat_id  номер чата
 action  идентификатор события

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


Примеры ботов

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


Второй бот Telegram_Search_EA присылает результаты поиска на сайте MQL5.com. Наверняка вам будет интересно посмотреть, как это работает, в следующем видео.


Третий бот Telegram_Signal_EA публикует на канале сигналы от стандартного индикатора MACD. Думаю, вы с легкостью сможете заменить MACD на свой любимый индикатор и задействовать этот код для себя.

//+------------------------------------------------------------------+
//|                                        Telegram_Signal_EA_v1.mq4 |
//|                        Copyright 2014, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"

//+------------------------------------------------------------------+
//|   Includes                                                       |
//+------------------------------------------------------------------+
#include <Telegram.mqh>

//--- Input parameters
input string InpChannelName="@forexsignalchannel";//Channel Name
input string InpToken="177791741:AAH0yB3YV7ywm80af_-AGqb7hzTR_Ud9DhQ";//Token

//--- Global variables
CCustomBot bot;
int macd_handle;
datetime time_signal=0;
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   time_signal=0;

//--- set token
   bot.Token(InpToken);

//--- get an indicator handle
   macd_handle=iMACD(NULL,0,12,26,9,PRICE_CLOSE);
   if(macd_handle==INVALID_HANDLE)
      return(INIT_FAILED);

//--- done
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {

//--- get time
   datetime time[1];
   if(CopyTime(NULL,0,0,1,time)!=1)
      return;

//--- check the signal on each bar
   if(time_signal!=time[0])
     {
      //--- first calc
      if(time_signal==0)
        {
         time_signal=time[0];
         return;
        }

      double macd[2]={0.0};
      double signal[2]={0.0};

      if(CopyBuffer(macd_handle,0,0,2,macd)!=2)
         return;
      if(CopyBuffer(macd_handle,1,0,2,signal)!=2)
         return;

      time_signal=time[0];

      //--- Send signal BUY
      if(macd[1]>signal[1] && 
         macd[0]<=signal[0])
        {
         string msg=StringFormat("Name: MACD Signal\nSymbol: %s\nTimeframe: %s\nType: Buy\nPrice: %s\nTime: %s",
                                 _Symbol,
                                 StringSubstr(EnumToString(_Period),7),
                                 DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits),
                                 TimeToString(time[0]));
         int res=bot.SendMessage(InpChannelName,msg);
         if(res!=0)
            Print("Error: ",GetErrorDescription(res));
        }

      //--- Send signal SELL
      if(macd[1]<signal[1] && 
         macd[0]>=signal[0])
        {
         string msg=StringFormat("Name: MACD Signal\nSymbol: %s\nTimeframe: %s\nType: Sell\nPrice: %s\nTime: %s",
                                 _Symbol,
                                 StringSubstr(EnumToString(_Period),7),
                                 DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits),
                                 TimeToString(time[0]));
         int res=bot.SendMessage(InpChannelName,msg);
         if(res!=0)
            Print("Error: ",GetErrorDescription(res));
        }
     }
  }
//+------------------------------------------------------------------+

В результате вы будете получать сообщения, показанные на рисунке 9.

Сигналы индикатора MACD

Рис.9. Сигналы индикатора MACD.


Заключение

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

Надеюсь, эта статья побудила вас использовать Telegram в трейдинге. Я не ставил своей целью останавливаться на подробностях, так как они хорошо изложены в документации по Bot API. Коды, прикрепленные к статье, адаптированы для работы на обеих платформах — и MetaTrader 4, и MetaTrader 5.