Генерация пользовательских событий

Помимо стандартных событий терминал поддерживает возможность программной генерации собственных событий, суть и информационное наполнение которых определяет  MQL-программа. Мы называем их пользовательскими или настраиваемыми (custom). Такие события попадают в общую очередь событий графика и могут обрабатываться в функции OnChartEvent всеми заинтересованными программами.

Под пользовательские события зарезервирован специальный диапазон целочисленных идентификаторов в количестве 65536: от CHARTEVENT_CUSTOM до CHARTEVENT_CUSTOM_LAST включительно. Иными словами пользовательское событие должно иметь идентификатор CHARTEVENT_CUSTOM + n, где n находится в пределах от 0 до 65535. CHARTEVENT_CUSTOM_LAST как раз равно CHARTEVENT_CUSTOM + 65535.

Для отправки пользовательского события на график существует функция EventChartCustom.

bool EventChartCustom(long chartId, ushort customEventId,
  long lparam, double dparam, string sparam)

chartId — идентификатор графика-получателя события, 0 означает текущий график. customEventId — идентификатор события (выбирается разработчиком MQL-программы). Этот идентификатор автоматически добавляется к значению CHARTEVENT_CUSTOM и преобразуется к целому типу — именно это значение поступит в обработчик OnChartEvent первым аргументом. Остальные параметры EventChartCustom соответствуют стандартным параметрам событий в OnChartEvent с типами long, double и string, и могут содержать произвольную информацию.

Функция возвращает true в случае удачной постановки пользовательского события в очередь или false в случае ошибки (код ошибки станет доступным в _LastError).

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

Чуть ранее, в главе про индикаторы, мы рассматривали мультивалютные индикаторы, но не обратили внимания на важный момент: несмотря на то, что индикаторы обрабатывали котировки разных символов, сам расчет запускался в обработчике OnCalculate, который срабатывает по приходу нового тика только одного символа — рабочего символа графика. Получается, что тики других инструментов по сути пропускаются. Например, если индикатор работает на символе A, по приходу его тика мы просто берем последние известные тики прочих инструментов (B, C, D), но вполне вероятно, что по каждому из них успели проскочить другие тики.

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

К сожалению, стандартное событие прихода нового тика работает в MQL5 только для одного символа — рабочего символа текущего графика. Как мы знаем, в индикаторах в такие моменты вызывается обработчик OnCalculate, а в экспертах за это отвечает обработчик OnTick.

Следовательно, необходимо изобрести некий механизм, чтобы MQL-программа могла получать уведомления о тиках на всех интересующих её инструментах. В этом нам и помогут пользовательские события. Разумеется, для программ, анализирующих только один инструмент, это не нужно.  

Мы сейчас разработаем пример индикатора EventTickSpy.mq5, который, будучи запущенным на конкретном символе X, сможет отправлять из своей функции OnCalculate уведомления о тике с помощью EventChartCustom. В результате, в обработчике OnChartEvent, который специально подготовлен для приема таких уведомлений, можно будет собрать уведомления из разных экземпляров индикатора с разных символов.

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

Прежде всего, придумаем для индикатора номер пользовательского события. Поскольку мы собираемся посылать уведомления о тиках множества разных символов из некоторого заданного списка, здесь можно выбрать разные тактики. Например, можно выбрать один идентификатор события, а номер символа в списке и/или само название символа передавать в параметрах lparam и sparam, соответственно. Или можно взять некую константу (больше и равную CHARTEVENT_CUSTOM), а номера событий получать, прибавляя к этой константе номер символа (тогда у нас остаются свободными все параметры, в частности, lparam и dparam, и их можно использовать для передачи цен Ask, Bid или чего-то еще).

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

#define TICKSPY 0xFEED // 65261

Это значение взято специально как довольно сильно отстоящее от первого разрешенного CHARTEVENT_CUSTOM.

При первоначальном (интерактивном) запуске индикатора пользователь должен указать перечень инструментов, тики которых он хочет отслеживать. Для этой цели опишем входную строковую переменную SymbolList — в ней символы указываются через запятую.

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

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

input string SymbolList = "EURUSD,GBPUSD,XAUUSD,USDJPY"// Список символов, через запятую (пример)
input ushort Message = TICKSPY;                          // Пользовательское сообщение
input long Chart = 0;                                    // Принимающий график (не редактировать)

В параметре SymbolList для примера уже указан список с 4-мя распространенными инструментами. Отредактируйте его при необходимости в соответствии с вашим Обзором рынка.

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

string Symbols[];
   
void OnInit()
{
   PrintFormat("Starting for chart %lld, msg=0x%X [%s]"ChartMessageSymbolList);
   if(Chart == 0)
   {
      if(StringLen(SymbolList) > 0)
      {
         const int n = StringSplit(SymbolList, ',', Symbols);
         for(int i = 0i < n; ++i)
         {
            if(Symbols[i] != _Symbol)
            {
               ResetLastError();
               // запускаем этот же индикатор на другом символе с другими настройками,
               // в частности, передаем наш ChartID, чтобы получать обратно уведомления
               iCustom(Symbols[i], PERIOD_CURRENTMQLInfoString(MQL_PROGRAM_NAME),
                  ""MessageChartID());
               if(_LastError != 0)
               {
                  PrintFormat("The symbol '%s' seems incorrect"Symbols[i]);
               }
            }
         }
      }
      else
      {
         Print("SymbolList is empty: tracking current symbol only!");
         Print("To monitor other symbols, fill in SymbolList, i.e."
            " 'EURUSD,GBPUSD,XAUUSD,USDJPY'");
      }
   }
}

В начале OnInit в журнал выводится информация о запускаемом экземпляре индикатора, чтобы было понятно, что происходит.

Если бы мы выбрали вариант с раздельными кодами событий для каждого символа, то должны были бы вызывать iCustom следующим образом (добавляем i к Message):

   iCustom(Symbols[i], PERIOD_CURRENTMQLInfoString(MQL_PROGRAM_NAME), "",
      Message + iChartID());

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

В функции OnCalculate, которая вызывается при получении нового тика, отправляем на график Chart пользовательское событие Message с помощью вызова EventChartCustom. При этом параметр lparam не используется (равен 0), в параметре dparam мы передаем текущую (последнюю) цену price[0] (это Bid или Last, в зависимости от того, по какому типу цены строится график: она же — цена последнего обработанного графиком тика), а в параметре sparam — название символа.

int OnCalculate(const int rates_totalconst int prev_calculated,
   const intconst double &price[])
{
   if(prev_calculated)
   {
      ArraySetAsSeries(pricetrue);
      if(Chart > 0)
      {
         // отправляем уведомление о тике на родительский график
         EventChartCustom(ChartMessage0price[0], _Symbol);
      }
      else
      {
         OnSymbolTick(_Symbolprice[0]);
      }
   }
  
   return rates_total;
}

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

Единственная задача OnSymbolTick в данной демонстрации — вывести в журнал название символа и новую цену.

void OnSymbolTick(const string &symbolconst double price)
{
   Print(symbol" "DoubleToString(price,
      (int)SymbolInfoInteger(symbolSYMBOL_DIGITS)));
}

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

void OnChartEvent(const int id,
   const long &lparamconst double &dparamconst string &sparam)
{
   if(id >= CHARTEVENT_CUSTOM + Message)
   {
      OnSymbolTick(sparamdparam);
      // ИЛИ (если использовать диапазон пользовательских событий):
      // OnSymbolTick(Symbols[id - CHARTEVENT_CUSTOM - Message], dparam);
   }
}

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

Запустим индикатор на графике EURUSD с настройками по умолчанию, включая тестовый список "EURUSD,GBPUSD,XAUUSD,USDJPY". Вот фрагмент журнала:

16:45:48.745 (EURUSD,H1) Starting for chart 0, msg=0xFEED [EURUSD,GBPUSD,XAUUSD,USDJPY]
16:45:48.761 (GBPUSD,H1) Starting for chart 132358585987782873, msg=0xFEED []
16:45:48.761 (USDJPY,H1) Starting for chart 132358585987782873, msg=0xFEED []
16:45:48.761 (XAUUSD,H1) Starting for chart 132358585987782873, msg=0xFEED []
16:45:48.777 (EURUSD,H1) XAUUSD 1791.00
16:45:49.120 (EURUSD,H1) EURUSD 1.13068 *
16:45:49.135 (EURUSD,H1) USDJPY 115.797
16:45:49.167 (EURUSD,H1) XAUUSD 1790.95
16:45:49.167 (EURUSD,H1) USDJPY 115.796
16:45:49.229 (EURUSD,H1) USDJPY 115.797
16:45:49.229 (EURUSD,H1) XAUUSD 1790.74
16:45:49.369 (EURUSD,H1) XAUUSD 1790.77
16:45:49.572 (EURUSD,H1) GBPUSD 1.35332
16:45:49.572 (EURUSD,H1) XAUUSD 1790.80
16:45:49.791 (EURUSD,H1) XAUUSD 1790.80
16:45:49.791 (EURUSD,H1) USDJPY 115.796
16:45:49.931 (EURUSD,H1) EURUSD 1.13069 *
16:45:49.931 (EURUSD,H1) XAUUSD 1790.86
16:45:49.931 (EURUSD,H1) USDJPY 115.795
16:45:50.056 (EURUSD,H1) USDJPY 115.793
16:45:50.181 (EURUSD,H1) XAUUSD 1790.88
16:45:50.321 (EURUSD,H1) XAUUSD 1790.90
16:45:50.399 (EURUSD,H1) EURUSD 1.13066 *
16:45:50.727 (EURUSD,H1) EURUSD 1.13067 *
16:45:50.773 (EURUSD,H1) GBPUSD 1.35334

Обратите внимание, что в колонке с (символом,таймфреймом) — источником записи мы вначале видим стартующие экземпляры индикаторов на 4-х запрошенных символах.

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

В левой колонке для справки сохранены временные метки.

Места, где две цены у двух последовательных событий с одного и того же символа совпадают, как правило, означают, что в тике менялась цена Ask (мы её просто не выводим здесь).

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