English Español Deutsch 日本語
preview
MQL5-советник, интегрированный в Telegram (Часть 7): Анализ команд для автоматизации индикаторов на графиках

MQL5-советник, интегрированный в Telegram (Часть 7): Анализ команд для автоматизации индикаторов на графиках

MetaTrader 5Торговые системы | 2 мая 2025, 13:36
664 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В предыдущей статье серии (Часть 6) мы интегрировали адаптивные кнопки для улучшения взаимодействия с ботом. Теперь автоматизируем добавление индикаторов на графики MetaTrader 5, используя команды, отправленные из Telegram. Мы создадим систему, в которой советник фиксирует заданные пользователем параметры индикатора через Telegram, анализирует данные и применяет указанные индикаторы к торговым графикам в реальном времени. 

Мы рассмотрим следующие темы:

  1. Обзор торговли на основе индикаторов с помощью Telegram: мы рассмотрим, как трейдеры могут использовать команды из Telegram для управления индикаторами в MetaTrader 5.
  2. Анализ и обработка команд индикатора из Telegram: В этом разделе подробно описывается, как правильно извлекать и обрабатывать параметры индикатора из Telegram-сообщений.
  3. Выполнение команд MQL5-индикаторов: Мы покажем, как использовать проанализированные команды для добавления и автоматизации индикаторов непосредственно в MetaTrader 5.
  4. Тестирование системы торговли на основе индикаторов: Тщательное тестирование обеспечит бесперебойную работу системы для точной автоматизации индикаторов.
  5. Заключение: Наконец, мы подведем итоги всего процесса и обсудим основные выводы.

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


Обзор торговли на основе индикаторов с помощью Telegram

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

Автоматизировав применение индикаторов через Telegram-команды, трейдеры могут улучшить свой технический анализ без необходимости ручного управления графиками. При правильной интеграции эти Telegram-команды можно анализировать и преобразовывать в исполняемые инструкции для MetaTrader 5, что позволяет добавлять индикаторы на графики в реальном времени. Это обеспечивает не только точность, но и более эффективный торговый процесс, позволяя трейдерам сосредоточиться на интерпретации результатов, а не на управлении настройками. Типичная визуализация команд индикатора показана ниже:

Формат Telegram-команды для индикатора

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


Анализ и обработка Telegram-команд индикатора

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

//+------------------------------------------------------------------+
//|   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.
};


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.
}
//+------------------------------------------------------------------+
int Class_Bot_EA::getChatUpdates(void){
   //--- 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
   }
   
   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);
   // THIS IS THE STRING RESPONSE WE GET // "ok":true,"result":[]}
   
   //--- If the request was successful
   if(res==0){
      //Print(out); //--- Optionally print the response
      
      //--- Create a JSON object to parse the response
      CJSONValue obj_json(NULL, jv_UNDEF);
      //--- Deserialize the JSON response
      bool done=obj_json.Deserialize(out);
      //--- If JSON parsing failed
      // Print(done);
      if(!done){
         Print("ERR: JSON PARSING"); //--- Print an error message if parsing fails
         return(-1); //--- Return with an error code
      }
      
      //--- 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
      }
      
      //--- 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++){
         //--- 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
         
         //--- 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
         
         //--- Update the ID for the next request
         member_update_id=obj_msg.update_id+1;
         
         //--- If it's the first update, skip processing
         if(member_first_remove){
            continue;
         }

         //--- 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
            }
         }
         
      }
      //--- After the first update, set the flag to false
      member_first_remove=false;
   }
   //--- Return the result of the POST request
   return(res);
}

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

Функция getChatUpdates создает URL-адрес для API Telegram, который предоставляет нам обновления для указанного бота. Идентификатор последнего обработанного обновления используется в качестве смещения в URL-адресе, что означает, что мы не получим никаких обновлений, которые уже обработаны. После создания URL-адреса мы отправляем POST-запрос к API и обрабатываем ответ сервера. Так как мы ожидаем получить данные в формате JavaScript Object Notation (JSON) обратно с сервера, мы проверяем наличие ошибок, пытаясь проанализировать данные. Если анализ не удался или поле "ok" в ответе JSON имеет значение false, мы выводим сообщение об ошибке и возвращаем ее код.

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

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

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



Выполнение команд MQL5-индикаторов

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

void Class_Bot_EA::ProcessMessages(void){

//...

}

Именно в этой функции начинается настоящая обработка. Первое, что нам нужно сделать, это просмотреть все полученные сообщения и обработать их по отдельности. Это важно, поскольку поставщик мог отправлять сигналы одновременно для нескольких торговых символов, например, "AUDUSD, EURUSD, GBPUSD, XAUUSD, XRPUSD, USDKES, USDJPY, EURCHF" и многих других. Мы достигаем этого с помощью следующей логики.

   //--- 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. Каждый объект чата извлекается с помощью функции GetNodeAtIndex. Мы проверяем, было ли обработано сообщение, связанное с этим чатом, оценивая флаг в структуре member_new_one. Если сообщение еще не обработано, флаг done (выполнено) устанавливается в значение true, что означает, что одно и то же сообщение не будет обрабатываться несколько раз.

Далее мы извлекаем содержимое сообщения. Оно хранится в поле message_text структуры member_new_one. Таким образом, мы можем работать с текстом напрямую, не беспокоясь о том, что уже обработано.

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

         string user_text = text;
         Print("USER'S PLAIN TEXT IS AS BELOW:\n",user_text);
         
         StringToUpper(user_text);
         Print("USER'S TRANSFORMED UPPERCASE TEXT IS AS BELOW:\n",user_text);

Здесь мы сначала сохраняем текст входящего сообщения из переменной text в новую переменную user_text. Это позволяет нам работать с содержимым сообщения, не изменяя исходную переменную. Затем мы печатаем текст сообщения пользователя с помощью функции Print, которая выводит сообщение в терминале MetaTrader 5 для регистрации событий.

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

Текст, переведенный в верхний регистр

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

         // MOVING AVERAGE
         //--- Initialize variables to hold extracted data
         string indicator_type = NULL;
         string indicator_symbol = NULL;
         string indicator_timeframe = NULL;
         long indicator_period = 0;
         long indicator_shift = 0;
         string indicator_method = NULL;
         string indicator_app_price = NULL;

Начнем с индикатора скользящей средней. Мы инициализируем несколько переменных, которые будут содержать извлеченные данные для настройки индикатора скользящей средней (MA) в MetaTrader 5 на основе пользовательского ввода из Telegram. Вот что представляет каждая переменная:

  • indicator_type - тип индикатора, в данном случае - скользящая средняя.
  • indicator_symbol - символ (валютная пара или актив), к которому будет применен индикатор.
  • indicator_timeframe - таймфрейм графика (например, M1, H1, D1), на котором будет построен индикатор.
  • indicator_period - количество периодов (или баров), которые скользящая средняя будет учитывать при расчете.
  • indicator_shift - смещение или сдвиг для перемещения индикатора вперед или назад на графике.
  • indicator_method - метод расчета скользящей средней (например, SMA, EMA).
  • indicator_app_price - цена расчета скользящей средней (например, цена закрытия, цена открытия).

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

         //--- Split the message by lines
         string lines[];
         StringSplit(user_text,'\n',lines);
         Print("SPLIT TEXT SEGMENTS IS AS BELOW:");
         ArrayPrint(lines,0,",");

Здесь мы разбиваем преобразованное сообщение пользователя на отдельные строки, чтобы упростить извлечение соответствующей информации об индикаторе. Сначала мы объявляем массив с именем lines для хранения каждой строки сообщения после его разделения. Затем мы применяем функцию StringSplit, используя символ новой строки ('\n') в качестве разделителя для разбиения сообщения на отдельные строки. Эта функция навсегда заполняет массив lines каждой частью текста, которая была отделена новой строкой.

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

         //--- Iterate over each line to extract information
         for (int i=0; i<ArraySize(lines); i++){
            StringTrimLeft(lines[i]);
            StringTrimRight(lines[i]);
            
            string selected_line = lines[i];
            Print(i,". ",selected_line);

            //...

         }

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

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

Итерации команд

Всё работает как надо. Мы можем приступить к поиску конкретных деталей в выбранной строке. Начнем с поиска типа технического индикатора. Сначала мы найдем текст типа индикатора скользящей средней, то есть INDICATOR TYPE. 

            if (StringFind(selected_line,"INDICATOR TYPE") >= 0){
               indicator_type = StringSubstr(selected_line,16);
               Print("Line @ index ",i," Indicator Type = ",indicator_type); //--- Print the extracted details
            }

Здесь мы обрабатываем сообщение пользователя Telegram, чтобы извлечь тип индикатора. Мы используем функцию StringFind для поиска в текущей строке ("selected_line") текста INDICATOR TYPE. Если этот текст найден, функция возвращает начальную позицию совпадения, которая является значением большим или равным нулю. После обнаружения совпадения мы извлекаем тип индикатора с помощью функции StringSubstr, которая извлекает подстроку с позиции после INDICATOR TYPE (начиная с индекса 16) до конца строки. Извлеченное значение сохраняется в переменной indicator_type. Наконец, мы выводим индекс строки и извлеченный indicator_type с помощью функции Print, чтобы подтвердить, что данные были успешно извлечены. При запуске мы получим следующий результат.

Подтверждение индикатора

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

            //--- Check for symbol in the list of available symbols and assign it
            for(int k = 0; k < SymbolsTotal(true); k++) { //--- Loop through all available symbols
               string selected_symbol = SymbolName(k, true); //--- Get the symbol name
               if (StringCompare(selected_line,selected_symbol,false) == 0){ //--- Compare the line with the symbol name
                  indicator_symbol = selected_symbol; //--- Assign the symbol if a match is found
                  Print("Line @ index ",i," SYMBOL = ",indicator_symbol); //--- Print the found symbol
               }
            }            

В этом блоке кода мы проверяем введенные пользователем данные по списку доступных торговых символов и присваиваем правильный символ переменной indicator_symbol. Во-первых, мы используем функцию SymbolsTotal, которая возвращает общее количество символов, доступных в данный момент на платформе. Аргумент true указывает, что нам нужно количество видимых символов. Затем мы перебираем все доступные символы, используя цикл for с переменной k в качестве индекса.

Внутри цикла мы используем функцию SymbolName для получения имени символа по индексу k. Второй аргумент, true, указывает, что нам нужно имя символа в его краткой форме. После извлечения имени символа и сохранения его в переменной selected_symbol, мы используем функцию StringCompare для сравнения selected_symbol с введенными пользователем данными (selected_line). Аргумент false указывает на то, что сравнение должно быть без учета регистра.

Если функция возвращает ноль, это означает, что две строки совпадают, и мы присваиваем selected_symbol переменной indicator_symbol. Наконец, мы выводим индекс строки и соответствующий indicator_symbol с помощью функции Print для подтверждения того, что мы правильно идентифицировали и назначили символ, введенный пользователем. Он не содержит никакого лишнего текста, поэтому мы осуществляем прямой поиск. При запуске текущего фрагмента кода мы не получим никаких результатов, поскольку извлеченный текст и символы по умолчанию не похожи друг на друга, поскольку они чувствительны к регистру, то есть "XAUUSDM" не равно "XAUUSDm". Вот имя символа, которое у нас есть:

Структура по умолчанию

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

            //--- Check for symbol in the list of available symbols and assign it
            for(int k = 0; k < SymbolsTotal(true); k++) { //--- Loop through all available symbols
               string selected_symbol = SymbolName(k, true); //--- Get the symbol name
               StringToUpper(selected_symbol);
               if (StringCompare(selected_line,selected_symbol,false) == 0){ //--- Compare the line with the symbol name
                  indicator_symbol = selected_symbol; //--- Assign the symbol if a match is found
                  Print("Line @ index ",i," SYMBOL = ",indicator_symbol); //--- Print the found symbol
               }
            }            

С помощью нового преобразования мы можем приступить к сравнению и получаем следующие результаты:

Выбранный символ

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

            if (StringFind(selected_line,"TIMEFRAME") >= 0){
               indicator_timeframe = StringSubstr(selected_line,12);
               Print("Line @ index ",i," Indicator Timeframe = ",indicator_timeframe); //--- Print the extracted details
            }
            if (StringFind(selected_line,"PERIOD") >= 0){
               indicator_period = StringToInteger(StringSubstr(selected_line,9));
               Print("Line @ index ",i," Indicator Period = ",indicator_period);
            }
            if (StringFind(selected_line,"SHIFT") >= 0){
               indicator_shift = StringToInteger(StringSubstr(selected_line,8));
               Print("Line @ index ",i," Indicator Shift = ",indicator_shift);
            }
            if (StringFind(selected_line,"METHOD") >= 0){
               indicator_method = StringSubstr(selected_line,9);
               Print("Line @ index ",i," Indicator Method = ",indicator_method);
            }
            if (StringFind(selected_line,"APPLIED PRICE") >= 0){
               indicator_app_price = StringSubstr(selected_line,16);
               Print("Line @ index ",i," Indicator Applied Price = ",indicator_app_price);
            }
         }

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

Информация об индикаторе

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

         //--- Final data
         Print("\nFINAL EXTRACTED DATA:"); //--- Print the final data for confirmation
         
         Print("Type = ",indicator_type);
         Print("Symbol = ",indicator_symbol);
         Print("Timeframe = ",indicator_timeframe);
         Print("Period = ",indicator_period);
         Print("Shift = ",indicator_shift);
         Print("Method = ",indicator_method);
         Print("Applied Price = ",indicator_app_price);

Мы печатаем окончательные извлеченные данные, чтобы подтвердить, что переменные были заполнены правильно. Сначала мы используем функцию «Печать» для отображения заголовка сообщения: "\nFINAL EXTRACTED DATA:". Это служит визуальной подсказкой в журналах, отмечающей, где будут показаны обработанные данные.

После этого мы последовательно выводим значения каждой из ключевых переменных — indicator_type, indicator_symbol, indicator_timeframe, indicator_period, indicator_shift, indicator_method и indicator_app_price. В каждом случае обращение к Print выводит имя переменной и ее текущее значение. Это важно для отладки и проверки, поскольку гарантирует, что данные, проанализированные на основе введенных пользователем данных (например, тип индикатора, символ, таймфрейм и т. д.), будут точно зафиксированы, прежде чем система продолжит добавление индикатора на график в MetaTrader 5. Результат представлен ниже:

Журнал

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

         if (indicator_type=="MOVING AVERAGE"){
                //...
         }

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

ENUM_MA_METHOD

Таким образом, нам необходимо более подробно объяснить это программе. Чтобы идти по порядку, начнем с самого очевидного — с таймфрейма.

            //--- Convert timeframe to ENUM_TIMEFRAMES
            ENUM_TIMEFRAMES timeframe_enum = _Period;
            if (indicator_timeframe == "M1") {
               timeframe_enum = PERIOD_M1;
            } else if (indicator_timeframe == "M5") {
               timeframe_enum = PERIOD_M5;
            } else if (indicator_timeframe == "M15") {
               timeframe_enum = PERIOD_M15;
            } else if (indicator_timeframe == "M30") {
               timeframe_enum = PERIOD_M30;
            } else if (indicator_timeframe == "H1") {
               timeframe_enum = PERIOD_H1;
            } else if (indicator_timeframe == "H4") {
               timeframe_enum = PERIOD_H4;
            } else if (indicator_timeframe == "D1") {
               timeframe_enum = PERIOD_D1;
            } else if (indicator_timeframe == "W1") {
               timeframe_enum = PERIOD_W1;
            } else if (indicator_timeframe == "MN1") {
               timeframe_enum = PERIOD_MN1;
            } else {
               Print("Invalid timeframe: ", indicator_timeframe);
            }

В этом разделе мы преобразуем предоставленный пользователем таймфрейм из извлеченной строки indicator_timeframe в соответствующее перечисление MetaTrader 5 ENUM_TIMEFRAMES. Этот шаг имеет решающее значение, поскольку MetaTrader 5 использует предопределенные таймфреймы, такие как PERIOD_M1 для 1-минутных графиков, PERIOD_H1 для 1-часового графика и так далее. Эти временные рамки определены как часть типа ENUM_TIMEFRAMES.

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

Далее мы используем ряд условных операторов if-else для проверки значения строки indicator_timeframe. Каждое условие сравнивает извлеченную строку с известными идентификаторами временных рамок, такими как M1 (1 минута), H1 (1 час) и так далее. Если совпадение найдено, соответствующее значение ENUM_TIMEFRAMES (например, PERIOD_M1 или PERIOD_H1) присваивается переменной timeframe_enum.

Если ни одно из условий не совпадает, срабатывает финальный блок else, печатая в журнале сообщение об ошибке "Invalid timeframe" (неверный таймфрейм) со значением indicator_timeframe. Это помогает гарантировать, что в процессе создания индикатора в MetaTrader 5 передаются только допустимые таймфреймы. Аналогично мы преобразуем остальные переменные в соответствующие им формы.

            //--- Convert MA method to ENUM_MA_METHOD
            ENUM_MA_METHOD ma_method = MODE_SMA;
            if (indicator_method == "SMA") {
               ma_method = MODE_SMA;
            } else if (indicator_method == "EMA") {
               ma_method = MODE_EMA;
            } else if (indicator_method == "SMMA") {
               ma_method = MODE_SMMA;
            } else if (indicator_method == "LWMA") {
               ma_method = MODE_LWMA;
            } else {
               Print("Invalid MA method: ", indicator_method);
            }
            
            //--- Convert applied price to ENUM_APPLIED_PRICE
            ENUM_APPLIED_PRICE app_price_enum = PRICE_CLOSE;
            
            if (indicator_app_price == "CLOSE") {
               app_price_enum = PRICE_CLOSE;
            } else if (indicator_app_price == "OPEN") {
               app_price_enum = PRICE_OPEN;
            } else if (indicator_app_price == "HIGH") {
               app_price_enum = PRICE_HIGH;
            } else if (indicator_app_price == "LOW") {
               app_price_enum = PRICE_LOW;
            } else if (indicator_app_price == "MEDIAN") {
               app_price_enum = PRICE_MEDIAN;
            } else if (indicator_app_price == "TYPICAL") {
               app_price_enum = PRICE_TYPICAL;
            } else if (indicator_app_price == "WEIGHTED") {
               app_price_enum = PRICE_WEIGHTED;
            } else {
               Print("Invalid applied price: ", indicator_app_price);
            }

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

            int handle_ma = iMA(indicator_symbol,timeframe_enum,(int)indicator_period,(int)indicator_shift,ma_method,app_price_enum);

Здесь мы создаем хэндл для индикатора скользящей средней (MA), используя функцию iMA. Эта функция генерирует хэндл индикатора на основе параметров, которые мы извлекли и обработали ранее. Хэндл позволит нам ссылаться на индикатор на графике и управлять им позже в коде. Мы передаем несколько аргументов функции iMA, каждый из которых соответствует параметру, который мы получили из команды Telegram:

  • indicator_symbol - финансовый инструмент (например, EURUSD), к которому будет применяться индикатор.
  • timeframe_enum - этот аргумент относится к временному интервалу (например, M1 - 1 минута, H1 - 1 час), который был ранее преобразован из введенных пользователем данных.
  • (int)indicator_period - преобразует извлеченный indicator_period из типа данных long в целое число, представляющее количество периодов скользящей средней.
  • (int)indicator_shift - аналогично преобразует indicator_shift в целое число, определяя, на сколько баров следует сместить индикатор на графике.
  • ma_method - метод расчета скользящей средней, например, простая скользящая (SMA) или экспоненциальная скользящая средняя (EMA), на основе введенных пользователем данных.
  • app_price_enum - применяемый тип цены (например, цена закрытия или цена открытия), на основе которого будет рассчитываться скользящая средняя.

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

            if (handle_ma != INVALID_HANDLE){
               Print("Successfully created the indicator handle!");

                //...

            }
            else if (handle_ma == INVALID_HANDLE){
               Print("Failed to create the indicator handle!");
            }

Здесь мы проверяем, был ли успешно создан хэндл индикатора скользящей средней, проверяя значение handle_ma. Если хэндл действителен, функция iMA вернет значение, которое не равно INVALID_HANDLE. В этом случае мы выводим сообщение об успешном завершении: "Successfully created the indicator handle!" (Успешно создан хэндл индикатора!). Это означает, что все параметры (символ, таймфрейм, период, метод и прочие) были правильно интерпретированы и индикатор скользящей средней готов к использованию на графике.

Если создание хэндла не удалось, значение handle_ma равно INVALID_HANDLE, выводим сообщение об ошибке: "Failed to create the indicator handle!" (Не удалось создать хэндл индикатора!). Это состояние указывает на то, что в процессе создания индикатора что-то пошло не так — например, недопустимый символ, неправильный таймфрейм или любой другой неверный параметр. Такая обработка ошибок помогает нам гарантировать, что система сможет обнаруживать проблемы и предоставлять информативную обратную связь в процессе настройки индикатора. Затем мы можем открыть график с указанным символом и таймфреймом, убедиться, что он синхронизирован с последними данными, и настроить его параметры для ясности.

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

Во-первых, мы используем функцию ChartOpen для открытия нового графика на основе indicator_symbol и timeframe_enum, которые мы ранее извлекли из Telegram-команды. Идентификатор графика возвращается и сохраняется в переменной сhart_id. Чтобы вывести этот график на передний план в MetaTrader 5, мы используем функцию ChartSetInteger, передающую идентификатор графика вместе с константой CHART_BRING_TO_TOP, чтобы гарантировать видимость графика для взаимодействия.

Далее мы реализуем проверку синхронизации, чтобы убедиться, что ценовые данные для графика полностью обновлены. Это делается путем повторения цикла до 60 раз с использованием функции SeriesInfoInteger для проверки синхронизированности ряда ценовых данных. Если синхронизация происходит до завершения цикла, мы выходим из него раньше времени. Убедившись, что данные актуальны, переходим к настройке внешнего вида графика. Разделители сетки и периодов скрываются с помощью функции ChartSetInteger, в которой мы передаем CHART_SHOW_GRID и CHART_SHOW_PERIOD_SEP как false, создавая более чистый вид графика.

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

               if (ChartIndicatorAdd(chart_id,0,handle_ma)) {
                  Print("SUCCESS. Indicator added to the chart.");
                  isIndicatorAddedToChart = true;
               }
               else {
                  Print("FAIL: Indicator not added to the chart.");
               }   

Здесь мы добавляем ранее созданный индикатор скользящей средней к графику, который мы открыли ранее. Мы достигаем этого с помощью функции ChartIndicatorAdd, в которую мы передаем chart_id, ноль (который указывает на то, что мы добавляем индикатор к основному графику) и handle_ma, который представляет собой хэндл созданного нами индикатора скользящей средней. Если добавление прошло успешно, мы выводим сообщение об успешном завершении: "SUCCESS. Indicator added to the chart" (индикатор успешно добавлен на график), и устанавливаем логическую переменную isIndicatorAddedToChart на true, указывая, что индикатор теперь активен на графике. Мы определили логическую переменную и инициализировали ее значением false вне оператора if, как показано ниже.

         bool isIndicatorAddedToChart = false;

И наоборот, если добавление не удалось, мы выводим сообщение об ошибке: "FAIL: Indicator not added to the chart" (индикатор не добавлен на график). Эта проверка имеет решающее значение, поскольку она гарантирует, что мы можем подтвердить, успешно ли применен индикатор к графику, что необходимо для последующих торговых операций и визуального анализа. Обрабатывая оба результата, мы сохраняем прозрачность нашего процесса и получаем информацию о состоянии индикатора на графике. Если мы создадим индикатор и добавим его на график, то сможем сообщить пользователю об успехе в той же структуре кода.

         if (isIndicatorAddedToChart){
            string message = "\nSUCCESS! THE INDICATOR WAS ADDED TO THE CHART WITH THE FOLLOWING DETAILS:\n"; //--- Prepare success message
            message += "\nType = "+indicator_type+"\n";
            message += "Symbol = "+indicator_symbol+"\n";
            message += "Timeframe = "+indicator_timeframe+"\n";
            message += "Period = "+IntegerToString(indicator_period)+"\n";
            message += "Shift = "+IntegerToString(indicator_shift)+"\n";
            message += "Applied Price = "+indicator_app_price+"\n";
            message+="\nHAPPY TRADING!"; //--- Add final message line
            Print(message); //--- Print the message in the terminal
            sendMessageToTelegram(chat.member_id,message,NULL); //--- Send the success message to Telegram
         }
 

Здесь мы проверяем, был ли индикатор успешно добавлен на график, оценивая логическую переменную isIndicatorAddedToChart. При true мы приступаем к подготовке сообщения об успешном завершении, в котором подробно описывается конфигурация индикатора. Начнем с инициализации строковой переменной с именем message с заголовком сообщения об успешном выполнении: "\nSUCCESS! THE INDICATOR WAS ADDED TO THE CHART WITH THE FOLLOWING DETAILS:\n". Затем мы объединяем различные данные об индикаторе, включая его тип, символ, временные рамки, период, сдвиг и примененную цену. Для числовых значений мы используем функцию IntegerToString, обеспечивающую их преобразование в строковый формат для правильной конкатенации.

Собрав всю эту информацию, мы добавляем к сообщению последнюю строку, начиная с "\nHAPPY TRADING!" (Удачной торговли!), чтобы передать позитивный настрой. Затем мы используем функцию Print для вывода полного сообщения в терминал, обеспечивающего четкое подтверждение добавления индикатора и его параметров. Наконец, мы вызываем функцию sendMessageToTelegram, чтобы отправить то же самое сообщение об успешном выполнении в Telegram, гарантируя, что соответствующий чат, идентифицированный по chat.member_id, будет уведомлен об успешной операции. При запуске мы получим следующий результат.

Сообщение об ошибке

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

            //--- Check for symbol in the list of available symbols and assign it
            for(int k = 0; k < SymbolsTotal(true); k++) { //--- Loop through all available symbols
               string selected_symbol = SymbolName(k, true); //--- Get the symbol name
               StringToUpper(selected_symbol);
               if (StringCompare(selected_line,selected_symbol,false) == 0){ //--- Compare the line with the symbol name
                  indicator_symbol = SymbolName(k, true); //--- Assign the symbol if a match is found
                  Print("Line @ index ",i," SYMBOL = ",indicator_symbol); //--- Print the found symbol
               }
            }            

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

Сообщение об успехе

Мы успешно добавили индикатор на график. В Telegram мы получаем ответ об успешном завершении, как показано ниже:

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

В торговом терминале открывается новый график и добавляется индикатор.

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

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


Тестирование торговой системы на основе индикатора

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

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



Заключение

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

Благодаря знаниям, полученным в результате этого внедрения, вы будете готовы разрабатывать более сложные системы, включающие дополнительные индикаторы и команды. Созданная основа позволит вам усовершенствовать свои торговые стратегии, адаптироваться к меняющимся рыночным условиям и в конечном итоге улучшить результаты торговли. Надеюсь, статья оказалась для вас понятной и дала идеи, необходимые для улучшения ваших торговых систем. Если у вас возникнут какие-либо вопросы или потребуются дополнительные разъяснения, не стесняйтесь изучать дополнительные источники или экспериментировать с концепциями, представленными в этой серии. Удачной торговли!

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

Прикрепленные файлы |
Компьютерное зрение для трейдинга (Часть 1): Создаем базовый простой функционал Компьютерное зрение для трейдинга (Часть 1): Создаем базовый простой функционал
Система прогнозирования EURUSD с применением компьютерного зрения и глубокого обучения. Узнайте, как сверточные нейронные сети могут распознавать сложные ценовые паттерны на валютном рынке и предсказывать движение курса с точностью до 54%. Статья раскрывает методологию создания алгоритма, использующего технологии искусственного интеллекта для визуального анализа графиков вместо традиционных технических индикаторов. Автор демонстрирует процесс трансформации ценовых данных в «изображения», их обработку нейронной сетью и уникальную возможность заглянуть в «сознание» ИИ через карты активации и тепловые карты внимания. Практический код на Python с использованием библиотеки MetaTrader 5 позволяет читателям воспроизвести систему и применить ее в собственной торговле.
Алгоритм хаотической оптимизации — Chaos optimization algorithm (COA): Продолжение Алгоритм хаотической оптимизации — Chaos optimization algorithm (COA): Продолжение
Продолжение исследования алгоритма хаотической оптимизации. Вторая часть статьи посвящена практическим аспектам реализации алгоритма, его тестированию и выводам.
Нейросети в трейдинге: Прогнозирование временных рядов при помощи адаптивного модального разложения (ACEFormer) Нейросети в трейдинге: Прогнозирование временных рядов при помощи адаптивного модального разложения (ACEFormer)
Предлагаем познакомиться с архитектурой ACEFormer — современным решением, сочетающим эффективность вероятностного внимания и адаптивное разложение временных рядов. Материал будет полезен тем, кто ищет баланс между вычислительной производительностью и точностью прогноза на финансовых рынках.
Скальпинг по потоку ордеров (Order Flow Scalping) с MQL5 Скальпинг по потоку ордеров (Order Flow Scalping) с MQL5
Данный советник для MetaTrader 5 реализует стратегию Scalping OrderFlow (стратегия скальпирования потока ордеров) с расширенным управлением рисками. В нем используется множество технических индикаторов для определения торговых возможностей на основе дисбалансов в потоке ордеров. Бэк-тестирование показывает потенциальную прибыльность, но подчеркивает необходимость дальнейшей оптимизации, особенно в области управления рисками и соотношения результатов торговли. Он подходит для опытных трейдеров и требует тщательного тестирования и понимания перед практическим применением.