📘

「给回放系统接上 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,挂图表跑一天看看残留对象就懂了。

MQL5 / C++
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 信号链任一段失效都可能漏单,实盘前务必在模拟环境压测。

MQL5 / C++
      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 行对比就能看到差别。外汇与贵金属品种波动大、加载脚本频繁,这种泄漏在高周期切换时更明显,属高风险环境下的基础隐患。

MQL5 / C++
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 跑一遍就能验证拆分逻辑。

MQL5 / C++
    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 的示例工程,建议先在策略测试器里跑一遍视频里的演示逻辑,再动通信层。外汇和贵金属杠杆高,模拟环境验证通了也别急着上实盘。

常见问题

在回放脚本里调用 ChartTrade 相关接口把面板挂到图表,指标只负责画信号,真正下单要走 EA 执行端。
指标运行在客户端分析层没有交易权限,必须另写 EA 监听图表事件来接收信号并调用 OrderSend 下单。
小布可读取图表事件流并解析问号分隔的信号串,直接给你标出哪根 K 线触发了哪类订单,不用手动解码。
用 StringSplit 按 '?' 切分,第一段取品种第二段取方向第三段取手数,依次填入下单参数即可。
在 OnDeinit 里对创建的解码器对象调用 Delete 或置空,避免残留句柄导致下次加载冲突。