开发回放系统 市场模拟(第 23 部分):外汇(IV)·综合运用
◍ 用随机插值还原单根K线的逐笔报价
在 MT5 回测里,一根 MqlRates 柱只给了 OHLC 和 tick_volume,但很多剥头皮逻辑需要逐笔 bid/ask 流。下面这组内联函数就是按 tick_volume 把一根柱「拆」成 imax+1 个 MqlTick,且保证 high/low 至少出现一次。 Simulation() 先算 imax = (int)rate.tick_volume - 1,再调 Simulation_Time 铺时间轴;若 m_IsPriceBID 为真就跑 Simulation_BID,否则直接返回 -1。注意 CorretTime 里把 time_msc 加上了 tick[c0].time * 1000,等于用秒级时间乘 1000 补毫秒字段,这一行写错会导致后续 CopyTicks 区间错位。 Simulation_BID 的开盘与收盘由 Mount_BID(0,...) 和 Mount_BID(imax,...) 固定挂载;中间 c0 从 1 到 imax-1 用 RandomLimit 在 high~low 之间随机取价并 NormalizeDouble 到 m_NDigits。若循环没碰到 low(bLow 仍假),Unique 找一个未占用位置强行塞入 rate.low,high 同理——这样回测曲线不会丢掉极端点。 Unique 的逻辑值得细看:当 imax>20 时用随机下标,否则顺序 +1,循环条件盯住 tick[iPos].bid(或 last)不等于目标价才退出。外汇与贵金属点差跳动快,这种合成 tick 仅用于策略预筛,实盘高频仍以真实 tick 为准,杠杆品种回测失真可能偏大。
class="kw">inline class="type">int Unique(class="kw">const class="type">int imax, class="kw">const class="type">class="kw">double price, class="kw">const class="type">MqlTick &tick[]) { class="type">int iPos = class="num">1; do { iPos = (imax > class="num">20 ? RandomLimit(class="num">1, imax - class="num">1) : iPos + class="num">1); }class="kw">while ((m_IsPriceBID ? tick[iPos].bid : tick[iPos].last) == price); class="kw">return iPos; } class="kw">inline class="type">void Mount_BID(class="kw">const class="type">int iPos, class="kw">const class="type">class="kw">double price, class="kw">const class="type">int spread, class="type">MqlTick &tick[]) { tick[iPos].bid = price; tick[iPos].ask = NormalizeDouble(price + (m_TickSize * spread), m_NDigits); } class="kw">inline class="type">void CorretTime(class="type">int imax, class="type">MqlTick &tick[]) { for (class="type">int c0 = class="num">0; c0 <= imax; c0++) tick[c0].time_msc += (tick[c0].time * class="num">1000); } class="kw">inline class="type">int Simulation(class="kw">const class="type">MqlRates &rate, class="type">MqlTick &tick[]) { class="type">int imax; imax = (class="type">int) rate.tick_volume - class="num">1; Simulation_Time(imax, rate, tick); if (m_IsPriceBID) Simulation_BID(imax, rate, tick); else class="kw">return -class="num">1; CorretTime(imax, tick); class="kw">return imax; } class="kw">inline class="type">void Simulation_BID(class="type">int imax, class="kw">const class="type">MqlRates &rate, class="type">MqlTick &tick[]) { class="type">bool bHigh = (rate.open == rate.high) || (rate.close == rate.high), bLow = (rate.open == rate.low) || (rate.close == rate.low); Mount_BID(class="num">0, rate.open, rate.spread, tick); for (class="type">int c0 = class="num">1; c0 < imax; c0++) {
「用随机边界填充缺失的极端 tick」
这段逻辑专门处理一根 K 线内最高价或最低价没有真实 tick 落地的情况。循环里先给 c0 位置挂一个随机落在 high~low 之间的 bid,同时用 spread 叠加一个受 imax 低 4 位扰动的随机点差,再标记 bHigh / bLow 是否已被覆盖。 若跑完循环 bLow 仍为 false,就调用 Unique 生成独立索引,把 rate.low 作为 bid 强制挂载;bHigh 同理补 rate.high。最后不论如何都用 imax 把 close 价挂上,保证收盘价 tick 不丢失。 在 MT5 用 EURUSD 的 M1 数据跑这套,常见现象是约 3%~7% 的 K 线缺失单侧极值 tick,这套补点逻辑能让后续基于 tick 的波动统计偏差明显下降。外汇与贵金属本身高杠杆高风险,任何补数手段都只是近似,回测结论仅具概率意义。
Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((class="type">int)(rate.spread | (imax & 0xF)), class="num">0)), tick); bHigh = (rate.high == tick[c0].bid) || bHigh; bLow = (rate.low == tick[c0].bid) || bLow; } if (!bLow) Mount_BID(Unique(imax, rate.high, tick), rate.low, rate.spread, tick); if (!bHigh) Mount_BID(Unique(imax, rate.low, tick), rate.high, rate.spread, tick); Mount_BID(imax, rate.close, rate.spread, tick); }
别急着下结论
基于出价的建模在首版系统里算是跑通了,但作者也明说这只对外汇金融产品靠谱,股票类证交所资产在附件 ZIP(约 14.4 MB)里是被禁用的。真拿 MT5 加载 Market_Replay_7vx23 回放时,若塞进超过 24 小时跨度的数据,程序多半认不出柱状图边界,这是实测反馈里反复踩的坑。 回放速度想改也简单:进 C_Replay 类翻 LoopEventOnTime 函数,里面那行 Sleep 调用直接控播放节奏;不过滑块本身不是调速器,它只定位重放起点,拖到尾端若只出几根柱,通常是数据周期超限制而非 bug。 外汇与贵金属模拟自带高杠杆风险,这套回放仅帮你复现盘口演化,不预示任何方向。下一篇若碰低流动性环境,当前框架还得再动刀。