Skip to content

定时器的使用

对于许多实际应用任务而言,能够按照预定的时间间隔执行操作是非常重要的。在 MQL5 里,定时器这一系统时间计数器提供了此功能,它可以被设置为定期向 MQL 程序发送通知。

MQL5 API 中有几个用于设置或取消定时器通知的函数,分别是 EventSetTimerEventSetMillisecondTimerEventKillTimer。这些通知会以特殊类型的事件形式进入程序,在源代码里,OnTimer 处理程序专门用于处理此类事件。本章将详细讨论这组函数。

需要注意的是,在 MQL5 中,只有在图表上运行的交互式程序(即指标和智能交易系统)才能接收事件。脚本和服务不支持任何事件,包括来自定时器的事件。

不过,在“时间处理函数”这一章节里,我们已经涉及到了相关主题:

  • 获取当前本地或服务器时钟的时间戳:可以使用 TimeLocalTimeCurrent 函数。
  • 暂停程序执行:通过 Sleep 函数能让程序在指定时间段内暂停执行。
  • 获取计算机系统时间计数器的状态:可以获取从操作系统启动开始计算的时间(使用 GetTickCount),或者自 MQL 程序启动以来的时间(使用 GetMicrosecondCount)。

这些功能所有类型的 MQL 程序都能使用。

在前面的章节中,尽管我们现在才正式介绍定时器函数,但其实已经多次使用过它们。由于定时器事件仅在指标或智能交易系统中可用,所以在掌握这些程序的创建方法之前,研究定时器会有一定难度。在我们学会创建指标之后,定时器的相关内容就成了顺理成章的后续学习内容。

基本上,我们主要利用定时器来等待时间序列的构建。在“数据等待”“多货币和多时间框架指标”“支持多交易品种和时间框架”“使用内置指标”等章节中都能找到这类示例。

此外,在“删除指标实例”章节的指标“动画”演示里,我们还设置了每 5 秒切换一次从属指标类型的定时器。

开启和关闭定时器:EventSetTimer/EventKillTimer

MQL5允许你开启或关闭标准定时器,以执行任何预定的操作。为此提供了两个函数:EventSetTimer和EventKillTimer。

bool EventSetTimer(int seconds)

该函数告知客户端终端,对于此智能交易系统或指标,需要按照指定的频率(以秒为单位,由参数seconds设置)从定时器生成事件。

函数返回操作成功(true)或出错(false)的标志。可以从_LastError获取错误代码。

为了处理定时器事件,智能交易系统或指标的代码中必须有OnTimer函数。第一次定时器事件不会在调用EventSetTimer后立即发生,而是在seconds秒之后。

对于每个调用EventSetTimer函数的智能交易系统或指标,会创建其自己的专用定时器。程序将仅接收来自该定时器的事件。不同程序中的定时器独立工作。

放置在图表上的每个交互式MQL程序都有一个单独的事件队列,接收到的事件会添加到该队列中。如果队列中已经存在OnTimer事件或者该事件正在处理状态,那么新的OnTimer事件不会被排入队列。

如果不再需要定时器,应该使用EventKillTimer函数将其禁用。

void EventKillTimer(void)

该函数停止之前由EventSetTimer函数(或我们接下来将讨论的EventSetMillisecondTimer函数)启用的定时器。该函数也可以从OnTimer处理程序中调用。因此,特别地,可以执行一次延迟操作。

在指标中调用EventKillTimer不会清空队列,因此之后你可能会收到最后残留的OnTimer事件。

当MQL程序终止时,如果定时器已创建但未通过EventKillTimer函数禁用,定时器将被强制销毁。

每个程序只能设置一个定时器。因此,如果你想以不同的时间间隔调用算法的不同部分,应该启用一个周期为所需周期的最小公倍数(在极限情况下,最小周期为1秒)的定时器,并在OnTimer处理程序中独立跟踪更长的周期。我们将在下一节中查看这种方法的示例。

MQL5还允许创建周期小于1秒的定时器:为此有一个函数EventSetMillisecondTimer

定时器事件:OnTimer

OnTimer 事件是 MQL5 程序支持的标准事件之一(请参阅“事件处理函数概述”部分)。为了在程序代码中接收定时器事件,应该描述一个具有以下原型的函数:

plaintext
void OnTimer(void)

OnTimer 事件由客户端终端定期为使用 EventSetTimerEventSetMillisecondTimer 函数激活定时器的智能交易系统或指标生成(请参阅下一节)。

注意!在通过其他程序调用 iCustomIndicatorCreate 创建的从属指标中,定时器不起作用,也不会生成 OnTimer 事件。这是 MetaTrader 5 的架构限制。

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

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

例如,让我们创建一些类,这些类可以在一个程序中组织多个逻辑定时器。所有逻辑定时器的周期将设置为基本周期的倍数,也就是说,单个硬件定时器的周期为标准处理程序 OnTimer 提供事件。在这个处理程序中,我们必须调用新的 MultiTimer 类的某个方法,该方法将管理所有逻辑定时器。

plaintext
void OnTimer()
{
   // 调用 MultiTimer 方法,在需要时检查并调用从属定时器
   MultiTimer::onTimer();
}

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

工作定时器的基类将是 TimerNotification。严格来说,这可以是一个接口,但将一般实现的一些细节输出到其中会很方便:特别是,存储计数器 chronometer 的读数,通过它我们将确保定时器以主定时器相对周期的某个倍数触发,以及一个用于检查定时器应触发时刻的方法 isTimeCome。这就是为什么 TimerNotification 是一个抽象类。它缺少两个虚方法的实现:notify(用于定时器触发时的操作)和 getInterval(用于获取确定特定定时器相对于主定时器周期的倍数)。

plaintext
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)),那么从 getInterval 返回 5 的 TimerNotification 子对象将每隔 5 秒接收一次对其 notify 方法的调用。

正如我们已经说过的,这样的定时器对象将由 MultiTimer 管理器对象管理。我们只需要一个这样的对象。因此,它的构造函数被声明为受保护的,并且在类内静态创建一个单例实例。

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

在这个类中,我们组织存储 TimerNotification 对象数组(我们将在几段之后看到它是如何填充的)。一旦我们有了这个数组,我们就可以轻松编写 checkTimers 方法,该方法会遍历所有逻辑定时器。为了外部访问,这个方法由公共静态方法 onTimer 复制,我们已经在全局 OnTimer 处理程序中看到过它。由于唯一的管理器实例是静态创建的,我们可以从静态方法访问它。

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

使用 bind 方法将 TimerNotification 对象添加到 subscribers 数组中。

plaintext
   void bind(TimerNotification &tn)
   {
      int i, n = ArraySize(subscribers);
      for(i = 0; i < n; ++i)
      {
         if(subscribers[i] == &tn) return; // 已经存在这样的对象
         if(subscribers[i] == NULL) break; // 找到一个空槽
      }
      if(i == n)
      {
         ArrayResize(subscribers, n + 1);
      }
      else
      {
         n = i;
      }
      subscribers[n] = &tn;
   }

该方法可防止对象被重复添加,并且如果可能的话,指针会被放置在数组的空元素中(如果有的话),这样就无需扩展数组。如果使用 unbind 方法删除了任何 TimerNotification 对象(定时器可以偶尔使用),数组中可能会出现空元素。

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

请注意,管理器并不拥有定时器对象的所有权,也不会尝试调用 delete。如果你打算在管理器中注册动态分配的定时器对象,你可以在清零之前的 if 语句中添加以下代码:

plaintext
            if(CheckPointer(subscribers[i]) == POINTER_DYNAMIC) delete subscribers[i];

现在还需要了解如何方便地组织 bind/unbind 调用,以免这些实用操作给应用程序代码带来负担。如果你“手动”进行操作,很容易忘记在某个地方创建定时器,或者相反,忘记删除定时器。

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

plaintext
   class SingleTimer: public TimerNotification
   {
   protected:
      int multiplier;
      MultiTimer *owner;
   
   public:
      // 使用指定的基本周期倍数创建定时器,可选择暂停
      // 自动在管理器中注册对象
      SingleTimer(const int m, const bool paused = false): multiplier(m)
      {
         owner = &MultiTimer::_mainTimer;
         if(!paused) owner.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 方法激活。

一些对象订阅其他对象事件的方案是面向对象编程(OOP)中流行的设计模式之一,称为“发布者/订阅者”模式。

需要注意的是,这个类也是抽象类,因为它没有实现 notify 方法。基于 SingleTimer,让我们描述具有附加功能的定时器类。

让我们从 CountableTimer 类开始。它允许指定它应该触发的次数,之后它将自动停止。特别是,使用它很容易组织单个延迟操作。CountableTimer 构造函数有用于设置定时器周期、暂停标志和重试次数的参数。默认情况下,重复次数不受限制,因此这个类将成为大多数应用定时器的基础。

plaintext
class CountableTimer: public MultiTimer::SingleTimer
{
protected:
   const uint repeat;
   uint count;
   
public:
   CountableTimer(const int m, const uint r = UINT_MAX, const bool paused = false):
      SingleTimer(m, paused), 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,我们必须在程序中如下描述派生类:

plaintext
// MultipleTimers.mq5 
class MyCountableTimer: public CountableTimer
{
public:
   MyCountableTimer(const int s, const uint r = UINT_MAX):
      CountableTimer(s, r) { }
   
   virtual void notify() override
   {
      Print(__FUNCSIG__, multiplier, " ", count);
   }
};

在这个 notify 方法的实现中,我们只是将定时器周期和触发次数记录到日志中。顺便说一下,这是 MultipleTimers.mq5 指标的一个片段,我们将其用作实际示例。

让我们将从 SingleTimer 派生的第二个类称为 FunctionalTimer。它的目的是为那些喜欢函数式编程风格并且不想编写派生类的人提供一个简单的定时器实现。FunctionalTimer 类的构造函数除了周期之外,还将接受一个指向特殊类型函数 TimerHandler 的指针。

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

在这个 notify 方法的实现中,对象通过指针调用函数。有了这样一个类,我们可以定义一个宏,当它放在花括号括起来的语句块之前时,将“使”该语句块成为定时器函数的主体。

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

然后在应用程序代码中可以这样编写:

plaintext
// MultipleTimers.mq5
bool OnTimerCustom(3)
{
   Print(__FUNCSIG__);
   return true;        // 继续运行定时器
}

这个构造声明了一个周期为 3 的定时器以及括号内的一组指令(这里只是打印到日志)。如果这个函数返回 false,这个定时器将停止。

让我们进一步考虑 MultipleTimers.mq5 指标。由于它不提供可视化,我们将图表数量指定为零。

plaintext
#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

为了使用逻辑定时器类,我们包含头文件 MultiTimer.mqh 并添加一个用于基本(全局)定时器周期的输入变量。

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

OnInit 中启动基本定时器。

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

回想一下,所有逻辑定时器的操作是通过拦截全局 OnTimer 事件来确保的。

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

除了上面的定时器应用类 MyCountableTimer 之外,让我们描述另一个暂停定时器类 MySuspendedTimer

plaintext
class MySuspendedTimer: public CountableTimer
{
public:
   MySuspendedTimer(const int s, const uint r = UINT_MAX):
      CountableTimer(s, r, true) { }
   virtual void notify() override
   {
      Print(__FUNCSIG__, multiplier, " ", count);
      if(count == repeat - 1) // 最后一次执行
      {
         Print("Forcing all timers to stop");
         EventKillTimer();
      }
   }
};

我们将在下面看到它是如何启动的。这里还需要注意的是,在达到指定的操作次数后,这个定时器将通过调用 EventKillTimer 关闭所有定时器。

现在让我们展示如何(在全局上下文中)描述这两个类的不同定时器的对象。

plaintext
MySuspendedTimer st(1, 5);
MyCountableTimer t1(2);
MyCountableTimer t2(4);

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

MyCountableTimer 类的 t1t2 定时器周期分别为 2(2 * BaseTimerPeriod)和 4(4 * BaseTimerPeriod)。默认值 BaseTimerPeriod = 1 时,所有周期都以秒为单位。这两个定时器在程序启动后立即启动。

我们还将以函数式风格创建两个定时器。

plaintext
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 秒。

让我们分析一个输出到日志的示例(时间戳在右侧示意性显示)。

plaintext
                                                // 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.

高精度定时器:EventSetMillisecondTimer

如果你的程序需要定时器触发的频率高于每秒一次,那么可以使用 EventSetMillisecondTimer 函数,而非 EventSetTimer

不同单位的定时器不能同时启动,也就是说,只能使用其中一个函数。实际运行的定时器类型由最后调用的函数决定。高精度定时器保留了标准定时器的所有特性。

plaintext
bool EventSetMillisecondTimer(int milliseconds)

该函数告知客户端终端,需要为这个智能交易系统或指标以小于一秒的频率生成定时器事件。周期以毫秒为单位设置(通过 milliseconds 参数)。

函数返回成功(true)或错误(false)的标志。

在策略测试器中运行时要注意,定时器周期越短,测试所需的时间就越长,因为定时器事件处理程序的调用次数会增加。

在正常运行时,由于硬件限制,定时器事件的生成频率不会超过每 10 - 16 毫秒一次。

为了展示如何使用毫秒级定时器,我们来扩展指标示例 MultipleTimers.mq5。由于全局定时器的激活由应用程序控制,我们可以轻松更改定时器的类型,同时保持逻辑定时器类不变。唯一的区别在于,这些类的乘数将应用于我们在 EventSetMillisecondTimer 函数中指定的以毫秒为单位的基本周期。

为了选择定时器类型,我们定义一个枚举并添加一个新的输入变量。

plaintext
enum TIMER_TYPE
{
   Seconds,
   Milliseconds
};
   
input TIMER_TYPE TimerType = Seconds;

默认情况下,我们使用秒级定时器。在 OnInit 函数中,启动所需类型的定时器。

plaintext
void OnInit()
{
   Print(__FUNCSIG__, " ", BaseTimerPeriod, " ", EnumToString(TimerType));
   if(TimerType == Seconds)
   {
      EventSetTimer(BaseTimerPeriod);
   }
   else
   {
      EventSetMillisecondTimer(BaseTimerPeriod);
   }
}

让我们看看选择毫秒级定时器时日志中会显示什么内容。

plaintext
                                               // time ms
17:27:54.483  void OnInit() 1 Milliseconds        |             
17:27:54.514  void MyCountableTimer::notify()2 0    |           +31
17:27:54.545  bool OnTimer3()                        |          +31
17:27:54.561  void MyCountableTimer::notify()2 1      |         +16
17:27:54.561  void MyCountableTimer::notify()4 0      |
17:27:54.577  bool OnTimer5()                          |        +16
17:27:54.608  void MyCountableTimer::notify()2 2        |       +31
17:27:54.608  bool OnTimer3()                           |
17:27:54.608  void MySuspendedTimer::notify()1 0        |
17:27:54.623  void MySuspendedTimer::notify()1 1         |      +15
17:27:54.655  void MyCountableTimer::notify()2 3          |     +32
17:27:54.655  void MyCountableTimer::notify()4 1          |
17:27:54.655  void MySuspendedTimer::notify()1 2          |
17:27:54.670  bool OnTimer3()                              |    +15
17:27:54.670  void MySuspendedTimer::notify()1 3           |
17:27:54.686  void MyCountableTimer::notify()2 4            |   +16
17:27:54.686  void MySuspendedTimer::notify()1 4            |
17:27:54.686  Forcing all timers to stop                    |

事件生成的顺序与秒级定时器的情况完全相同,但一切发生得更快,几乎是瞬间完成。

由于系统定时器的精度限制在几十毫秒以内,事件之间的实际间隔会明显超过理论上无法达到的 1 毫秒。此外,每次“步进”的时间间隔也存在差异。因此,即使使用毫秒级定时器,也建议不要设置小于几十毫秒的周期。

Talk is cheap, show me the code!