开发回放系统(第33部分):订单系统(二)(基础篇)
「用MQL5把历史订单捞出来逐条看」
在 MT5 里做回放系统,第一步往往是把历史订单拉出来核对。MQL5 提供 HistorySelect 按时间区间筛选,再用 HistoryDealsTotal 拿到成交笔数,循环里用 HistoryDealGetTicket 取每一笔的 ticket。 下面这段演示了如何把最近 30 天的成交全部选中并打印总笔数。实际调试时,把 Print 换成你自己的回放缓存写入逻辑即可。 外汇与贵金属品种的历史成交可能包含挂单成交、部分平仓等复杂状态,直接遍历时务必留意 deal type 字段,否则回放会漏掉关键动作。
class="type">bool selected = HistorySelect(class="num">0, TimeCurrent()); if(selected) { class="type">int total = HistoryDealsTotal(); Print("Deals found: ", total); for(class="type">int i=class="num">0; i<total; i++) { class="type">class="kw">ulong ticket = HistoryDealGetTicket(i); class=class="str">"cmt">// 这里可继续用 HistoryDealGetInteger / HistoryDealGetDouble 取字段 } }
先让 EA 在真服务器上跑起来
上一部分里我们写了订单系统的基础类 C_Orders,只放了起步必需的函数。有人会觉得这些功能太单薄、覆盖不了完整订单逻辑,这看法不算错,但现阶段我们故意不做全套。 这个早期系统不直接服务回放/模拟,而是先对接真实交易服务器——模拟账户和真实账户都能用。MT5 平台天生就和服务器通信,从一开始就借满它的能力,比自己造轮子更稳。若等回放/模拟器做好才去适配 EA,后面很难再让两边行为对齐。 所以 C_Orders 的方法数被刻意压低。它肯定撑不起后续全部需求,但眼下目标是让 MT5 替我们扛住交易服务器交互,避免在不必要的地方重复造组件。等真服务器上 EA 跑顺了,回放系统再来匹配它,代价最小。 外汇与贵金属交易属高风险,真账户验证前请先在模拟环境确认逻辑。不过在继续扩订单系统前,有个 MT5 里的‘不便’要先处理——它不算 bug,但会绊脚。
◍ 让图表对象选中逻辑更顺手
从其他平台切到 MT5 的人,常卡在一个细节:MT5 默认要双击图表对象才能选中,而很多平台单击即可。若想贴近旧习惯,可按 CTRL+O 打开选项,在「图表」选项卡勾选「单击鼠标选择对象」,但这样仍缺一些事件级控制能力。 想在代码层彻底解决,可给 C_Terminal 类补一段消息分发逻辑,让对象点击与创建时自动管理选中状态,同时尊重对象属性里「禁止选中」的设定。下面这段代码已包含核心处理: virtual void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { static string st_str = ""; switch (id) { case CHARTEVENT_CHART_CHANGE: m_Infos.Width = (int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); m_Infos.Height = (int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); break; case CHARTEVENT_OBJECT_CLICK: if (st_str != sparam) ObjectSetInteger(m_Infos.ID, st_str, OBJPROP_SELECTED, false); if (ObjectGetInteger(m_Infos.ID, sparam, OBJPROP_SELECTABLE) == true) ObjectSetInteger(m_Infos.ID, st_str = sparam, OBJPROP_SELECTED, true); break; case CHARTEVENT_OBJECT_CREATE: if (st_str != sparam) ObjectSetInteger(m_Infos.ID, st_str, OBJPROP_SELECTED, false); st_str = sparam; break; } } C_Terminal() { m_Infos.ID = ChartID(); CurrentSymbol(); m_Mem.Show_Descr = ChartGetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR); 逐行看:static string st_str 存上一次选中对象名,初值空串避免首帧误判。CHARTEVENT_CHART_CHANGE 下取图表宽高像素,纯界面同步。OBJECT_CLICK 里,若旧名与新 sparam 不同先取消旧对象选中,再判 OBJPROP_SELECTABLE 为真才把新对象置为选中——这正好跳过图01里勾了「不可选」的物件。OBJECT_CREATE 同理清旧选中并记新名。 注意 CHARTEVENT_OBJECT_CREATE 不会自动进你的程序,必须先用 ChartSetInteger 开 CHART_EVENT_OBJECT_CREATE 通知,不需要时再关掉。外汇与贵金属图表对象操作属高风险环境,自动化选中逻辑仅降低误操作概率,不保证分析正确率。
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="kw">static class="type">class="kw">string st_str = ""; class="kw">switch (id) { case CHARTEVENT_CHART_CHANGE: 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); class="kw">break; case CHARTEVENT_OBJECT_CLICK: if (st_str != sparam) ObjectSetInteger(m_Infos.ID, st_str, OBJPROP_SELECTED, class="kw">false); if (ObjectGetInteger(m_Infos.ID, sparam, OBJPROP_SELECTABLE) == true) ObjectSetInteger(m_Infos.ID, st_str = sparam, OBJPROP_SELECTED, true); class="kw">break; case CHARTEVENT_OBJECT_CREATE: if (st_str != sparam) ObjectSetInteger(m_Infos.ID, st_str, OBJPROP_SELECTED, class="kw">false); st_str = sparam; class="kw">break; } } C_Terminal() { m_Infos.ID = ChartID(); CurrentSymbol(); m_Mem.Show_Descr = ChartGetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR);
「图表状态接管与析构还原」
在指标或 EA 初始化阶段,先通过 ChartGetInteger 把图表原有的日期轴显示状态存进 m_Mem.Show_Date,随后用 ChartSetInteger 关掉 CHART_SHOW_OBJECT_DESCR、CHART_SHOW_DATE_SCALE,并打开 OBJECT_DELETE 与 OBJECT_CREATE 事件开关。这一步等于临时接管当前图表的视觉与交互环境,避免手动画线干扰脚本逻辑。 符号层面的参数用 SymbolInfoInteger 和 SymbolInfoDouble 一次性拉全:报价小数位 nDigits、图表像素宽高、每跳点值 PointPerTick、每点金额 ValuePerPoint、最小交易量步长 VolumeMinimal。AdjustToTrade 由 ValuePerPoint 除以 PointPerTick 得到,是后续把价格跳动换算成账户盈亏系数的关键比值。 析构函数 ~C_Terminal() 负责把日期轴与对象描述状态还原成进入前的样子,并关闭那两个对象事件开关。你不还原的话,MT5 图表会停留在被脚本改过的显示模式,下一次手动操作可能莫名其妙收不到对象事件。 直接把这段放进自己的 C_Terminal 类里,编译挂到 XAUUSD 的 5 分钟图,切出去画一根趋势线再回来,若没触发 CREATE/DELETE 事件就说明开关没生效,优先查 m_Infos.ID 是不是取到了正确图表句柄。外汇与贵金属杠杆高,这类环境接管代码若忘了析构还原,实盘界面异常的概率会明显上升。
m_Mem.Show_Date = ChartGetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, class="kw">false); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, class="num">0, true); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_CREATE, class="num">0, true); ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, class="kw">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.ValuePerPoint / m_Infos.PointPerTick; m_Infos.ChartMode = (ENUM_SYMBOL_CHART_MODE) SymbolInfoInteger(m_Infos.szSymbol, SYMBOL_CHART_MODE); ResetLastError(); } ~C_Terminal() { ChartSetInteger(m_Infos.ID, CHART_SHOW_DATE_SCALE, m_Mem.Show_Date); ChartSetInteger(m_Infos.ID, CHART_SHOW_OBJECT_DESCR, m_Mem.Show_Descr); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_DELETE, class="num">0, class="kw">false); ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_CREATE, class="num">0, class="kw">false); }