跨平台智能交易系统: 时间过滤器·进阶篇
(2/3)· 从 CTime 基类到 CTimeFilter 子过滤器,把横盘规避与周末平仓写成可复用代码
「计时基类的字段与接口布局」
在 MT5 用 C++ 风格封装一个跨周期计时器时,先把底层状态量定清楚能少走很多弯路。下面这段类声明给出了基类的核心成员:用 uint 存秒数、int 存总次数与已耗时,datetime 存起始时间,三个变量足以支撑后续任意周期判断。 构造函数提供了两种入口,一个是单 int 参数(可能用于快捷按总秒数初始化),另一个是六个 uint 参数,依次对应年、月、日、时、分、秒的细粒度设定,析构函数为空实现。 公开接口里最实用的是那组成对出现的取值/赋值函数:Year()、Month()、Days()、Hours()、Minutes()、Seconds() 都写了 const 读版本和带参写版本,Total() 返回 bool 表示是否累计完成,TimeStart() 管理起点时刻。 别把正态当圣经:虚函数 Validate() 和 Elapsed() 都留了 virtual,说明派生类(比如按交易日计时的子类)要自己重写合法性校验与到期逻辑,直接拿基类调 Elapsed() 只会走空壳。 开 MT5 新建一个 include 头文件,把下面结构原样贴进去,编译不报错就说明你的环境支持这套重载写法,后面接策略计时直接继承即可。
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 数据。
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 打印出来核对,避免时区偏移导致误判。
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。外汇与贵金属杠杆高,时段误判可能让策略在流动性骤变时误触发,务必先回测再上实盘。
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 遍历逻辑,就能跑通时间维度的信号闸门。
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 跑一遍再上实盘。
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));