计时器事件:OnTimer

OnTimer事件是 MQL5 程序支持的标准事件之一(请参阅 事件处理函数概述章节)。要在程序代码中接收计时器事件,应声明如下原型的函数。

void OnTimer(void)

对于通过 EventSetTimerEventSetMillisecondTimer 函数激活计时器的 EA 交易或指标,客户端终端会定期生成 OnTimer事件(请参阅下一节)。

注意!在通过其他程序调用 iCustomIndicatorCreate 创建的从属指标中,计时器无效,且不会生成OnTimer 事件。这是 MetaTrader 5 架构方面的局限性所致。

需要理解的是,启用计时器和OnTimer处理程序不会使 MQL 程序成为多线程程序。每个 MQL 程序最多分配一个线程(同一交易品种上的指标甚至可以与其他指标共享一个线程),因此OnTimer和其他处理程序的调用始终按照事件队列的顺序依次进行。如果某个处理程序(包括 OnTimer)开始执行冗长的计算,这将暂停所有其他事件和程序代码段的执行。

如果需要组织并行数据处理,则应同时运行多个 MQL 程序(例如在不同 图表图表对象上运行同一程序的多个实例),并使用自定义协议(例如使用 自定义事件)在它们之间交换命令和数据。

作为示例,我们将创建能够在一个程序中组织多个逻辑计时器的类。所有逻辑计时器的周期将被设置为基本周期的整数倍,即单一硬件计时器向标准处理程序OnTimer提供事件的周期。在该处理程序中,我们必须调用新的 MultiTimer类的某种方法,该类将管理所有逻辑计时器。

void OnTimer()
{
   // call the MultiTimer method to check and call dependent timers when needed
   MultiTimer::onTimer();
}

MultiTimer类和各个计时器的相关类将组合在一个文件 MultiTimer.mqh 中。

工作计时器的基类将是TimerNotification。严格来说,这可以是一个接口,但将通用实现的部分细节输出到其中会很方便:特别是存储计数器 chronometer的读数,我们将使用它来确保计时器按照主计时器相对周期的特定倍数触发,同时采用isTimeCome 方法用于校验计时器应触发时机。这是TimerNotification为什么是抽象类的原因。它缺少两个虚方法的实现:notify- 计时器触发时执行的操作,getInterval - 用于获取设定单个计时器周期相对于主计时器周期倍率的乘数。

class TimerNotification
{
protected:
   int chronometer// counter of timer checks (isTimeCome calls)
public:
   TimerNotification(): chronometer(0)
   {
   }
   
   // timer work event
   // pure virtual method, it is required to be described in the heirs
   virtual void notify() = 0;
   // returns the period of the timer (it can be changed on the go)
   // pure virtual method, it is required to be described in the heirs
   virtual int getInterval() = 0;
   // check if it's time for the timer to fire, and if so, call notify
   virtual bool isTimeCome()
   {
      if(chronometer >= getInterval() - 1)
      {
         chronometer = 0// reset the counter
         notify();        // notify application code
         return true;
      }
      
      ++chronometer;
      return false;
   }
};

所有逻辑都包含在isTimeCome方法中。每次调用该方法时,chronometer计数器都会递增,如果根据 getInterval 方法达到最后一次迭代,则调用 notify 方法通知应用程序代码。

例如,如果主计时器以 1 秒为周期启动(EventSetTimer(1)),则当子类对象 TimerNotification,getInterval 返回 5 时,其 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对象通过 bind 方法被添加到 subscribers 数组中。

   void bind(TimerNotification &tn)
   {
      int in = ArraySize(subscribers);
      for(i = 0i < n; ++i)
      {
         if(subscribers[i] == &tnreturn// there is already such an object
         if(subscribers[i] == NULLbreak// found an empty slot
      }
      if(i == n)
      {
         ArrayResize(subscribersn + 1);
      }
      else
      {
         n = i;
      }
      subscribers[n] = &tn;
   }

该方法可防止对象被重复添加,并且在可能的情况下,如果数组中存在空元素,会将指针放置在该空元素中,从而避免数组扩容。当使用 unbind方法移除任意 TimerNotification 对象时,数组中可能会出现空元素(计时器可能仅偶尔使用)。

   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 调用,以免让应用代码被这些工具性操作拖累。如果“手动”处理,很容易忘记在某处创建计时器,或者相反,忘记删除计时器。

我们来开发一个从 TimerNotification派生的 SingleTimer 类,分别在构造函数和析构函数中实现 bindunbind 调用。此外,我们在该类中定义了 multiplier变量来存储计时器周期。

   class SingleTimerpublic TimerNotification
   {
   protected:
      int multiplier;
      MultiTimer *owner;
   
   public:
      // creating a timer with the specified base period multiplier, optionally paused
      // automatically register the object in the manager
      SingleTimer(const int mconst bool paused = false): multiplier(m)
      {
         owner = &MultiTimer::_mainTimer;
         if(!pausedowner.bind(this);
      }
   
      // automatically disconnect the object from the manager
      ~SingleTimer()
      {
         owner.unbind(this);
      }
   
      // return timer period
      virtual int getInterval() override 
      {
         return multiplier;
      }
   
      // pause this timer
      virtual void stop()
      {
         owner.unbind(this);
      }
   
      // resume this timer
      virtual void start()
      {
         owner.bind(this);
      }
   };

构造函数的第二个参数(paused)允许创建对象,但不立即启动计时器。这种延迟启动的计时器后续可通过 start方法激活。

某些对象订阅其他对象事件的机制是 OOP 中常见的设计模式之一,被称为“发布者/订阅者”模式。

要注意的是,该类也是抽象类,因为它未实现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;
      }
      // delegate the time check to the parent class,
      // increment our counter only if the timer fired (returned true)
      return SingleTimer::isTimeCome() && (bool)++count;
   }
   // reset our counter on stop
   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;        // continue the timer
}

这种结构声明了一个周期为 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// execute last time
      {
         Print("Forcing all timers to stop");
         EventKillTimer();
      }
   }
};

稍后我们将演示其启动方式。此处还需重点指出,在达到指定操作次数后,该计时器将通过调用 EventKillTimer终止所有计时器。

下面我们来展示(在全局上下文中)如何定义这两类的不同计时器对象。

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

MySuspendedTimer类的 st 计时器周期为 1(1*BaseTimerPeriod),并应在执行 5 次操作后停止。

MyCountableTimer类的 t1t2 计时器周期分别为 2(2 * BaseTimerPeriod)和 4(4 * BaseTimerPeriod)。在默认值BaseTimerPeriod = 1的情况下,所有周期均以秒为单位。这两个计时器会在程序启动后立即开始运行。

我们还将以函数式样式创建两个计时器。

bool OnTimerCustom(5)
{
   Print(__FUNCSIG__);
   st.start();         // start delayed timer
   return false;       // and stop this timer object
}
   
bool OnTimerCustom(3)
{
   Print(__FUNCSIG__);
   return true;        // this timer keeps running
}

请注意,OnTimerCustom5仅有一项任务:在程序启动 5 个周期后,它需要启动延迟计时器st并终止自身执行。考虑到延迟计时器应在 5 个周期后停用所有计时器,在默认设置下,程序将运行 10 秒。

在此期间,OnTimerCustom3计时器应触发 3 次。

因此,我们有 5 个不同周期的计时器:1 秒、2 秒、3 秒、4 秒和 5 秒。

我们将分析一个输出到日志的示例(右侧以示意图形式显示时间戳)。

                                                // time
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 秒到达(我们说“约”是因为硬件计时器存在精度限制,此外,计算机的其他负载也会影响执行)。一秒后,三秒计时器首次触发。两秒计时器的第二次触发与四秒计时器的第一次输出同时发生。五秒计时器执行一次后,一秒计时器的消息开始定期出现在日志中(其计数器从 0 增大至 4)。在最后一次迭代中,它将停止所有计时器运行。