开发回放系统  市场模拟(第 20 部分):外汇(I)·综合运用
📘

开发回放系统 市场模拟(第 20 部分):外汇(I)·综合运用

第 3/3 篇

◍ 回放引擎里的成交量与价差合成

在 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 倍,能直接看到回放中假突破毛刺明显变多。

MQL5 / C++
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,保证关图能退出而非死循环。

MQL5 / C++
<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)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;u_Interprocess Info;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">int</span> iPos, iTest;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iTest = <span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">while</span> ((iTest == <span class="number">class="num">0</span>) &amp;&amp; (!<span class="predefines">_StopFlag</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iTest = (<span class="functions">ChartSymbol</span>(m_IdReplay) != <span class="class="type">class="kw">string">""</span> ? iTest : -<span class="number">class="num">1</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iTest = (<span class="functions">GlobalVariableGet</span>(def_GlobalVariableReplay, Info.u_Value.df_Value) ? iTest : -<span class="number">class="num">1</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iTest = (iTest == <span class="number">class="num">0</span> ? (Info.s_Infos.isPlay ? <span class="number">class="num">1</span> : iTest) : iTest);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> ((iTest &lt; <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>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;AdjustPositionToReplay(bViewBuider);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iPos = <span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">while</span> ((m_ReplayCount &lt; m_Ticks.nTicks) &amp;&amp; (!<span class="predefines">_StopFlag</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="stroke">iPos = (<span class="keyword">class="type">int</span>)(m_ReplayCount &lt; 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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="stroke">m_MountBar.delay += (iPos &lt; <span class="number">class="num">0</span> ? iPos + <span class="number">class="num">1000</span> : iPos);</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iPos += (<span class="keyword">class="type">int</span>)(m_ReplayCount &lt; (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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CreateBarInReplay(bViewMetrics, <span class="macro">true</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="stroke"><span class="keyword">if</span> (m_MountBar.delay &gt; <span class="number">class="num">400</span>)</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">while</span> ((iPos &gt; <span class="number">class="num">200</span>) &amp;&amp; (!<span class="predefines">_StopFlag</span>))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">GlobalVariableGet</span>(def_GlobalVariableReplay, Info.u_Value.df_Value);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (!Info.s_Infos.isPlay) <span class="keyword">class="kw">return</span> <span class="macro">true</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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重播属高频逻辑,实盘误用有极高风险。

MQL5 / C++
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 磨两周。

常见问题

在回放循环里用全局变量记录上次活跃时间,超过阈值就强制唤醒并重置计时器间隔,避免品种休眠后引擎停摆。
按历史 tick 的密度估算单位时间成交量,价差则取同时段买卖价中位差并加随机扰动,仅作模拟参考,不代表真实盘口。
小布可扫描回放循环里的延时与全局变量落点,标出易漏的休眠唤醒和价差异常点,你直接照提示改参数即可。
建议从 10–30 毫秒起步,用全局变量落点比对实际处理耗时,偏高就上调,偏低再压,没有统一值。
多是遗留坑位没清:随机扰动累积且无回归中枢,需在循环里加价差均值回复,否则模拟失真概率偏高。