计时器事件:OnTimer
OnTimer事件是 MQL5 程序支持的标准事件之一(请参阅 事件处理函数概述章节)。要在程序代码中接收计时器事件,应声明如下原型的函数。
void OnTimer(void)
对于通过 EventSetTimer 或 EventSetMillisecondTimer 函数激活计时器的 EA 交易或指标,客户端终端会定期生成 OnTimer事件(请参阅下一节)。
注意!在通过其他程序调用 iCustom或 IndicatorCreate 创建的从属指标中,计时器无效,且不会生成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 = 0; i < 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 i, n = ArraySize(subscribers);
for(i = 0; i < n; ++i)
{
if(subscribers[i] == &tn) return; // there is already such an object
if(subscribers[i] == NULL) break; // found an empty slot
}
if(i == n)
{
ArrayResize(subscribers, n + 1);
}
else
{
n = i;
}
subscribers[n] = &tn;
}
|
该方法可防止对象被重复添加,并且在可能的情况下,如果数组中存在空元素,会将指针放置在该空元素中,从而避免数组扩容。当使用 unbind方法移除任意 TimerNotification 对象时,数组中可能会出现空元素(计时器可能仅偶尔使用)。
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内添加以下代码:
if(CheckPointer(subscribers[i]) == POINTER_DYNAMIC) delete subscribers[i];
|
现在需要思考如何便捷地组织 bind/unbind 调用,以免让应用代码被这些工具性操作拖累。如果“手动”处理,很容易忘记在某处创建计时器,或者相反,忘记删除计时器。
我们来开发一个从 TimerNotification派生的 SingleTimer 类,分别在构造函数和析构函数中实现 bind 和unbind 调用。此外,我们在该类中定义了 multiplier变量来存储计时器周期。
class SingleTimer: public 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 m, const bool paused = false): multiplier(m)
{
owner = &MultiTimer::_mainTimer;
if(!paused) owner.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 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;
}
// 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 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 的指针。
// 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方法的实现中,对象通过指针调用函数。借助这样的类,我们可以定义一个宏,若将这个宏放在由花括号括起来的语句块之前,就会将该语句块“变成”计时器函数的主体。
// MultiTimer.mqh
#define OnTimerCustom(P) OnTimer##P(); \
FunctionalTimer ft##P(P, OnTimer##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 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) // execute last time
{
Print("Forcing all timers to stop");
EventKillTimer();
}
}
};
|
稍后我们将演示其启动方式。此处还需重点指出,在达到指定操作次数后,该计时器将通过调用 EventKillTimer终止所有计时器。
下面我们来展示(在全局上下文中)如何定义这两类的不同计时器对象。
MySuspendedTimer st(1, 5);
MyCountableTimer t1(2);
MyCountableTimer t2(4);
|
MySuspendedTimer类的 st 计时器周期为 1(1*BaseTimerPeriod),并应在执行 5 次操作后停止。
MyCountableTimer类的 t1 和 t2 计时器周期分别为 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)。在最后一次迭代中,它将停止所有计时器运行。