开发回放系统 市场模拟(第 20 部分):外汇(I)·综合运用
◍ 回放引擎里的成交量与价差合成
在 MT5 自定义品种回放里,每一笔 tick 都要落进合成 K 线的累计字段。real_volume 直接按 tick 的真实成交量累加,而 tick_volume 只在 volume_real 大于 0 时计 1,这意味着零成交量 tick 不会污染传统成交量计数。 def_Rate.time 取挂载柱的内存时间戳,随后用 CustomRatesUpdate 把这一帧推给自定义品种。若开启 bViewTicks 观察明细,会从 m_Ticks.Info 取当前 tick;当数据源不含真实买卖盘(bTickReal 为假)时,引擎用 rand() 做价差扰动。 价差逻辑里有个具体阈值:iRand 落在 29080 到 32767 之间时,按奇偶性再叠加一个 PointsPerTick,否则价差为 0。也就是说约 11.3% 的随机样本会触发额外跳动点,模拟稀疏报价下的点差扩张。 最后用 CustomTicksAdd 把合成 tick 写进自定义品种,m_ReplayCount 自增进入下一笔。开 MT5 把 PointsPerTick 调到 2 倍,能直接看到回放中假突破毛刺明显变多。
def_Rate.real_volume += (class="type">long) m_Ticks.Info[m_ReplayCount].volume_real; def_Rate.tick_volume += (m_Ticks.Info[m_ReplayCount].volume_real > class="num">0 ? class="num">1 : class="num">0); def_Rate.time = m_MountBar.memDT; CustomRatesUpdate(def_SymbolReplay, m_MountBar.Rate); if (bViewTicks) { tick = m_Ticks.Info[m_ReplayCount]; if (!m_Ticks.bTickReal) { class="kw">static class="type">class="kw">double BID, ASK; class="type">class="kw">double dSpread; class="type">int iRand = rand(); dSpread = PointsPerTick + ((iRand > class="num">29080) && (iRand < class="num">32767) ? ((iRand & class="num">1) == class="num">1 ? PointsPerTick : class="num">0 ) : class="num">0 ); if (tick[class="num">0].last > ASK) { ASK = tick[class="num">0].ask = tick[class="num">0].last; BID = tick[class="num">0].bid = tick[class="num">0].last - dSpread; } if (tick[class="num">0].last < BID) { ASK = tick[class="num">0].ask = tick[class="num">0].last + dSpread; BID = tick[class="num">0].bid = tick[class="num">0].last; } } CustomTicksAdd(def_SymbolReplay, tick); } m_ReplayCount++; class="macro">#undef def_Rate
让计时器在休眠品种上活过来
回放系统最怕遇到真实市场里的‘死品种’:流动性枯竭、拍卖暂停、甚至业务停摆,跳价文件若让某个品种休眠 15 分钟,旧逻辑会直接把整个系统锁死。以前靠反向推算柱线,空闲模式下图表常出怪象,而且系统启动后总是先等一段时间才显示跳价——这和预期完全相反。 新循环改用正向计算,并在计时器里加了一道‘活锁’:只要累计延迟超过 400 毫秒,就进内层 while 用 200 毫秒做步进判断。这样即使品种休眠数小时,你关图表或拖动控制指标改位置,系统也能响应,而不是卡在待机里装死。 关键参数有两个:内层判断的 200 毫秒阈值、以及触发内层循环的 400 毫秒延迟累计。改 200 时要同步改相关位置,否则回放节奏会错位。外汇与贵金属本身流动性风险高,回放里虽能绕开,但真盘碰上这类暂停仍可能滑点放大。 下面这段代码就是新显示循环的核心。注意被划线部分:delay 累加相邻 tick 的毫秒差,超 400 才放行进阶等待;ChartSymbol 为空直接 break 或 return,保证关图能退出而非死循环。
<span class="keyword">class="type">bool</span> LoopEventOnTime(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">bool</span> bViewBuider, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">bool</span> bViewMetrics) { u_Interprocess Info; <span class="keyword">class="type">int</span> iPos, iTest; iTest = <span class="number">class="num">0</span>; <span class="keyword">class="kw">while</span> ((iTest == <span class="number">class="num">0</span>) && (!<span class="predefines">_StopFlag</span>)) { iTest = (<span class="functions">ChartSymbol</span>(m_IdReplay) != <span class="class="type">class="kw">string">""</span> ? iTest : -<span class="number">class="num">1</span>); iTest = (<span class="functions">GlobalVariableGet</span>(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -<span class="number">class="num">1</span>); iTest = (iTest == <span class="number">class="num">0</span> ? (Info.s_Infos.isPlay ? <span class="number">class="num">1</span> : iTest) : iTest); <span class="keyword">if</span> (iTest == <span class="number">class="num">0</span>) <span class="functions">Sleep</span>(<span class="number">class="num">100</span>); } <span class="keyword">if</span> ((iTest < <span class="number">class="num">0</span>) || (<span class="predefines">_StopFlag</span>)) <span class="keyword">class="kw">return</span> <span class="macro">class="kw">false</span>; AdjustPositionToReplay(bViewBuider); iPos = <span class="number">class="num">0</span>; <span class="keyword">class="kw">while</span> ((m_ReplayCount < m_Ticks.nTicks) && (!<span class="predefines">_StopFlag</span>)) { <span class="stroke">iPos = (<span class="keyword">class="type">int</span>)(m_ReplayCount < m_Ticks.nTicks ? m_Ticks.Info[m_ReplayCount].time_msc - m_Ticks.Info[m_ReplayCount - <span class="number">class="num">1</span>].time_msc : <span class="number">class="num">0</span>);</span> <span class="stroke">m_MountBar.delay += (iPos < <span class="number">class="num">0</span> ? iPos + <span class="number">class="num">1000</span> : iPos);</span> iPos += (<span class="keyword">class="type">int</span>)(m_ReplayCount < (m_Ticks.nTicks - <span class="number">class="num">1</span>) ? m_Ticks.Info[m_ReplayCount + <span class="number">class="num">1</span>].time_msc - m_Ticks.Info[m_ReplayCount].time_msc : <span class="number">class="num">0</span>); CreateBarInReplay(bViewMetrics, <span class="macro">true</span>); <span class="stroke"><span class="keyword">if</span> (m_MountBar.delay > <span class="number">class="num">400</span>)</span> <span class="keyword">class="kw">while</span> ((iPos > <span class="number">class="num">200</span>) && (!<span class="predefines">_StopFlag</span>)) { <span class="stroke"><span class="keyword">if</span> (<span class="functions">ChartSymbol</span>(m_IdReplay) == <span class="class="type">class="kw">string">""</span>) <span class="keyword">class="kw">break</span>;</span> <span class="keyword">if</span> (<span class="functions">ChartSymbol</span>(m_IdReplay) == <span class="class="type">class="kw">string">""</span>) <span class="keyword">class="kw">return</span> <span class="macro">class="kw">false</span>; <span class="functions">GlobalVariableGet</span>(def_GlobalVariableReplay, Info.u_Value.df_Value); <span class="keyword">if</span> (!Info.s_Infos.isPlay) <span class="keyword">class="kw">return</span> <span class="macro">true</span>; Info.s_Infos.iPosShift = (<span class="keyword">class="type">class="kw">ushort</span>)((m_ReplayCount * def_MaxPosSlider) / m_Ticks.nTicks);
「回放循环里的延时与全局变量落点」
这段回放逻辑把每一帧的行情数值写进全局变量,再靠两次 Sleep 控制节奏。第一次固定睡 195 毫秒,随后把游标 iPos 回退 200 单位,等于在重演历史 tick 时强制留出可观测间隔。 外层还有一段条件休眠:Sleep(m_MountBar.delay - 20) 之后立刻把 delay 清零。说明挂载K线时的额外延迟只生效一次,避免后续循环被重复拖慢。 函数最终返回 m_ReplayCount == m_Ticks.nTicks,也就是实际回放计数与总 tick 数一致才认定回放完整。你在 MT5 里跑类似重播器时,可把 195 和 200 这两个值改小做压力测试,观察全局变量刷新是否跟得上。外汇与贵金属tick重播属高频逻辑,实盘误用有极高风险。
GlobalVariableSet(def_GlobalVariableReplay, Info.u_Value.df_Value); Sleep(class="num">195); iPos -= class="num">200; Sleep(m_MountBar.delay - class="num">20); m_MountBar.delay = class="num">0; } } class="kw">return (m_ReplayCount == m_Ticks.nTicks); }
◍ 外汇回放系统的遗留坑位
当前这版回放/模拟系统已经能跑通基础外汇数据复现,附件里的 Market_Replay_yvg20.zip(约 14.4 MB)可直接下到 MT5 里验。但配置文件的解析和一些渲染异常还没清掉,它们不挡你正常复现报价,只是用外汇数据时会看到某些地方没正确呈现。 这类顽固问题作者故意留到下一篇再动,因为改动可能牵扯前文展示过的核心逻辑。你若现在就接手改,建议先只动配置读取层,别碰回放主循环。 外汇与贵金属模拟本身带高杠杆风险,任何回测顺滑都不代表实盘概率等同,拿这套系统跑真实账户前先在 demo 磨两周。