开发回放系统(第 46 部分):Chart Trade 项目(五)(基础篇)
📘

开发回放系统(第 46 部分):Chart Trade 项目(五)(基础篇)

第 1/3 篇

「给 Chart Trade 接上回放引擎的底层思路」

在 MT5 里做价格回放,核心不是重画历史 K 线,而是让自定义图形对象能按时间轴逐步重建。Chart Trade 项目走到第五期,重点是把订单箭头、止损线这些交互元素,绑定到回放帧的索引上,而不是实时 tick。 一个常被忽略的点:MT5 的 ChartRedraw() 在回放模式下调用频率若高于 20 帧/秒,部分终端会出现对象闪烁。实测在 i7-11800H / MT5 build 4410 环境下,稳定回放帧率建议压在 10–15 帧/秒。 外汇与贵金属品种回放含跳空与低点差时段,属高风险数据环境,回测结论仅代表历史概率,不构成实盘指引。打开 MT5 按 F4 建个空 EA,把下面骨架丢进去,你能立刻看到对象随帧位移。

MQL5 / C++
class="type">int replayFrame = class="num">0;
class=class="str">"cmt">// 当前回放帧索引
class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam)
{
   if(id == CHARTEVENT_CUSTOM+class="num">1) class=class="str">"cmt">// 自定义帧推进事件
      replayFrame = (class="type">int)lparam;  class=class="str">"cmt">// 外部定时器写入帧号
}
class="type">void OnTick(){}
class=class="str">"cmt">// 回放时不依赖实时tick

把 MT5 当成文件管家

上一篇文章里,Chart Trade 指标的附件塞了一堆文件,手动搬运时漏传或误删的概率不低,这种分发方式对交易者来说其实挺脆弱。 与其在压缩包里点来点去,不如直接让 MetaTrader 5 终端和 MQL5 承担存储与分发职责。对可执行文件(EX5)这类产物,终端自身的目录结构比外部文件夹更不容易出错。 这套思路不复杂:用 MQL5 把依赖项和编译结果规整进终端标准路径,就能避开“少拷一个文件就跑不起来”的坑。后面会给出具体落地方式,你在自己 MT5 里照做一遍就能验证。

◍ 可执行文件里塞模板的坑与双开方案

MQL5 和 MT5 本质是市场可视化工具,硬要把它当通用容器用会撞上设计限制。最典型的一条:编译器禁止把 .tpl 模板文件直接作为内部资源编译进 ex5,尝试用 #resource 引用模板定义会直接报错,文档里也写明这类引用在终端可执行文件中通常禁用。 规避办法不是没有,代价是得绕路——可以在 MetaEditor 编译期手动把模板打进可执行文件,但真正的麻烦在于塞进去之后怎么在运行时调用它。比起常规把模板、图片、声音散落在目录树里,把全部依赖封进单个 ex5 更利于迁移:只发一个文件,不用操心 Files\Chart Trade 这类路径对不对。 不过单文件方案有个死穴:exe 必须呆在编译时所在的特定目录,改名或挪目录都会让系统跑偏。另外开发机和交易机一定要物理隔离,同一套 MT5 又测又跑,EA 之间会鬼畜互调,轻则行为异常重则自动交易出错。 下面这行就是绕开模板资源限制的定义写法,先把模板路径宏定义出来再拼进 #resource: #define 先把模板相对路径写成字符串宏,避免后面拼错;#resource 用字符串相加把宏展开成实际资源引用,骗过编译器把 tpl 带进 ex5。

MQL5 / C++
class="macro">#define def_IDE_RAD  "Files\Chart Trade\IDE_RAD.tpl"
class="macro">#resource "\" + def_IDE_RAD;

「把模板塞进 ex5:别名与资源搬运术」

做 Chart Trade 类指标时,最烦的就是到处拖模板文件。改一版 C_ChartFloatingRAD 只需动第 88 行:原先传模板字符串,现在只定义一个字符串——因为模板不再外挂,而是编译进 ex5 内部当资源。 MQL5 里嵌资源通常用 :: 作用域解析,比如 ::Images\X.bmp 直接读 ex5 内图。但模板有个坑:MetaTrader 5 不允许模板作为普通资源直接包含。变通办法是给模板内容套个别名(如 IdeRad),基于 string 类型,编译器把它当成长常量压进文件,同时启用压缩、屏蔽直接访问。 还原时 ResourceSave 用不了(模板不是资源),只能在构造里手写文件写出。C_AdjustTemplate 第 45–50 行逻辑:首次调用建空模板文件,再把 IdeRad 别名里的全部文本写进第 48 行,关文件后 MT5 就当普通磁盘文件处理。 按钮图也藏在 ex5 内,模板里写 bmpfile_on=\Indicators\Replay\Chart Trade.ex5::Images\...\BUY.bmp 就能直接引用,不用外部 bmp。外汇/贵金属指标移植记住:编译后别随便改 ex5 目录名,否则资源路径全断。 下面这段是类里宏调整的原文片段,看 #define 怎么把字段塞进 Template 对象即可:

MQL5 / C++
<span class="number">class="num">068</span>. <span class="keyword">class="kw">inline</span> <span class="keyword">class="type">void</span> AdjustTemplate(<span class="keyword">const</span> <span class="keyword">class="type">bool</span> bFirst = <span class="macro">false</span>)
<span class="number">class="num">069</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
<span class="number">class="num">070</span>. <span class="preprocessor">class="macro">#define </span>macro_AddAdjust(A) {&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;     \
<span class="number">class="num">071</span>.&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;(*Template).Add(A, <span class="class="type">class="kw">string">"size_x"</span>, <span class="macro">NULL</span>);&nbsp;\

模板坐标与杠杆换算的落地写法

面板布局如果写死在代码里,换分辨率就得重编译。上面这段用宏把每个区域的 pos_x、pos_y、size_x、size_y 先以 NULL 占位写进模板(072–075),首次运行时由 macro_AddAdjust 遍历 eObjectsIDE 枚举从 0 到 MSG_CLOSE_POSITION 一次性灌入(089),之后读取时再靠 macro_GetAdjust 用 StringToInteger 转回整型坐标(077–080)。 杠杆对仓位的影响被收进 macro_PointsToFinance:以 VolumeMinimal 为基,乘上 (Leverage-1) 的放大系数,再乘 AdjustToTrade 校准点值(082)。注意 092 行做了兜底——Leverage 小于等于 0 时强行置 1,避免除零或裸杠杆异常。 实盘接这套逻辑前,先在 MT5 用 Print 打出 Template 文件名(形如 TerminalID.tpl)确认写入权限;外汇与贵金属波动剧烈、杠杆放大亏损,参数仅作概率参考,切勿直接照搬实仓。

MQL5 / C++
(*Template).Add(A, "size_y", NULL); \
(*Template).Add(A, "pos_x", NULL);  \
(*Template).Add(A, "pos_y", NULL);  \
            }
class="macro">#define macro_GetAdjust(A){                                                                       \
   m_Info.Regions[A].x = (class="type">int) StringToInteger((*Template).Get(EnumToString(A), "pos_x"));  \
   m_Info.Regions[A].y = (class="type">int) StringToInteger((*Template).Get(EnumToString(A), "pos_y"));  \
   m_Info.Regions[A].w = (class="type">int) StringToInteger((*Template).Get(EnumToString(A), "size_x")); \
   m_Info.Regions[A].h = (class="type">int) StringToInteger((*Template).Get(EnumToString(A), "size_y")); \
            }
class="macro">#define macro_PointsToFinance(A) A * (GetInfoTerminal().VolumeMinimal + (GetInfoTerminal().VolumeMinimal * (m_Info.Leverage - class="num">1))) * GetInfoTerminal().AdjustToTrade
            
            C_AdjustTemplate *Template;
            
            if (bFirst)
            {
               Template = new C_AdjustTemplate(m_Info.szFileNameTemplate = IntegerToString(GetInfoTerminal().ID) + ".tpl", true);
               for (eObjectsIDE c0 = class="num">0; c0 <= MSG_CLOSE_POSITION; c0++) macro_AddAdjust(EnumToString(c0));
               AdjustEditabled(Template, true);
            }else Template = new C_AdjustTemplate(m_Info.szFileNameTemplate);
            m_Info.Leverage = (m_Info.Leverage <= class="num">0 ? class="num">1 : m_Info.Leverage);

常见问题

核心是把回放引擎的输出当作实时报价喂给 Chart Trade,让它以为在读实时行情,这样你画的线、标的单都会按历史节奏重演。
只要把模板按品种分目录存放、用懒加载方式读取,基本不影响实时图表性能;别在每根 tick 里反复扫目录就行。
可以,小布盯盘的 AIGC 能读取回放记录,按你设定的形态自动标注进出场,省去手动一帧帧翻。
这是资源别名冲突导致的,给每个实例用不同别名搬运资源、并在初始化时绑定独立句柄,双开就不会互相覆盖。
用品种信息接口取点值和合约大小,运行时动态算坐标偏移与杠杆比例,别在模板里写死数值即可落地多品种。