构建自动运行的 EA(第 09 部分):自动化(II)·进阶篇
⏱️

构建自动运行的 EA(第 09 部分):自动化(II)·进阶篇

(2/3)· 半手动 EA 仍靠人盯盘?用 C_ControlOfTime 给系统加上时段闸门,避免过度交易

含代码示例实战向 第 2/3 篇
很多交易者以为挂上 EA 就万事大吉,其实半手动系统仍要人去挪止损、发指令。给 EA 加上时间控制类,它才真正只在你定义的窗口里干活,其余时间安静待命。

◍ 按星期切割交易时段的控制类骨架

在 EA 架构里把「哪天能跑、几点到几点」做成结构化约束,比在 OnTick 里写一堆 if(TimeHour()...) 要干净得多。下面这段从订单管理基类派生的构造函数,就顺手把一周七天的控制缓冲区清零了。 构造函数 C_ControlOfTime 继承 C_Orders(magic),进来先 ResetLastError() 清掉环境错误码,再用 for 从 SUNDAY 到 SATURDAY 遍历 ENUM_DAY_OF_WEEK,对 m_InfoCtrl[c0] 调 ZeroMemory。也就是说,默认状态下每天的交易信息结构体都是空白的,后续要靠 SetInfoCtrl 填具体时段。 SetInfoCtrl 被标成 final,外部子类不能重写,保证时间规则只有这一处入口。它先判 _LastError 非 0 就直接 return,再卡 index 必须在 SUNDAY~SATURDAY 范围内,越界也 return,这两道闸能挡掉大部分乱传参数的调用。 真正填数据靠 StringSplit(szArg, '-', szRes) == 2,也就是参数得是 "HH:MM-HH:MM" 这种用短横分隔的字符串。拆出来后拿 StringToTime 转成秒,再对 86400 取模——因为 MT5 的 TimeToStruct 之前,StringToTime 返回的是带日期的时间戳,取模 86400 刚好剥掉日期只留当日秒数。Init 和 End 都这么处理,最后要求 Init <= End 才认这段合法,否则 bLocal 为 false 后面会被拒掉。 可以现在开 MT5 新建一个继承自 C_Orders 的类,把这段粘进去编译,用 SetInfoCtrl(MONDAY, "08:00-17:00") 测一下 m_InfoCtrl[MONDAY].Init 是不是 28800、End 是不是 61200。外汇和贵金属品种波动受时段影响明显,这类时间控制能降低非活跃段撮合异常的概率,但杠杆品种本身高风险,时段过滤不保证胜率。

MQL5 / C++
C_ControlOfTime(class="kw">const class="type">class="kw">ulong magic)
  :C_Orders(magic)
  {
  ResetLastError();
  for (ENUM_DAY_OF_WEEK c0 = SUNDAY; c0 <= SATURDAY; c0++) ZeroMemory(m_InfoCtrl[c0]);
  }
class="kw">virtual class="type">void SetInfoCtrl(class="kw">const ENUM_DAY_OF_WEEK index, class="kw">const class="type">class="kw">string szArg) final
  {
  class="type">class="kw">string szRes[];
  class="type">bool bLocal;
  
  if (_LastError != ERR_SUCCESS) class="kw">return;
  if ((index > SATURDAY) || (index < SUNDAY)) class="kw">return;
  if (bLocal = (StringSplit(szArg, &class="macro">#x27;-&class="macro">#x27;, szRes) == class="num">2))
  {
  m_InfoCtrl[index].Init = (StringToTime(szRes[class="num">0]) % class="num">86400);
  m_InfoCtrl[index].End = (StringToTime(szRes[class="num">1]) % class="num">86400);
  bLocal = (m_InfoCtrl[index].Init <= m_InfoCtrl[index].End);
  if (_LastError == ERR_WRONG_STRING_DATE) ResetLastError();
  }
  if ((_LastError != ERR_SUCCESS) || (!bLocal))
  {

时段字符串解析的坑与本地时间比对

在 EA 初始化时解析「星期-时段」参数,常见写法是用 StringSplit 按 '-' 切分,再 StringToTime 转成秒数存进结构体。代码里对切分结果做了严格校验:必须恰好分成两段,且 Init 小于等于 End 才认为合法。 下面这组实测样例能直接看出边界:"12:34 - - 18:34"、"12:32 18:34"、" - 18:34"、"12:34 - " 全被判定为错误;只有 "12:32 - 18:34"(含标准空格分隔)能通过。多余空格或缺失端点都会让 bLocal 为 false,触发 ExpertRemove 自卸载。

[CODE]if ((_LastError != ERR_SUCCESS)(!bLocal))

{ Print("Error in the declaration of the time of day: ", EnumToString(index)); ExpertRemove(); }[/CODE] 上面这段就是校验失败后的自清理:打印具体星期枚举并移除 Expert,避免脏配置继续跑。 运行时比对走 CtrlTimeIsPassed:取本地时间拆出 hour*3600+min*60 得到当日秒数,再与对应星期段的 Init/End 闭区间比。注意它用的是 TimeLocal 而非服务器时间,跨时区账户要自己算偏移,外汇和贵金属品种受此影响可能出现信号触发偏差,属于高风险配置点。 [CODE]virtual const bool CtrlTimeIsPassed(void) final { datetime dt; MqlDateTime mdt; TimeToStruct(TimeLocal(), mdt); dt = (mdt.hour * 3600) + (mdt.min * 60); return ((m_InfoCtrl[mdt.day_of_week].Init <= dt) && (m_InfoCtrl[mdt.day_of_week].End >= dt)); }[/CODE] 逐行看:TimeToStruct 把本地时间填进 mdt;dt 只算时分折算秒,忽略日期;return 用闭区间判断,当前秒落在周内设定段内才返回 true。开 MT5 把这段塞进类里,改 Init/End 打印就能验证你自己的时段参数有没有踩坑。

MQL5 / C++
if ((_LastError != ERR_SUCCESS) || (!bLocal))
{
   Print("Error in the declaration of the time of day: ", EnumToString(index));
   ExpertRemove();
}

class="kw">virtual class="kw">const class="type">bool CtrlTimeIsPassed(class="type">void) final
            {
               class="type">class="kw">datetime dt;
               class="type">MqlDateTime mdt;
               
               TimeToStruct(TimeLocal(), mdt);
               dt = (mdt.hour * class="num">3600) + (mdt.min * class="num">60);
               class="kw">return ((m_InfoCtrl[mdt.day_of_week].Init <= dt) && (m_InfoCtrl[mdt.day_of_week].End >= dt));
            }

「用 final 锁死时间闸门」

MQL5 里给虚函数加 final,能阻止派生类重写该方法。在交易机器人里,常把“当前是否过了可交易时段”的判断做成带 final 的虚函数,避免子类误改时间逻辑导致意外开仓。 下面这段代码声明了一个不可被覆盖的 CtrlTimeIsPassed 方法,返回 const bool,语义上就是“时段是否已过”的只读判定。 [CODE] virtual const bool CtrlTimeIsPassed(void) final //+------------------------------------------------------------------+ [/CODE] 逐行看:virtual 表示可多态调用;const bool 说明返回常量布尔且对象状态不变;CtrlTimeIsPassed(void) 是无参方法;final 强制任何继承类都不能重写它。注释行只是 MQL5 向导生成的分割线。 在 MT5 里新建 EA 基类时加上 final,可让小布类策略的时间过滤模块不被下游篡改。外汇与贵金属波动剧烈,这类硬编码护栏能降低非预期交易概率,但无法消除高风险。

MQL5 / C++
class="kw">virtual class="kw">const class="type">bool CtrlTimeIsPassed(class="type">void) final
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 把时间控制类挂进管理器

让 EA 支持按时间段开关交易,最干净的做法是让 C_Manager 公开继承 C_ControlOfTime。原文里把原本 private 继承 C_Orders 的那行划掉,换成 class C_Manager : public C_ControlOfTime,等于把时间控制能力整包并进管理器,且不改它原有的稳定性。 如果哪天 C_ControlOfTime 引发异常,只要删掉这一句继承和相关引用,C_Manager 立刻回到原先的健壮状态,不影响下单逻辑。这种「插拔式」扩展比散函数堆砌安全得多,也方便 MT5 上快速回退验证。 构造函数要同步把 magic 传给时间类:原代码里 :C_Orders(magic) 被替换为 :C_ControlOfTime(magic),后者再链式调 C_Orders 的构造拿魔幻数字。实际控时间只用在需要限制的函数里贴一行高亮代码,比如尾随停止;不想要的地方删掉那行即可。 EA 主循环里只需加一个用户交互点来捕获时间参数并抛给 C_Manager,系统就能自动控交易时段。外汇与贵金属杠杆高、滑点猛,这类开关只是降低乱开仓概率,不保证收益。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "C_Orders.mqh"
class="macro">#include "C_ControlOfTime.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_MAX_LEVERAGE                class="num">10
class="macro">#define def_ORDER_FINISH                class="kw">false
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Manager : class="kw">private C_Orders
class C_Manager : class="kw">public C_ControlOfTime
class=class="str">"cmt">//+------------------------------------------------------------------+
                C_Manager(class="kw">const class="type">class="kw">ulong magic, class="type">class="kw">double FinanceStop, class="type">class="kw">double FinanceTake, class="type">uint Leverage, class="type">bool IsDayTrade, class="type">class="kw">double Trigger)
                        :C_ControlOfTime(magic),
                        :C_Orders(magic),
                        m_bAccountHedging(class="kw">false),
                        m_TicketPending(class="num">0),
                        m_Trigger(Trigger)
                        {
                                class="type">class="kw">string szInfo;
                                
                                ResetLastError();
                                ZeroMemory(m_Position);
                                m_InfosManager.FinanceStop = FinanceStop;
class=class="str">"cmt">// ... The rest of the constructor code....
                        }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
                class="type">void CreateOrder(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double Price)
                        {

挂单与市价单的触发闸门

这段逻辑把开仓动作卡在两道门槛后面:时间控制没过就直接 return,杠杆触顶或已有挂单/对冲持仓也一律不出手。 挂单分支里,m_TicketPending 大于 0 就拦掉,避免重复挂单;市价分支在对冲账户下只看持仓 ticket,非对冲则补录本次成交号。 输入参数给了明确可调点:user01 杠杆系数默认 1,user02 止盈金额默认 100,user03 止损金额默认 75,user04 默认按日内交易处理。外汇与贵金属波动剧烈,高杠杆可能放大回撤,调这些数前先在 MT5 策略测试器跑一遍。 把下面代码贴进 EA 的收尾段,就能直接看拦截效果。

MQL5 / C++
if (!CtrlTimeIsPassed()) class="kw">return;
if ((m_StaticLeverage >= def_MAX_LEVERAGE) || (m_TicketPending > class="num">0) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) class="kw">return;
m_TicketPending = C_Orders::CreateOrder(type, Price, (def_ORDER_FINISH ? class="num">0 : m_InfosManager.FinanceStop), (def_ORDER_FINISH ? class="num">0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ToMarket(class="kw">const ENUM_ORDER_TYPE type)
{
   class="type">class="kw">ulong tmp;

   if (!CtrlTimeIsPassed()) class="kw">return;
   if ((m_StaticLeverage >= def_MAX_LEVERAGE) || (m_bAccountHedging && (m_Position.Ticket > class="num">0))) class="kw">return;
   tmp = C_Orders::ToMarket(type, (def_ORDER_FINISH ? class="num">0 : m_InfosManager.FinanceStop), (def_ORDER_FINISH ? class="num">0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
   m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > class="num">0 ? m_Position.Ticket : tmp));
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <Generic Auto Trader\C_Manager.mqh>
class="macro">#include <Generic Auto Trader\C_Mouse.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
C_Manager *manager;
C_Mouse  *mouse;
class=class="str">"cmt">//+------------------------------------------------------------------+
input class="type">int      user01   = class="num">1;                class=class="str">"cmt">//Leverage Factor
input class="type">class="kw">double   user02   = class="num">100;              class=class="str">"cmt">//Take Profit( FINANCE )
input class="type">class="kw">double   user03   = class="num">75;               class=class="str">"cmt">//Stop Loss( FINANCE )
input class="type">bool     user04   = true;             class=class="str">"cmt">//Day Trade ?
input class="type">class="kw">color    user05   = clrBlack;         class=class="str">"cmt">//Price Line Color
把时段巡检交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 活跃时段与成交密度,你只需核对是否越界。

常见问题

MQL5 不支持 C++ 式多重继承,可用组合或单层继承保留所需接口,把时间判断逻辑封在独立类里,降低耦合。
可能定时器事件未正确过滤,或挂单在窗口外成交;需在 OnTradeTransaction 里二次校验时间边界。
可以,小布的品种页会标注当前会话成交笔数,若接近你设的阈值会给出提示,但决策仍由你做。
回测用历史时间戳,实盘用本地或经纪商时间,建议在类内统一时区转换,并在初始化打印偏移量。