
MQL5-советник, интегрированный в Telegram (Часть 2): Отправка сигналов из MQL5 в Telegram
Введение
В первой части нашей серии о разработке MQL5-советников, интегрированных с Telegram, мы рассмотрели основные шаги, необходимые для связи MQL5 и Telegram. Первым шагом была настройка самого приложения. После этого мы перешли к коду. Причина такого порядка действий, надеюсь, станет яснее ниже. В результате у нас теперь есть бот, который может получать сообщения, а также программа, которая может их отправлять. Мы также написали простую программу на языке MQL5, демонстрирующую, как отправить сообщение через бота в приложение.
Заложив основу в первой части, мы можем перейти к следующему шагу: передаче торговых сигналов в Telegram с использованием MQL5. Наш новый усовершенствованный советник не только открывает и закрывает сделки на основе заданных условий, но и передает сигнал в групповой чат Telegram, чтобы сообщить нам о совершении сделки. Сами торговые сигналы были немного переработаны, чтобы гарантировать, что информация, которую мы отправляем в Telegram, является максимально ясной и краткой. Наш новый советник лучше справляется с общением в Telegram-группе, чем наша предыдущая версия, и делает это с той же или более высокой скоростью, чем старая версия советника, что означает, что мы можем ожидать получения сигналов практически в реальном времени по мере совершения или закрытия сделок.
Мы будем генерировать и отправлять сигналы на основе известной системы пересечения скользящих средних. Кроме того, если вы помните, в первой части серии у нас было всего одно сообщение, которое могло быть довольно длинным, и если кто-то хотел добавить сегменты к сообщению, это приводило к ошибке. Таким образом, за один раз можно было отправить только одно сообщение, а при наличии дополнительных сегментов, их пришлось бы передавать в разных отдельных сообщениях. Например, сообщения "A buy signal has been generated" (сгенерирован сигнал на покупку) и "Open a buy order" (откройте ордер на покупку) должны иметь форму одного длинного либо двух коротких сообщений. В этой части мы объединим их и изменим сообщение так, чтобы одно сообщение могло содержать несколько текстовых сегментов и символов. Мы обсудим весь процесс в следующих разделах:
- Обзор стратегии
- Реализация средствами MQL5
- Тестирование интеграции
- Заключение
В итоге мы создадим советника, который будет отправлять торговую информацию, такую как сгенерированные сигналы и размещенные ордера из торгового терминала в указанный чат Telegram. Давайте начнем.
Обзор стратегии
Мы генерируем торговые сигналы с помощью пересечений скользящих средних — одного из наиболее широко используемых инструментов технического анализа. Мы опишем наиболее простой и понятный, по нашему мнению, метод использования пересечений скользящих средних для выявления потенциальных возможностей покупки или продажи. Метод основан на сигнальной природе самих пересечений без добавления каких-либо других инструментов или индикаторов. Для простоты мы рассмотрим только две скользящие средние разных периодов - краткосрочную и долгосрочную.
Мы рассмотрим функцию пересечений скользящих средних и то, как они дают торговые сигналы, на которые можно реагировать. Скользящие средние берут ценовые данные и сглаживают их, создавая своего рода плавную линию, которая гораздо лучше подходит для определения тренда, чем фактический ценовой график. Это связано с тем, что, как правило, средняя линия всегда более четкая и ее легче отслеживать, чем ломаную линию. При использовании двух скользящих средних с разными периодами в какой-то момент они пересекутся.
Чтобы применить на практике сигналы пересечения скользящих средних с использованием MQL5, начнем с определения краткосрочных и долгосрочных периодов средней, которые наиболее соответствуют нашей торговой стратегии. Для этой цели мы будем использовать стандартные периоды, такие как 50 и 200 для долгосрочных трендов и 10 и 20 - для краткосрочных. После вычисления скользящих средних мы сравним значения событий пересечения на каждом новом тике или баре и преобразуем эти обнаруженные сигналы пересечения в бинарные события buy (покупка) или sell (продажа), на основании которых будет действовать наш советник. Чтобы легче понять, что мы имеем в виду, давайте представим себе эти два случая.
Пересечение снизу вверх:
Пересечение сверху вниз:
Сгенерированные сигналы будут объединены с нашей текущей структурой обмена сообщениями между MQL5 и Telegram. Для достижения этой цели код из первой части будет адаптирован для включения обнаружения и форматирования сигнала. При обнаружении пересечения будет создано сообщение с названием актива, направлением пересечения (покупка/продажа) и временем сигнала. Своевременная доставка этого сообщения в специальный Telegram-чат позволит нашей торговой группе быть в курсе потенциальных торговых возможностей. Помимо всего прочего, уверенность в получении сообщения сразу после того, как произошло пересечение, означает, что у нас будет возможность инициировать сделку на основе рассматриваемого сигнала или даже открыть рыночную позицию и передать данные о позиции.
Реализация средствами MQL5
Во-первых, убедимся, что можем сегментировать наше сообщение и отправлять его целиком. В первой части, когда мы отправляли сложное сообщение, включающее специальные символы, такие как переносы строк, мы получали ошибку и могли отправить его только как единое сообщение, без структуры. Например, у нас был следующий фрагмент кода, который получает событие инициализации, остаток на счете, а также доступную свободную маржу:
double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY); double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); string msg = "🚀 EA INITIALIZED ON CHART " + _Symbol + " 🚀" +"📊 Account Status 📊; Equity: $" +DoubleToString(accountEquity,2) +"; Free Margin: $" +DoubleToString(accountFreeMargin,2);
Отправляя сообщение целиком, мы получаем следующее:
Хотя мы и можем отправить сообщение, его структура не выглядит привлекательной. Предложение инициализации должно быть в первой строке, затем во второй строке — статус счета, в следующей строке — эквити, а в последней строке — информация о свободной марже. Чтобы добиться этого, символ перевода строки \n необходимо оформить так:
double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY); double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); string msg = "🚀 EA INITIALIZED ON CHART " + _Symbol + " 🚀" +"\n📊 Account Status 📊" +"\nEquity: $" +DoubleToString(accountEquity,2) +"\nFree Margin: $" +DoubleToString(accountFreeMargin,2);
Однако при запуске программы мы получаем ошибку в журнале, как показано ниже, и сообщение не отправляется в чат Telegram:
Чтобы убедиться, что сообщение успешно отправлено, нам необходимо установить его кодировку. Для нашей интеграции требуется кодировка сообщений для правильной обработки специальных символов. Например, если наше сообщение содержит что-либо похожее на пробел или символы &, ? и прочие, оно может быть неправильно прочитано API Telegram из-за недостаточной осторожности с нашей стороны во время интеграции. Необходимо отнестить к этому максимально серьезно. Мы видели и другие варианты использования кодировки символов, например, при открытии некоторых видов документов, как показано на скриншоте.
Кодировка устраняет проблемы, когда API не понимает, что мы пытаемся ему отправить, и не может сделать то, что мы хотим.
Например, сообщение, отправленное в API и содержащее специальный символ, может нарушить структуру URL — то, как URL "видят" компьютеры, — приводя к ошибкам в интерпретации. API может интерпретировать специальный символ как инструкцию или какую-то другую часть кода, а не как часть сообщения. Этот сбой связи может произойти на любом этапе: при отправке сообщения из программы или при его получении на другом конце кодировка не выполняет свою основную функцию — сделать невидимую часть сообщения безопасной для "просмотра". Кроме того, использование схемы кодировки означает, что у нас есть сообщение в формате, совместимом с принимающей стороной — в данном случае с API Telegram. В деле задействовано несколько разных систем, и у каждой из них есть свои требования к тому, как именно ей передаются данные. Поэтому первое, что мы сделаем, это создадим функцию, которая будет кодировать наши сообщения.
// FUNCTION TO ENCODE A STRING FOR USE IN URL string UrlEncode(const string text) { string encodedText = ""; // Initialize the encoded text as an empty string int textLength = StringLen(text); // Get the length of the input text ... }
Здесь мы начинаем с функции типа данных string UrlEncode, которая принимает один параметр, аргумент или текст типа string, который предназначен для преобразования предоставленного текста в формат, читаемый URL. Затем мы инициализируем пустую строку encodedText, которая будет использоваться для создания результата, читаемого в URL, при обработке входного текста. Далее мы определяем длину входной строки, используя функцию StringLen, которая сохраняет эту длину в целочисленной переменной textLength. Этот шаг имеет решающее значение, поскольку он позволяет нам узнать, сколько символов нам необходимо обработать. Сохранив длину, мы можем эффективно перебирать каждый символ строки в цикле, гарантируя, что все символы правильно закодированы в соответствии с правилами кодирования URL. Для итерации нам понадобится использовать цикл.
// Loop through each character in the input string for (int i = 0; i < textLength; i++) { ushort character = StringGetCharacter(text, i); // Get the character at the current position ... }
Здесь мы инициируем цикл for для итерации по всем символам, содержащимся во входном сообщении или тексте, начиная с первого с индексом 0 и далее. Мы получаем значение выбранного символа с помощью функции StringGetCharacter, которая обычно возвращает значение символа, расположенного в указанной позиции строки. Положение определяется индексом i. Мы сохраняем символ в переменной ushort под именем "character".
// Check if the character is alphanumeric or one of the unreserved characters if ((character >= 48 && character <= 57) || // Check if character is a digit (0-9) (character >= 65 && character <= 90) || // Check if character is an uppercase letter (A-Z) (character >= 97 && character <= 122) || // Check if character is a lowercase letter (a-z) character == '!' || character == '\'' || character == '(' || character == ')' || character == '*' || character == '-' || character == '.' || character == '_' || character == '~') { // Append the character to the encoded string without encoding encodedText += ShortToString(character); }
Здесь мы проверяем, является ли заданный символ буквенно-цифровым или одним из незарезервированных символов, обычно используемых в URL-адресах. Цель состоит в том, чтобы определить, необходимо ли кодировать символ или его можно непосредственно добавить к закодированной строке. Сначала мы смотрим, является ли символ цифрой, проверяя, находится ли его значение ASCII в диапазоне от 48 до 57. Далее мы смотрим, является ли символ заглавной буквой, проверяя, находится ли его значение ASCII в диапазоне от 65 до 90. Аналогично смотрим, является ли символ строчной буквой, проверяя, находится ли его значение ASCII в диапазоне от 97 до 122. Значения можно посмотреть в "таблице ASCII".
Цифровые символы - от 48 до 57:
Заглавные буквы - от 65 до 90:
Строчные буквы - от 97 до 122:
Помимо этих буквенно-цифровых символов мы также проверяем наличие определенных незарезервированных символов, используемых в URL-адресах. К ним относятся '!', ''', '(', ')', '*', '-', '.', '_' и '~'. Если символ соответствует любому из этих критериев, это означает, что он является либо буквенно-цифровым, либо одним из незарезервированных символов.
Если символ соответствует любому из этих условий, мы добавляем его в строку encodedText, не кодируя его. Это достигается путем преобразования символа в его строковое представление с помощью функции ShortToString, которая гарантирует, что символ будет добавлен в закодированную строку в своем исходном виде. Если ни одно из этих условий не выполняется, мы переходим к проверке наличия пробелов.
// Check if the character is a space else if (character == ' ') { // Encode space as '+' encodedText += ShortToString('+'); }
Здесь мы используем оператор else if для проверки, является ли символ пробелом, путем сравнения его с символом пробела. Если символ действительно является пробелом, нам необходимо закодировать его способом, подходящим для URL-адресов. Вместо использования типичного кодирования пробелов в виде процентов (%20), как мы видели на скриншоте выше, мы решили кодировать пробелы знаком плюс (+), что является еще одним распространенным методом представления пробелов в URL-адресах, особенно в компоненте запроса. Таким образом, мы преобразуем знак плюс в его строковое представление с помощью функции ShortToString, а затем добавляем его к строке encodedText.
Если к этому моменту у нас остаются некодированные символы, нам предстоит поломать голову, поскольку речь идет о сложных символах, таких как эмодзи. Таким образом, нам необходимо будет обрабатывать все символы, которые не являются буквенно-цифровыми, незарезервированными или пробелами, кодируя их с помощью формата UTF-8, чтобы любой символ, который не попадает в ранее проверенные категории, будет безопасно закодирован для включения в URL.
// For all other characters, encode them using UTF-8 else { uchar utf8Bytes[]; // Array to hold the UTF-8 bytes int utf8Length = ShortToUtf8(character, utf8Bytes); // Convert the character to UTF-8 for (int j = 0; j < utf8Length; j++) { // Convert each byte to its hexadecimal representation prefixed with '%' encodedText += StringFormat("%%%02X", utf8Bytes[j]); } }
Сначала мы объявляем массив utf8Bytes для хранения байтового представления символа в формате UTF-8. Затем мы вызываем функцию ShortToUtf8, передавая в качестве аргументов character и массив utf8Bytes. Мы вернемся к описанию функции позже. Сейчас я лишь отмечу, что функция преобразует символ в его представление в UTF-8 и возвращает количество байтов, использованных при преобразовании, сохраняя эти байты в массиве utf8Bytes.
Далее мы используем цикл for для перебора каждого байта в массиве utf8Bytes. Каждый байт мы преобразуем в его шестнадцатеричное представление с префиксом %, что является стандартным способом кодирования символов в URL-адресах. Мы используем функцию StringFormat для форматирования каждого байта как двузначного шестнадцатеричного числа с префиксом %. Наконец, мы добавляем это закодированное представление к строке encodedText. В конце концов мы просто возвращаем результаты.
return encodedText; // Return the URL-encoded string
Полный фрагмент кода функции выглядит следующим образом:
// FUNCTION TO ENCODE A STRING FOR USE IN URL string UrlEncode(const string text) { string encodedText = ""; // Initialize the encoded text as an empty string int textLength = StringLen(text); // Get the length of the input text // Loop through each character in the input string for (int i = 0; i < textLength; i++) { ushort character = StringGetCharacter(text, i); // Get the character at the current position // Check if the character is alphanumeric or one of the unreserved characters if ((character >= 48 && character <= 57) || // Check if character is a digit (0-9) (character >= 65 && character <= 90) || // Check if character is an uppercase letter (A-Z) (character >= 97 && character <= 122) || // Check if character is a lowercase letter (a-z) character == '!' || character == '\'' || character == '(' || character == ')' || character == '*' || character == '-' || character == '.' || character == '_' || character == '~') { // Append the character to the encoded string without encoding encodedText += ShortToString(character); } // Check if the character is a space else if (character == ' ') { // Encode space as '+' encodedText += ShortToString('+'); } // For all other characters, encode them using UTF-8 else { uchar utf8Bytes[]; // Array to hold the UTF-8 bytes int utf8Length = ShortToUtf8(character, utf8Bytes); // Convert the character to UTF-8 for (int j = 0; j < utf8Length; j++) { // Convert each byte to its hexadecimal representation prefixed with '%' encodedText += StringFormat("%%%02X", utf8Bytes[j]); } } } return encodedText; // Return the URL-encoded string }
Давайте теперь рассмотрим функцию, отвечающую за преобразование символов в UTF-8.
//+-----------------------------------------------------------------------+ //| Function to convert a ushort character to its UTF-8 representation | //+-----------------------------------------------------------------------+ int ShortToUtf8(const ushort character, uchar &utf8Output[]) { ... }
Функция имеет целочисленный тип данных и принимает два входных параметра: значение символа и выходной массив.
Сначала мы преобразуем однобайтовые символы.
// Handle single byte characters (0x00 to 0x7F) if (character < 0x80) { ArrayResize(utf8Output, 1); // Resize the array to hold one byte utf8Output[0] = (uchar)character; // Store the character in the array return 1; // Return the length of the UTF-8 representation }
Преобразование однобайтовых символов, имеющих значения в диапазоне от 0x00 до 0x7F, осуществляется просто, поскольку они представлены в UTF-8 непосредственно одним байтом. Сначала мы проверяем, меньше ли символ 0x80. Если это так, мы изменяем размер массива utf8Output до одного байта, используя функцию ArrayResize. Это позволяет нам получить правильный размер выходного представления UTF-8. Затем мы вставляем символ в первый элемент массива, приводя символ к типу uchar (приведение типов). Это то же самое, что копировать значение символа в массив. Мы возвращаем 1, указывая, что представление UTF-8 имеет длину один байт. Этот процесс эффективно преобразует любой однобайтовый символ в форму UTF-8, независимо от операционной системы.
Их представление будет следующим.
0x00, UTF-8:
0x7F, UTF-8:
Как видим, десятичное представление чисел охватывает диапазон от 0 до 127. Эти символы идентичны исходным символам Unicode. В шестнадцатеричной системе счисления 0x80 и 0x7F представляют собой определенные значения, которые можно преобразовать в десятичную систему для лучшего понимания. Шестнадцатеричное число 0x80 эквивалентно 128 в десятичном формате. Это связано с тем, что шестнадцатеричная система счисления имеет основание 16, в которой каждая цифра представляет собой степень числа 16. В числе 0x80 "8" представляет собой 8, умноженное на 16^1 (что равно 128), а "0" представляет собой 0 умноженное на 16^0 (что равно 0), что в сумме дает 128.
С другой стороны, 0x7F эквивалентно 127 в десятичной системе счисления. В шестнадцатеричной системе 7F означает 7, умноженное на 16^1, плюс 15, умноженное на 16^0. В ходе расчетов мы получаем 7 раз по 16 (что равно 112) плюс F (что равно 15), что в итоге дает 127. A-F показаны ниже. Десятичная дробь под шестнадцатеричной F равна 15.
Таким образом, 0x80 — это 128 в десятичной системе счисления, а 0x7F — это 127 в десятичной системе счисления. Это означает, что 0x80 всего на единицу больше, чем 0x7F, что делает его границей, где однобайтовое представление в кодировке UTF-8 меняется на многобайтовое представление.
Я привожу все эти подробные объяснения, чтобы у вас не возникало вопросов о форматах. Перейдем к двухбайтовым символам.
// Handle two-byte characters (0x80 to 0x7FF) if (character < 0x800) { ArrayResize(utf8Output, 2); // Resize the array to hold two bytes utf8Output[0] = (uchar)((character >> 6) | 0xC0); // Store the first byte utf8Output[1] = (uchar)((character & 0x3F) | 0x80); // Store the second byte return 2; // Return the length of the UTF-8 representation }
Здесь мы преобразуем символы, которым требуется два байта в их представлении UTF-8, в частности, символов, значения которых лежат в диапазоне от 0x80 до 0x7FF. Для этого мы сначала проверяем, меньше ли рассматриваемый символ 0x800 (2048 в десятичной системе), что гарантирует, что он действительно находится в этом диапазоне. Если это условие выполняется, мы изменяем размер массива utf8Output так, чтобы он содержал два байта (поскольку для представления символа в UTF-8 потребуется два байта). Затем мы вычисляем фактическое представление UTF-8.
Первый байт получается путем сдвига символа вправо на 6 бит, а затем объединения его с 0xC0 с помощью логической операции ИЛИ. Это вычисление устанавливает самые значимые биты первого байта в префикс UTF-8 для двухбайтового символа. Второй байт вычисляется путем маскирования символа с помощью 0x3F для получения нижних 6 бит, а затем его объединения с 0x80. Эта операция гарантирует, что второй байт имеет правильный префикс UTF-8.
В конце мы помещаем эти два байта в массив utf8Output и возвращаем 2 вызывающей стороне, указывая, что символу требуется два байта в его представлении UTF-8. Это необходимая и правильная кодировка для символа, использующего в два раза больше бит по сравнению с однобайтовым символом. Затем у нас идут 3-байтовые символы.
// Handle three-byte characters (0x800 to 0xFFFF) if (character < 0xFFFF) { ... }
Теперь вы понимаете, что это значит. Здесь шестнадцатеричное число 0xFFFF преобразуется в 65 535 в десятичном виде. Каждая шестнадцатеричная цифра представляет собой степень числа 16. Для 0xFFFF каждая цифра — это F, что в десятичной системе равно 15 — мы это уже видели. Чтобы вычислить десятичное значение, мы оцениваем вклад каждой цифры на основе ее положения. Начнем с самого высокого разряда, который равен (15 * 16^3), что дает нам (15 * 4096 = 61 440). Далее мы вычисляем (15 * 16^2), что равно (15 * 256 = 3840). Тогда (15 * 16^1) дает (15 * 16 = 240). Наконец, (15 * 16^0) равно (15 * 1 = 15). Сложив эти результаты, получаем 61 440 + 3 840 + 240 + 15, что в сумме составляет 65 535. То есть 0xFFFF — это 65 535 в десятичной системе счисления. Учитывая это, можно предположить, что может быть три экземпляра трехбайтовых символов. Давайте рассмотрим первый случай.
if (character >= 0xD800 && character <= 0xDFFF) { // Ill-formed characters ArrayResize(utf8Output, 1); // Resize the array to hold one byte utf8Output[0] = ' '; // Replace with a space character return 1; // Return the length of the UTF-8 representation }
Здесь мы обрабатываем символы, которые попадают в диапазон Unicode от 0xD800 до 0xDFFF, которые известны как суррогатные половины и не являются допустимыми как отдельные символы. Начнем с проверки того, находится ли персонаж в этом диапазоне. Когда мы сталкиваемся с таким неправильно сформированным символом, мы сначала изменяем размер массива utf8Output так, чтобы он вмещал только один байт, гарантируя, что наш выходной массив готов к хранению только одного байта.
Затем мы заменяем недопустимый символ на пробел, устанавливая первый элемент массива utf8Output равным пробелу. Эта замена нужна для корректной обработки недопустимого ввода. Наконец, мы возвращаем 1, указывая, что представление UTF-8 этого неправильно сформированного символа имеет длину в один байт. Далее мы проверяем наличие символов эмодзи. Это означает, что мы имеем дело с символами, которые лежат в диапазоне Unicode от 0xE000 до 0xF8FF. К этим символам относятся эмодзи и другие расширенные символы.
else if (character >= 0xE000 && character <= 0xF8FF) { // Emoji characters int extendedCharacter = 0x10000 | character; // Extend the character to four bytes ArrayResize(utf8Output, 4); // Resize the array to hold four bytes utf8Output[0] = (uchar)(0xF0 | (extendedCharacter >> 18)); // Store the first byte utf8Output[1] = (uchar)(0x80 | ((extendedCharacter >> 12) & 0x3F)); // Store the second byte utf8Output[2] = (uchar)(0x80 | ((extendedCharacter >> 6) & 0x3F)); // Store the third byte utf8Output[3] = (uchar)(0x80 | (extendedCharacter & 0x3F)); // Store the fourth byte return 4; // Return the length of the UTF-8 representation }
Начнем с определения того, попадает ли символ в диапазон эмодзи. Поскольку символы, находящиеся в этом диапазоне, требуют четырехбайтового представления в UTF-8, мы сначала расширяем значение символа, выполняя побитовую операцию ИЛИ с 0x10000. Этот шаг позволяет нам корректно обрабатывать символы из дополнительных плоскостей.
Затем мы изменяем размер массива utf8Output до четырех байтов. Это гарантирует, что у нас будет достаточно места для хранения всей кодировки UTF-8 в массиве. Таким образом, расчет представления UTF-8 основан на выведении и объединении четырех частей (четырех байтов). Для первого байта мы берем extendedCharacter и сдвигаем его вправо на 18 бит. Затем мы логически объединяем (используя побитовую операцию ИЛИ, или |) это значение с 0xF0, чтобы получить соответствующие "старшие" биты для первого байта. Для второго байта мы сдвигаем extendedCharacter вправо на 12 бит и используем аналогичную технику для получения следующей части.
Аналогично мы вычисляем третий байт, сдвигая вправо расширенный символ на 6 бит и маскируя следующие 6 бит. Объединяем его с 0x80, чтобы получить первую часть третьего байта. Чтобы получить вторую часть, маскируем расширенный символ с помощью 0x3F (что дает нам последние 6 бит расширенного символа) и объединяем с 0x80. После вычисления и сохранения этих двух байтов в массиве utf8Output мы возвращаем 4, что указывает на то, что символ занимает 4 байта в UTF-8. Например, у нас может быть эмодзи-символ 1F4B0. Это эмодзи в виде мешка с деньгами.
Чтобы вычислить его десятичное представление, мы начнем с преобразования шестнадцатеричных цифр в десятичные значения. Цифра 1 в позиции 16^4 дает 1×65 536=65 536. Цифра F, которая в десятичной системе равна 15, в позиции 16^3 дает 15×4096=61 440. Цифра 4 в позиции 16^2 дает 4×256=1024. Цифра B, которая в десятичной системе равна 11, в позиции 16^1 дает 11×16=176. Наконец, цифра 0 в позиции 16^0 дает 0×1=0.
Сложив всё это, получаем 65 536 + 61 440 + 1 024 + 176 + 0=128 176. Таким образом, 0x1F4B0 преобразуется в 128 176 в десятичной системе счисления. Вы можете убедиться в этом, посмотрев на скриншот.
Наконец, мы рассмотрим символы, которые выходят за пределы определенных диапазонов, обрабатывавшихся ранее, и которым требуется трехбайтовое представление UTF-8.
else { ArrayResize(utf8Output, 3); // Resize the array to hold three bytes utf8Output[0] = (uchar)((character >> 12) | 0xE0); // Store the first byte utf8Output[1] = (uchar)(((character >> 6) & 0x3F) | 0x80); // Store the second byte utf8Output[2] = (uchar)((character & 0x3F) | 0x80); // Store the third byte return 3; // Return the length of the UTF-8 representation }
Начнем с изменения размера массива utf8Output, чтобы он мог содержать необходимые три байта. Каждый байт имеет размер 8, поэтому для хранения трех байтов нам потребуется место для 24 бит. Затем мы побайтно вычисляем каждый из трех байтов кодировки UTF-8. Первый байт определяется по верхней части символа. Чтобы вычислить второй байт, мы сдвигаем символ на 6 бит вправо, маскируем полученное значение, чтобы получить следующие 6 бит, и объединяем с 0x80, чтобы установить биты продолжения. Третий байт получаем аналогично, за исключением того, что мы не делаем никакого сдвига. Вместо этого мы маскируем, чтобы получить последние 6 бит, и объединяем их с 0x80. После определения трех байтов, которые хранятся в массиве utf8Output, мы возвращаем 3, что указывает на то, что представление охватывает три байта.
Наконец, нам необходимо обработать случаи, когда символ недопустим или не может быть правильно закодирован, заменив его заменяющим символом Unicode U+FFFD.
// Handle invalid characters by replacing with the Unicode replacement character (U+FFFD) ArrayResize(utf8Output, 3); // Resize the array to hold three bytes utf8Output[0] = 0xEF; // Store the first byte utf8Output[1] = 0xBF; // Store the second byte utf8Output[2] = 0xBD; // Store the third byte return 3; // Return the length of the UTF-8 representation
Начнем с изменения размера массива utf8Output до трех байтов, что гарантирует наличие достаточного места для замены символа. Далее мы устанавливаем байты массива utf8Output в представление U+FFFD в кодировке UTF-8. Этот символ отображается в UTF-8 как последовательность байтов 0xEF, 0xBF и 0xBD, которые являются прямыми байтами, назначенными непосредственно utf8Output, где 0xEF — первый байт, 0xBF — второй, а 0xBD — третий. Наконец, мы возвращаем 3, что означает, что представление UTF-8 заменяющего символа занимает три байта. Это полная функция, которая гарантирует, что мы сможем преобразовать символ в UTF-8. Можно также использовать более продвинутый формат UTF-16, но пока давайте не будем усложнять ситуацию. Таким образом, полный код функции выглядит следующим образом:
//+-----------------------------------------------------------------------+ //| Function to convert a ushort character to its UTF-8 representation | //+-----------------------------------------------------------------------+ int ShortToUtf8(const ushort character, uchar &utf8Output[]) { // Handle single byte characters (0x00 to 0x7F) if (character < 0x80) { ArrayResize(utf8Output, 1); // Resize the array to hold one byte utf8Output[0] = (uchar)character; // Store the character in the array return 1; // Return the length of the UTF-8 representation } // Handle two-byte characters (0x80 to 0x7FF) if (character < 0x800) { ArrayResize(utf8Output, 2); // Resize the array to hold two bytes utf8Output[0] = (uchar)((character >> 6) | 0xC0); // Store the first byte utf8Output[1] = (uchar)((character & 0x3F) | 0x80); // Store the second byte return 2; // Return the length of the UTF-8 representation } // Handle three-byte characters (0x800 to 0xFFFF) if (character < 0xFFFF) { if (character >= 0xD800 && character <= 0xDFFF) { // Ill-formed characters ArrayResize(utf8Output, 1); // Resize the array to hold one byte utf8Output[0] = ' '; // Replace with a space character return 1; // Return the length of the UTF-8 representation } else if (character >= 0xE000 && character <= 0xF8FF) { // Emoji characters int extendedCharacter = 0x10000 | character; // Extend the character to four bytes ArrayResize(utf8Output, 4); // Resize the array to hold four bytes utf8Output[0] = (uchar)(0xF0 | (extendedCharacter >> 18)); // Store the first byte utf8Output[1] = (uchar)(0x80 | ((extendedCharacter >> 12) & 0x3F)); // Store the second byte utf8Output[2] = (uchar)(0x80 | ((extendedCharacter >> 6) & 0x3F)); // Store the third byte utf8Output[3] = (uchar)(0x80 | (extendedCharacter & 0x3F)); // Store the fourth byte return 4; // Return the length of the UTF-8 representation } else { ArrayResize(utf8Output, 3); // Resize the array to hold three bytes utf8Output[0] = (uchar)((character >> 12) | 0xE0); // Store the first byte utf8Output[1] = (uchar)(((character >> 6) & 0x3F) | 0x80); // Store the second byte utf8Output[2] = (uchar)((character & 0x3F) | 0x80); // Store the third byte return 3; // Return the length of the UTF-8 representation } } // Handle invalid characters by replacing with the Unicode replacement character (U+FFFD) ArrayResize(utf8Output, 3); // Resize the array to hold three bytes utf8Output[0] = 0xEF; // Store the first byte utf8Output[1] = 0xBF; // Store the second byte utf8Output[2] = 0xBD; // Store the third byte return 3; // Return the length of the UTF-8 representation }
Вооружившись функцией кодирования, мы теперь можем закодировать наше сообщение и отправить его повторно.
double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY); double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); string msg = "🚀EA INITIALIZED ON CHART " + _Symbol + " 🚀" +"\n📊Account Status 📊" +"\nEquity: $" +DoubleToString(accountEquity,2) +"\nFree Margin: $" +DoubleToString(accountFreeMargin,2); string encloded_msg = UrlEncode(msg); msg = encloded_msg;
Здесь мы просто объявляем строковую переменную с именем encoded_msg, в которой хранится наше сообщение, закодированное в URL, и, наконец, добавляем результат к исходному сообщению, что технически перезаписывает его содержимое, а не просто объявляет другую переменную. При запуске мы получаем следующее:
Констатируем частичный успех. Мы получили сообщение в структурированном виде. Однако изначально присутствовавшие в сообщении символы эмодзи отбрасываются. Это потому, что мы их закодировали, и теперь, чтобы получить их обратно, нам нужно ввести их соответствующие форматы. Если вам не нужно их удалять, вам необходимо их жестко запрограммировать. Таким образом, фрагмент эмодзи в функции просто игнорируется. Для нас важно иметь их в соответствующем формате, чтобы их можно было автоматически кодировать.
double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY); double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); string msg = "\xF680 EA INITIALIZED ON CHART " + _Symbol + "\xF680" +"\n\xF4CA Account Status \xF4CA" +"\nEquity: $" +DoubleToString(accountEquity,2) +"\nFree Margin: $" +DoubleToString(accountFreeMargin,2); string encloded_msg = UrlEncode(msg); msg = encloded_msg;
Здесь мы представляем символ в формате "\xF***". Если за представлением следует слово, обязательно используйте пробел или обратную косую черту "\" для различения, то есть "\xF123 " или "\xF123\". При запуске мы получаем следующий результат:
Мы видим, что теперь у нас правильный формат сообщения, в котором все символы закодированы правильно. Это успех! Теперь мы можем приступить к созданию реальных сигналов.
Так как функция WebRequest не будет работать в тестере стратегий, а ожидание генерации сигнала на основе стратегии пересечения скользящих средних потребует некоторого времени для ожидания подтверждения, давайте разработаем какую-нибудь другую быструю стратегию при инициализации программы. Стратегию скользящей средней мы применим позже. Мы оцениваем предыдущий бар при инициализации и, если это бычий бар, открываем ордер на покупку. В противном случае, если это медвежий или нулевой бар, мы открываем ордер на продажу.
Фрагмент кода, используемый для логики, выглядит следующим образом:
double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); double Price_Open = iOpen(_Symbol,_Period,1); double Price_Close = iClose(_Symbol,_Period,1); bool isBuySignal = Price_Open < Price_Close; bool isSellSignal = Price_Open >= Price_Close;
Здесь мы определяем ценовые котировки, то есть цены bid и ask. Затем мы получаем цену открытия предыдущего бара с индексом 1, используя функцию iOpen, которая принимает 3 аргумента или параметра, то есть символ торгового инструмента, период и индекс бара, для которого требуется получить значение. Для получения цены закрытия используется функция iClose. Затем мы определяем логические переменные isBuySignal и isSellSignal, которые сравнивают значения цен открытия и закрытия, и если цена открытия меньше цены закрытия или цена открытия больше или равна цене закрытия, мы сохраняем флаги сигналов покупки и продажи в переменных соответственно.
Для открытия ордеров нам нужен метод.
#include <Trade/Trade.mqh>
CTrade obj_Trade;
В глобальной области видимости, желательно в верхней части кода, мы включаем торговый класс с помощью ключевого слова #include. Он даст нам доступ к классу CTrade, который мы будем использовать для создания торгового объекта. Доступ нужен для открытия сделок.
Препроцессор заменит строку #include <Trade/Trade.mqh> с содержимым файла Trade.mqh. Угловые скобки указывают, что файл Trade.mqh будет взят из стандартного каталога (обычно это каталог_установки_терминала\MQL5\Include). Текущий каталог не включен в поиск. Строку можно разместить в любом месте программы, но обычно все включения размещаются в начале исходного кода для лучшей структуры кода и удобства ссылок. Благодаря разработчикам MQL5 объявление объекта obj_Trade класса CTrade предоставит нам легкий доступ к методам, содержащимся в этом классе.
Теперь можем открывать позиции.
double lotSize = 0, openPrice = 0,stopLoss = 0,takeProfit = 0; if (isBuySignal == true){ lotSize = 0.01; openPrice = Ask; stopLoss = Bid-1000*_Point; takeProfit = Bid+1000*_Point; obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit); } else if (isSellSignal == true){ lotSize = 0.01; openPrice = Bid; stopLoss = Ask+1000*_Point; takeProfit = Ask-1000*_Point; obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit); }
Мы определяем переменные double для хранения объема торгов, цены открытия ордеров, уровней стоп-лосса и тейк-профита, а также инициализации их нулем. Чтобы открыть позиции, мы сначала проверяем, содержит ли isBuySignal флаг true, означающий, что предыдущий бар действительно был бычьим, а затем открываем позицию на покупку. Размер лота инициализируется до 0,01, цена открытия — котировка ask, уровни стоп-лосса и тейк-профита рассчитываются на основе запрашиваемой котировки, а результаты используются для открытия позиции на покупку. Аналогично, для открытия позиции на продажу значения вычисляются и используются в функции.
После открытия позиций мы можем собрать информацию о сгенерированном сигнале и открытой позиции в одном сообщении и передать ее в Telegram.
string position_type = isBuySignal ? "Buy" : "Sell"; ushort MONEYBAG = 0xF4B0; string MONEYBAG_Emoji_code = ShortToString(MONEYBAG); string msg = "\xF680 OPENED "+position_type+" POSITION." +"\n====================" +"\n"+MONEYBAG_Emoji_code+"Price = "+DoubleToString(openPrice,_Digits) +"\n\xF412\Time = "+TimeToString(iTime(_Symbol,_Period,0),TIME_SECONDS) +"\n\xF551\Time Current = "+TimeToString(TimeCurrent(),TIME_SECONDS) +"\n\xF525 Lotsize = "+DoubleToString(lotSize,2) +"\n\x274E\Stop loss = "+DoubleToString(stopLoss,_Digits) +"\n\x2705\Take Profit = "+DoubleToString(takeProfit,_Digits) +"\n_________________________" +"\n\xF5FD\Time Local = "+TimeToString(TimeLocal(),TIME_DATE) +" @ "+TimeToString(TimeLocal(),TIME_SECONDS) ; string encloded_msg = UrlEncode(msg); msg = encloded_msg;
Здесь мы создаем четкое сообщение, содержащее информацию, связанную с торговым сигналом. Мы форматируем сообщение с помощью эмодзи и других данных, которые, по нашему мнению, облегчат усвоение информации для получателей. Мы начинаем с определения того, какой перед нами сигнал - на покупку или на продажу. Это достигается с помощью использования тернарного оператора. Затем мы создаем сообщение, включающее эмодзи-изображение пачки денег, которое, по нашему мнению, подходит для сигнала на покупку или продажу. Мы использовали реальные символы эмодзи в формате ushort, а затем преобразовали код символа в строковую переменную с помощью функции ShortToString, чтобы показать, что необязательно всегда использовать строковые форматы. Как вы можете заметить, процесс преобразования занимает некоторое время и место, но если вы хотите дать имена соответствующим символам, это лучший метод.
Затем мы объединяем информацию об открытой торговой позиции в строку. Эта строка, преобразованная в сообщение, содержит сведения о сделке: ее тип, цена открытия, время сделки, текущее время, размер лота, стоп-лосс, тейк-профит и т. д. Сообщение делается визуально привлекательным и простым для интерпретации.
После составления сообщения мы вызываем функцию UrlEncode, чтобы закодировать его для безопасной передачи в URL. Мы особенно заботимся о том, чтобы все специальные символы и эмодзи были правильно обработаны и подходили для отображения в Интернете. Затем мы сохраняем закодированное сообщение в переменной с именем encloded_msg и перезаписываем закодированное сообщение исходным или, как правило, меняем их местами. При этом мы получаем следующий результат:
Как видите, мы успешно закодировали сообщение и отправили его в Telegram в целевой структуре. Полный исходный код, отвечающий за отправку, выглядит так:
//+------------------------------------------------------------------+ //| TELEGRAM_MQL5_SIGNALS_PART2.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include <Trade/Trade.mqh> CTrade obj_Trade; // Define constants for Telegram API URL, bot token, and chat ID const string TG_API_URL = "https://api.telegram.org"; // Base URL for Telegram API const string botTkn = "7456439661:AAELUurPxI1jloZZl3Rt-zWHRDEvBk2venc"; // Telegram bot token const string chatID = "-4273023945"; // Chat ID for the Telegram chat // The following URL can be used to get updates from the bot and retrieve the chat ID // CHAT ID = https://api.telegram.org/bot{BOT TOKEN}/getUpdates // https://api.telegram.org/bot7456439661:AAELUurPxI1jloZZl3Rt-zWHRDEvBk2venc/getUpdates //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { char data[]; // Array to hold data to be sent in the web request (empty in this case) char res[]; // Array to hold the response data from the web request string resHeaders; // String to hold the response headers from the web request //string msg = "EA INITIALIZED ON CHART " + _Symbol; // Message to send, including the chart symbol ////--- Simple Notification with Emoji: //string msg = "🚀 EA INITIALIZED ON CHART " + _Symbol + " 🚀"; ////--- Buy/Sell Signal with Emoji: //string msg = "📈 BUY SIGNAL GENERATED ON " + _Symbol + " 📈"; //string msg = "📉 SELL SIGNAL GENERATED ON " + _Symbol + " 📉"; ////--- Account Balance Notification: //double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE); //string msg = "💰 Account Balance: $" + DoubleToString(accountBalance, 2) + " 💰"; ////--- Trade Opened Notification: //string orderType = "BUY"; // or "SELL" //double lotSize = 0.1; // Example lot size //double price = 1.12345; // Example price //string msg = "🔔 " + orderType + " order opened on " + _Symbol + "; Lot size: " + DoubleToString(lotSize, 2) + "; Price: " + DoubleToString(price, 5) + " 🔔"; ////--- Stop Loss and Take Profit Update: //double stopLoss = 1.12000; // Example stop loss //double takeProfit = 1.13000; // Example take profit //string msg = "🔄 Stop Loss and Take Profit Updated on " + _Symbol + "; Stop Loss: " + DoubleToString(stopLoss, 5) + "; Take Profit: " + DoubleToString(takeProfit, 5) + " 🔄"; ////--- Daily Performance Summary: //double profitToday = 150.00; // Example profit for the day //string msg = "📅 Daily Performance Summary 📅; Symbol: " + _Symbol + "; Profit Today: $" + DoubleToString(profitToday, 2); ////--- Trade Closed Notification: //string orderType = "BUY"; // or "SELL" //double profit = 50.00; // Example profit //string msg = "❌ " + orderType + " trade closed on " + _Symbol + "; Profit: $" + DoubleToString(profit, 2) + " ❌"; // ////--- Account Status Update: // double accountEquity = AccountInfoDouble(ACCOUNT_EQUITY); // double accountFreeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); // string msg = "\xF680 EA INITIALIZED ON CHART " + _Symbol + "\xF680" // +"\n\xF4CA Account Status \xF4CA" // +"\nEquity: $" // +DoubleToString(accountEquity,2) // +"\nFree Margin: $" // +DoubleToString(accountFreeMargin,2); // // string encloded_msg = UrlEncode(msg); // msg = encloded_msg; double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); double Price_Open = iOpen(_Symbol,_Period,1); double Price_Close = iClose(_Symbol,_Period,1); bool isBuySignal = Price_Open < Price_Close; bool isSellSignal = Price_Open >= Price_Close; double lotSize = 0, openPrice = 0,stopLoss = 0,takeProfit = 0; if (isBuySignal == true){ lotSize = 0.01; openPrice = Ask; stopLoss = Bid-1000*_Point; takeProfit = Bid+1000*_Point; obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit); } else if (isSellSignal == true){ lotSize = 0.01; openPrice = Bid; stopLoss = Ask+1000*_Point; takeProfit = Ask-1000*_Point; obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit); } string position_type = isBuySignal ? "Buy" : "Sell"; ushort MONEYBAG = 0xF4B0; string MONEYBAG_Emoji_code = ShortToString(MONEYBAG); string msg = "\xF680 OPENED "+position_type+" POSITION." +"\n====================" +"\n"+MONEYBAG_Emoji_code+"Price = "+DoubleToString(openPrice,_Digits) +"\n\xF412\Time = "+TimeToString(iTime(_Symbol,_Period,0),TIME_SECONDS) +"\n\xF551\Time Current = "+TimeToString(TimeCurrent(),TIME_SECONDS) +"\n\xF525 Lotsize = "+DoubleToString(lotSize,2) +"\n\x274E\Stop loss = "+DoubleToString(stopLoss,_Digits) +"\n\x2705\Take Profit = "+DoubleToString(takeProfit,_Digits) +"\n_________________________" +"\n\xF5FD\Time Local = "+TimeToString(TimeLocal(),TIME_DATE) +" @ "+TimeToString(TimeLocal(),TIME_SECONDS) ; string encloded_msg = UrlEncode(msg); msg = encloded_msg; // Construct the URL for the Telegram API request to send a message // Format: https://api.telegram.org/bot{HTTP_API_TOKEN}/sendmessage?chat_id={CHAT_ID}&text={MESSAGE_TEXT} const string url = TG_API_URL + "/bot" + botTkn + "/sendmessage?chat_id=" + chatID + "&text=" + msg; // Send the web request to the Telegram API int send_res = WebRequest("POST", url, "", 10000, data, res, resHeaders); // Check the response status of the web request if (send_res == 200) { // If the response status is 200 (OK), print a success message Print("TELEGRAM MESSAGE SENT SUCCESSFULLY"); } else if (send_res == -1) { // If the response status is -1 (error), check the specific error code if (GetLastError() == 4014) { // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal Print("PLEASE ADD THE ", TG_API_URL, " TO THE TERMINAL"); } // Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM MESSAGE"); } else if (send_res != 200) { // If the response status is not 200 or -1, print the unexpected response code and error code Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError()); } return(INIT_SUCCEEDED); // Return initialization success status }
Теперь нам необходимо включить торговые сигналы, основанные на пересечениях скользящих средних. Сначала нам необходимо объявить два хэндла индикатора скользящей средней и их массивы хранения данных.
int handleFast = INVALID_HANDLE; // -1 int handleSlow = INVALID_HANDLE; // -1 double bufferFast[]; double bufferSlow[]; long magic_no = 1234567890;
Сначала мы объявляем переменные целочисленного типа данных с именами handleFast и handleSlow для размещения индикаторов быстрой и медленной скользящей средней соответственно. Мы инициализируем хэндлы значением INVALID_HANDLE, -1, что означает, что в настоящее время они не ссылаются ни на один допустимый экземпляр индикатора. Далее мы определяем два массива double - bufferFast и bufferSlow, где мы храним значение, которое извлекаем из быстрых и медленных индикаторов соответственно. Наконец, мы объявляем переменную long для хранения магического числа открываемых позиций. Вся эта логика имеет глобальный масштаб.
В функции OnInit мы инициализируем хэндлы индикаторов и устанавливаем массивы хранения как временные ряды.
handleFast = iMA(Symbol(),Period(),20,0,MODE_EMA,PRICE_CLOSE); if (handleFast == INVALID_HANDLE){ Print("UNABLE TO CREATE FAST MA INDICATOR HANDLE. REVERTING NOW!"); return (INIT_FAILED); }
Здесь мы создаем хэндл индикатора быстрой скользящей средней. Это делается с помощью функции iMA, которая вызывается с параметрами Symbol, Period, 20, 0, MODE_EMA и PRICE_CLOSE. Параметр Symbol — это встроенная функция, которая возвращает название текущего инструмента. Параметр Period возвращает текущий таймфрейм. Следующий параметр, 20, — это количество периодов скользящей средней. Четвертый параметр, 0, указывает, что скользящая средняя применяется к самым последним ценовым барам. Пятый параметр, MODE_EMA, указывает, что мы хотим рассчитать экспоненциальную скользящую среднюю (EMA). Последний параметр — PRICE_CLOSE - показывает, что мы рассчитываем скользящую среднюю на основе цен закрытия. Функция handleFast возвращает хэндл, который однозначно идентифицирует этот экземпляр скользящей средней.
После попытки создания индикатора мы проверяем корректность хэндла. Результат INVALID_HANDLE для handleFast говорит о том, что нам не удалось создать хэндл для индикатора быстрой скользящей средней. В этом случае мы выводим в журнал сообщение уровня ERROR (ошибка). Пользователь получает сообщение "UNABLE TO CREATE FAST MA INDICATOR HANDLE. REVERTING NOW!" (Не удалось создать хэндл индикатора быстрой скользящей средней. Выполняется возврат!). В сообщении четко указано, что отсутствие хэндла означает отсутствие индикатора. Поскольку без этого индикатора нет торговой системы, что делает программу бесполезной, нет смысла продолжать ее использовать. Возвращаем INIT_FAILED. Это остановит дальнейшую работу программы и удалит ее с графика.
Та же логика применима и к индикатору медленной скользящей средней.
handleSlow = iMA(Symbol(),Period(),50,0,MODE_SMA,PRICE_CLOSE); if (handleSlow == INVALID_HANDLE){ Print("UNABLE TO CREATE FAST MA INDICATOR HANDLE. REVERTING NOW!"); return (INIT_FAILED); }
Если мы распечатаем эти хэндлы индикаторов, мы получим начальное значение 10, а если маркеров индикаторов будет больше, их значение будет увеличиваться на 1 для каждого хэндла. Давайте распечатаем их и посмотрим, что получится. Сделаем это с помощью следующего кода:
Print("HANDLE FAST MA = ",handleFast); Print("HANDLE SLOW MA = ",handleSlow);
Получаем следующий результат:
Наконец, мы задаем массивы хранения данных как временные ряды, а также магическое число.
ArraySetAsSeries(bufferFast,true); ArraySetAsSeries(bufferSlow,true); obj_Trade.SetExpertMagicNumber(magic_no);
Настройка массивов как временных рядов достигается с помощью функции ArraySetAsSeries.
В функции OnDeinit освобождаем хэндлы индикатора из памяти с помощью функции IndicatorRelease, а также освобождаем массивы хранения с помощью функции ArrayFree. Это гарантирует освобождение памяти от ненужных процессов.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { // Code to execute when the expert is deinitialized IndicatorRelease(handleFast); IndicatorRelease(handleSlow); ArrayFree(bufferFast); ArrayFree(bufferSlow); }
В обработчике событий OnTick выполняем код, который будет использовать хэндлы индикаторов и проверять генерацию сигнала. Это функция, которая вызывается на каждом тике, то есть при изменении котировок цен, для получения последних цен.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { // Code to execute on every tick event ... }
Это обработчик событий, из которого нам нужно извлечь значения индикатора.
if (CopyBuffer(handleFast,0,0,3,bufferFast) < 3){ Print("UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS. REVERTING"); return; }
Сначала мы пытаемся получить данные из буфера индикатора быстрой скользящей средней, используя функцию CopyBuffer. Мы вызываем его с параметрами: handleFast, 0, 0, 3 и bufferFast. Первый параметр, handleFast, — это целевой индикатор, из которого мы получаем значения индикатора. Вторым параметром является номер буфера, из которого мы берем значения, обычно в том виде, в котором они отображаются в окне данных. Для скользящего среднего он всегда равен 0. Третий параметр — начальная позиция индекса бара, откуда мы получаем значения, 0 в данном случае означает текущий бар. Четвертый параметр — количество извлекаемых значений, то есть баров. В данном случае 3 означает первые 3 бара. Последний параметр — bufferFast - представляет собой целевой массив, в котором мы сохраняем три извлеченных значения.
Теперь проверяем, успешно ли функция извлекла запрошенные значения, то есть 3. Если возвращаемое значение меньше 3, это означает, что функция не смогла получить запрошенные данные. В таком случае мы выводим сообщение об ошибке: "UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS. REVERTING" (Невозможно получить данные для дальнейшего анализа. Возврат). Это уведомляет нас о том, что извлечение данных не удалось, и мы не можем продолжать поиск сигналов, поскольку у нас недостаточно данных для процесса. Дальнейшее выполнение этой части программы останавливается в ожидании следующего тика.
Тот же процесс выполняется для извлечения данных медленной скользящей средней.
if (CopyBuffer(handleSlow,0,0,3,bufferSlow) < 3){ Print("UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS. REVERTING"); return; }
Так как функция OnTick запускется на каждом тике, нам нужно разработать логику, которая обеспечит запуск нашего кода поиска сигнала один раз за бар. Логика представлена ниже.
int currBars = iBars(_Symbol,_Period); static int prevBars = currBars; if (prevBars == currBars) return; prevBars = currBars;
Сначала мы объявляем целочисленную переменную currBars, которая хранит рассчитанное количество текущих баров на графике для указанного торгового символа и таймфрейма. Это делается с помощью функции iBars, которая принимает всего два аргумента: symbol и period.
Затем объявим еще одну статическую целочисленную переменную prevBars для хранения общего количества предыдущих баров на графике при генерации нового бара и по-прежнему инициализируем ее значением текущих баров на графике для первого запуска функции. Использовать ее для сравнения текущего количества баров с предыдущим, чтобы определить момент появления нового бара на графике.
Наконец, используем условный оператор, чтобы проверить, равно ли текущее количество баров предыдущему. Если они равны, это означает, что новый бар не сформировался, поэтому мы прекращаем дальнейшее выполнение и используем return. В противном случае, если текущее и предыдущее количество баров не равны, это указывает на то, что образовался новый бар. В этом случае мы приступаем к обновлению переменной предыдущих баров до текущих баров, так что на следующем тике она будет равна количеству баров на графике, если только мы не перейдем к новому.
Затем мы определяем переменные, в которых мы можем легко хранить наши данные для дальнейшего анализа, как показано ниже.
double fastMA1 = bufferFast[1]; double fastMA2 = bufferFast[2]; double slowMA1 = bufferSlow[1]; double slowMA2 = bufferSlow[2];
Используя эти переменные, мы теперь можем проверять наличие пересечений и предпринимать необходимые действия.
if (fastMA1 > slowMA1 && fastMA2 <= slowMA2){ for (int i = PositionsTotal()-1; i>= 0; i--){ ulong ticket = PositionGetTicket(i); if (ticket > 0){ if (PositionSelectByTicket(ticket)){ if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == magic_no){ if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){ obj_Trade.PositionClose(ticket); } } } } } double lotSize = 0.01; double openPrice = Ask; double stopLoss = Bid-1000*_Point; double takeProfit = Bid+1000*_Point; obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit); }
Здесь мы ищем определенное условие пересечения: если последняя быстрая скользящая средняя (fastMA1) больше соответствующей медленной скользящей средней (slowMA1), а предыдущая быстрая скользящая средняя (fastMA2) была меньше или равна предыдущей медленной скользящей средней (slowMA2), то мы наблюдаем бычье пересечение, которое указывает на потенциальный сигнал к покупке.
При обнаружении бычьего пересечения мы просматриваем текущие позиции, чтобы проверить наличие открытых позиций на продажу. При необходимости мы закрываем позиции на продажу перед открытием новых позиций на покупку. Мы идем от самой последней позиции к самой ранней.
Для каждой торговой позиции мы получаем номер тикета, используя функцию PositionGetTicket. Если номер тикета больше 0, это означает, что у нас есть правильный номер тикета, и мы выбираем позицию по функции PositionSelectByTicket. Мы продолжаем проверять корректность позиции и ее принадлежность текущему символу и магическому числу. В случае продажи мы используем функцию obj_Trade.PositionClose для закрытия позиции. После закрытия всех существующих позиций на продажу мы открываем новую позицию на покупку, устанавливая параметры нашей сделки: размер лота, цену открытия, стоп-лосс и тейк-профит. После открытия позиции мы информируем пользователя, отправляя запись в журнал.
// BUY POSITION OPENED. GET READY TO SEND MESSAGE TO TELEGRAM Print("BUY POSITION OPENED. SEND MESSAGE TO TELEGRAM NOW.");
Наконец, мы отправляем сообщение точно так же, как и в разделе инициализации программы.
ushort MONEYBAG = 0xF4B0; string MONEYBAG_Emoji_code = ShortToString(MONEYBAG); string msg = "\xF680 Opened Buy Position." +"\n====================" +"\n"+MONEYBAG_Emoji_code+"Price = "+DoubleToString(openPrice,_Digits) +"\n\xF412\Time = "+TimeToString(iTime(_Symbol,_Period,0),TIME_SECONDS) +"\n\xF551\Time Current = "+TimeToString(TimeCurrent(),TIME_SECONDS) +"\n\xF525 Lotsize = "+DoubleToString(lotSize,2) +"\n\x274E\Stop loss = "+DoubleToString(stopLoss,_Digits) +"\n\x2705\Take Profit = "+DoubleToString(takeProfit,_Digits) +"\n_________________________" +"\n\xF5FD\Time Local = "+TimeToString(TimeLocal(),TIME_DATE) +" @ "+TimeToString(TimeLocal(),TIME_SECONDS) ; string encloded_msg = UrlEncode(msg); msg = encloded_msg;
Для сигнала пересечения на продажу сохраняется та же структура кода с обратными условиями.
else if (fastMA1 < slowMA1 && fastMA2 >= slowMA2){ for (int i = PositionsTotal()-1; i>= 0; i--){ ulong ticket = PositionGetTicket(i); if (ticket > 0){ if (PositionSelectByTicket(ticket)){ if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == magic_no){ if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){ obj_Trade.PositionClose(ticket); } } } } } double lotSize = 0.01; double openPrice = Bid; double stopLoss = Ask+1000*_Point; double takeProfit = Ask-1000*_Point; obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit); // SELL POSITION OPENED. GET READY TO SEND MESSAGE TO TELEGRAM Print("SELL POSITION OPENED. SEND MESSAGE TO TELEGRAM NOW.");
На данный момент структура кода практически завершена. Теперь нам нужно автоматически добавить индикаторы на график после загрузки программы для визуализации. Таким образом, в обработчике событий инициализации мы создаем логику для автоматического добавления индикаторов следующим образом:
//--- Add indicators to the chart automatically ChartIndicatorAdd(0,0,handleFast); ChartIndicatorAdd(0,0,handleSlow);
Здесь мы просто вызываем функцию ChartIndicatorAdd для добавления индикаторов на график, где первый и второй параметры указывают окно графика и подокно соответственно. Третий параметр — это хэндл индикатора, который необходимо добавить.
Таким образом, полный код обработчика событий OnTick, отвечающий за генерацию и распределение сигналов, выглядит так:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { // Code to execute on every tick event if (CopyBuffer(handleFast,0,0,3,bufferFast) < 3){ Print("UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS. REVERTING"); return; } if (CopyBuffer(handleSlow,0,0,3,bufferSlow) < 3){ Print("UNABLE TO RETRIEVE THE REQUESTED DATA FOR FURTHER ANALYSIS. REVERTING"); return; } double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); int currBars = iBars(_Symbol,_Period); static int prevBars = currBars; if (prevBars == currBars) return; prevBars = currBars; double fastMA1 = bufferFast[1]; double fastMA2 = bufferFast[2]; double slowMA1 = bufferSlow[1]; double slowMA2 = bufferSlow[2]; if (fastMA1 > slowMA1 && fastMA2 <= slowMA2){ for (int i = PositionsTotal()-1; i>= 0; i--){ ulong ticket = PositionGetTicket(i); if (ticket > 0){ if (PositionSelectByTicket(ticket)){ if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == magic_no){ if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){ obj_Trade.PositionClose(ticket); } } } } } double lotSize = 0.01; double openPrice = Ask; double stopLoss = Bid-1000*_Point; double takeProfit = Bid+1000*_Point; obj_Trade.Buy(lotSize,_Symbol,openPrice,stopLoss,takeProfit); // BUY POSITION OPENED. GET READY TO SEND MESSAGE TO TELEGRAM Print("BUY POSITION OPENED. SEND MESSAGE TO TELEGRAM NOW."); char data[]; // Array to hold data to be sent in the web request (empty in this case) char res[]; // Array to hold the response data from the web request string resHeaders; // String to hold the response headers from the web request ushort MONEYBAG = 0xF4B0; string MONEYBAG_Emoji_code = ShortToString(MONEYBAG); string msg = "\xF680 Opened Buy Position." +"\n====================" +"\n"+MONEYBAG_Emoji_code+"Price = "+DoubleToString(openPrice,_Digits) +"\n\xF412\Time = "+TimeToString(iTime(_Symbol,_Period,0),TIME_SECONDS) +"\n\xF551\Time Current = "+TimeToString(TimeCurrent(),TIME_SECONDS) +"\n\xF525 Lotsize = "+DoubleToString(lotSize,2) +"\n\x274E\Stop loss = "+DoubleToString(stopLoss,_Digits) +"\n\x2705\Take Profit = "+DoubleToString(takeProfit,_Digits) +"\n_________________________" +"\n\xF5FD\Time Local = "+TimeToString(TimeLocal(),TIME_DATE) +" @ "+TimeToString(TimeLocal(),TIME_SECONDS) ; string encloded_msg = UrlEncode(msg); msg = encloded_msg; const string url = TG_API_URL + "/bot" + botTkn + "/sendmessage?chat_id=" + chatID + "&text=" + msg; // Send the web request to the Telegram API int send_res = WebRequest("POST", url, "", 10000, data, res, resHeaders); // Check the response status of the web request if (send_res == 200) { // If the response status is 200 (OK), print a success message Print("TELEGRAM MESSAGE SENT SUCCESSFULLY"); } else if (send_res == -1) { // If the response status is -1 (error), check the specific error code if (GetLastError() == 4014) { // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal Print("PLEASE ADD THE ", TG_API_URL, " TO THE TERMINAL"); } // Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM MESSAGE"); } else if (send_res != 200) { // If the response status is not 200 or -1, print the unexpected response code and error code Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError()); } } else if (fastMA1 < slowMA1 && fastMA2 >= slowMA2){ for (int i = PositionsTotal()-1; i>= 0; i--){ ulong ticket = PositionGetTicket(i); if (ticket > 0){ if (PositionSelectByTicket(ticket)){ if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == magic_no){ if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){ obj_Trade.PositionClose(ticket); } } } } } double lotSize = 0.01; double openPrice = Bid; double stopLoss = Ask+1000*_Point; double takeProfit = Ask-1000*_Point; obj_Trade.Sell(lotSize,_Symbol,openPrice,stopLoss,takeProfit); // SELL POSITION OPENED. GET READY TO SEND MESSAGE TO TELEGRAM Print("SELL POSITION OPENED. SEND MESSAGE TO TELEGRAM NOW."); char data[]; // Array to hold data to be sent in the web request (empty in this case) char res[]; // Array to hold the response data from the web request string resHeaders; // String to hold the response headers from the web request ushort MONEYBAG = 0xF4B0; string MONEYBAG_Emoji_code = ShortToString(MONEYBAG); string msg = "\xF680 Opened Sell Position." +"\n====================" +"\n"+MONEYBAG_Emoji_code+"Price = "+DoubleToString(openPrice,_Digits) +"\n\xF412\Time = "+TimeToString(iTime(_Symbol,_Period,0),TIME_SECONDS) +"\n\xF551\Time Current = "+TimeToString(TimeCurrent(),TIME_SECONDS) +"\n\xF525 Lotsize = "+DoubleToString(lotSize,2) +"\n\x274E\Stop loss = "+DoubleToString(stopLoss,_Digits) +"\n\x2705\Take Profit = "+DoubleToString(takeProfit,_Digits) +"\n_________________________" +"\n\xF5FD\Time Local = "+TimeToString(TimeLocal(),TIME_DATE) +" @ "+TimeToString(TimeLocal(),TIME_SECONDS) ; string encloded_msg = UrlEncode(msg); msg = encloded_msg; const string url = TG_API_URL + "/bot" + botTkn + "/sendmessage?chat_id=" + chatID + "&text=" + msg; // Send the web request to the Telegram API int send_res = WebRequest("POST", url, "", 10000, data, res, resHeaders); // Check the response status of the web request if (send_res == 200) { // If the response status is 200 (OK), print a success message Print("TELEGRAM MESSAGE SENT SUCCESSFULLY"); } else if (send_res == -1) { // If the response status is -1 (error), check the specific error code if (GetLastError() == 4014) { // If the error code is 4014, it means the Telegram API URL is not allowed in the terminal Print("PLEASE ADD THE ", TG_API_URL, " TO THE TERMINAL"); } // Print a general error message if the request fails Print("UNABLE TO SEND THE TELEGRAM MESSAGE"); } else if (send_res != 200) { // If the response status is not 200 or -1, print the unexpected response code and error code Print("UNEXPECTED RESPONSE ", send_res, " ERR CODE = ", GetLastError()); } } } //+------------------------------------------------------------------+
Мы достигли нашей второй цели — отправки сигналов из торгового терминала в чат или группу Telegram. Теперь нам нужно протестировать интеграцию, чтобы убедиться в ее корректной работе и выявить любые возникающие проблемы.
Тестирование интеграции
Для проверки интеграции мы отключаем логику теста инициализации, закомментировав ее, чтобы предотвратить открытие большого количества сигналов, перейдем на меньший период, 1 минуту, и поменяем периоды индикатора на 5 и 10, чтобы генерировать более быстрые сигналы. Ниже представлены полученные результаты.
Подтверждение сигнала на продажу в терминале:
Подтверждение сигнала на продажу в Telegram:
Подтверждение сигнала на покупку в терминале:
Подтверждение сигнала на покупку в Telegram:
На изображениях видно, что интеграция проходит успешно. После обнаружения и подтверждения сигнала его данные кодируются в одно сообщение и отправляются с торгового терминала в групповой чат Telegram. Таким образом, мы успешно достигли своей цели.
Заключение
Статья внесла значительный вклад в развитие нашего MQL5-советника, интегрированного в Telegram, достигнув главной цели — отправки торговых сигналов непосредственно из торгового терминала в чат Telegram. Однако мы не ограничились лишь установлением связи между MQL5 и Telegram, как в первой части серии. Вместо этого мы сосредоточились на самих торговых сигналах, использовав популярный инструмент технического анализа — пересечение скользящих средних. Мы подробно рассмотрели логику этих сигналов, а также надежную систему, которая теперь используется для их отправки через Telegram. Результатом является значительное улучшение нашего интегрированного советника.
В этой статье мы подробно рассмотрели технические аспекты генерации и отправки этих сигналов. Мы внимательно рассмотрели, как безопасно кодировать и отправлять сообщения, как управлять хэндлами индикаторов и как совершать сделки на основе обнаруженных сигналов. Мы создали код и интегрировали его в Telegram, чтобы мы могли мгновенно получать уведомления о торговых сигналах, даже если мы находимся вдали от нашей торговой платформы. Практические примеры и подробные объяснения, представленные в этой статье, должны дать четкое представление о том, как создать нечто подобное, используя ваши торговые стратегии.
В третьей части нашей серии мы добавим еще один уровень к нашей интеграции с Telegram. На этот раз мы будем работать над отправкой скриншотов графиков в Telegram. Способность визуально анализировать рынок в контексте торговых сигналов улучшит понимание ситуации трейдерами. Текст в сочетании с визуальными данными дает еще более мощные сигналы. Это именно то, чего мы хотим здесь добиться: не просто отправлять сигналы, но также улучшить алгоритмическую торговлю и ситуационную осведомленность через торговый Telegram-канал. Оставайтесь с нами!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/15495





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
привет,
как насчет закрытых сделок?
Как советник отправляет уведомления TG?
Я новичок. Спасибо.