开发回放系统 市场模拟(第 07 部分):首次改进(II)(基础篇)
给回放引擎加第一道加速
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 验证这套退出链。
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,避免自触发死循环。外汇与贵金属图表上跑这类指标属高风险操作,参数误用可能导致面板残留在多图表环境。
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 分钟数据跑一遍删除事件是否漏对象。
}
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();