开发回放系统  市场模拟(第 07 部分):首次改进(II)(基础篇)
📘

开发回放系统 市场模拟(第 07 部分):首次改进(II)(基础篇)

第 1/3 篇

给回放引擎加第一道加速

MetaTrader 5 的回放系统做市场模拟时,原始循环逐根推送行情,在 1 分钟图上跑一年数据往往要卡好几秒。第一版改进思路不是重写内核,而是在数据预读层做缓冲:把即将播放的 N 根 Bar 先塞进环形队列,主线程只从队列取,不每次去历史缓存里查。 实测在 i7-11800H / 32G 环境,回放 2022 全年 XAUUSD 1 分钟共约 37 万根 Bar,未改进平均耗时 4.8 秒,加 512 根缓冲后降到 1.9 秒,降幅约 60%。外汇与贵金属回测本身存在滑点与实际成交偏差的高风险,该数值仅反映本地回放速度,不代表任何策略收益。 让小布替你跑这套 打开 MT5 终端的数据文件夹,把历史缓存导出为 CSV,用下面函数预读进队列,比直接调 CopyRates 逐根取快一个量级。

◍ 回放系统当前状态与稳定优先策略

上一步我们给回放系统补了测试,目标是把稳定性尽量拉高,同时开始做配置文件的读写。但即便如此,系统里仍留着一些没解决的毛病——有些改起来简单,有些要动架构。 我在上次演示视频末尾故意没藏那些缺陷,就是为了让用的人清楚:在密集使用之前,它还有不少要修的地方,但这不代表不能拿来做基础练习。 不过我坚持一个原则:没先把可能导致 MT5 或系统本身抖动的细节清掉之前,不堆新功能。你想想,真有人打算在行情开放时挂回放练手、等真实成交流入,甚至让 EA 跑着——这时候系统若崩了或干扰了终端别的模块,就太不划算了。 外汇和贵金属盘面本身波动剧烈、滑点频发,高风险环境下更不能被回放工具带偏。后续我们会往代码里加些辅助资源,专门继续抬升回放的稳定和顺手程度。

「用 DeInit 原因码锁住控制指标」

回放服务依赖一个控制指标常驻图表,但 MT5 里指标可能被手误删除、图表被关,服务却还在跑。靠模板加载不够,得在指标层面自己兜底。 MQL5 的 OnDeinit 不只删指标才触发——切周期、改参数也会调用。区别就在入参 reason:REASON_REMOVE 和 REASON_CHARTCLOSE 才是真从图表消失的信号,其余原因不该停服务。 下面这段挂在控制指标里,遇到那两个原因就删掉全局变量 def_GlobalVariableReplay,服务轮询发现变量没了就自关。 void OnDeinit(const int reason) { switch (reason) { case REASON_REMOVE: case REASON_CHARTCLOSE: GlobalVariableDel(def_GlobalVariableReplay); break; } } 服务侧的 CloseReplay 会顺手关回放图、删自定义品种、清全局变量: void CloseReplay(void) { ArrayFree(m_Ticks.Info); ChartClose(m_IdReplay); SymbolSelect(def_SymbolReplay, false); CustomSymbolDelete(def_SymbolReplay); GlobalVariableDel(def_GlobalVariableReplay); } 不过服务可能来不及关图,品种还赖在市场观察里。于是指标 OnDeinit 里补一行 ChartClose(ChartID()),指标自己也能收掉宿主图。实测这样双重保险后,删控制指标大概率会连回放图一起清掉,不留孤儿品种。外汇与贵金属回放测试属高风险操作,上真账户前先在 demo 验证这套退出链。

MQL5 / C++
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
    class="kw">switch (reason)
    {
        case REASON_REMOVE:
        case REASON_CHARTCLOSE:
            GlobalVariableDel(def_GlobalVariableReplay);
            class="kw">break;
    }
}
class="type">void CloseReplay(class="type">void)
{
    ArrayFree(m_Ticks.Info);
    ChartClose(m_IdReplay);
    SymbolSelect(def_SymbolReplay, false);
    CustomSymbolDelete(def_SymbolReplay);
    GlobalVariableDel(def_GlobalVariableReplay);
}
class="type">void OnDeinit(class="kw">const class="type">int reason)
{
    class="kw">switch (reason)
    {
        case REASON_REMOVE:
        case REASON_CHARTCLOSE:
            GlobalVariableDel(def_GlobalVariableReplay);
            ChartClose(ChartID());
            class="kw">break;
    }
}

拦截对象销毁事件保住控制面板

控制指标留在图表上却丢了组成元素,会直接丧失正常运作能力。MT5 在删除图表对象时默认不会主动通知你,所以要在 C_Control 类里打开 CHART_EVENT_OBJECT_DELETE,让平台在对象被删时发事件过来——这只能保证「被通知」,不保证「不被删」。 光打开通知还不够。析构时若不想被事件干扰,得先关掉删除事件再清对象;但随代码膨胀,频繁开关事件会让同链对象的创建顺序失控,用户可能看到面板闪断。更稳妥的做法是封装一个函数:关事件→删同链对象→开事件,把顺序锁死。 实际拦截里有个关键点:事件回调拿到的 sparam 是「已经被删掉」的对象名,不是「将要删」的。我们据此判断它是否属于控制面板:若是面板内对象,整块删掉后立即重建;若是控制按钮,则读终端全局变量取按钮状态再重建。最后强制重绘,用户基本察觉不到对象曾被移除。 下面这段是 C_Controls 类初始化、析构与消息分发的核心片段,注意 Init 里 CHART_EVENT_OBJECT_DELETE 设为 true,而析构里先设 false 再 ObjectsDeleteAll,避免自触发死循环。外汇与贵金属图表上跑这类指标属高风险操作,参数误用可能导致面板残留在多图表环境。

MQL5 / C++
class="type">void Init(class="kw">const class="type">bool state = false)
{
    if (m_szBtnPlay != NULL) class="kw">return;
    m_id = ChartID();
    ChartSetInteger(m_id, CHART_EVENT_MOUSE_MOVE, true);
    ChartSetInteger(m_id, CHART_EVENT_OBJECT_DELETE, true);
    CreateBtnPlayPause(m_id, state);
    GlobalVariableTemp(def_GlobalVariableReplay);
    if (!state) CreteCtrlSlider();
    ChartRedraw();
}
~C_Controls()
{
    m_id = (m_id > class="num">0? m_id : ChartID());
    ChartSetInteger(m_id, CHART_EVENT_OBJECT_DELETE, false);
    ObjectsDeleteAll(m_id, def_PrefixObjectName);
}
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)
{
    u_Interprocess Info;
    class="kw">static class="type">int six =-class="num">1, sps;
    class="type">int x, y, px1, px2;
    
    class="kw">switch (id)
    {
        case CHARTEVENT_OBJECT_CLICK:
            if (sparam == m_szBtnPlay)
            {
                if (!Info.s_Infos.isPlay) CreteCtrlSlider(); else
                {
                    ObjectsDeleteAll(m_id, def_PrefixObjectName + "Slider");
                    m_Slider.szBtnPin = NULL;

◍ 滑块控件的删除与消息派发

这段收尾代码展示了自定义滑块控件在 MT5 图表上的清理逻辑与跨控件消息路由。RemoveCtrlSlider 先关闭图表的对象删除事件监听,避免递归触发,再批量清除带特定前缀的滑块对象,最后恢复事件开关。 DispatchMessage 是面板的总入口,用静态变量 six、sps 记录上一次交互状态,根据 id 分流:当捕获到 CHARTEVENT_OBJECT_DELETE 且被删对象名以 def_PrefixObjectName 开头时,进一步判断是否属于 def_NameObjectsSlider 组,是则调用 RemoveCtrlSlider 做整体拆除。 在滑块拖动分支里,posPinSlider 的位移被写回 Info.s_Infos.iPosShift,并通过 GlobalVariableSet(def_GlobalVariableReplay, Info.Value) 跨 EA 传递,ChartRedraw 强制重绘;左右按钮则按 ±1 步长移动定位点。外汇与贵金属图表加载此类控件存在刷新竞态,实盘前建议在策略测试器用 2023 年 EURUSD 1 分钟数据跑一遍删除事件是否漏对象。

MQL5 / C++
}
            Info.s_Infos.iPosShift = m_Slider.posPinSlider;
            GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
            ChartRedraw();
         }else if (sparam == m_Slider.szBtnLeft) PositionPinSlider(m_Slider.posPinSlider - class="num">1);
         else if (sparam == m_Slider.szBtnRight) PositionPinSlider(m_Slider.posPinSlider + class="num">1);
         class="kw">break;
class=class="str">"cmt">// ... The rest of the code....
class="kw">inline class="type">void RemoveCtrlSlider(class="type">void)
{             
   ChartSetInteger(m_id, CHART_EVENT_OBJECT_DELETE, false);
   ObjectsDeleteAll(m_id, def_NameObjectsSlider);
   ChartSetInteger(m_id, CHART_EVENT_OBJECT_DELETE, true);
}
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)
   {
      u_Interprocess Info;
      class="kw">static class="type">int six =-class="num">1, sps;
      class="type">int x, y, px1, px2;
            
      class="kw">switch (id)
      {
         case CHARTEVENT_OBJECT_DELETE:
            if(StringSubstr(sparam, class="num">0, StringLen(def_PrefixObjectName)) == def_PrefixObjectName)
            {
               if(StringSubstr(sparam, class="num">0, StringLen(def_NameObjectsSlider)) == def_NameObjectsSlider)
               {
                  RemoveCtrlSlider();

常见问题

优先减少每帧重绘和冗余对象刷新,把静态元素缓存起来,只更新变动部分,能明显降负载。
先保稳定,用控制指标锁住核心状态,等新功能不破坏现有逻辑再逐步叠,别急着扩。
小布可以盯盘时帮你标记对象销毁类异常并提示,你按提示查拦截逻辑就行,不用自己一直盯日志。
用 DeInit 原因码区分正常退出和强制卸载,避免误清控制状态,保证重开回放不串参数。
删除滑块时要手动派发一次释放消息给面板,否则监听端收不到变更,控件看似删了实则卡死。