开发回放系统(第 34 部分):订单系统 (三)(基础篇)
📘

开发回放系统(第 34 部分):订单系统 (三)(基础篇)

第 1/2 篇

「给回放系统接上订单内核」

在 MT5 回放系统的第 34 部分,订单系统进入第三层实现:不再只记录价格走动,而是把模拟下单、成交回报、持仓状态变更都挂到回放时间轴上。这样复盘时你能看到「当时若下市价单,会在哪根棒被撮合」,而不是凭肉眼猜。 原文示例发布于 2024 年 6 月 17 日 13:50,至整理时已累计 487 次查看、0 条评论,说明这类底层订单逻辑少人深究但刚需。Daniel Jose 的这套实现把 COrder 类的状态机和回放帧绑定,避免用真实账户测试策略的风险。 外汇与贵金属品种用这套回放验证订单流,仍属高风险:滑点、点差在回放里是固定的,实盘可能放大数倍,结论只能作概率参考。

先摸清系统再上平台

上一篇文章里我们搭了订单系统的代码骨架,也点出了实现时要踩的坑。但代码看着轻巧,真落到 MT5 里跑完全不是回事。这一节要把 C_Manager 类里最后一个方法拆开讲,再给 EA 改动部分加注释——你不用开终端也能预判它大概怎么动。 附录会附完整代码,你可以直接拖进 MetaTrader 5 回放验证。但别指望等系统“更完美”再测:现在不搞清楚运行逻辑,后面改参数、看表现都会卡壳。 如果你只想要现成系统、不想参与演化,那现在跳过测试以后会更被动——一旦某段没展示且你不会改,就调不出自己要的输出。系统是一节节长的,早测早知道边界在哪。

◍ DispatchMessage 怎么接管图表事件

C_Manager 里的 DispatchMessage 是整个事件调度的中枢,MT5 抛过来的图表事件(比如 CHARTEVENT_MOUSE_MOVE、CHARTEVENT_KEYDOWN)都先汇到这里,再分发给终端访问类和鼠标类各自的处理器。把事件集中在一处,EA 主文件里的代码量能明显压下来,跨项目搬代码时也不用反复改事件顺序,调试时少踩坑。 键盘事件里有个容易忽略的点:lparam 虽然带了按键码,但做快捷键不能只靠它。代码里用 TerminalInfoInteger 去识别 CTRL 状态,组合 CTRL+UP 市价买入、CTRL+DOWN 市价卖出;单独按方向键 EA 不会发单。这套判断必须留在 CHARTEVENT_KEYDOWN 内,因为只有当 MT5 真的触发按键事件时这段才执行,挪出去会平白跑无用代码。外汇与贵金属杠杆高,误触快捷键可能直接市价进场,实盘前务必改键或加确认。 鼠标移动事件 CHARTEVENT_MOUSE_MOVE 通过 C_Study 访问 C_Mouse,顺带捕捉 SHIFT/CTRL 状态——注意这里的修饰键属于鼠标体系(含带侧键的特殊鼠标),别和按键事件里的搞混。挂单流程分三段:先通知平台并借三个图表对象定限价位、隐藏鼠标;用户确认后发挂单请求;最后重现鼠标并删对象。 递归藏在 EventChartCustom 的调用里。图 02 的绿箭头显示:DispatchMessage 内调自定义事件会重新触发 OnChartEvent → 再进 DispatchMessage,至少跑两倍 OnChartEvent 体量。若不小心在 C_Mouse 里又用 EventChartCustom 处理同一事件,就死循环了。当前写法因 C_Manager 不重复处理该自定义事件而安全,递归成本可接受,但复杂模型下可能拖慢平台。

MQL5 / C++
class="type">void DispatchMessage(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam)
  {
    class="kw">static class="type">class="kw">double price = class="num">0;
    class="type">bool bBuy, bSell;
    
    def_AcessTerminal.DispatchMessage(id, lparam, dparam, sparam);
    def_AcessMouse.DispatchMessage(id, lparam, dparam, sparam);
    class="kw">switch (id)
    {

「键盘鼠标双通道下单的事件分发」

这段事件处理把键盘和鼠标两条交互路径并到同一个 OnChartEvent 里。按住 Ctrl 再按上下方向键,TerminalInfoInteger 读到的 TERMINAL_KEYSTATE_UP / DOWN 就会触发市价买或卖,比右键菜单快半拍,适合贵金属跳空时抢位。 鼠标分支里,Shift 和 Ctrl 的按下状态由 C_Mouse::CheckClick 判定,bBuy 与 bSell 互斥时才画三条水平线:价格线、止盈线、止损线,颜色分别走 corPrice / corTake / corStop。ObjectMove 把止盈止损按 FinanceToPoints 算出的点数偏移挂到鼠标价位两侧,买则上方止盈、下方止损,卖则反过来。 左键真正落单前要求 price==0,避免重复发单;一旦 bBuy==bSell(都没按修饰键),EventChartCustom 发显示鼠标事件并 ObjectsDeleteAll 清掉所有前缀对象,把 price 复位。外汇与贵金属杠杆高,这种一键发单逻辑在实盘里可能误触,建议先在 MT5 策略测试器用模拟图表跑通再挂真户。

MQL5 / C++
case CHARTEVENT_KEYDOWN:
      if (TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL))
      {
         if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP))  ToMarket(ORDER_TYPE_BUY);
         if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN))ToMarket(ORDER_TYPE_SELL);
      }
      break;
      case CHARTEVENT_MOUSE_MOVE:
         bBuy = def_AcessMouse.CheckClick(C_Mouse::eSHIFT_Press);
         bSell = def_AcessMouse.CheckClick(C_Mouse::eCTRL_Press);
         if (bBuy != bSell)
         {
            if (!m_Objects.bCreate)
            {
               def_AcessTerminal.CreateObjectGraphics(def_LINE_PRICE, OBJ_HLINE, m_Objects.corPrice, class="num">0);
               def_AcessTerminal.CreateObjectGraphics(def_LINE_STOP, OBJ_HLINE, m_Objects.corStop, class="num">0);
               def_AcessTerminal.CreateObjectGraphics(def_LINE_TAKE, OBJ_HLINE, m_Objects.corTake, class="num">0);
               EventChartCustom(def_InfoTerminal.ID, C_Mouse::ev_HideMouse, class="num">0, class="num">0, "");
               m_Objects.bCreate = true;
            }
            ObjectMove(def_InfoTerminal.ID, def_LINE_PRICE, class="num">0, class="num">0, def_InfoMouse.Position.Price);
            ObjectMove(def_InfoTerminal.ID, def_LINE_TAKE, class="num">0, class="num">0, def_InfoMouse.Position.Price + (Terminal.FinanceToPoints(m_Infos.FinanceTake, m_Infos.Leverage) * (bBuy ? class="num">1 : -class="num">1)));
            ObjectMove(def_InfoTerminal.ID, def_LINE_STOP, class="num">0, class="num">0, def_InfoMouse.Position.Price + (Terminal.FinanceToPoints(m_Infos.FinanceStop, m_Infos.Leverage) * (bSell ? class="num">1 : -class="num">1)));
            if ((def_AcessMouse.CheckClick(C_Mouse::eClickLeft)) && (price == class="num">0)) CreateOrder((bBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), price = def_InfoMouse.Position.Price);
         }else if (m_Objects.bCreate)
         {
            EventChartCustom(def_InfoTerminal.ID, C_Mouse::ev_ShowMouse, class="num">0, class="num">0, "");
            ObjectsDeleteAll(def_InfoTerminal.ID, def_Prefix);
            m_Objects.bCreate = class="kw">false;
            price = class="num">0;
         }
         break;
      }
   }
def_AcessTerminal.DispatchMessage(id, lparam, dparam, sparam);

常见问题

直接上平台容易漏掉事件冲突和状态不同步。先理清订单状态机与事件源,能少踩一半坑,建议先在离线环境跑通再接图表。
它按消息类型把图表事件路由到对应处理函数;鼠标无响应多是事件没进分发链或处理函数未注册,检查消息钩子是否覆盖左键按下。
可以。小布能扫描你的事件分发逻辑,标出未覆盖的按键和鼠标动作,并给出补丁建议,省去手动逐行查。
在分发层加去重锁,同一 tick 内只认首个有效指令;用订单序列号比对,重复信号直接丢弃。
按品种历史波动率取近 20 根 K 线的平均点差倍 1~2 倍,别写死固定值,否则回放曲线会失真。