Обзор функций обработки событий

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

Название каждой функции соответствует смыслу события, с добавлением префикса On. Например, OnStart — это главная функция для "старта" скриптов и сервисов: она вызывается терминалом в момент размещения скрипта на графике или запуска экземпляра сервиса.

В рамках данной книги будем называть событие и соответствующий ему обработчик одинаковым именем.

В следующей таблице перечислены все типы событий и то, в каких программах они поддерживаются (indicator_small — индикатор, expert_small — эксперт, script_small — скрипт, services_small — сервис). Подробное описание событий приводится в разделах соответствующих типов программ. Причинами событий инициализации и деинициализации могут быть многие факторы: размещение программы на графике, смена её настроек, смена символа/таймфрейма графика (или шаблона, или профиля), смена счета и другие (см. раздел Особенности запуска и остановки программ разных типов).

Тип программ

Событие/Обработчик

Индикатор

Эксперт

Скрипт

Сервис

Описание

OnStart

-

-

запуск/выполнение

OnInit

+

+

-

-

инициализация после загрузки (см. подробности в разделе Особенности запуска и остановки программ разных типов)

OnDeinit

+

+

-

-

деинициализация перед остановкой и выгрузкой

OnTick

-

+

-

-

получение новой цены (тика)

OnCalculate

-

-

-

запрос на пересчет индикатора из-за получения новой цены или синхронизации старых цен

OnTimer

+

+

-

-

срабатывание таймера с заданной периодичностью

OnTrade

-

+

-

-

завершение торговой операции на сервере

OnTradeTransaction

-

+

-

-

изменение состояния торгового счета (приказов, сделок, позиций)

OnBookEvent

+

+

-

-

изменение стакана заявок

OnChartEvent

+

+

-

-

действия пользователя или MQL-программ на графике

OnTester

-

+

-

-

окончание одиночного прохода в тестере

OnTesterInit

-

+

-

-

инициализация перед оптимизацией

OnTesterDeinit

-

+

-

-

деинициализация после оптимизации

OnTesterPass

-

+

-

-

поступление данных оптимизации с агента тестирования

Обязательные обработчики помечены символом '●', опциональные — символом '+'.

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

Все MQL-программы (за исключением библиотек) обязаны иметь хотя бы один обработчик события. В противном случае компилятор выдаст ошибку "не найдена функция обработки события" ("event handling function not found").

Наличие некоторых функций-обработчиков определяет тип программы в отсутствие директив #property, устанавливающих другой тип. Например, наличие обработчика OnCalculate приводит к генерации индикатора (даже если он размещен в другой папке, например, скриптов или экспертов). Наличие обработчика OnStart (если нет OnCalculate) подразумевает создание скрипта. При этом, если в индикаторе помимо OnCalculate встретится OnStart, получим предупреждение компилятора "функция OnStart определена в программе, не являющейся скриптом" ("OnStart function defined in the non-script program").

К книге прилагается пара файлов AllInOne.mq5 и AllInOne.mqh. В заголовочном файле описаны почти пустые заготовки всех основных обработчиков событий: в них ничего нет, кроме вывода названия обработчика в журнал. Синтаксис и особенности применения каждого из обработчиков мы рассмотрим в разделах, посвященных конкретным типам MQL-программ. Смысл данного файла — предоставить поле для экспериментов с компиляцией разных типов программ, в зависимости от наличия тех или иных обработчиков и директив-свойств (#property).

Некоторые сочетания могут приводить к ошибкам или предупреждениям.

Если компиляция прошла успешно, то получившийся тип программы автоматически выводится в журнал после её загрузки за счет следующей строки:

const string type = 
   PRTF(EnumToString((ENUM_PROGRAM_TYPE)MQLInfoInteger(MQL_PROGRAM_TYPE)));

Перечисление ENUM_PROGRAM_TYPE и функцию MQLInfoInteger мы изучали в разделе Тип и лицензия программы.

Файл AllInOne.mq5, в который включен AllInOne.mqh, находится изначально в каталоге MQL5Book/Scripts/p5/, но его можно скопировать в любую другую папку, в том числе в соседние ветви Навигатора (например, в папку советников или индикаторов). Внутри файла в комментариях оставлены варианты для подключения тех или иных конфигураций сборки программы. По умолчанию, если не редактировать файл, получится, как ни странно, советник.

//+------------------------------------------------------------------+
//| Раскомментируйте следующую строку для получения сервиса          |
//| NB: также следует активировать #define _OnStart OnStart          |
//+------------------------------------------------------------------+
//#property service
  
//+------------------------------------------------------------------+
//| Раскомментируйте следующую строку для получения библиотеки       |
//+------------------------------------------------------------------+
//#property library
  
//+------------------------------------------------------------------+
//| Раскомментируйте следующую строку для скрипта или                |
//| сервиса (должно быть включено свойство #property service)        |
//+------------------------------------------------------------------+
//#define _OnStart OnStart
  
//+------------------------------------------------------------------+
//| Раскомментируйте одну из двух следующих строк для индикатора     |
//+------------------------------------------------------------------+
//#define _OnCalculate1 OnCalculate
//#define _OnCalculate2 OnCalculate
  
#include <MQL5Book/AllInOne.mqh>

Если прикрепить программу к графику, получим в журнале запись:

EnumToString((ENUM_PROGRAM_TYPE)MQLInfoInteger(MQL_PROGRAM_TYPE))=PROGRAM_EXPERT / ok
OnInit
OnChartEvent
OnTick
OnTick
OnTick
...

Также, скорее всего, начнет генерироваться поток записей из обработчика OnTick, если рынок открыт.

Если продублировать mq5-файл под другим именем и, например, раскомментировать директиву #property service, компилятор создаст сервис, но выдаст несколько предупреждений.

no OnStart function defined in the script
OnInit function is useless for scripts
OnDeinit function is useless for scripts

Первое из них, об отсутствии функции OnStart, является на самом деле существенным, потому что при создании экземпляра сервиса в нем не будет вызвана ни одна функция, а только выполнится инициализация глобальных переменных. Правда, за счет этого в журнал (закладка Эксперты в терминале) все же будет выведен тип PROGRAM_SERVICE. Но в принципе, в сервисах, также как и в скриптах, предполагается наличие функции OnStart.

Два других предупреждения возникают из-за того, что наш заголовочный файл содержит обработчики на все случаи жизни, и компилятор напоминает нам о том, что OnInit и OnDeinit бесполезны (не будут вызываться терминалом и даже не будут включены в бинарный образ программы). Разумеется, в реальных программах таких предупреждений быть не должно, то есть все обработчики должны быть задействованы, а всё лишнее — убрано из исходного кода либо физически, либо логически — с помощью директив препроцессора для условной компиляции.

Если создать еще одну копию AllInOne.mq5 и в ней не только активировать директиву #property service, но и макрос #define _OnStart OnStart, в результате его компиляции получим полностью рабочий сервис. Он при запуске не только выведет название своего типа, но и название сработавшего обработчика OnStart.

Макрос потребовался, чтобы иметь возможность по желанию включать/отключать стандартный обработчик OnStart. В тексте AllInOne.mqh данная функция описана так:

void _OnStart() // "лишнее" подчеркивание делает функцию пользовательской
{
   Print(__FUNCTION__);
}

Имя, начинающееся с подчеркивания, делает её не стандартным обработчиком, а просто пользовательской функцией с похожим прототипом. Когда мы включаем макрос, компилятор заменяет по ходу компиляции _OnStart на OnStart, и в результате получается уже стандартный обработчик. Если бы мы явным образом назвали функцию OnStart, то согласно приоритетам характеристик, определяющих тип MQL-программы (см. раздел Особенности MQL-программ различных типов), она не позволила бы получить заготовку советника (поскольку OnStart идентифицирует программу как скрипт или сервис).

Аналогичная настраиваемая компиляция при помощи макросов _OnCalculate1 или _OnCalculate2 потребовалась, чтобы по желанию "прятать" обработчик со стандартным именем OnCalculate: в противном случае, при его наличии, у нас всегда получался бы индикатор.

Действительно, если в очередной копии программы активировать макрос #define _OnCalculate1 OnCalculate, мы получим пример индикатора (хоть он пустой и ничего не делает). Как мы узнаем далее, для индикаторов существует две разных формы обработчика OnCalculate, в связи с чем они представлены под нумерованными именами (_OnCalculate1 и _OnCalculate2). Если запустить индикатор на графике, в журнале можно увидеть имена событий OnCalculate (по приходу тиков) и OnChartEvent (например, по клику мыши).

При компиляции индикатора компилятор выдаст два предупреждения:

no indicator window property is defined, indicator_chart_window is applied
no indicator plot defined for indicator

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

Очередь событий

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

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

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

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

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

Подход по организации работы программ в ответ на поступающие события называется событийно-ориентированным (event-driven). Еще его можно назвать асинхронным, потому что постановка события в очередь программы и его извлечение (вместе с обработкой) происходят в различные моменты (в идеале, разделенные микроскопическим интервалом, но идеал достижим не всегда). Однако из 4-х типов MQL-программ лишь индикаторы и эксперты в полной мере следуют этому подходу. Скрипты и сервисы имеют, по сути, только главную функцию, которая, будучи вызванной, должна либо быстро выполнить требуемое действие и завершиться, либо запустить бесконечный цикл, в котором поддерживать некую активность (например, чтение данных из сети) вплоть до остановки пользователем. Мы видели примеры таких циклов:

while(!IsStopped())
{
   полезный код
   ...
   Sleep(...);

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

Данный подход можно назвать циклическим или синхронным, или даже, если хотите, режимом "реального времени" (realtime), поскольку период "сна" можно выбирать, чтобы обеспечить постоянную частоту обработки данных, например, так:

int rhythm = 100// 100 мс, 10 раз в секунду
while(!IsStopped())
{
   const int start = (int)GetTickCount();
   полезный код
   ...
   Sleep(rhythm - ((int)GetTickCount() - start));

Разумеется, "полезный код" должен укладываться по времени в отведенное "окно".

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

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