开发回放系统·玩转服务(三):基础篇厘清数据洪峰下的模拟限流逻辑(基础篇)
(1/3)·一分钟柱形里塞进异常多报价时,MT5连真实服务器都卡,重放更会崩,本篇先铺底层概念
「用服务把回放调度从图表里摘出来」
MT5 的回放系统做到第 62 部分时,一个容易被忽略的落点是:把回放控制逻辑塞进 Service 而不是 Indicator/EA。Service 在终端后台独立跑,不绑定具体图表,关掉图表不会中断回放调度,这对长时间重演历史行情很关键。 官方示例里这一版(2025-05-09 发布,原文阅读量 566、评论 0)展示了三处服务化改造:回放时钟由 Service 的 OnTimer 驱动、历史数据缓冲通过全局变量跨程序共享、UI 只负责发命令不负责算时间。读者可以开 MT5 新建一个 Empty Service 模板,把回放帧推进逻辑挪进去验证独立性。 贵金属与外汇历史回放涉及杠杆与滑点,重演结果和实际成交可能有偏差,相关测试仅作策略逻辑验证,实盘仍属高风险。
◍ 为什么重放系统必须给数据速率上锁
上一篇文章里我们暴露出一个隐患:模拟模式下系统整体响应速度跟不上,应用层没法把涌入的数据全部妥善处理。这不是程序崩了,而是吞吐上限被触碰——只要数据源速率超过平台消化能力,计时精度就会塌方。 更麻烦的是,这个问题在连真实交易服务器时也存在。某些期货合约在一根 1 分钟柱内可能塞进异常多的报价或成交,MT5 处理和显示价格变动会直接变卡。很多人误以为是电脑硬件或系统故障,其实那是外行对计算模型理解偏差放出的误导。 外汇与贵金属市场同样存在极端报价风暴的高风险,重放历史数据时若不加约束,灾难会被放大。所以我给模拟和真实/外部数据都设定了最大速率限制,避免平台数据处理瓶颈暴露或引发连锁问题。接下来我们看新代码怎么落地这个限流。
模拟器分时报价上限的硬约束
重做回放系统时发现,模拟类 C_Simulation 里多了一个以前没有的变量,专门管单次柱形最多模拟多少分时报价。这个变量的初值不在构造函数里给,而是在第 134、135 行附近、调用模拟函数时才落地——调用时必须传一个“最大报价数”参数,不传也有默认值。 Defines.mqh 第 15 行新增了宏 def_MaxTicksVolume 设为 2000,这就是系统内部可模拟分时报价的天花板。如果你传的值 ≤0,类会强制按最小 1 处理,因为外汇里最小报价交易量就是 1 手/1 笔;传得比 2000 大,类直接忽略,用 2000 封顶。 把 1 分钟柱形除以 2000,得到每个分时报价间隔约 30 毫秒。这个间隔不是拍脑袋定的,它让绘图过程在 MT5 上看起来平稳、连续,不会因报价过稀而跳变。 第 135 行才是真正拍板的地方:它会拿第 134 行你给的值和实际柱子里已有的分时报价数做比对。你给的数比柱内真实报价少,就用你的数;柱内真实报价更少,就弃用你的输入、改认柱内实际数。所以你想压低模拟密度,只能在这个范围内调,超界无效。 目前只解决了“系统能模拟多少”,但还没给用户开口子去主动调小模拟报价量。下一段要动的就是真实与外部模拟报价共存时的上限逻辑。
class="macro">#define def_MaxTicksVolume class="num">2000
「回放引擎的事件枚举与随机边界函数」
在 MT5 里做价格行为回放,先得把界面元素和定时器事件收拢到一个枚举里统一管理。下面这段枚举把鼠标价格线、K线时间、日内波幅、瞬时波幅的显隐,以及服务器时间重放、回放控制初始化都列清楚了,后续用 ChartEvent 或自定义消息驱动时直接传枚举值即可。 别把正态当圣经:rand() 在 MQL5 里返回 0~32767,代码里用 32767 做掩码、再除以 32737.0 把区间压到 [0,1) 附近,这只是线性映射,不代表真实 tick 分布。外汇与贵金属属高风险品种,用这种伪随机生成的价格序列只能做界面逻辑验证,不能当行情统计依据。 RandomLimit 是个模板函数,接收两个同类型边界 Limit01、Limit02,先算差值 a,再按随机比例在两者之间取值。注意第 23 行返回时做了大小判断,保证下界永远不大于上界,调用方传反了也不会越界。 让小布替你跑这套:把下面代码贴进 MT5 的 include 目录对应路径,建个 EA 调用 C_Simulation::RandomLimit<int>(10, 50),在 EURUSD 的 M1 上循环打印,能直接看到随机边界是否如预期落在 10~50。
enum EnumEvents { evHideMouse, class=class="str">"cmt">//Hide mouse price line evShowMouse, class=class="str">"cmt">//Show mouse price line evHideBarTime, class=class="str">"cmt">//Hide bar time evShowBarTime, class=class="str">"cmt">//Show bar time evHideDailyVar, class=class="str">"cmt">//Hide daily variation evShowDailyVar, class=class="str">"cmt">//Show daily variation evHidePriceVar, class=class="str">"cmt">//Hide instantaneous variation evShowPriceVar, class=class="str">"cmt">//Show instantaneous variation evSetServerTime, class=class="str">"cmt">//Replay/simulation system timer evCtrlReplayInit, class=class="str">"cmt">//Initialize replay control }; class="macro">#include "..\..\Defines.mqh" class C_Simulation { class="kw">private : class="type">int m_NDigits; class="type">bool m_IsPriceBID; class="type">class="kw">double m_TickSize; class="kw">struct st00 { class="type">bool bHigh, bLow; class="type">int iMax; }m_Marks; class="kw">template < class="kw">typename T > class="kw">inline T RandomLimit(const T Limit01, const T Limit02) { T a = (Limit01 > Limit02 ? Limit01 - Limit02 : Limit02 - Limit01); class="kw">return (Limit01 >= Limit02 ? Limit02 : Limit01) + ((T)(((rand() & class="num">32767) / class="num">32737.0) * a)); }
◍ 把模拟 tick 的时间与报价钉进数组
Simulation_Time 按 m_Marks.iMax 把一根 K 线的 60 秒切成等份,每份 60000/iMax 毫秒,再用 RandomLimit 在每段里挑偏移量写进 tick[c0].time 和 time_msc。例如 iMax=60 时每段宽 1000ms,tick 时间戳会散布在整分钟而非整点对齐,回测里更贴近真实成交流。 CorretTime 只是把 time(秒)乘 1000 叠加回 time_msc,统一成毫秒级绝对时标,避免后面排序或比对时跨秒错位。 Unique 负责找一脚不和当前 price 撞车的 tick 下标:iMax>20 就随机跳,否则顺序 +1,保证挂价位置价格不重复。MountPrice 按 m_IsPriceBID 决定写 bid 还是 last,并依 spread 用 m_TickSize 算 ask,NormalizeDouble 控小数点位数。外汇与贵金属杠杆高,模拟 tick 分布只影响历史回测形态,不等于实盘成交概率。
class="kw">inline class="type">void Simulation_Time(const class="type">MqlRates &rate, class="type">MqlTick &tick[]) { for (class="type">int c0 = class="num">0, iPos, v0 = (class="type">int)(class="num">60000 / m_Marks.iMax), v1 = class="num">0, v2 = v0; c0 <= m_Marks.iMax; c0++, v1 = v2, v2 += v0) { iPos = RandomLimit(v1, v2); tick[c0].time = rate.time + (iPos / class="num">1000); tick[c0].time_msc = iPos % class="num">1000; } } class="kw">inline class="type">void CorretTime(class="type">MqlTick &tick[]) { for (class="type">int c0 = class="num">0; c0 <= m_Marks.iMax; c0++) tick[c0].time_msc += (tick[c0].time * class="num">1000); } class="kw">inline class="type">int Unique(const class="type">class="kw">double price, const class="type">MqlTick &tick[]) { class="type">int iPos = class="num">1; do { iPos = (m_Marks.iMax > class="num">20 ? RandomLimit(class="num">1, m_Marks.iMax - class="num">1) : iPos + class="num">1); }class="kw">while ((m_IsPriceBID ? tick[iPos].bid : tick[iPos].last) == price); class="kw">return iPos; } class="kw">inline class="type">void MountPrice(const class="type">int iPos, const class="type">class="kw">double price, const class="type">int spread, class="type">MqlTick &tick[]) { if (m_IsPriceBID) { tick[iPos].bid = NormalizeDouble(price, m_NDigits); tick[iPos].ask = NormalizeDouble(price + (m_TickSize * spread), m_NDigits); }else