开发回放系统 市场模拟(第 14 部分):模拟器的诞生(IV)·进阶篇
(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 增长是否平滑。
{
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 把时间均匀铺开,从而让步进倾向贴合边界:
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 历史。
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那种收在随机点上的情况,概率极低,别指望它。 外汇和贵金属这种高波动品种,用受限随机游走做历史回放模拟,尖刺假突破会误导策略评估,实盘验证前得清楚这是合成数据的固有噪声。 想去掉所有松散点、让每个预设位置都被走访,还要再改模拟系统,逻辑更绕。眼下这版已能跑,拿不同资产多测几遍,你才会知道框内随机走势到底贴不贴你心里的真实盘口。
<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> &rate, <span class="predefines">class="type">MqlTick</span> &tick[]) { <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>() & <span class="number">class="num">32767</span>) / <span class="number">class="num">32767.0</span>) * <span class="functions">MathAbs</span>(B - A))) <span class="keyword">class="type">long</span> il0, max; <span class="keyword">class="type">class="kw">double</span> v0, v1; <span class="keyword">class="type">bool</span> bLowOk, bHighOk; <span class="functions">ArrayResize</span>(m_Ticks.Rate, (m_Ticks.nRate > <span class="number">class="num">0</span> ? m_Ticks.nRate + <span class="number">class="num">3</span> : def_BarsDiary), def_BarsDiary); m_Ticks.Rate[++m_Ticks.nRate] = rate; max = rate.tick_volume - <span class="number">class="num">1</span>; v0 = <span class="number">class="num">4.0</span>; v1 = (<span class="number">class="num">60000</span> - v0) / (max + <span class="number">class="num">1.0</span>); <span class="keyword">for</span> (<span class="keyword">class="type">int</span> c0 = <span class="number">class="num">0</span>; c0 <= max; c0++, v0 += v1) { tick[c0].last = <span class="number">class="num">0</span>; tick[c0].flags = <span class="number">class="num">0</span>; il0 = (<span class="keyword">class="type">long</span>)v0;