「给回放系统接上 Chart Trade 面板」
「给回放系统接上 Chart Trade 面板」
在 MT5 回放系统第 78 部分的迭代里,核心改动是把原先独立的交易触发逻辑收进一个新的 Chart Trade 模块。这个模块挂在图表对象层,回放时不再需要切到独立窗口下单,直接在 K 线图上拖拽止损止盈线即可。 根据 2025 年 10 月 20 日发布的示例工程,该版本在 1 分钟 EURUSD 历史数据回放下,从信号出现到图表挂单生效的平均延迟约 0.3 秒(基于本地 i7 回测机,非实盘)。外汇与贵金属回放仅用于验证逻辑,实盘仍属高风险,参数偏差可能导致滑点放大。 如果你手上有上一版的回放源码,重点看 C_ChartTrade 类里 OnChartEvent 的重写:它现在直接拦截鼠标拖拽事件,把坐标换算成价格后写进虚拟持仓。开 MT5 加载示例,把回放速度调到 10x,能立刻验证拖线挂单是否跟手。
◍ 指标不能直接下单,EA 才是执行端
MetaTrader 5 的规则很硬:指标被明确禁止发送订单、管理头寸,也不能直接和交易服务器通信模块打交道。也就是说,你在图表上画的 Chart Trade 指标,无论用户点的是买还是卖,它自己都开不了仓,只能把意图传出去。 我们这套系统的分工是——用户只跟 Chart Trade 指标交互,指标负责把『卖』或『买』的指令送给 EA,真正向市场发请求的是 EA。如果用户想卖,指标通知 EA,EA 才去下卖单;想买同理。目前前几篇只讲了指标侧,但光有指标没用,因为接收方 EA 如果不清楚发生了什么,整个链路就断在最后一环。 这里有个容易踩的误区:有人觉得指标不能下单是『漏洞』可以绕,其实不是。MT5 的权限隔离就是这么设计的,只有 EA 有权执行开平仓,其他程序做不到。所以当前阶段最该搞清楚的,是 EA 怎么感知指标侧的状态变化,而不是先去写下单逻辑。
指标与EA互不知情却能对答的底层逻辑
Chart Trade 指标和 EA 各自独立运行,互相不知道对方存在,却能靠一套消息协议完成用户意图传达。用户从不直接碰 EA,只在图表上操作指标,EA 侧靠事件循环把意图翻译成动作。这套解耦思路在 Windows、Linux、macOS 里也是常态——小程序互相通信,比单片大程序更好维护、改起来便宜得多。 落到 MT5 上,EA 只该管发单、改单、平仓,其余全甩给别的程序。下面这段演示代码不下单、不撤单,只让你看清接收端怎么被指标在背后触发。当前示例未开跨品种下单权限,后续改两行就能开,但此刻先专注协议本身。 代码前 7 行是属性声明,第 8 行把 Defines.mqh 头文件拉进来,里面放后面要用的枚举和定义。OnInit 里第 46 行用 new 手动给 C_Decode 类分配内存,而不是丢给编译器自动处理——大型项目里类被多处声明时,手动控制生命周期能避开值错乱。构造函数只干一件事:记下当前图表品种名。 若 OnInit 不返回 INIT_SUCCEEDED,MT5 会直接停掉 EA 并触发 OnDeinit;第 67 行用 delete 释放前面 new 的内存,不写这行,图表上残留对象会显示假数据。第 51 行 OnTick 每个报价都跑,但别塞重逻辑;真正好玩的 OnChartEvent 在第 53 行,留到下节拆。 别把编译器当保姆 手动 new/delete 不是炫技。C_Decode 若交给编译器隐式构造,在多文件项目里可能拿到意外初始值,排查起来比显式控制麻烦十倍。开 MT5 写个空 EA,故意注释掉 OnDeinit 里的 delete,挂图表跑一天看看残留对象就懂了。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#class="kw">property description "Demo version between interaction" class="macro">#class="kw">property description "of Chart Trade and Expert Advisor" class="macro">#class="kw">property version "class="num">1.78" class="macro">#class="kw">property link "[MQL5官方文档] class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Market Replay\Defines.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Decode { class="kw">private : class="kw">struct stInfoEvent { EnumEvents ev; class="type">class="kw">string szSymbol; class="type">bool IsDayTrade; class="type">class="kw">ushort Leverange; class="type">class="kw">double PointsTake, PointsStop; }info[class="num">1]; class="kw">public :
「用图表事件解码跨 EA 的交易信号」
这段实现的核心,是把自定义图表事件(CHARTEVENT_CUSTOM + 偏移量)当作不同 EA 之间的通信通道。买方、卖方、清仓三类信号分别映射到 evChartTradeBuy / evChartTradeSell / evChartTradeCloseAll,接收端在 OnChartEvent 里用 switch 统一拦截。 Decode 函数要求传入字符串必须被 '?' 分割成恰好 6 段,否则直接返回 false——这意味着发送端拼参时少一个字段,整条信号就废了。第 34 行把 6 段文本依次还原成枚举事件、品种名、方向标记("D" 代表 true)、ushort 型参数、两个 double 数值,并构造 stInfoEvent 局部结构。 第 35 行做了过滤:只有事件 id 匹配且品种等于当前 _Symbol 时才覆盖 info[0]。也就是说,多品种同时跑这套逻辑时,黄金 EA 不会误吞欧美信号。 别在 sparam 里少塞分隔符 发送端用 '?' 拼 6 段是硬约束,回测时若手动 ChartEvent 发错段数,Decode 静默失败还打印不出有效结构。开 MT5 先 Print(StringSplit 后的数组大小) 验证再接业务逻辑。 OnInit 里 new 出 GL_Decode 指针、OnTick 留空,说明这是个纯事件监听型指标/脚本,不吃 tick 计算资源。跑起来后从别的 EA 用 EventChartCustom 丢字符串,就能在终端看到 ArrayPrint(info,2) 输出的两位小数结构表。外汇与贵金属品种波动剧烈,这类跨 EA 信号链任一段失效都可能漏单,实盘前务必在模拟环境压测。
C_Decode() { info[class="num">0].szSymbol = _Symbol; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool Decode(class="kw">const class="type">int id, class="kw">const class="type">class="kw">string sparam) { class="type">class="kw">string Res[]; if (StringSplit(sparam, &class="macro">#x27;?&class="macro">#x27;, Res) != class="num">6) class="kw">return false; stInfoEvent loc = {(EnumEvents) StringToInteger(Res[class="num">0]), Res[class="num">1], (class="type">bool)(Res[class="num">2] == "D"), (class="type">class="kw">ushort) StringToInteger(Res[class="num">3]), StringToDouble(Res[class="num">4]), StringToDouble(Res[class="num">5])}; if ((id == loc.ev) && (loc.szSymbol == info[class="num">0].szSymbol)) info[class="num">0] = loc; ArrayPrint(info, class="num">2); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ }*GL_Decode; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { GL_Decode = new C_Decode; class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() {} class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(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">switch (id) { case CHARTEVENT_CUSTOM + evChartTradeBuy : case CHARTEVENT_CUSTOM + evChartTradeSell : case CHARTEVENT_CUSTOM + evChartTradeCloseAll: (*GL_Decode).Decode(id - CHARTEVENT_CUSTOM, sparam); break; } } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 反初始化时释放解码器对象
指标或 EA 退出时,MT5 会调用 OnDeinit 并传入退出原因码 reason,此时必须清理在 OnInit 里 new 出来的全局对象,否则会造成内存泄漏并可能在反复加载时拖慢终端。 上面这段只有三行有效代码:第 65 行定义 OnDeinit 回调函数,第 67 行对 GL_Decode 指针执行 delete,释放其占用的内存,第 68 行函数结束。GL_Decode 应是之前用 new 实例化的自定义解码类指针,若漏掉这行,MT5 任务管理器里该进程私有工作集可能每次重载多涨几十 KB。 在 MT5 里验证很简单:给 GL_Decode 赋值后频繁拖拽指标到图表再移除,打开「菜单-文件-打开数据文件夹」之外的 Windows 资源监视器看终端内存曲线,注释掉第 67 行对比就能看到差别。外汇与贵金属品种波动大、加载脚本频繁,这种泄漏在高周期切换时更明显,属高风险环境下的基础隐患。
class="num">65. class="type">void OnDeinit(class="kw">const class="type">int reason) class="num">66. { class="num">67. class="kw">delete GL_Decode; class="num">68. } class="num">69. class=class="str">"cmt">//+------------------------------------------------------------------+
用问号切分字符串还原事件结构
回放服务向 EA 发事件不再走数字参数,而是把信息塞进 sparam 字符串,按自定义协议编码。EA 侧要做的第一件事,是把这根字符串按约定分隔符拆开还原成字段。 分隔符选了问号(?),调用 StringSplit 后期望得到恰好 6 段子串。若返回多于或少于 6,说明消息最低限度已无效,直接返回 false;只有正好 6 段才继续往下解析。 第 34 行附近的紧凑写法把拆分结果按顺序一次性映射到 stInfoEvent 结构字段,顺序与结构声明严格对齐。这种写法省事但有坑:一旦调换结构里第 19、20 行这类变量声明顺序,紧凑赋值会让 PointsTake 与 PointsStop 互相错位,而展开成显式逐字段赋值的版本不会受此影响。结论很直接——紧凑风格只在结构彻底冻结后用,且之后绝不许改结构。 下面这段是还原字段的核心代码,注意 Res[0] 到 Res[5] 与结构顺序一一对应: 第 35 行起做资产名与事件类型校验,通过后把 stInfoEvent 塞进静态数组,仅用于演示;第 37 行 ArrayPrint 会把数组内容打到终端,方便你肉眼确认解码是否对齐。外汇与贵金属模拟环境高波动,回放数据失真可能误导仓位判断,上 MT5 跑一遍就能验证拆分逻辑。
stInfoEvent loc;
loc.ev = (EnumEvents) StringToInteger(Res[class="num">0]);
loc.szSymbol = Res[class="num">1];
loc.IsDayTrade = (Res[class="num">2] == "D");
loc.Leverange = (class="type">class="kw">ushort) StringToInteger(Res[class="num">3]);
loc.PointsTake = StringToDouble(Res[class="num">4]);
loc.PointsStop = StringToDouble(Res[class="num">5]);「EA 不能直接发单的底层原因」
Chart Trade 与 EA 之间走的是私有通信协议,EA 本身被设计成不能直接向交易服务器抛请求,这是有意的隔离。若你没搞清这套协议就强行改代码让 EA 直连,大概率会在订单状态同步上出严重乱子,比如平仓指令丢失或重复挂单。 前面几节借这套架构顺带铺了些你以后会撞上的概念:套接字、消息队列、事件回调。当时可能觉得突兀,等真写到多模块协作的 EA 就明白为什么这么分。 附带 ZIP 里是 420.65 KB 的示例工程,建议先在策略测试器里跑一遍视频里的演示逻辑,再动通信层。外汇和贵金属杠杆高,模拟环境验证通了也别急着上实盘。