Событие таймера: OnTimer

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

void OnTimer(void)

Событие OnTimer периодически генерируется клиентским терминалом для эксперта или индикатора, который активизировал таймер при помощи функций EventSetTimer или EventSetMillisecondTimer (см. следующий раздел).

Внимание! В зависимых индикаторах, создаваемых с помощью вызова iCustom или IndicatorCreate из других программ, таймер не работает, и событие OnTimer не генерируется. Это архитектурное ограничение MetaTrader 5.

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

Если необходимо организовать параллельную обработку данных, следует запустить одновременно несколько MQL-программ (возможно, экземпляров одной и той же программы на разных графиках или объектах-графиках) и обмениваться между ними командами и данными по собственному протоколу — например, используя пользовательские события.

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

void OnTimer()
{
   // вызваем метод MultiTimer чтобы проверить и вызвать зависимые таймеры, когда нужно
   MultiTimer::onTimer();
}

Класс MultiTimer и связанные с ним классы отдельных таймеров объединим в одном файле MultiTimer.mqh.

Базовым классом для рабочих таймеров выступит TimerNotification. Строго говоря, это мог бы быть интерфейс, но в него удобно вывести некоторые детали общей реализации: в частности, хранить значение счетчика chronometer, за счет которого мы обеспечим срабатывание таймера с неким множителем относительного периода основного таймера, а также метод для проверки момента, когда таймер должен сработать isTimeCome. Поэтому TimerNotification представляет собой абстрактный класс. В нем отсутствуют реализации двух виртуальных методов: notify — для действий при срабатывании таймера и getInterval для получения множителя, определяющего период конкретного таймера относительно периода основного таймера.

class TimerNotification
{
protected:
   int chronometer// счетчик проверок таймера (вызовов isTimeCome)
public:
   TimerNotification(): chronometer(0)
   {
   }
   
   // рабочее событие таймера
   // чистый виртуальный метод, требуется описать в наследниках
   virtual void notify() = 0;
   // возвращает период таймера (его можно изменить на ходу)
   // чистый виртуальный метод, требуется описать в наследниках
   virtual int getInterval() = 0;
   // проверка, не пора ли таймеру сработать, и если да - вызов notify
   virtual bool isTimeCome()
   {
      if(chronometer >= getInterval() - 1)
      {
         chronometer = 0// сбрасываем счетчик
         notify();        // уведомляем прикладной код
         return true;
      }
      
      ++chronometer;
      return false;
   }
};

Вся логика "зашита" в методе isTimeCome. При каждом его вызове инкрементируется счетчик chronometer, и если он достиг последней итерации согласно методу getInterval, то вызывается метод notify уведомления прикладного кода.

Например, если основной таймер запущен с периодом 1 секунда (EventSetTimer(1)), то объект-наследник TimerNotification, который будет возвращать 5 из метода getInterval, будет получать вызовы своего метода notify каждые 5 секунд.

Управлять такими объектами-таймерами будет, как мы уже сказали, объект-менеджер MultiTimer. Он нужен в единственном числе. Поэтому его конструктор объявлен защищенным, а единственный экземпляр создается статически внутри класса.

class MultiTimer
{
protected:
   static MultiTimer _mainTimer;
   
   MultiTimer()
   {
   }
   ...

Внутри этого класса организуем хранение массива объектов TimerNotification (о том, как он заполняется, — через пару абзацев). При наличии массива легко написать метод checkTimers, который проверяет в цикле все логические таймеры. Для доступа извне это метод дублируется публичным статическим методом onTimer, который мы уже видели в глобальном обработчике OnTimer. Поскольку единственный экземпляр менеджера создается статически, мы можем обращаться к нему из статического метода.

   ...
   TimerNotification *subscribers[];
   
   void checkTimers()
   {
      int n = ArraySize(subscribers);
      for(int i = 0i < n; ++i)
      {
         if(CheckPointer(subscribers[i]) != POINTER_INVALID)
         {
            subscribers[i].isTimeCome();
         }
      }
   }
   
public:
   static void onTimer()
   {
      _mainTimer.checkTimers();
   }
   ...

Для добавления объекта TimerNotification в массив subscribers предусмотрен метод bind.

   void bind(TimerNotification &tn)
   {
      int in = ArraySize(subscribers);
      for(i = 0i < n; ++i)
      {
         if(subscribers[i] == &tnreturn// уже есть такой объект
         if(subscribers[i] == NULLbreak// нашли пустой слот
      }
      if(i == n)
      {
         ArrayResize(subscribersn + 1);
      }
      else
      {
         n = i;
      }
      subscribers[n] = &tn;
   }

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

   void unbind(TimerNotification &tn)
   {
      const int n = ArraySize(subscribers);
      for(int i = 0i < n; ++i)
      {
         if(subscribers[i] == &tn)
         {
            subscribers[i] = NULL;
            return;
         }
      }
   }

Обратите внимание, что менеджер не становится владельцем объекта-таймера и не пытается вызвать для него delete. Если вы собираетесь регистрировать в менеджере динамически распределенные объекты таймеров, можно добавить внутрь if, перед обнулением, примерно такой код:

            if(CheckPointer(subscribers[i]) == POINTER_DYNAMICdelete subscribers[i];

Теперь осталось понять, как удобно организовать вызовы bind/unbind, чтобы не нагружать этими утилитарными операциями прикладной код. Если делать это "вручную", то легко где-нибудь забыть создать или наоборот удалить таймер.

Разработаем класс SingleTimer, производный от TimerNotification, в котором реализуем вызовы bind и unbind из конструктора и деструктора, соответственно. Кроме того, опишем в нем переменную multiplier для хранения периода таймера.

   class SingleTimerpublic TimerNotification
   {
   protected:
      int multiplier;
      MultiTimer *owner;
   
   public:
      // создаем таймер с указанным множителем базового периода, опционально на паузе
      // автоматически регистрируем объект в менеджере
      SingleTimer(const int mconst bool paused = false): multiplier(m)
      {
         owner = &MultiTimer::_mainTimer;
         if(!pausedowner.bind(this);
      }
   
      // автоматически отключаем объект от менеджера
      ~SingleTimer()
      {
         owner.unbind(this);
      }
   
      // возвращаем период таймера
      virtual int getInterval() override 
      {
         return multiplier;
      }
   
      // приостанавливаем работу этого таймера
      virtual void stop()
      {
         owner.unbind(this);
      }
   
      // возобновляем работу этого таймера
      virtual void start()
      {
         owner.bind(this);
      }
   };

Второй параметр конструктора (paused) позволяет создать объект, но не запускать таймер моментально. Такой отложенный таймер можно затем активировать с помощью метода start.

Схема подписки одних объектов на события в других является одним из популярных паттернов проектирования в ООП и называется "publisher/subscriber".

Важно отметить, что этот класс тоже является абстрактным, потому что в нем не реализован метод notify. На основе SingleTimer опишем классы таймеров с дополнительным функционалом.

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

class CountableTimerpublic MultiTimer::SingleTimer
{
protected:
   const uint repeat;
   uint count;
   
public:
   CountableTimer(const int mconst uint r = UINT_MAXconst bool paused = false):
      SingleTimer(mpaused), repeat(r), count(0) { }
   
   virtual bool isTimeCome() override
   {
      if(count >= repeat && repeat != UINT_MAX)
      {
         stop();
         return false;
      }
      // делегируем проверку времени родительскому классу,
      // увеличиваем свой счетчик, только если таймер сработал (вернул true)
      return SingleTimer::isTimeCome() && (bool)++count;
   }
   // сбрасываем свой счетчик при остановке
   virtual void stop() override
   {
      SingleTimer::stop();
      count = 0;
   }
 
   uint getCount() const
   {
      return count;
   }
   
   uint getRepeat() const
   {
      return repeat;
   }
};

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

// MultipleTimers.mq5 
class MyCountableTimerpublic CountableTimer
{
public:
   MyCountableTimer(const int sconst uint r = UINT_MAX):
      CountableTimer(sr) { }
   
   virtual void notify() override
   {
      Print(__FUNCSIG__multiplier" "count);
   }
};

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

Второй класс, производный от SingleTimer, назовем FunctionalTimer. Его назначение — обеспечить простую реализацию таймера для тех, кому нравится функциональный стиль программирования и не хочется описывать производные классы. Конструктор класса FunctionalTimer будет принимать помимо периода указатель на функцию специального типа TimerHandler.

// MultiTimer.mqh
typedef bool (*TimerHandler)(void);
   
class FunctionalTimerpublic MultiTimer::SingleTimer
{
   TimerHandler func;
public:
   FunctionalTimer(const int mTimerHandler f):
      SingleTimer(m), func(f) { }
      
   virtual void notify() override
   {
      if(func != NULL)
      {
         if(!func())
         {
            stop();
         }
      }
   }
};

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

// MultiTimer.mqh
#define OnTimerCustom(POnTimer##P(); \
FunctionalTimer ft##P(POnTimer##P); \
bool OnTimer##P()

Тогда в прикладном коде можно написать так:

// MultipleTimers.mq5
bool OnTimerCustom(3)
{
   Print(__FUNCSIG__);
   return true;        // продолжить работу таймера
}

Эта конструкция объявляет таймер с периодом 3 и набором инструкций внутри скобок (здесь просто печать в журнал). Если эта функция вернет false, данный таймер будет остановлен.

Рассмотрим индикатор MultipleTimers.mq5 подробнее. Поскольку в нем не предусмотрено визуализации, укажем количество диаграмм, равное нулю.

#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

Для использования классов логических таймеров включим заголовочный файл MultiTimer.mqh и добавим входную переменную для периода базового (глобального) таймера.

#include <MQL5Book/MultiTimer.mqh>
   
input int BaseTimerPeriod = 1;

Запуск базового таймера производится в OnInit.

void OnInit()
{
   Print(__FUNCSIG__" "BaseTimerPeriod" Seconds");
   EventSetTimer(BaseTimerPeriod);
}

Напомним, что работу всех логических таймеров обеспечивает перехват глобального события OnTimer.

void OnTimer()
{
   MultiTimer::onTimer();
}

В дополнение к прикладному классу таймера MyCountableTimer, приведенному выше, опишем еще один класс приостановленного таймера MySuspendedTimer.

class MySuspendedTimerpublic CountableTimer
{
public:
   MySuspendedTimer(const int sconst uint r = UINT_MAX):
      CountableTimer(srtrue) { }
   virtual void notify() override
   {
      Print(__FUNCSIG__multiplier" "count);
      if(count == repeat - 1// выполняемся последний раз
      {
         Print("Forcing all timers to stop");
         EventKillTimer();
      }
   }
};

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

Теперь покажем, как описаны (в глобальном контексте) объекты разных таймеров этих двух классов.

MySuspendedTimer st(15);
MyCountableTimer t1(2);
MyCountableTimer t2(4);

Таймер st класса MySuspendedTimer имеет период 1 (1 * BaseTimerPeriod) и должен остановиться после 5 срабатываний.

Таймеры t1 и t2 класса MyCountableTimer имеют периоды 2 (2 * BaseTimerPeriod) и 4 (4 * BaseTimerPeriod), соответственно. При значении по умолчанию BaseTimerPeriod = 1 все периоды обозначают секунды. Данные два таймера запускаются сразу после старта программы.

Также создадим два таймера в функциональном стиле.

bool OnTimerCustom(5)
{
   Print(__FUNCSIG__);
   st.start();         // запускаем отложенный таймер
   return false;       // а данный объект-таймер останавливаем
}
   
bool OnTimerCustom(3)
{
   Print(__FUNCSIG__);
   return true;        // этот таймер продолжает работать
}

Обратите внимание, что задача OnTimerCustom5 одна — через 5 периодов после старта программы запустить отложенный таймер st, а свое выполнение прекратить. Учитывая, что отложенный таймер должен деактивировать все таймеры через 5 периодов, получим 10 секундную активность программы при настройках по умолчанию.

Таймер OnTimerCustom3 должен успеть сработать за это время 3 раза.

Итак, у нас имеется 5 таймеров с разными периодами: 1, 2, 3, 4, 5 секунд.

Проанализируем пример того, что выводится в журнал (справа схематично показаны временные метки).

                                                //   время
17:08:45.174  void OnInit() 1 Seconds             |
17:08:47.202  void MyCountableTimer::notify()2 0    |
17:08:48.216  bool OnTimer3()                        |
17:08:49.230  void MyCountableTimer::notify()2 1      |
17:08:49.230  void MyCountableTimer::notify()4 0      |
17:08:50.244  bool OnTimer5()                          |
17:08:51.258  void MyCountableTimer::notify()2 2        |
17:08:51.258  bool OnTimer3()                           |
17:08:51.258  void MySuspendedTimer::notify()1 0        |
17:08:52.272  void MySuspendedTimer::notify()1 1         |
17:08:53.286  void MyCountableTimer::notify()2 3          |
17:08:53.286  void MyCountableTimer::notify()4 1          |
17:08:53.286  void MySuspendedTimer::notify()1 2          |
17:08:54.300  bool OnTimer3()                              |
17:08:54.300  void MySuspendedTimer::notify()1 3           |
17:08:55.314  void MyCountableTimer::notify()2 4            |
17:08:55.314  void MySuspendedTimer::notify()1 4            |
17:08:55.314  Forcing all timers to stop                    |

Первое сообщение от 2-х секундного таймера поступает, как и ожидалось, примерно через 2 секунды после старта ("примерно" — потому что аппаратный таймер имеет ограничение по точности и, кроме того, на выполнение оказывает влияние прочая загрузка компьютера). Еще через секунду в первый раз срабатывает 3-х секундный таймер. Второе срабатывание 2-х секундного таймера совпадает с первым выводом от 4-х секундного. После однократного выполнения 5-ти секундного таймера в логе начинают регулярно появляться сообщения от 1-секундного таймера (его счетчик увеличивается от 0 до 4). На последней своей итерации он останавливает все таймеры.