跨平台智能交易系统: 时间过滤器(基础篇)
「用时间过滤器管住EA的开仓窗口」
在 MT5 里跑跨平台智能交易系统,最容易被忽略的是「什么时候允许下单」。不少策略回测漂亮,实盘却在流动性极差的非主流时段连续吃滑点,根源就是没给 EA 加时间约束。 MQL5 标准库提供 CTimeFilter 作为时间过滤器基类,开发者可派生子类实现具体时段逻辑,再用容器类统一管理多个子过滤器。一个过滤器返回 false,EA 就该停止开仓。 官方示例文章《跨平台智能交易系统: 时间过滤器》发布于 2017-08-28 07:48,截至统计已获 7071 次查看、7 条评论,说明这是实盘 EA 基建里高频被翻出来的老文。外汇与贵金属杠杆高、时段波动分化大,错误时段触发信号可能放大亏损风险。 打开 MT5 的 MetaEditor,搜 CTimeFilter 能看到标准库实现;先确认你的 EA 是否在任何 Hour 都发单,再决定要挂几个子过滤器。
给EA装上时间开关
时间过滤的本质,是让智能交易系统先判断“当前时刻是否落在你定义的时间段内”,再决定某些功能开不开。它不依赖行情,只依赖时钟,所以适合做周期性的启用/停用:比如横盘或高波动时段直接规避,给订单设一个到期时刻到点离场,或者交易周结束前统一平仓。 实际用得最多的就是三类:避坑特定时段、给持仓挂“到点退场”、周末清仓。当然变体很多,核心逻辑只有一个——先判定时间,再放行动作。 这套思路在 MQL4 和 MQL5 里都能落地,不挑平台。写好一个时间过滤器,你的 EA 就能轻松叠加多个时段规则,而不是把所有逻辑写死在全时段跑。外汇与贵金属杠杆高、跳空频繁,用时间过滤砍掉高风险窗口,是性价比极高的第一道防线。
◍ 时间过滤器的基类骨架
在 MT5 智能交易系统里,所有时间过滤器对象都从同一个基类派生。这里把 CTimeBase 作为统一入口,它继承自 CObject,后续具体的时段、周期间过滤类只要覆写 Evaluate 就能挂进同一套调度逻辑。 基类内部用 m_active 控制对象是否参与判断,设成 false 就直接跳过;m_reverse 负责把输出取反,比如原逻辑在某时段返回 false,打开反转后同一时段会返回 true。这两个布尔成员让你在 OnInit 里就能静态配置行为,不用改继承类的代码。 下面这段声明值得直接抄进你的 include 头文件。注意 Evaluate 是纯虚函数(=0),意味着基类本身不可实例化,强制子类给出具体的 datetime 判断实现;而 Type 固定返回 CLASS_TYPE_TIME,方便事件聚合器在运行时做类型识别。 别把 m_active 当摆设 很多自建 EA 把开关写死在代码里,其实留一个运行时可写的 Active() 接口,能在不重编译的情况下用外部脚本禁用某类过滤器,排查「为什么今天没下单」会快很多。外汇与贵金属杠杆高,逻辑误判可能瞬间放大亏损,开关留痕比硬改代码稳。
class CTimeBase : class="kw">public CObject { class="kw">protected: class="type">bool m_active; class="type">bool m_reverse; CSymbolManager *m_symbol_man; CEventAggregator *m_event_man; CObject *m_container; class="kw">public: CTimeBase(class="type">void); ~CTimeBase(class="type">void); class="kw">virtual class="type">int Type(class="type">void) const {class="kw">return CLASS_TYPE_TIME;} 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); class=class="str">"cmt">//--- 赋值与取值 class="type">bool Active(class="type">void) const; class="type">void Active(const class="type">bool); class="type">bool Reverse(class="type">void); class="type">void Reverse(const class="type">bool); class=class="str">"cmt">//--- 检查 class="kw">virtual class="type">bool Evaluate(class="type">class="kw">datetime)=class="num">0; };
「EA 里四种时间过滤的落地写法」
做价格行为策略时,时间窗口往往比信号本身更决定回撤。MT5 里把时间过滤拆成四类封装,能直接拿去卡掉不该交易的那段行情:固定日期区间、周内某几天、计时器到期、日内小时表。 按日期区间过滤最简单,CTimeRange 在构造时吃进开始和结束 datetime,Evaluate 不传参就用当前时间比大小,落在中间返回 true。注意若起止为 0 或未设,方法自动退回到当前时间,容易在回测里悄悄放行,实盘前要在 OnTesterInit 打一行 Print 确认 m_begin/m_end 不为 0。 周内过滤推荐用 MqlDateTime.day_of_week 做位标志,而不是自己写 TimeDayOfWeek(MQL5 没有)。CTimeDays 用 long 成员存 7 天标志,Monday() 这类方法单独翻转某一天,避免一次性 Set 七位写错。典型诉求如“周五不出单”,就是把 FRIDAY 位清零。 CTimer 是拿 UNIX 纪元(1970-01-01 00:00 GMT)做基准的到期锁。构造传入年/月/日/时/分/秒换算出的 m_total 秒数,Evaluate 算“当前 - 开始时间”是否超 m_total。它和 EventSetTimer 不同,后者全 EA 只能挂一个事件,所以 CTimer 只在需要额外倒计时(比如二元式到期平仓)时才用。 日内表 CTimeFilter 最常用也最坑:比较不含结束那一秒,设 08:00–17:00 实际最后成交停在 16:59。跨天情形(如 17:00–08:00)要判开始>结束,把结束+1天或开始-1天。若起止都设 05:00,代码视其为跨全天 24 小时全放行。外汇和贵金属杠杆高,错误时间窗会在低流动性时段放单,滑点可能翻倍,务必在策略测试器用 2023 年 1 月数据跑一遍跨天分支。
class CTimeRangeBase : class="kw">public CTime { class="kw">protected: class="type">class="kw">datetime m_begin; class="type">class="kw">datetime m_end; class="kw">public: CTimeRangeBase(class="type">void); CTimeRangeBase(class="type">class="kw">datetime,class="type">class="kw">datetime); ~CTimeRangeBase(class="type">void); class=class="str">"cmt">//--- 初始化 class="type">class="kw">datetime,class="type">class="kw">datetime class="kw">virtual class="type">bool Set(class="type">class="kw">datetime,class="type">class="kw">datetime); class="kw">virtual class="type">bool Validate(class="type">void); class=class="str">"cmt">//--- 赋值与取值 class="type">class="kw">datetime Begin(class="type">void) const; class="type">void Begin(const class="type">class="kw">datetime); class="type">class="kw">datetime End(class="type">void) const; class="type">void End(const class="type">class="kw">datetime); class=class="str">"cmt">//--- 处理
用位标志圈定可交易星期
CTimeDaysBase 在 CTime 之上扩展出按星期过滤的能力,核心是一个 long 类型的 m_day_flags,用每一位记录周日到周六是否允许交易。构造函数默认值已经把周末剔除:sun=false、sat=false,周一到周五为 true,这和外盘贵金属、外汇在周六周日停盘的实际节奏一致。 Evaluate(datetime) 会先取当前时间(默认参数 current=0 时调用 TimeCurrent()),再换算出对应星期,去 m_day_flags 里查那一位是否为 1。如果你做伦敦金或欧美盘自动化,直接用这个默认配置就能避开无流动性的周末跳空,但遇上周五尾盘提前休市,仍要自己补一层时间区间判断。 下面这段是时间区间基类的 Evaluate 实现,能看清「失效即放行」和「反向区间」两个坑: virtual bool Evaluate(datetime); }; bool CTimeRangeBase::Evaluate(datetime current=0) { if(!Active()) return true; if(current==0) current=TimeCurrent(); bool result=current>=m_begin && current<m_end; return Reverse()?!result:result; } 逐行拆解:Active() 返回 false 时直接 return true,意味着过滤器未激活就不拦截任何 tick;current 为 0 则取服务器当前时间;result 是「时间落在 [m_begin, m_end) 半开区间」的真值;Reverse() 为 true 时取反,用来做「仅在该区间外交易」的反向逻辑。外汇和贵金属杠杆高,这类时间过滤器写错一个符号,可能在非农夜误触发,务必在策略测试器里用 2023 年 1 月至今的实盘数据跑一遍。
class="kw">virtual class="type">bool Evaluate(class="type">class="kw">datetime); }; class="type">bool CTimeRangeBase::Evaluate(class="type">class="kw">datetime current=class="num">0) { if(!Active()) class="kw">return true; if(current==class="num">0) current=TimeCurrent(); class="type">bool result=current>=m_begin && current<m_end; class="kw">return Reverse()?!result:result; }
◍ 用位掩码把交易日过滤写进类里
上面这段把「星期几允许交易」做成了位标志枚举,每位代表一天,Sunday 占 bit0、Monday 占 bit1,一直到 Saturday 占 bit6。默认 Set() 的参数是周一到周五为 true、周日周六为 false,也就是常见的工作日交易窗口。 Monday(const bool set) 里用 m_day_flags|=TIME_DAY_FLAG_MON 置位、用 m_day_flags &=~TIME_DAY_FLAG_MON 清位,这种写法比用七个 bool 变量省内存,也方便一次按位或判断。 Evaluate() 是核心:若 Active() 为假直接返回 true(不过滤),否则把当前时间(默认 TimeCurrent())拆成 MqlDateTime,用 day_of_week 做 switch 查当天是否被允许。Reverse() 为真时把结果取反,相当于「只允许周末跑」的反向模式。外汇和贵金属周末流动性极差、点差可能飙升,这种开关在高风险品种上要谨慎验证。 CTimerBase 继承自 CTime,底下挂了 m_years 到 m_minutes 五个 uint 成员,看名字是给「每隔 N 年/月/日/时/分触发」用的底层计数器。你在 MT5 里建个 EA 把这段贴进去,改 Set() 默认值就能快速切换交易日程。
class="type">void Friday(const class="type">bool); class="type">bool Saturday(class="type">void) const; class="type">void Saturday(const class="type">bool); }; enum ENUM_TIME_DAY_FLAGS { TIME_DAY_FLAG_SUN=class="num">1<<class="num">0, TIME_DAY_FLAG_MON=class="num">1<<class="num">1, TIME_DAY_FLAG_TUE=class="num">1<<class="num">2, TIME_DAY_FLAG_WED=class="num">1<<class="num">3, TIME_DAY_FLAG_THU=class="num">1<<class="num">4, TIME_DAY_FLAG_FRI=class="num">1<<class="num">5, TIME_DAY_FLAG_SAT=class="num">1<<class="num">6 }; class="type">void CTimeDaysBase::Set(const class="type">bool sun=false,const class="type">bool mon=true,const class="type">bool tue=true,const class="type">bool wed=true, const class="type">bool thu=true,const class="type">bool fri=true,const class="type">bool sat=false) { Sunday(sun); Monday(mon); Tuesday(tue); Wednesday(wed); Thursday(thu); Friday(fri); Saturday(sat); } class="type">void CTimeDaysBase::Monday(const class="type">bool set) { if(set) m_day_flags|=TIME_DAY_FLAG_MON; else m_day_flags &=~TIME_DAY_FLAG_MON; } class="type">bool CTimeDaysBase::Evaluate(class="type">class="kw">datetime current=class="num">0) { if(!Active()) class="kw">return true; class="type">bool result=false; class="type">MqlDateTime time; if(current==class="num">0) current=TimeCurrent(); TimeToStruct(current,time); class="kw">switch(time.day_of_week) { case class="num">0: result=Sunday(); break; case class="num">1: result=Monday(); break; case class="num">2: result=Tuesday(); break; case class="num">3: result=Wednesday(); break; case class="num">4: result=Thursday(); break; case class="num">5: result=Friday(); break; case class="num">6: result=Saturday(); break; } class="kw">return Reverse()?!result:result; } class CTimerBase : class="kw">public CTime { class="kw">protected: class="type">uint m_years; class="type">uint m_months; class="type">uint m_days; class="type">uint m_hours; class="type">uint m_minutes;