开发回放系统 市场模拟(第 15 部分):模拟器的诞生(V)- 随机游走(基础篇)
「用随机游走造一个能跑的行情模拟器」
在 MT5 里做市场回放,最省事的起点不是去抓真实 tick,而是先让价格按随机游走自己走起来。这样你能脱离实盘数据,反复验证指标在“无记忆”行情下的反应,顺便压测 EA 的异常处理。外汇与贵金属杠杆高,模拟结果仅代表概率行为,不等于实盘盈亏。 随机游走的核心就是每一步价格变动独立同分布。下面这段 MQL5 用 MathRandom() 生成标准正态分布扰动,再乘上波动率缩放到点数,叠加到上一根收盘价上。 [CODE] double gen_random_walk(double last_close, double vol_points) { double u1 = MathRandom(); double u2 = MathRandom(); double z = MathSqrt(-2*MathLog(u1))*MathCos(2*M_PI*u2); return last_close + z * vol_points * _Point; } [/CODE] 逐行看:u1、u2 取两个均匀随机数;用 Box-Muller 变换把均匀随机转成标准正态 z;最后 z 乘波动率点数和 _Point,得到新价格。把 vol_points 设成 50,EURUSD 上每一步大约 ±5 美元波动,足够把均线晃出假信号。 接好这根函数,在 OnTick 里用上一根 close 去调它,就能在离线图表里看到一条会自己游走的线。开 MT5 新建脚本粘进去,把 vol_points 从 10 调到 100,你能直观感到噪声一大,任何突破策略的胜率都会塌。
class="type">class="kw">double gen_random_walk(class="type">class="kw">double last_close, class="type">class="kw">double vol_points) { class="type">class="kw">double u1 = MathRandom(); class="type">class="kw">double u2 = MathRandom(); class="type">class="kw">double z = MathSqrt(-class="num">2*MathLog(u1))*MathCos(class="num">2*M_PI*u2); class="kw">return last_close + z * vol_points * _Point; }
用监督随机游走补上模拟器缺口
前几篇里我们搭过一个市场回放系统,能从简陋的策略测试器式模型,演进到能生成近似真实走势的随机游走。但它面对预定义文件或数据库时仍不够用:数据库会硬性规定必须覆盖的衡量度,而旧随机游走没法稳定吃下全部范围。 极稀有情况下,旧模型会从开盘出发,触达最高或最低后完全反向,最后停在收盘价——像巧合而非机制。我们不能靠机遇跑模拟,外汇与贵金属市场本身高风险,模拟失真会直接误导复盘判断。 思路不复杂:构造一种监督随机游走,在允许区间内尽量随机,同时强制完整覆盖柱线所有关键点位。配合代码实现,您可以在 MT5 里直接改几个边界参数,验证它是否触达了开盘、最高、最低、收盘四个价位。
◍ 从之字折线到受限随机游走的建模切换
做回放或模拟系统时,图例 01 那种典型之字折线(ZigZag)虽然在 MT5 策略测试器里够用,但生成的跳价数量明显少于真实市场,复杂度也只是真实走势的简单改编,撑不起我们要做的模拟器。 数据库里的 1 分钟柱线文件(图例 02)已经给了开、高、低、收四价,这是硬边界。完全在范围内随机撒价格(图例 03)图形高度随机,实盘里极少出现这种走法,不能直接用。 老式游戏里的魔方、15 拼图其实都暗含随机游走数学:表面随机移动,底层是简单规则叠加。把这套数学搬到「价格×时间」里,先做出无约束随机游走(图例 06)——在前价上加或减一个单位生成新价,比如随机奇数为加、偶数为减,规则一改图形就变。 但模拟器必须守住数据库上下界,所以收敛成图例 07 的受限随机游走:边界由数据库指定,复杂度够且不可完全预测。不过它只保证起点(开盘价)命中,收盘/最高/最低不一定触到,而模拟器要求所有特殊点必须绝对到达。 再微调就得到图例 08 的强制随机游走:基于同一份数据库,走势不像图例 03 那样混乱,特殊点确定会在某时刻触及,和真实建柱过程非常接近。上篇代码用的阶段模型在这套要求下会漏点,下一节我们看怎么让命中更自然。
「把随机游走驯成收敛通道」
做价格模拟时,纯随机游走太散,完全照不到真实 K 线形态。这里的核心思路不是给走势加条件,而是用一条逐渐收窄的通道把随机路径「逼」向收盘点,同时保留两侧振荡的自由度,看起来就像自然走出来的。 先写一个新函数替代旧游走逻辑:计算到达目标点所需步数,记录游走区间的最大最小值,然后随机贯穿若干点,每一步都微调边界把价格锁进通道内。零模式下抵达目标即停,能加大随机化程度。通道末尾不彻底闭合,留一条类似布林带的窄带,让收盘价正好落在终点。 别把正态当圣经 通道闭合顺序有讲究:先合底部,抵达出口段再从顶部收。这样最高价、最低价、收盘价三个点都能被访问到,开盘点则始终作为起点。跑出来的图形接近一个大型之字折线——第一段长度不固定,由游走函数随机判定结束,二、三段在限定边界内往返。 让小布替你跑这套 若用 1 分钟柱线数据跑上述系统,图形会出现几处「撞墙」式折角,和真实订单簿大战里的拒价有点像。为更自然,给游走函数加边界触碰检查:第二段里价格触高写最低有效位、触低写第二有效位,用 OR 运算合成一个 0~3 的整值。值为 3 时不再压缩边界,走势重新放开,形成「激增」点。 末段若显得勉强,就在函数里交替加减边界值:起点等距于极值则出对称三角形,退出点贴边则出非对称三角形。最终用相同数据库跑出的图(例 10)比早期版本自然得多,外汇与贵金属模拟请牢记这是高风险资产的近似,实盘验证前只作技术推演。
class="kw">inline class="type">void Simulation(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; class="type">bool bLowOk, bHighOk; 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;
用线性插值拼出一根 K 线的逐笔轨迹
把单根蜡烛拆成 max+1 个 tick 时,最省事的做法是先按线性步长把首末价拉通,再往中间塞随机扰动。下面这段就是按 open→close 等距铺时间、再用 m_PointsPerTick 做双向跳动的核心循环。 v0 起点设为 4.0 只是个占位偏移,真正有用的是 v1 = (60000 - v0)/(max+1.0) 这一步:它把总跨度 60000 摊到 max 段上,循环里 v0 每次加 v1,得到 il0 再拆成秒和毫秒写进 tick[c0].time 与 time_msc。这样生成的 tick 序列时间轴是连续的,方便后续按真实成交量二次分配。 tick[0].last 钉死为 rate.open、tick[max].last 钉死为 rate.close,中间段 c0 从 1 到 max-1 用 rand()&1 决定加减一个 m_PointsPerTick,高低点标记 bLowOk/bHighOk 先清零留待后面判。real_volume 与 tick_volume 的差值用 macroRandomLimits(0,max) 随机挑 tick 加 volume_real,模拟非均匀成交。外汇和贵金属 tick 重建本身只是概率近似,实盘高杠杆下误差会被放大,仅作回测用。 把这段代码直接贴进 MT5 的 EA 或脚本里,改 m_PointsPerTick 和 max 两个量,就能看不同颗粒度下重采样 K 线对策略信号的影响。
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); 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; bLowOk = bHighOk = false; for (class="type">int c0 = class="num">1; c0 < max; c0++) { v0 = tick[c0 - class="num">1].last + (m_PointsPerTick * ((rand() & class="num">1) == class="num">1 ? class="num">1 : -class="num">1)); }
◍ 用随机游走的tick回填K线高低点
这段逻辑在合成历史 tick 时,先把虚拟价格 v0 和当前 K 线最高价 rate.high 做比较:若没超过上轨,就取 v0 与最低价之间的有效值,否则沿上一 tick 回落一个最小变动点 m_PointsPerTick。 bLowOk 与 bHighOk 两个布尔量跟踪本根 K 线的高低点是否已被 tick 命中;若遍历完仍未命中,就用 macroRandomLimits 在 30% 长度的区间内强行插一根到 rate.low 或 rate.high,保证影线不丢。 最后把 tick[] 数组逐根通过 ArrayResize 追加进 m_Ticks.Info,nTicks 自增。外汇与贵金属属高风险品种,合成数据仅用于本地回测验证,实盘概率仍受滑点影响。 开 MT5 把这段塞进你的 tick 生成器,重点调 il0 = (long)(max * 0.3) 里的 0.3:调小会让高低点插入更靠近两端,影线分布可能更陡。
if (v0 <= rate.high) v0 = tick[c0].last = (v0 >= rate.low ? v0 : tick[c0 - class="num">1].last + m_PointsPerTick); else v0 = tick[c0].last = tick[c0 - class="num">1].last - m_PointsPerTick; bLowOk = (v0 == rate.low ? true : bLowOk); bHighOk = (v0 == rate.high ? true : bHighOk); } il0 = (class="type">long)(max * (class="num">0.3)); if (!bLowOk) tick[macroRandomLimits(il0, il0 * class="num">2)].last = rate.low; if (!bHighOk) 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 } class="kw">inline class="type">long RandomWalk(class="type">long pIn, class="type">long pOut, const class="type">MqlRates &rate, class="type">MqlTick &tick[], class="type">int iMode) { class="type">class="kw">double vStep, vNext, price, vHigh, vLow;