开发回放系统(第33部分):订单系统(二)·进阶篇
📘

开发回放系统(第33部分):订单系统(二)·进阶篇

第 2/2 篇

◍ 给订单系统接上回放练习能力

把手动或半自动的订单逻辑升级成可回放练习的模块,思路直接沿用「自动工作EA」系列里已经验证过的框架,不必从零造轮子。MetaTrader 5 自带的策略测试器能跑历史数据,但本文想解决的是另一种场景:行情休市时,你仍能用模拟环境反复练下单与撤单的节奏。 具体落地从计时器类(timer class)切入,它负责在本地模拟时间轴推进订单状态,不依赖真实报价流。第一阶段我们会快速搬运该系列里现成的功能,重点放在「方便访问交易服务器」这一层,不做深究。 若你想抠实现细节,建议直接翻原系列第15篇关于自动化的章节;本文只做工程拼装,不重复底层解释。外汇与贵金属杠杆高,模拟练手不等于实盘安全,参数未经多周期验证前勿直接上真仓。

「用时间表掐住 EA 的开仓时段」

不少做外汇、贵金属的老手都吃过情绪交易的亏:亚盘早段流动性差、点差跳开,明明计划等欧美重叠时段再动,手却比脑子快。C_ControlOfTime 这个类就是用来把「人管不住手」变成「EA 替你挡枪」的,它继承自 C_Orders,但只扩展不外露,EA 主体并不会直接继承那套下单逻辑。 类里最容易被忽略的一行是 m_InfoCtrl[SATURDAY + 1]。矩阵长度必须 +1,因为枚举从 0 开始、SUNDAY 到 SATURDAY 共 7 个值,若不加一就只能装下 6 天,周六直接越界。构造函数里先调 C_Orders 初始化,再用 for 从 SUNDAY 跑到 SATURDAY 做 ZeroMemory——这里依赖枚举数值 SUNDAY 小于 SATURDAY,否则下标访问会乱序。 SetInfoCtrl 接收形如 "09:00-17:30" 的字符串,用 '-' 切分后各自 StringToTime 再对 86400 取模,只留当日秒数。它顺手校验了 Init<=End 以及时分不超 23/59,否则整段配置直接作废。这套写法把时间控制写得很接近自然语言,接手你代码的人哪怕不懂 MQL5 也能看懂哪天哪段能跑。 外汇与贵金属杠杆高、滑点突发,时间过滤器只是降低情绪干扰的概率手段,不保证避开所有坑。把上面代码塞进 MT5 的 include,用 SATURDAY+1 的矩阵跑一遍周回调试,比看十篇鸡汤有用。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "C_Orders.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_ControlOfTime : class="kw">protected C_Orders
{
  class="kw">private :
    class="kw">struct st_00
    {
      class="type">class="kw">datetime Init,
               End;
    }m_InfoCtrl[SATURDAY + class="num">1];
class=class="str">"cmt">//+------------------------------------------------------------------+
C_ControlOfTime(C_Terminal *arg, class="kw">const class="type">class="kw">ulong magic)
            :C_Orders(arg, magic)
  {
    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[], sz1[];
    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);
      for (class="type">char c0 = class="num">0; (c0 <= class="num">1) && (bLocal); c0++)
        if (bLocal = (StringSplit(szRes[class="num">0], &class="macro">#x27;:&class="macro">#x27;, sz1) == class="num">2))
          bLocal = (StringToInteger(sz1[class="num">0]) <= class="num">23) && (StringToInteger(sz1[class="num">1]) <= class="num">59);
      if (_LastError == ERR_WRONG_STRING_DATE) ResetLastError();

日内时段校验与退出保护

这段逻辑把“时段声明是否合法”和“当前是否处于可控交易窗口”拆成了两层处理。若声明阶段出现 _LastError 非 ERR_SUCCESS,或本地标识 bLocal 为假,直接 Print 具体枚举并调用 ExpertRemove 终止 EA,避免脏配置继续跑。 CtrlTimeIsPassed 用 TimeCurrent 取本地 MqlDateTime,把小时和分钟折算成当日秒数:dt = hour*3600 + min*60。再比对 m_InfoCtrl[day_of_week] 里存的 Init 与 End 秒数边界,落在 [Init, End] 内才返回 true。 实盘里这套写法意味着:周几配错、边界越界都会让 EA 自爆而非乱下单;验证时把 m_InfoCtrl 某天的 End 设成小于 Init,MT5 会在初始化后立刻在日志看到 'Error in the declaration of time of day' 并卸载。

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;

      TimeCurrent(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));
   }

◍ 把 EA 复杂度关进 C_Manager 笼子里

C_Manager 类干的事很简单也很关键:把 EA 本身的逻辑压到几行,所有下单、账户识别、对象管理都收进类里。这样后期接回放/模拟器时,EA 主体不用反复改,代价是当前阶段订单系统只认 MT5 原生能力,不支持交叉品种下单——也就是 EA 处理的标的之外,发不了别的资产订单。外汇与贵金属杠杆高,这种隔离能降低误触其他品种的风险。 类开头用宏定义了一组前缀和快捷访问符,比如 def_LINE_PRICE 拼出 "Manager_PRICE" 用来挂图线,def_InfoTerminal 直接取终端信息。私有区把全局变量塞进 st00、st01 两个结构,st00 装止损止盈金额、杠杆、是否日内及账户锁仓模式,st01 装画线颜色和创建开关。用结构打包,以后删改模块时不用满屏找变量。 第一个对外函数是 CreateOrder,只接收订单类型和价格,内部先跑 CtrlTimeIsPassed() 做时间闸门——超过计划交易时段直接返回 false,不发单。真正填单用的止损、止盈、杠杆都来自 m_Infos 结构,调用方不用管。另一个私有函数走市价成交,只传买卖方向,其余照全局变量填。 全局变量绝不在 EA 里直接碰,初始化交给构造函数,虽长但只是按 EA 传入参数填一遍,并顺手用 TerminalInfoString 查账户类型留作分析。析构函数基本空转,真出事才清掉类建的对象。下面这段是当前类的骨架代码,复制进 MT5 能直接看结构。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "C_ControlOfTime.mqh"
class="macro">#include "..\System EA\Auxiliar\Study\C_Study.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_Prefix "Manager"
class="macro">#define def_LINE_PRICE   def_Prefix + "_PRICE"
class="macro">#define def_LINE_TAKE    def_Prefix + "_TAKE"
class="macro">#define def_LINE_STOP    def_Prefix + "_STOP"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_AcessTerminal(*Terminal)
class="macro">#define def_InfoTerminal         def_AcessTerminal.GetInfoTerminal()
class="macro">#define def_AcessMouse(*Study)
class="macro">#define def_InfoMouse            def_AcessMouse.GetInfoMouse()
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Manager : class="kw">public C_ControlOfTime
{
   class="kw">private :
      class="kw">struct st00
      {
         class="type">class="kw">double  FinanceStop,
                 FinanceTake;
         class="type">uint    Leverage;
         class="type">bool    IsDayTrade,
                 AccountHedging;
      }m_Infos;
      class="kw">struct st01
      {
         class="type">color   corPrice,
                 corTake,
                 corStop;
         class="type">bool    bCreate;
      }m_Objects;
class=class="str">"cmt">//+------------------------------------------------------------------+
      C_Terminal  *Terminal;
      C_Study     *Study;
class="type">bool CreateOrder(class="kw">const ENUM_ORDER_TYPE type, class="kw">const class="type">class="kw">double Price)
   {
      class="type">class="kw">ulong tmp;
      if (!CtrlTimeIsPassed()) class="kw">return class="kw">false;
      tmp = C_Orders::CreateOrder(type, Price, m_Infos.FinanceStop, m_Infos.FinanceTake, m_Infos.Leverage, m_Infos.IsDayTrade);
   }

「下单前先让时间闸和账户模式说话」

这段 MQL5 片段把「能不能下单」拆成了两道关卡:时间控制与账户对冲属性。ToMarket 里先调 CtrlTimeIsPassed(),不通过直接返回 false,说明策略层把交易时段卡死,避免在非活跃窗口瞎单。 C_Manager 构造函数用 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 读账户保证金模式,分三种:RETAIL_HEDGING 置 AccountHedging=true 并打印 "HEDGING";RETAIL_NETTING 标 "NETTING";EXCHANGE 标 "EXCHANGE"。净仓与对冲账户的平仓逻辑不同,EA 启动时就该知道自己在哪种场子里跑。 外汇与贵金属杠杆高、滑点跳空频繁,HEDGING 模式锁仓不抵占用保证金,NETTING 模式同方向加仓合并,写 EA 时不区分可能让风控参数直接失效。开 MT5 接这段,把 Print 出来的账户类型对一下,再决定 FinanceStop / FinanceTake 怎么传。

MQL5 / C++
  class="kw">return tmp > class="num">0;
  }
class="type">bool ToMarket(class="kw">const ENUM_ORDER_TYPE type)
  {
    class="type">class="kw">ulong tmp;

    if (!CtrlTimeIsPassed()) class="kw">return class="kw">false;
    tmp = C_Orders::ToMarket(type, m_Infos.FinanceStop, m_Infos.FinanceTake, m_Infos.Leverage, m_Infos.IsDayTrade);

    class="kw">return tmp > class="num">0;
  }
C_Manager(C_Terminal *arg1, C_Study *arg2, class="type">color cPrice, class="type">color cStop, class="type">color cTake, class="kw">const class="type">class="kw">ulong magic, class="kw">const class="type">class="kw">double FinanceStop, class="kw">const class="type">class="kw">double FinanceTake, class="type">uint Leverage, class="type">bool IsDayTrade)
            :C_ControlOfTime(arg1, magic)
  {
    class="type">class="kw">string szInfo = "HEDGING";

    Terminal = arg1;
    Study = arg2;
    if (CheckPointer(Terminal) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid);
    if (CheckPointer(Study) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid);
    if (_LastError != ERR_SUCCESS) class="kw">return;
    m_Infos.FinanceStop     = FinanceStop;
    m_Infos.FinanceTake     = FinanceTake;
    m_Infos.Leverage        = Leverage;
    m_Infos.IsDayTrade      = IsDayTrade;
    m_Infos.AccountHedging  = class="kw">false;
    m_Objects.corPrice      = cPrice;
    m_Objects.corStop       = cStop;
    m_Objects.corTake       = cTake;
    m_Objects.bCreate       = class="kw">false;
    class="kw">switch ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE))
    {
      case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: m_Infos.AccountHedging = true; class="kw">break;
      case ACCOUNT_MARGIN_MODE_RETAIL_NETTING: szInfo = "NETTING";        class="kw">break;
      case ACCOUNT_MARGIN_MODE_EXCHANGE      : szInfo = "EXCHANGE";       class="kw">break;
    }
    Print("Detected Account ", szInfo);
  }
~C_Manager()
  {
    ObjectsDeleteAll(def_InfoTerminal.ID, def_Prefix);
  }

把工具请下神坛

C_Manager 类还剩一个方法没拆完,作者把它留到了下一篇单独讲 EA 代码的故事,理由是怕这篇读起来太冗长。当前这版 EA 代码仍处在开发阶段,不是能直接上线的成品。 本篇刻意没贴任何代码——不是藏私,是怕你在没吃透机制前拿去跑实盘。外汇和贵金属杠杆高,盲用半成品策略大概率亏多赚少。 知识是能复用的:不凭空造轮子,总是在旧东西上改、随时间长进。下一篇见那个方法和 EA 实码时,你再开 MT5 新建个类照着敲一遍,比现在空看更有用。

常见问题

用回放系统把历史行情按时间表重放,EA在模拟环境里跑订单逻辑,不接真实账户就能验证开仓和退出保护是否靠谱。
在订单系统里加时段校验,只放行你设定的日内时间窗;窗外的下单请求直接拦截,并在退出保护里平掉超时持仓。
小布可加载回放记录与时段配置,自动标出违规开仓点和退出保护触发位,你只需对照提示改时间闸参数。
会。下单前先读账户模式,锁仓模式禁止同品种反向平仓,代码里要分叉处理,否则回放时就会报校验错误。
不用。复杂逻辑封在C_Manager里,你只调时间表和账户模式开关,就能控制开仓时段与退出保护行为。