跨平台智能交易系统: 时间过滤器·进阶篇
⏱️

跨平台智能交易系统: 时间过滤器·进阶篇

(2/3)· 从 CTime 基类到 CTimeFilter 子过滤器,把横盘规避与周末平仓写成可复用代码

含代码示例 第 2/3 篇
很多交易者把时间判断硬写在 OnTick 里,结果横盘时段没躲开、周末持仓忘了平。一旦策略要同时管多个时间段,散落的判断逻辑就会互相打架,回测和实盘表现脱节。

「计时基类的字段与接口布局」

在 MT5 用 C++ 风格封装一个跨周期计时器时,先把底层状态量定清楚能少走很多弯路。下面这段类声明给出了基类的核心成员:用 uint 存秒数、int 存总次数与已耗时,datetime 存起始时间,三个变量足以支撑后续任意周期判断。 构造函数提供了两种入口,一个是单 int 参数(可能用于快捷按总秒数初始化),另一个是六个 uint 参数,依次对应年、月、日、时、分、秒的细粒度设定,析构函数为空实现。 公开接口里最实用的是那组成对出现的取值/赋值函数:Year()、Month()、Days()、Hours()、Minutes()、Seconds() 都写了 const 读版本和带参写版本,Total() 返回 bool 表示是否累计完成,TimeStart() 管理起点时刻。 别把正态当圣经:虚函数 Validate() 和 Elapsed() 都留了 virtual,说明派生类(比如按交易日计时的子类)要自己重写合法性校验与到期逻辑,直接拿基类调 Elapsed() 只会走空壳。 开 MT5 新建一个 include 头文件,把下面结构原样贴进去,编译不报错就说明你的环境支持这套重载写法,后面接策略计时直接继承即可。

MQL5 / C++
class="type">uint                m_seconds;
class="type">int                 m_total;
class="type">int                 m_elapsed;
class="type">class="kw">datetime            m_time_start;
class="kw">public:
                      CTimerBase(const class="type">int);
                      CTimerBase(const class="type">uint,const class="type">uint,const class="type">uint,const class="type">uint,const class="type">uint,const class="type">uint);
                     ~CTimerBase(class="type">void);
  class=class="str">"cmt">//--- 初始化
  class="kw">virtual class="type">bool      Set(const class="type">uint,const class="type">uint,const class="type">uint,const class="type">uint,const class="type">uint,const class="type">uint);
  class="kw">virtual class="type">bool      Validate(class="type">void);
  class=class="str">"cmt">//--- 赋值与取值
  class="type">uint              Year(class="type">void) const;
  class="type">void              Year(const class="type">uint);
  class="type">uint              Month(class="type">void) const;
  class="type">void              Month(const class="type">uint);
  class="type">uint              Days(class="type">void) const;
  class="type">void              Days(const class="type">uint);
  class="type">uint              Hours(class="type">void) const;
  class="type">void              Hours(const class="type">uint);
  class="type">uint              Minutes(class="type">void) const;
  class="type">void              Minutes(const class="type">uint);
  class="type">uint              Seconds(class="type">void) const;
  class="type">void              Seconds(const class="type">uint);
  class="type">bool              Total(class="type">void) const;
  class="type">class="kw">datetime          TimeStart(class="type">void) const;
  class="type">void              TimeStart(const class="type">class="kw">datetime);
  class=class="str">"cmt">//--- 处理  
  class="kw">virtual class="type">bool      Elapsed(class="type">void) const;

◍ 把时间拆成秒来喂给定时器基类

做跨周期触发逻辑时,最怕直接用「1小时」「1天」这种人类语言去比大小。MT5 里 datetime 本质是秒数,所以先把年、月、日、时、分、秒统一折算成秒常量,后续累加和比较才不会错位。 下面这组宏定义就是干这个的:年按 31536000 秒(365×86400,忽略闰年)、月按 2419200 秒(28天)、日 86400、时 3600、分 60。回测里若用 MONTH_SECONDS 去卡每月信号,实际会偏出 2~3 天,这是已知误差,别当精确月线用。 CTimerBase 的构造函数把传入的 years/months/days/hours/minutes/seconds 先全初始化为 0(m_years(0) 一直到 m_seconds(0)),真正的赋值在初始化列表之后完成。你可以直接把这段抄进 EA 的 include 头文件,开 MT5 建个空 EA 引用它,编译看有没有隐式类型告警。 外汇和贵金属波动受杠杆放大,用这类定时触发做仓位切换时,滑点和跳空可能让秒级计划失效,实盘前务必用策略测试器跑至少 3 个月 tick 数据。

MQL5 / C++
class="kw">virtual class="type">bool      Evaluate(class="type">class="kw">datetime);
class="kw">virtual class="type">void      RecalculateTotal(class="type">void);
};
class="macro">#define YEAR_SECONDS class="num">31536000
class="macro">#define MONTH_SECONDS class="num">2419200
class="macro">#define DAY_SECONDS class="num">86400
class="macro">#define HOUR_SECONDS class="num">3600
class="macro">#define MINUTE_SECONDS class="num">60
CTimerBase::CTimerBase(const class="type">uint years,const class="type">uint months,const class="type">uint days,const class="type">uint hours,const class="type">uint minutes,const class="type">uint seconds) : m_years(class="num">0),
                    m_months(class="num">0),
                    m_days(class="num">0),
                    m_hours(class="num">0),
                    m_minutes(class="num">0),
                    m_seconds(class="num">0),

计时器基类的构造与过期判定

CTimerBase 把时间段拆成 years/months/days/hours/minutes/seconds 与累计秒数 m_total 两类状态。无参构造里用 Set() 把各分量清零,带 total_time 参数的构造则只填 m_total、其余分量留 0,两种初始化路径在回测里分别对应「按日历字段触发」和「按总秒数倒计时」两种需求。 TimeStart() 有读写两个重载:返回 m_time_start 的 const 版本供外部查询,传入 datetime 的版本负责把计时起点钉死。注意 m_time_start 默认是 0,若不在 OnInit 或策略启动时显式写入,Elapsed 计算会直接拿 1970 年起点去减,结果必然失真。 Evaluate() 是核心:先判 Active(),非激活直接返 true;current 默认取 TimeCurrent(),也可手动传。m_elapsed 用 (int)(current - m_time_start) 得秒差,>= m_total 则 result=false。最后 Reverse() 会把结果取反——这意味着同一个计时器,正逻辑用于「到期停止」、反逻辑用于「未到期不触发」,一套代码覆盖开仓禁窗与放行窗。 外汇与贵金属波动受时段切换影响大,用这类计时器框定新闻禁交易段时,务必在 MT5 策略测试器里把 m_time_start 打印出来核对,避免时区偏移导致误判。

MQL5 / C++
m_total(class="num">0),
 m_time_start(class="num">0)
{
 Set(years,months,days,hours,minutes,seconds);
}
CTimerBase::CTimerBase(const class="type">int total_time) : m_years(class="num">0),
                     m_months(class="num">0),
                     m_days(class="num">0),
                     m_hours(class="num">0),
                     m_minutes(class="num">0),
                     m_seconds(class="num">0),
                     m_total(class="num">0),
                     m_time_start(class="num">0)
{
 m_total=total_time;
}
class="type">class="kw">datetime CTimerBase::TimeStart(class="type">void) const
{
 class="kw">return m_time_start;
}
class="type">void CTimerBase::TimeStart(const class="type">class="kw">datetime time_start)
{
 m_time_start=time_start;
}
class="type">bool CTimerBase::Evaluate(class="type">class="kw">datetime current=class="num">0)
{
 if(!Active())
    class="kw">return true;
 class="type">bool result=true;
 if(current==class="num">0)
    current= TimeCurrent();
 m_elapsed=(class="type">int)(current-m_time_start);
 if(m_elapsed>=m_total) result=false;
 class="kw">return Reverse()?!result:result;
}
class CTimeFilterBase : class="kw">public CTime
{
class="kw">protected:
  class="type">MqlDateTime      m_filter_start;
  class="type">MqlDateTime      m_filter_end;
  CArrayObj        m_time_filters;
class="kw">public:

「时间过滤器基类怎么处理跨日时段」

做交易时段过滤时,最常踩的坑是结束小时小于开始小时——比如伦敦尾盘 22:00 到次日 02:00,直接用 hour 比较会整段失效。CTimeFilterBase 的 Evaluate 里用了一段跨日修正逻辑来兜住这种情况。 当 m_filter_start.hour >= m_filter_end.hour 时,若当前 hour 已大于等于起始小时,就把结束日的 day / day_of_week / day_of_year 各加 1;若当前 hour 还小于等于结束小时,则把起始日的对应字段减 1。这样把时间窗口顺延或前移一天,跨午夜的比较才成立。 Set 方法接收 gmt 偏移与起止的时分秒,把 starthour+gmt 写进 m_filter_start.hour,endhour+gmt 写进 m_filter_end.hour。默认 startminute、endminute、startseconds、endseconds 均为 0,调用时若只传小时,精度就卡在整点。 开 MT5 把下面代码塞进 EA 测一遍:设 gmt=0、starthour=22、endhour=2,用 TimeCurrent 喂给 Evaluate,观察 day 字段在 23:30 与 01:30 是否分别 +1 / -1。外汇与贵金属杠杆高,时段误判可能让策略在流动性骤变时误触发,务必先回测再上实盘。

MQL5 / C++
CTimeFilterBase(class="type">void);
CTimeFilterBase(const class="type">int,const class="type">int,const class="type">int,const class="type">int,const class="type">int,const class="type">int,const class="type">int);
~CTimeFilterBase(class="type">void);
 class="kw">virtual class="type">bool      Init(CSymbolManager*,CEventAggregator*);
 class="kw">virtual class="type">bool      Validate(class="type">void);
 class="kw">virtual class="type">bool      Evaluate(class="type">class="kw">datetime);
 class="kw">virtual class="type">bool      Set(const class="type">int,const class="type">int,const class="type">int,const class="type">int,const class="type">int,const class="type">int,const class="type">int);
 class="kw">virtual class="type">bool      AddFilter(CTimeFilterBase*);
};
class="type">bool CTimeFilterBase::Set(const class="type">int gmt,const class="type">int starthour,const class="type">int endhour,const class="type">int startminute=class="num">0,const class="type">int endminute=class="num">0,const class="type">int startseconds=class="num">0,const class="type">int endseconds=class="num">0)
 {
 m_filter_start.hour=starthour+gmt;
 m_filter_start.min=startminute;
 m_filter_start.sec=startseconds;
 m_filter_end.hour=endhour+gmt;
 m_filter_end.min=endminute;
 m_filter_end.sec=endseconds;
 class="kw">return true;
 }
class="type">bool CTimeFilterBase::Evaluate(class="type">class="kw">datetime current=class="num">0)
 {
 if(!Active())
    class="kw">return true;
 class="type">bool result=true;
 class="type">MqlDateTime time;
 if(current==class="num">0)
    current=TimeCurrent();
 TimeToStruct(current,time);
 m_filter_start.year= time.year;
 m_filter_start.mon = time.mon;
 m_filter_start.day = time.day;
 m_filter_start.day_of_week = time.day_of_week;
 m_filter_start.day_of_year = time.day_of_year;
 m_filter_end.year= time.year;
 m_filter_end.mon = time.mon;
 m_filter_end.day = time.day;
 m_filter_end.day_of_week = time.day_of_week;
 m_filter_end.day_of_year = time.day_of_year;
 class=class="str">"cmt">/*
   其它任务在这里处理
 */
}
if(m_filter_start.hour>=m_filter_end.hour)
 {
  if(time.hour>=m_filter_start.hour)
   {
     m_filter_end.day++;
     m_filter_end.day_of_week++;
     m_filter_end.day_of_year++;
   }
  else if(time.hour<=m_filter_end.hour)
   {
     m_filter_start.day--;
     m_filter_start.day_of_week--;
     m_filter_start.day_of_year--;
   }
 }

◍ 时间过滤器的容器类怎么搭

时间过滤器跟前面几类对象一样,需要一个容器来存指针,统一调 Evaluate 做评估。逻辑很直接:只要里面每个时间过滤器的 Evaluate 都返回 true,也就是当前时间没被任何规则拦掉,容器本身也就返回 true。 这个职责落在 CTimes 类上,它继承自 CTimesBase。CTimesBase 本身是从 CArrayObj 派生的,等于直接复用了 MQL5 标准库的对象数组能力,省得自己写增删查。 下面这段是 CTimesBase 的类定义骨架。m_active 控制整体开关,m_selected 记录当前选中项,m_event_man 和 m_container 分别接事件聚合器和父容器指针。Type 写死返回 CLASS_TYPE_TIMES,方便运行时做类型识别。 Evaluate 是虚函数,入参是 datetime,子类 CTimes 会遍历数组里每个过滤器去挨个判断。Validate 留作初始化后的自检,CreateElement 则按索引造具体的过滤器元素。开 MT5 把这段塞进头文件,继承后补齐 Evaluate 遍历逻辑,就能跑通时间维度的信号闸门。

MQL5 / C++
class CTimesBase : class="kw">public CArrayObj
  {
class="kw">protected:
   class="type">bool               m_active;
   class="type">int                m_selected;
   CEventAggregator *m_event_man;
   CObject           *m_container;
class="kw">public:
                     CTimesBase(class="type">void);
                    ~CTimesBase(class="type">void);
   class="kw">virtual class="type">int       Type(class="type">void) const {class="kw">return CLASS_TYPE_TIMES;}
   class=class="str">"cmt">//-- 初始化
   class="kw">virtual class="type">bool      Init(CSymbolManager*,CEventAggregator*);
   class="kw">virtual CObject *GetContainer(class="type">void);
   class="kw">virtual class="type">void      SetContainer(CObject*);
   class="kw">virtual class="type">bool      Validate(class="type">void) const;
   class=class="str">"cmt">//--- 激活和蛰伏
   class="type">bool              Active(class="type">void) const;
   class="type">void              Active(const class="type">bool);
   class="type">int               Selected(class="type">void);
   class=class="str">"cmt">//--- 检查
   class="kw">virtual class="type">bool      Evaluate(class="type">class="kw">datetime) const;
   class=class="str">"cmt">//--- 复原
   class="kw">virtual class="type">bool      CreateElement(const class="type">int);
   };

用子过滤器拆开非连续交易时段

时间过滤器容器要求它持有的所有主过滤器都返回 true,才肯放行。绝大多数过滤器在单个 EA 里只用一个实例就够了,CTimeFilter 却是例外——它最常用来框定日内可交易窗口,而真实盘面里这个窗口往往被午餐之类的空白切成了几段。 比如想做 8:00–17:00 但跳过 12:00–13:00,直观写法是在容器里挂两个 CTimeFilter:一个 8–12、一个 13–17。这段代码必然一直返回 false,因为容器要两者同时为真,而任意时刻只有一个能真。 [CODE] CTimes times = new CTimes(); CTimeFilter time1 = new CTimeFilter(gmt,8,12); CTimeFilter time2 = new CTimeFilter(gmt,13,17); CTimes times = new CTimes(); CTimeFilter time1 = new CTimeFilter(gmt,8,12); CTimeFilter time2 = new CTimeFilter(gmt,13,17); times.Add(GetPointer(time1)); times.Add(GetPointer(time2)); [/CODE] 上面这段只是示意错误用法:容器里塞了两个平行实例,time1 管上午、time2 管下午,Evaluate 时互相否决,EA 永远拿不到入场许可。 正确路子是容器里只留一个主 CTimeFilter 指针,把其余段挂成它的子过滤器。子过滤器存在成员 m_time_filters 里,用 AddFilter 加进去。主过滤器自己先判,只有返回 false 才去挨个问子过滤器,任一子过滤器为真则整体翻真。 [CODE] if(!result) { for(int i=0;i<m_time_filters.Total();i++) { CTimeFilter *filter=m_time_filters.At(i); if(filter.Evaluate(current)) { return true; } } } [/CODE] 这段就是类里评估子过滤器的核心:result 为主过滤器的初判,若 false 则遍历 m_time_filters,At(i) 取出子过滤器指针,Evaluate(current) 逐个验当前时间,有一个真就直接 return true。 落地写法如下:time0 设成 0–0 再调 Reverse(),让它恒返回 false 从而强制走子过滤器分支;time1、time2 作为子项加进 time0,容器只 Add 一个 time0。 [CODE] CTimes times = new CTimes(); CTimeFilter time1 = new CTimeFilter(gmt,8,12); CTimeFilter time2 = new CTimeFilter(gmt,13,17); CTimeFilter time0 = new CTimeFilter(gmt,0,0); time0.Reverse(); time0.AddFilter(GetPointer(time1)); time0.AddFilter(GetPointer(time2)); times.Add(GetPointer(time0)); [/CODE] 如此时间表变成:time0 恒 false → 查子项,8–12 真或 13–17 真则整体真。两个以上的子过滤器也照此规则,但日内组合两层足够,没必要再嵌套子过滤器的子过滤器。外汇与贵金属杠杆高、滑点跳空频繁,这类时间切面最好先在 MT5 策略测试器用历史 tick 跑一遍再上实盘。

MQL5 / C++
CTimes times = new CTimes();
CTimeFilter time1 = new CTimeFilter(gmt,class="num">8,class="num">17);
times.Add(GetPointer(time1));
CTimes times = new CTimes();
CTimeFilter time1 = new CTimeFilter(gmt,class="num">8,class="num">12);
CTimeFilter time2 = new CTimeFilter(gmt,class="num">13,class="num">17);
times.Add(GetPointer(time1));
times.Add(GetPointer(time2));
if(!result)
  {
   for(class="type">int i=class="num">0;i<m_time_filters.Total();i++)
     {
      CTimeFilter *filter=m_time_filters.At(i);
      if(filter.Evaluate(current))
      {
       class="kw">return true;
      }  
     }
  }
CTimes times = new CTimes();
CTimeFilter time1 = new CTimeFilter(gmt,class="num">8,class="num">12);
CTimeFilter time2 = new CTimeFilter(gmt,class="num">13,class="num">17);
CTimeFilter time0 = new CTimeFilter(gmt,class="num">0,class="num">0);
time0.Reverse();
time0.AddFilter(GetPointer(time1));
time0.AddFilter(GetPointer(time2));
times.Add(GetPointer(time0));
把多过滤器编排交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到各时段波动与持仓到期提示,你只需把本文的过滤器逻辑接进 EA,重复劳动交给小布,你专注决策。

常见问题

m_reverse 用于反转类对象的布尔输出,原本时间段内返回 true 时改为 false,方便在同一套过滤器上做例外禁用而不必重写判断。
小布盯盘目前提供时段诊断与到期提醒的 AIGC 视图,不直接编译 EA,但可对照你过滤器输出的时间段做盘口验证,减少手动核对。
MQL5 没有 TimeDayOfWeek,用结构转换可把方法统一放进基类,兼顾 MQL4 兼容并避免跨平台分支。
容器持有 CTimeFilter 实例集合,循环 Evaluate 并按逻辑与或组合结果,使 EA 能同时约束交易窗与禁交易窗,具体见本篇第5节。