开发回放系统 市场模拟(第 24 部分):外汇(V)·综合运用
让随机游走挑最省力的路
纯随机化在 1 分钟甚至 10/15 分钟周期上都给不出像样的走势,理想状态是渐进式绘制,外表像随机、底层却是简单数学在控场。要做到顺畅,就得删掉部分旧函数、按可控规则引导,而不是强行指定方向——只需给 MT5 定好规矩,让它自己处理过程。 修改后的 RandomWalk 用绿色新行替换了旧逻辑,变化细微但灵活度更高。旧版是连续跳价、无空隙,模拟平滑游走要海量成交;新版在 1 分钟柱内引入空隙,把必需交易量显著压下来,你能用不同成交量和柱参数去试系统。OHLC 四个值一到位,游走就生成适配图形,过渡行为由函数自行判定。 关键在调用逻辑:现在不再通篇统一检查创建可能性,评估收进函数内部。系统会尝试仅用 10 笔最小交易量去跑游走;等于或低于该阈值时改用纯随机,在这种上下文里比游走更高效。1 分钟柱内造空隙靠特殊计算保证,且至少要生成 1 次跳价游走才能正常工作。 额外控制不可少:1 分钟内成交量超 1000,模拟可随机选先去高点还是低点;小于阈值,初始方向按开盘价离高点或低点更近来定。这叫“最省力路径”——当所需走势次数少于游走总距离时有效,避免又长又绕的路线。 EURUSD 的演示里,基于真实跳价数据的图与最省力模拟图乍看一致,细看数据源标注不同。你在 MT5 里加载该资产,用应用提供的最后成交价和出价两种绘制类型做实验,就能比对模拟和真实数据的贴合度。外汇品种波动剧烈、杠杆风险高,验证结果仅作概率参考。
class="kw">inline class="type">int RandomWalk(class="type">int In, class="type">int Out, const class="type">class="kw">double Open, const class="type">class="kw">double Close, class="type">class="kw">double High, class="type">class="kw">double Low, const class="type">int Spread, class="type">MqlTick &tick[], class="type">int iMode, class="type">int iDesloc) { class="type">class="kw">double vStep, vNext, price, vH = High, vL = Low; class="type">char i0 = class="num">0; vNext = vStep = (Out - In) / ((High - Low) / m_TickSize); for (class="type">int c0 = In, c1 = class="num">0, c2 = class="num">0; c0 <= Out; c0++, c1++) { price = (m_IsPriceBID ? tick[c0 - class="num">1].bid : tick[c0 - class="num">1].last) + (m_TickSize * ((rand() & class="num">1) == class="num">1 ? -class="num">1 : class="num">1)); price = (price > vH ? price - m_TickSize : (price < vL ? price + m_TickSize : price)); price = (m_IsPriceBID ? tick[c0 - class="num">1].bid : tick[c0 - class="num">1].last) + (m_TickSize * ((rand() & class="num">1) == class="num">1 ? -iDesloc : iDesloc)); price = (price > vH ? vH : (price < vL ? vL : price)); MountPrice(c0, price, (Spread + RandomLimit((class="type">int)(Spread | (m_Marks.iMax & 0xF)), class="num">0)), tick); class="kw">switch (iMode) { case class="num">1: i0 |= (price == High ? 0x01 : class="num">0); i0 |= (price == Low ? 0x02 : class="num">0); vH = (i0 == class="num">3 ? High : vH); vL = (i0 == class="num">3 ? Low : vL); break; case class="num">0: if (price == Close) class="kw">return c0; class="kw">default: break; }
「K线内部tick路径的随机游走拼装」
这段逻辑干的事,是把一根 MqlRates 的 OHLC 拆成近似真实的逐笔 tick 序列。核心在 Simulation() 里先按成交量切三段:i0 取 tick_volume 的 1/5 与 1/3 的较小值,i1 落到 iMax-i0,剩下尾段推到收盘。 i2 控制每段价格步长,等于 (high-low)/TickSize 再除以 i0,若除出来是 0 则强制为 1,避免除零死循环。b0 决定首段先摸高还是先摸低:成交量 ≥1000 时靠 rand() 抛硬币,否则比较 (high-open) 与 (open-low) 谁更近。 RandomWalk 分三次调用,把 open→极值→反向极值→close 的路径填进 tick[]。下面这段是模式分支里对上下轨 vL/vH 的刷新,iMode==2 走交替锯齿,其余按收线方向推 TickSize。 别把正态当圣经:rand() 只决定首段方向,真实盘口在重大数据前往往单边抽,这段代码不会复现那种跳空密度,回测黄金 1 分钟需自行加滑点惩罚。
if (((class="type">int)floor(vNext)) >= c1) class="kw">continue; else if ((++c2) <= class="num">3) class="kw">continue; vNext += vStep; vL = (iMode != class="num">2 ? (Close > vL ? (i0 == class="num">3 ? vL : vL + m_TickSize) : vL) : (((c2 & class="num">1) == class="num">1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize))); vH = (iMode != class="num">2 ? (Close > vL ? vH : (i0 == class="num">3 ? vH : vH - m_TickSize)) : (((c2 & class="num">1) == class="num">1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH))); if (iMode == class="num">2) { vL = (((c2 & class="num">1) == class="num">1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize)); vH = (((c2 & class="num">1) == class="num">1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH)); }else { if (Close > vL) vL = (i0 == class="num">3 ? vL : vL + m_TickSize); else vH = (i0 == class="num">3 ? vH : vH - m_TickSize); } class="type">int i0, i1, i2; class="type">bool b0; m_Marks.iMax = (class="type">int) rate.tick_volume - class="num">1; m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high); m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low); Simulation_Time(rate, tick); MountPrice(class="num">0, rate.open, rate.spread, tick); if (CheckViability(rate)) if (m_Marks.iMax > class="num">10) { i0 = (class="type">int)(MathMin(m_Marks.iMax / class="num">3.0, m_Marks.iMax * class="num">0.2)); i1 = m_Marks.iMax - i0; i2 = (class="type">int)(((rate.high - rate.low) / m_TickSize) / i0); i2 = (i2 == class="num">0 ? class="num">1 : i2); b0 = (m_Marks.iMax >= class="num">1000 ? ((rand() & class="num">1) == class="num">1) : (rate.high - rate.open) < (rate.open - rate.low)); i0 = RandomWalk(class="num">1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, class="num">0, i2); RandomWalk(i0, i1, (m_IsPriceBID ? tick[i0].bid : tick[i0].last), (b0 ? rate.low : rate.high), rate.high, rate.low, rate.spread, tick, class="num">1, i2); RandomWalk(i1, m_Marks.iMax, (m_IsPriceBID ? tick[i1].bid : tick[i1].last), rate.close, rate.high, rate.low, rate.spread, tick, class="num">2, i2);
◍ 标记位缺失时的补价逻辑
当 K 线的高低点标记尚未建立时,引擎需要把缺失的边界价格补挂进 tick 流,否则后续成交量分布会漏掉极端值。上面这段片段展示了在随机价格生成之后,如何按 bLow / bHigh 标志位决定补 MountPrice 的调用。 m_Marks.bHigh = m_Marks.bLow = true 这一行出现在某分支里,意味着若走该分支则高低标记同时置位,后续两个 if(!m_Marks.bLow) 与 if(!m_Marks.bHigh) 都不会再触发补价。反之若走 else 的 Random_Price,则标记可能仍为空,补价逻辑才真正生效。 最后 MountPrice(m_Marks.iMax, rate.close, rate.spread, tick) 把收盘价挂到最大索引位,CorretTime 校正时间后返回 iMax。开 MT5 把这段接进自定义 tick 生成器,打印 bLow/bHigh 在两种分支下的命中次数,能直接验证补价是否如预期只补一次。外汇与贵金属 tick 仿真属高风险验证环境,结论仅代表回测逻辑倾向,不代表实盘概率。
m_Marks.bHigh = m_Marks.bLow = true; }else Random_Price(rate, tick); if (!m_IsPriceBID) DistributeVolumeReal(rate, tick); if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick); if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick); MountPrice(m_Marks.iMax, rate.close, rate.spread, tick); CorretTime(tick); class="kw">return m_Marks.iMax; }
记住这一条就够了
回放与模拟系统要真正跑起来,先把文件备齐是硬前提。作者按资产拆了四个包:BOLSA 约 1.36 MB、FOREX 约 3.74 MB、FUTUROS 高达 11.4 MB、主程序包 44 KB,全部得下进 MQL5 编辑器指定目录,缺一个结构就断。 下一篇才会讲最终设置,但这一阶段决定了你在测试环境里能不能看清系统真实表现。外汇与贵金属模拟涉及高杠杆与跳价风险,回测结论只代表历史概率,别直接当实盘依据。 东西备好、目录放对,后面接新功能时才不会卡在加载环节——这一步懒不得。