开发回放系统(第32部分):订单系统(一)(基础篇)
「回放引擎里的订单系统从哪搭起」
MetaTrader 5 的回放系统做到第 32 部分,这一节先把订单系统的骨架立起来,不急着写完整撮合逻辑。原文示例发布于 2024 年 5 月 16 日 09:32,至今在源处累计阅读 1 179 次,说明这类底层模块对实盘复盘工具的需求一直不低。 做回放和做实盘 EA 最大的区别,是回放要把历史 tick 重新灌进订单管理对象,而不是交给券商服务器。订单系统第一节只解决一件事:用统一结构承接市价单的开仓、挂单登记与状态字段,后续小节再接执行与撤改。 外汇与贵金属品种在回放里同样要走这套结构,但点差跳空和周末停盘会带来状态异常,属于高风险验证项,建议先在 EURUSD 的 M1 历史上跑通再换 XAUUSD。
◍ 让EA一次编译就能跑实盘和回放
回放系统里最棘手的一块是订单系统:它要模拟交易服务器的行为,却不让使用者察觉自己身在模拟环境。难点不在画界面,而在于 EA 本身不能为了测试另编一版、为了实盘再编一版——用户若每次切换都要重编译,这套系统就废了。 目前的类继承(图01)把 C_Terminal 塞进继承链,看似省事,实则破坏了封装:下层类会知道比干活所需更多的终端细节。正确做法是把 C_Terminal 从继承块里抽出来,改成由 EA 代码以参数形式注入,各子类只在被允许时才能碰到它。 新继承图(图02)下,类间访问完全由 EA 主控。已有代码只需少量改动即可迁移,不会动摇整体结构。这一步是后续让同一份 EA 同时跑 demo、live 与回放的前提,外汇/贵金属杠杆高,模拟与实盘行为一致才能避免上线即踩坑。
拆掉继承后怎么把终端类接进鼠标和指标
在把 C_Terminal 改成非继承依赖前,先在类里塞一个错误枚举 eErrUser,值只有 ERR_Unknown 和 ERR_PointerInvalid 两种。它配合 _LastError 变量,让系统在指针没挂好时能被外部感知,而不是静默崩掉。
原先 C_Mouse 用 : public C_Terminal 继承,现在去掉继承关系,改为持有一个 C_Terminal* 指针。构造函数不再调 C_Terminal() 的初始化列表,而是接收一个外部传入的 arg 指针,并用 CheckPointer 验证:若返回 POINTER_INVALID,就通过 SetUserError(C_Terminal::ERR_PointerInvalid) 写进用户错误位。
这里有个坑:如果 Terminal 指针无效,构造函数必须直接 return,否则后面访问 def_AcessTerminal 这类宏会踩到未初始化内存。MT5 里 _LastError 若被置为 4301(ERR_MARKET_UNKNOWN_SYMBOL),其实不代表资产真没识别到,只是 C_Terminal 还没初始化名称。可在 C_Terminal 构造函数里强制清一次错误位,避免 EA 被误删。
C_Study 也照此改造,构造函数收一个 C_Terminal* 并转交给 C_Mouse。由于它继承的是 C_Mouse 而非 Terminal,初始化链变成 :C_Mouse(arg, corH, corP, corN),但指针安全检查逻辑与鼠标类一致。DispatchMessage 里原来调用 C_Terminal::DispatchMessage 的依赖要删掉,否则会留下不自然的跨类耦合。外汇与贵金属图表上跑这套代码属高风险操作,参数或指针错乱可能导致图形对象错位。
class C_Terminal { class="kw">protected: enum eErrUser {ERR_Unknown, ERR_PointerInvalid}; class=class="str">"cmt">// ... Internal code ... }; class="macro">#define def_AcessTerminal(*Terminal) class="macro">#define def_InfoTerminal def_AcessTerminal.GetInfoTerminal() class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Mouse : class="kw">public C_Terminal { class="kw">protected: class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// ... Internal fragment .... class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">private : class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// ... Internal fragment ... C_Terminal *Terminal; class=class="str">"cmt">//+------------------------------------------------------------------+ C_Mouse(C_Terminal *arg, class="type">color corH, class="type">color corP, class="type">color corN) :C_Terminal() { Terminal = arg; if (CheckPointer(Terminal) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); if (_LastError != ERR_SUCCESS) class="kw">return; ZeroMemory(m_Info); m_Info.corLineH = corH; m_Info.corTrendP = corP; m_Info.corTrendN = corN; m_Info.Study = eStudyNull; m_Mem.CrossHair = (class="type">bool)ChartGetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL); ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL, false); def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH); } class="kw">virtual 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="type">int w = class="num">0; class="kw">static class="type">class="kw">double memPrice = class="num">0; C_Terminal::DispatchMessage(id, lparam, dparam, sparam); class="kw">switch (id) { class=class="str">"cmt">//.... C_Study(C_Terminal *arg, class="type">color corH, class="type">color corP, class="type">color corN) :C_Mouse(arg, corH, corP, corN) {
「终端对象初始化时顺手接管图表属性」
这段构造函数把图表终端的运行时参数一次性抓进内存,顺带关掉对象描述与日期轴显示,避免面板绘制时被原生 UI 干扰。 先看指针校验:传入的 Terminal 若为 POINTER_INVALID 就通过 SetUserError 抛自定义错误码 ERR_PointerInvalid,随后判断 _LastError 非 ERR_SUCCESS 直接 return,确保后续内存操作不会踩空。 ZeroMemory(m_Info) 清空结构体后,把状态置为 eCloseMarket,并用 iClose 取日线收盘价:当符号是回放符号,或当前日期与 D1 最新 K 线日期不一致时取 0 号柱(当日),否则取 1 号柱(前一日收盘)。 CreateObjectInfo 连续建了三个扩展按钮,坐标分别是 (2,110) 带 PaleTurquoise 色、(2,53) 与 (58,53) 默认色,这是面板布局的固定锚点。 C_Terminal() 里用 ChartID 锁定当前图,ChartGetInteger 读出 CHART_SHOW_OBJECT_DESCR 和 CHART_SHOW_DATE_SCALE 旧值存进 m_Mem,再强制设 false 隐藏;同时开 CHART_EVENT_OBJECT_DELETE 事件捕获。 品种精度与尺寸也在此落地:SYMBOL_DIGITS 转 nDigits,CHART_WIDTH_IN_PIXELS / HEIGHT_IN_PIXELS 拿画布像素;SYMBOL_TRADE_TICK_SIZE 与 TICK_VALUE 算出的 AdjustToTrade = PointPerTick / ValuePerPoint,是后续把价格点折算成金额的关键比率。外汇与贵金属杠杆高,这类比率在重算前务必确认点值没被券商临时调整。
Terminal = arg; if (CheckPointer(Terminal) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid); if (_LastError != ERR_SUCCESS) class="kw">return; ZeroMemory(m_Info); m_Info.Status = eCloseMarket; m_Info.Rate.close = iClose(def_InfoTerminal.szSymbol, PERIOD_D1, ((def_InfoTerminal.szSymbol == def_SymbolReplay) || (macroGetDate(TimeCurrent()) != macroGetDate(iTime(def_InfoTerminal.szSymbol, PERIOD_D1, class="num">0))) ? class="num">0 : class="num">1)); m_Info.corP = corP; m_Info.corN = corN; CreateObjectInfo(class="num">2, class="num">110, def_ExpansionBtn1, clrPaleTurquoise); CreateObjectInfo(class="num">2, class="num">53, def_ExpansionBtn2); CreateObjectInfo(class="num">58, class="num">53, def_ExpansionBtn3); } C_Terminal() { m_Infos.ID = ChartID(); CurrentSymbol(); m_Mem.Show_Descr = ChartGetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR); m_Mem.Show_Date = ChartGetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, false); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, class="num">0, true); ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, false); m_Infos.nDigits = (class="type">int) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_DIGITS); m_Infos.Width = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); m_Infos.Height = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); m_Infos.PointPerTick = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_SIZE); m_Infos.ValuePerPoint = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_TRADE_TICK_VALUE); m_Infos.VolumeMinimal = SymbolInfoDouble(m_Infos.szSymbol, SYMBOL_VOLUME_STEP); m_Infos.AdjustToTrade = m_Infos.PointPerTick / m_Infos.ValuePerPoint; ResetLastError(); }