开发回放系统(第 77 部分):新 Chart Trade(四)(基础篇)
◍ 在 MT5 里搭一个可回放的图表交易框架
MetaTrader 5 的回放系统做到第 77 部分时,核心落点是把历史行情变成可交互的 Chart Trade 环境。也就是说,你不用等实盘,就能在离线图表上用和线上一致的下单逻辑跑一遍策略。 这套思路的价值在于:外汇与贵金属属高风险品种,实盘试错成本不低;用回放把同一段行情重复播放,你能稳定复现滑点、点差和成交拒单的边界情况。 原文示例发布于 13 十月 2025 08:43,帖内标记浏览量 472、评论 0,说明该回放框架仍属小众验证阶段,建议直接开 MT5 自建测试工程而非照搬社区参数。
「通信协议前先打的补丁」
上篇我们把 DispatchMessage 里最关键的一段讲透了,也铺开了通信过程该怎么设计——或者说通信协议该长什么样。那套逻辑到目前为止仍然成立,不用推倒重来。 不过真要进本文重点之前,得先对前面给过的代码做一处微调。这不是功能新增,而是为了系统在 MT5 实盘里跑得稳,不改就容易在消息高频来回时出诡异卡顿。 补完这个稳定点,后面才能放心啃真正的通信协议设计,不会刚调通就被底层抖动带偏。
鼠标指标与图表交易的点击冲突怎么消
鼠标指标和 Chart Trade 面板同时挂在图表上时,点击某些控件偶尔会失灵。现象很具体:若先加载鼠标指标,再操作 Chart Trade,部分按钮无响应,只能删掉指标重插才恢复,这种交互缺陷在调试阶段很难追溯根因。 实测有效的做法是把 CHARTEVENT_OBJECT_CLICK 事件从 DispatchMessage 的处理逻辑里拿掉。对应到上篇代码,需删除 381 到 403 行整段;同时把原 314、341 行移入 346 行的条件测试,并在 297、300、303、306、309、312、336 行统一引用静态变量 sz 做状态判定。 下面这段来自 C_ChartFloatingRAD.mqh 的 DispatchMessage 片段展示了调整后的核心结构,注意 273 行用 static short sz=-1 承接跨事件状态,宏里只做边界裁剪不触发对象点击: 别把对象点击当万能钩子 Chart Trade 控件依赖自身事件链,外挂指标抢 CHARTEVENT_OBJECT_CLICK 会打断这条链。删掉它之后,外汇与贵金属图表上的悬浮面板稳定性倾向明显提升,但 MT5 环境仍属高风险,参数改动前建议在模拟盘验证。
class="num">259. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">260. 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="num">261. { class="num">262. class="macro">#define macro_AdjustMinX(A, B) { \ class="num">263. B = (A + m_Info.Regions[MSG_TITLE_IDE].w) > x; \ class="num">264. mx = x - m_Info.Regions[MSG_TITLE_IDE].w; \ class="num">265. A = (B ? (mx > class="num">0 ? mx : class="num">0) : A); \ class="num">266. } class="num">267. class="macro">#define macro_AdjustMinY(A, B) { \ class="num">268. B = (A + m_Info.Regions[MSG_TITLE_IDE].h) > y; \ class="num">269. my = y - m_Info.Regions[MSG_TITLE_IDE].h; \ class="num">270. A = (B ? (my > class="num">0 ? my : class="num">0) : A); \ class="num">271. } class="num">272. class="num">273. class="kw">static class="type">short sx = -class="num">1, sy = -class="num">1, sz = -class="num">1; class="num">274. class="kw">static eObjectsIDE obj = MSG_NULL; class="num">275. class="type">short x, y, mx, my; class="num">276. class="type">class="kw">double dvalue; class="num">277. class="type">bool b1, b2, b3, b4; class="num">278. class="type">class="kw">ushort ev = evChartTradeCloseAll; class="num">279. class="num">280. class="kw">switch (id) class="num">281. { class="num">282. case CHARTEVENT_CHART_CHANGE: class="num">283. x = (class="type">short)ChartGetInteger(GetInfoTerminal().ID, CHART_WIDTH_IN_PIXELS); class="num">284. y = (class="type">short)ChartGetInteger(GetInfoTerminal().ID, CHART_HEIGHT_IN_PIXELS);
◍ 用鼠标事件接管面板的状态切换
这段逻辑挂在图表事件回调里,靠 CHARTEVENT_MOUSE_MOVE 和左键点击状态去驱动面板内部字段的改写。CheckClick 先确认是左键,再把 lparam / dparam 强转成 short 交给 CheckMousePosition 定位命中了哪个控件区。 命中 MSG_MAX_MIN 时,只要滚轮方向 sz<0 就翻转 IsMaximized 标记(第297行),相当于在面板上点一下切换最大化。而 MSG_DAY_TRADE、MSG_LEVERAGE_VALUE、MSG_TAKE_VALUE 这几个分支都额外卡了 IsMaximized 为真且 sz<0 才响应,说明它们只在最大化展开态下允许交互。 前面那段宏调整坐标的代码(285–289行)也值得注意:b1~b4 任一为真就调 AdjustTemplate(),意味着面板位置或最小边界被拖动后模板会重绘。开 MT5 把这段塞进自己的 C_Mouse 派生类,滚轮滚一下看 IsMaximized 是否按预期翻转,就能验证命中区有没有画错。
macro_AdjustMinX(m_Info.x, b1); macro_AdjustMinY(m_Info.y, b2); macro_AdjustMinX(m_Info.minx, b3); macro_AdjustMinY(m_Info.miny, b4); if (b1 || b2 || b3 || b4) AdjustTemplate(); class="kw">break; case CHARTEVENT_MOUSE_MOVE: if ((*m_Mouse).CheckClick(C_Mouse::eClickLeft)) { class="kw">switch (CheckMousePosition(x = (class="type">short)lparam, y = (class="type">short)dparam)) { case MSG_MAX_MIN: if (sz < class="num">0) m_Info.IsMaximized = (m_Info.IsMaximized ? class="kw">false : true); class="kw">break; case MSG_DAY_TRADE: if ((m_Info.IsMaximized) && (sz < class="num">0)) m_Info.IsDayTrade = (m_Info.IsDayTrade ? class="kw">false : true); class="kw">break; case MSG_LEVERAGE_VALUE: if ((m_Info.IsMaximized) && (sz < class="num">0)) CreateObjectEditable(obj = MSG_LEVERAGE_VALUE, m_Info.Leverage); class="kw">break; case MSG_TAKE_VALUE: if ((m_Info.IsMaximized) && (sz < class="num">0)) CreateObjectEditable(obj = MSG_TAKE_VALUE, m_Info.FinanceTake); class="kw">break; case MSG_STOP_VALUE: