开发回放系统  市场模拟(第 14 部分):模拟器的诞生(IV)·进阶篇
🎲

开发回放系统 市场模拟(第 14 部分):模拟器的诞生(IV)·进阶篇

(2/3)· 从自由随机到受限走势,拆解模拟器核心游走函数的数学约束与实现细节

含代码示例偏理论 第 2/3 篇
不少人拿到模拟器生成的数据,直接当真实行情回测,却没想过这些点值是怎么被算出来的。若不清楚随机游走的边界约束,你调出来的走势要么过于规整、要么杂乱到无法训练。先搞懂宏定义里的范围限定,后续复杂函数才看得明白。

动态扩容 tick 缓存的写法

在实时接收行情时,tick 数组不能写死长度,否则高频行情下会越界崩 EA。上面这段用 ArrayResize 在每次新 tick 到达时把 m_Ticks.Info 扩一行,第三参 def_MaxSizeArray 给了内存上限,避免无限增长吃光 MT5 终端内存。 m_Ticks.nTicks 先当索引又把自身加一,等于“先存后增”,tick[c0] 落进数组末尾。外汇与贵金属 tick 流在重大数据发布时可能每秒数十笔,这种预扩容策略能让回放与实时对齐更稳,但高频场景仍要警惕滑点与断线重连导致重复计数。 开 MT5 把这段塞进 OnTick 前的预处理宏里,把 def_MaxSizeArray 设成你历史最大 tick 数的 1.5 倍,跑一晚看日志里 nTicks 增长是否平滑。

MQL5 / C++
{
   ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + class="num">1), def_MaxSizeArray);
   m_Ticks.Info[m_Ticks.nTicks++] = tick[c0];
   }
}

class="macro">#undef macroRandomLimits

「自由随机游走的边界失控」

把模拟函数从随机跳跃改成随机步进后,加载方式不变、跳价数量也不变,但生成的价格轨迹仍可能越界。用默认配置文件跑回放,服务只作用在第一根 1 分钟柱线上,配置里上限 108375、下限 107850,可图形上肉眼就能看出界限根本没被遵守。 图例 04 那种完全自由的随机游走,下限远远被击穿;只在极特殊时刻才可能碰巧贴边。另一个麻烦是孤立点——它们会在柱线上制造出怪异的成交分布,这个问题得一步步解决,但先处理边界。 我们模拟依据的是已获得的真实数据(高低点、幅度),所以必须让系统受限于这些数据,不能接受自由漂移。很多人不喜欢受限系统,但要把模拟当分析工具,就得加检查逻辑强行守住限定。 别只信数据文件,一定要把模拟图形丢进 MT5 或 Excel 里肉眼核对。纯随机跳转系统在 MT5 图表上几乎不可行,而连续步进虽能画出来,孤立点仍会让某根柱线看起来不正常。若不想导 Excel,可改代码让每个跳价直接绘在图表上(参考“时间和交易”里画真实跳价的方法),但信息密度会变高、更难读。 下面这段改造后的 Simulation 函数,用宏把随机值钳在 A、B 之间,并按 1 分钟柱内的 tick_volume 把时间均匀铺开,从而让步进倾向贴合边界:

MQL5 / C++
class="kw">inline class="type">void Simulation(class="kw">const class="type">MqlRates &rate, class="type">MqlTick &tick[])
{
class="macro">#define macroRandomLimits(A, B) (class="type">int)(MathMin(A, B) + (((rand() & class="num">32767) / class="num">32767.0) * MathAbs(B - A)))
						class="type">long il0, max;
						class="type">class="kw">double v0, v1;
						
						ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > class="num">0 ? m_Ticks.nRate + class="num">3 : def_BarsDiary), def_BarsDiary);
						m_Ticks.Rate[++m_Ticks.nRate] = rate;
						max = rate.tick_volume - class="num">1;
						v0 = class="num">4.0;
						v1 = (class="num">60000 - v0) / (max + class="num">1.0);
						for (class="type">int c0 = class="num">0; c0 <= max; c0++, v0 += v1)
						{
										tick[c0].last = class="num">0;
										tick[c0].flags = class="num">0;
											il0 = (class="type">long)v0;
										tick[c0].time = rate.time + (class="type">class="kw">datetime) (il0 / class="num">1000);

◍ 把 K 线拆回逐笔 tick 的还原逻辑

这段片段在做一件事:用一根已收盘的 MqlRates 柱,反推出它内部可能走过的 tick 流。思路不是精确复现,而是按成交量缺口和随机游走,凑出一段「像那么回事」的盘口轨迹,供回测或可视化用。外汇与贵金属市场高风险,这种合成 tick 只能用于验证机制,不能当作真实成交痕迹。 tick[c0].time_msc = il0 % 1000 给每条 tick 打毫秒时间戳,volume_real 固定填 1.0,相当于假设每笔都是标准一手迷你量。rate.open 和 rate.close 被钉在 tick[0] 与 tick[max] 的 last 上,保证柱首柱尾价不错位。 循环里先用 macroRandomLimits(0, max) 把 real_volume 与 tick_volume 的差额随机摊到某些 tick 的 volume_real 上,补上「非 tick 计价的成交量」。随后从 c0=1 开始,每条 tick 的 last 在前一条基础上加减一个 PointPerTick,方向由 rand()&1 抛硬币决定——这就是最朴素的随机游走了。 il0 取 max*0.3 做锚点,强制把某条 tick 的 last 拉到 rate.low,另一段拉到 rate.high,避免随机过程跑出柱体边界。最后 ArrayResize 把 tick 逐条塞进 m_Ticks.Info,nTicks 同步自增,整根柱的伪 tick 序列就拼完了。 别把正态当圣经:rand() 给出的均匀随机并不能复刻真实报价聚簇,真要测滑点还是得接真实 tick 历史。

MQL5 / C++
tick[c0].time_msc = il0 % class="num">1000;
         tick[c0].volume_real = class="num">1.0;
         }
         tick[class="num">0].last = rate.open;
         tick[max].last = rate.close;
         for (class="type">int c0 = (class="type">int)(rate.real_volume - rate.tick_volume); c0 > class="num">0; c0--)
               tick[macroRandomLimits(class="num">0, max)].volume_real += class="num">1.0;
         for (class="type">int c0 = class="num">1; c0 < max; c0++)
               tick[c0].last = macroRandomLimits(rate.low, rate.high);
               tick[c0].last = tick[c0 - class="num">1].last + (m_PointsPerTick * ((rand() & class="num">1) == class="num">1 ? class="num">1 : -class="num">1));
         il0 = (class="type">long)(max * (class="num">0.3));
         tick[macroRandomLimits(il0, il0 * class="num">2)].last = rate.low;
         tick[macroRandomLimits(max - il0, max)].last = rate.high;
         for (class="type">int c0 = class="num">0; c0 <= max; c0++)
         {
               ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + class="num">1), def_MaxSizeArray);
               m_Ticks.Info[m_Ticks.nTicks++] = tick[c0];
         }
class="macro">#undef macroRandomLimits
         }

把随机游走锁进1分钟框里

上一节放开跑的随机游走,在实盘回放里常会戳破1分钟K线的上下沿。这一节只动了两个新变量和一段边界检查,就把走势强行摁进限定的价格通道内。 核心逻辑是先查上限:若生成的随机点越过通道上界,立刻回退到界内;没越上限再查下限,越了同样拉回。两次都通过才接受该点。代码里 macroRandomLimits 用 rand()&32767 做归一化随机偏移,MathMin/MathAbs 夹住区间,保证落点不裸奔出框。 [CODE] 里 v0 从 4.0 起步,v1 = (60000 - v0)/(max+1.0) 把一分钟内 tick 数均摊到毫秒轴;循环里 tick[c0].last 与 flags 清零,il0 只是把累计毫秒强转 long 备用。 跑出来的图例很有意思:图例05恰好所有松散点都被随机走到,整根K线几乎完美在框内,但这纯属运气。图例06才是典型——收盘跳价点根本没被走到,MT5回放会留下一根尖锐突刺。极端如07那种收在随机点上的情况,概率极低,别指望它。 外汇和贵金属这种高波动品种,用受限随机游走做历史回放模拟,尖刺假突破会误导策略评估,实盘验证前得清楚这是合成数据的固有噪声。 想去掉所有松散点、让每个预设位置都被走访,还要再改模拟系统,逻辑更绕。眼下这版已能跑,拿不同资产多测几遍,你才会知道框内随机走势到底贴不贴你心里的真实盘口。

MQL5 / C++
<span class="keyword">class="kw">inline</span> <span class="keyword">class="type">void</span> Simulation(<span class="keyword">class="kw">const</span> <span class="predefines">class="type">MqlRates</span> &amp;rate, <span class="predefines">class="type">MqlTick</span> &amp;tick[])
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
<span class="preprocessor">class="macro">#define </span>macroRandomLimits(A, B) (<span class="keyword">class="type">int</span>)(<span class="functions">MathMin</span>(A, B) + (((<span class="functions">rand</span>() &amp; <span class="number">class="num">32767</span>) / <span class="number">class="num">32767.0</span>) * <span class="functions">MathAbs</span>(B - A)))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">long</span>&nbsp;&nbsp;&nbsp;&nbsp;il0, max;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;v0, v1;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;bLowOk, bHighOk;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span>(m_Ticks.Rate, (m_Ticks.nRate &gt; <span class="number">class="num">0</span> ? m_Ticks.nRate + <span class="number">class="num">3</span> : def_BarsDiary), def_BarsDiary);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;m_Ticks.Rate[++m_Ticks.nRate] = rate;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;max = rate.tick_volume - <span class="number">class="num">1</span>;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;v0 = <span class="number">class="num">4.0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;v1 = (<span class="number">class="num">60000</span> - v0) / (max + <span class="number">class="num">1.0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> c0 = <span class="number">class="num">0</span>; c0 &lt;= max; c0++, v0 += v1)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tick[c0].last = <span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;tick[c0].flags = <span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;il0 = (<span class="keyword">class="type">long</span>)v0;
把游走诊断交给小布盯盘
这些游走函数的输出分布小布盯盘的 AIGC 已内置比对,打开对应品种页即可看到模拟偏差提示,你专注调参逻辑。

常见问题

与运算把随机数截断在指定上限内,再经归一化和偏移得到目标区间,避免额外边界检查,保证生成值在可接受范围。
自由游走输出更接近纯噪声,图形混乱;受限走势加入趋势或波动约束,令模拟价格行为更贴近真实市场结构。
可以,小布盯盘支持常见 CSV 与 EXCEL 导出的模拟序列,AIGC 会自动标注游走类型与偏差概率,省去手工比对。
本篇引入更严谨的游走约束,令生成柱线在 1 分钟内精度更高,后续 EXCEL 统计的拟合度会明显提升。