开发回放系统(第 38 部分):铺路(II)(基础篇)
📘

开发回放系统(第 38 部分):铺路(II)(基础篇)

第 1/2 篇

◍ 回放系统的底层数据通道怎么搭

做 MT5 回放系统,第一道坎不是画 K 线,而是把历史数据按可控节奏喂给内存。原文示例工程在 2024-08-26 公开,基础框架已迭代到第 38 部分,说明这条路坑不少但能走通。 核心思路是用自定义缓冲区替代直接用 CopyRates 全量拉取,避免回放时卡主线程。下面这段是原文给出的初始化骨架,负责开一条轻量数据通道: [CODE] int init_replay_buffer(string symbol, ENUM_TIMEFRAMES tf, int max_bars) { ArrayResize(g_replay_rates, max_bars); g_handle = CopyRates(symbol, tf, 0, max_bars, g_replay_rates); if(g_handle < 0) return(-1); g_cursor = 0; return(0); } [/CODE] 逐行看:第 1 行定义函数,入参是品种、周期和最大承载 bar 数;第 3 行用 ArrayResize 给全局数组 g_replay_rates 预分配内存,max_bars 直接决定回放能往前翻多远;第 4 行 CopyRates 只拉一次初始段,返回句柄存进 g_handle;第 5 行句柄为负说明拉取失败,直接退 -1;第 6 行游标归零,表示从第一根开始回放;第 7 行正常返回 0。 实盘或回放接外汇、贵金属都属高风险,数据断点可能导致回放错位。把 max_bars 设成 5000 在 EURUSD M1 上约覆盖 3.5 个交易日,先开 MT5 按这个骨架跑通再谈后续步进逻辑。

MQL5 / C++
class="type">int init_replay_buffer(class="type">class="kw">string symbol, ENUM_TIMEFRAMES tf, class="type">int max_bars)
  {
   ArrayResize(g_replay_rates, max_bars);
   g_handle = CopyRates(symbol, tf, class="num">0, max_bars, g_replay_rates);
   if(g_handle < class="num">0) class="kw">return(-class="num">1);
   g_cursor = class="num">0;
   class="kw">return(class="num">0);
  }

「不让指标在图上叠第二次」

MetaTrader 5 里有个很实用的平台级约束:当某个指标被设定为不可重叠时,用户无论如何都无法在同一图表上加载第二个副本。上一节我们用几行代码就实现了这一点,核心逻辑是让指标声明自身独占图表区域,平台会直接拦截重复添加的动作。 这件事看起来小,但在 EA 与指标做双向通信的架构里很关键。如果图表上可能冒出两个同名指标,EA 在读取数据时就会搞不清自己对接的是哪一个实例。外汇和贵金属市场本身波动剧烈、风险极高,程序化系统若连‘信号来源是否唯一’都确认不了,后续逻辑再复杂也是建立在沙子上。 不过要泼点冷水:上一节的指标只是‘不重叠’,它根本不会回应 EA 的询问。也就是说,EA 目前还没法主动找指标要任何信息。本文开始站在 EA 的视角重新设计,但前提是指标端先得改造——否则双向通信这条路连起点都算不上。

用 iCustom 打通 EA 与指标的第一次对话

想看清 EA 和自定义指标之间到底怎么传话,得先埋几个控制点。指标侧只多了一行关键声明:第 10 行用 input double user00 = 0.0 暴露一个双精度入口,目的不是算指标,而是替掉终端全局变量,专门接 EA 丢过来的数。若这套机制以后被证伪,随时可退回全局变量模型。 EA 侧第 13 行常被人漏写——它向 MT5 索要已加载指标的句柄,有了这句就不用手动把指标拖上图;若图表上还没有,第 15 行 iCustom 才上场。iCustom 首参写 NULL 而非 _Symbol,编译器眼里二者等价,指标会绑死 EA 所在品种;周期填 PERIOD_CURRENT 则让指标跟 EA 同周期跑,想跨周期分析就直接改这参。 第三个参数最容易翻车:不仅要写指标名,还得带路径和 .ex5 后缀,因为 MT5 默认只在 indicators 根目录找,省了根目录前缀反而定位准。EA 第 16 行负责把指标真正跑在图上,删了它用户就能重复挂实例,算出来的东西和 EA 控制的不是同一份;第 25 行则在退出时按指标第 24 行声明的同名短名删掉实例,子窗口用不到就填 0。 验证别偷懒:先全量编译跑一遍,再依次删掉 16+25 行、只删 16 行保留 25 行各测一次。外汇与贵金属杠杆高,MT5 自动化通信失误可能瞬间引发异常挂单,动手前先在策略测试器用闲余品种复现这三种状况。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property link      ""
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_plots class="num">0
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_ShortName       "SWAP MSG"
class="macro">#define def_ShortNameTmp    def_ShortName + "_Tmp"
class=class="str">"cmt">//+------------------------------------------------------------------+
input class="type">class="kw">double user00 = class="num">0.0;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">long m_id;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
   m_id = ChartID();
   IndicatorSetString(INDICATOR_SHORTNAME, def_ShortNameTmp);
   if (ChartWindowFind(m_id, def_ShortName) != -class="num">1)
   {

◍ 用 iCustom 给当前图挂一个单例指标

这段脚本演示了如何在 EA 的 OnInit 里用 iCustom 把自定义指标「SWAP MSG」挂到当前图表,并强制做成单例,避免重复加载。user00 作为外部输入参数默认 2.0,会原样传给指标实例,日志里能直接看到配置值。 核心判断在第 13 行:先用 ChartIndicatorGet 按名字「SWAP MSG」查是否已存在,若返回 INVALID_HANDLE 才真正创建并添加;否则跳过。这样同一图表只允许一个实例运行,外汇与贵金属品种上反复加载 EA 也不会刷出多套指标,降低资源占用。 OnDeinit 里用 ChartIndicatorDelete 按同名删除、IndicatorRelease 释放句柄,保证 EA 卸载时图表干净。OnCalculate 仅返回 rates_total 做占位,说明这个指标不参与逐根计算,纯粹当挂载载体用。 想验证就开 MT5 把上面代码存成 EA,把「Mode Swap\Swap MSG.ex5」换成你本地已有的自定义指标路径,加载到 XAUUSD 的 M5 图,观察专家日志里是否打印「Success」以及 user00=2.0。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class="macro">#class="kw">property link      ""
class="macro">#class="kw">property version   "class="num">1.00"
class=class="str">"cmt">//+------------------------------------------------------------------+
input class="type">class="kw">double user00 = class="num">2.0;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int m_handle;
class="type">long m_id;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
   m_id = ChartID();
   if ((m_handle = ChartIndicatorGet(m_id, class="num">0, "SWAP MSG")) == INVALID_HANDLE)
   {
      m_handle = iCustom(NULL, PERIOD_CURRENT, "Mode Swap\Swap MSG.ex5", user00);
      ChartIndicatorAdd(m_id, class="num">0, m_handle);
   }
   Print("Indicator loading result:", m_handle != INVALID_HANDLE ? "Success" : "Failed");
   
   class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
{
   ChartIndicatorDelete(m_id, class="num">0, "SWAP MSG");
   IndicatorRelease(m_handle);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
{
}
class=class="str">"cmt">//+------------------------------------------------------------------+

常见问题

用环形缓冲或预分配数组存历史 tick,避免实时动态扩容;通道层只传价格和时间戳,指标计算放独立线程。
多半是 EA 和脚本各调了一次创建函数。用单例判断:图上已有同名的就跳过创建,只取已有句柄。
可以。小布能扫当前图表对象列表,标出同名指标实例,并提示你保留单例句柄,省去手动翻代码。
确认指标暴露的输入参数顺序和类型一致;先判句柄是否为 INVALID_HANDLE,再读缓冲,避免空指针报错。
用 ChartObjectsTotal 遍历找同名指标,找不到才调 iCustom 创建,拿到句柄后存全局变量复用即可。