开发回放系统(第 34 部分):订单系统 (三)(基础篇)
「给回放系统接上订单内核」
在 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 不重复处理该自定义事件而安全,递归成本可接受,但复杂模型下可能拖慢平台。
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 策略测试器用模拟图表跑通再挂真户。
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);