开发回放系统  市场模拟(第 24 部分):外汇(V)·进阶篇
📉

开发回放系统 市场模拟(第 24 部分):外汇(V)·进阶篇

(2/3)· 当 1 分钟柱量能枯竭令模拟崩溃,用共用结构复用随机游走逻辑救场

案例拆解 第 2/3 篇

接上篇,我们继续深挖外汇回放里最后成交价模拟的适配。很多自行写回放的朋友卡在柱线交易量过低时系统直接崩,却没意识到出价与最后成交价本可共用一套随机时间跳价机制。

◍ 逐笔重建里的边界判定与随机填充

在 MT5 里做历史 tick 重建,第一步是把这根 K 线的可挂载 tick 数量定下来:用整型强转拿 tick_volume 再减 1,赋值给 m_Marks.iMax,这意味着实际模拟的 tick 下标从 0 走到 tick_volume-2,最后一笔留给收盘价。 紧接着要标记高低点是否已被开盘或收盘「天然覆盖」:若 open 或 close 等于 high,bHigh 为真;等于 low 则 bLow 为真。后面随机补价时就不会重复去挂已经存在的高低点。 [CODE]m_Marks.iMax = (int) rate.tick_volume - 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(0, rate.open, rate.spread, tick); if (m_IsPriceBID) Simulation_BID(rate, tick); else Simulation_LAST(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); return m_Marks.iMax;[/CODE] 上面这段代码逐行拆解:第 1 行定 tick 上限;2、3 行判高低点天然标记;第 4 行铺时间轴;第 5 行挂 0 号 tick 为开盘价加点差;6、7 行按报价模式分流到 BID 或 LAST 模拟;8、9 行仅在高低点未被覆盖时才补挂;第 10 行把收盘价挂到末尾;第 11 行校正时间;最后返回 tick 数。 Random_Price 负责在 1 到 iMax-1 之间循环补随机价:用 NormalizeDouble 把 RandomLimit 吐出的 high~low 区间内值规整到 m_NDigits 位小数,点差则叠加一个随机扰动。每补一笔就刷新 bHigh / bLow 标记。 CheckViability 里有个 macro_AdjustSafetyFator,把输入 A 放大到 A + ceil(A*1.7),即约 2.7 倍安全系数。它用 (high-low)/TickSize 算理论最少 tick 数,再加 open 到两端的距离放大值,若总和仍小于 tick_volume 才认为这根 K 线体积够密、可模拟。外汇与贵金属波动剧烈,tick_volume 虚高时这套校验能拦掉一批不可信样本。

MQL5 / C++
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 (m_IsPriceBID) Simulation_BID(rate, tick);
else Simulation_LAST(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;

合成tick里的价格分支与挂载逻辑

这段片段来自一个自定义行情合成函数,核心是在每根bar里决定 tick 价格怎么来、挂到哪里。先判断 CheckViability(rate) 是否通过,不通过就走 Random_Price 随机填充,通过则留空由后续处理。 是否 BID 侧决定两条互斥路径:非 BID 时按真实成交量分布 DistributeVolumeReal 铺点;BID 时改走 Random_Price,否则用 Simulation_LAST 模拟末价。这里 BID 标志直接切换了价格生成范式。 低价位和高价位各自有挂载守卫:若 bLow 未标记,用 Unique(rate.high, tick) 算出的价挂 low;bHigh 未标记则对称挂 high,都带 rate.spread。最后 MountPrice(m_Marks.iMax, rate.close, rate.spread, tick) 把收盘价挂到极值位,CorretTime 校正时间戳后返回 iMax。 开 MT5 把这段嵌进你自己的合成器,把 m_IsPriceBID 从 true 翻 false,能直接看到 tick 密度从随机散布变成按成交量聚集,外汇与贵金属仿真属高风险,结果仅作概率参考。

MQL5 / C++
  if (CheckViability(rate))
  {
  }else Random_Price(rate, tick);
  if (!m_IsPriceBID) DistributeVolumeReal(rate, tick);
  if (m_IsPriceBID) Random_Price(rate, tick);
  else Simulation_LAST(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;

「出价与成交价的随机游走落地」

把随机游走直接塞进模拟器的生成代码里,而不是当外部依赖调用,好处是萌新也能顺着读写。系统每根柱线先判断能不能用随机游走:能就用,不能就退到替代路径,保证外汇或贵金属行情里始终有价格位移,不会卡死在静止状态。 实际跑起来有个现象值得留意:某根 1 分钟柱可能因为交易量不够被判定不适合随机游走,下一根却立刻切回去了。于是价格从平稳突然变陡,这不代表回放系统出故障,只是那根柱需要快速位移,而随机游走本身未必带等量跳价密度。 当 1 分钟柱内成交数略低于阈值,或最高最低价间距逼近跳价数量时,纯随机游走画出来的走势会显得很假。这时候得借用更早一篇提过的柱内逆转思路——不再以时间为轴,而以 OHLC 加跳价量为唯一依据,在不平滑与更平滑的走势之间让模拟器自己选。 下面这段是核心函数,入参里的 Open/Close/High/Low 是双精度价,Spread 为点差,iMode 控制收尾逻辑(0=触收盘即返,1=碰高低点置位)。逐行看:先按 (Out-In) 除以价格跨度算每跳步长 vStep;循环里用 rand() 奇偶决定上一 tick 加减一个 TickSize 生成新价,越界就反向夹回;MountPrice 写回带随机点差扰动;iMode=1 时用 i0 位标记高低点是否都触到,触全才允许放宽 vH/vL。

MQL5 / C++
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">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 > High ? price - m_TickSize : (price < Low ? price + m_TickSize : 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">0:
	            if (price == Close) class="kw">return c0; else break;
	        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;
	        class="kw">default: break;
	        }
	        if (((class="type">int)floor(vNext)) >= c1) class="kw">continue;
	        if ((++c2) <= class="num">3) class="kw">continue;
	        vNext += vStep;
	        if (iMode != class="num">2)
	        {

◍ 用随机游走向 K 线内填真实tick

这段 Simulation 函数做的是把一根 MqlRates 柱体,按成交tick_volume拆成若干模拟 tick 并写回 tick[] 数组,供回测或数据重建使用。外汇与贵金属品种点差跳动剧烈,这种合成 tick 仅用于验证行情重建逻辑,实盘高风险,结论只具概率意义。 核心切分在 i0 = (int)MathMin(m_Marks.iMax / 3.0, m_Marks.iMax * 0.2),即首段游走的 tick 数取「总量三分之一」与「总量 20%」的较小值;随后用 RandomWalk 分三段把 open→极值→反向极值→close 的路径填进去。 若 CheckViability 不通过则走 Random_Price 完全随机填充;末尾对 bLow / bHigh 未触及时补挂 Unique 价,并 CorretTime 对齐时间戳。想验证可直接把函数挂到 MT5 脚本里,打印 tick 数组看某根柱体的 bid/last 分布是否贴合原 OHLC。

MQL5 / C++
class="kw">inline class="type">int Simulation(const class="type">MqlRates &rate, class="type">MqlTick &tick[])
  {
     class="type">int    i0, i1;
     class="type">bool   b0 = ((rand() & class="num">1) == class="num">1);
                    
     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))
     {
        i0 = (class="type">int)(MathMin(m_Marks.iMax / class="num">3.0, m_Marks.iMax * class="num">0.2));
        i1 = m_Marks.iMax - i0;
        i0 = RandomWalk(class="num">1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, class="num">0);
        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);
        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);
m_Marks.bLow = m_Marks.bHigh = 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;
  }
把双模式校验交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到低量能柱线的回放风险标记,你专注决策而非底层兼容。

常见问题

系统在极低业务量时难以生成跳价序列,原阻塞限制未分离 Bid 与 Last 模式,量能不足即触发崩溃风险,本篇通过共用结构解除该限制。
可以,小布盯盘内置了低量能柱线的识别与标记,能直观提示哪些时段回放模拟可能失真,减少手动排查。
跳价间时间调整方法在 Bid 与 Last 中逻辑一致,改动一方另一方受益,本篇将其抽入私密结构提升复用效率。
通过柱线可用跳价数量判定覆盖区域,再在可能跳价间分配交易量,具体函数负责检查随机游走生成的可行性边界。