开发回放系统(第 62 部分):玩转服务(三)·进阶篇
(2/3)· 一分钟柱里报价爆量时,MetaTrader 5 连实盘都卡,重放更会崩,这篇给解法
◍ 用随机游走把K线拆成逐笔tick
这段内联函数负责把一根 MqlRates 柱面还原成若干笔 MqlTick,核心在 Random_Price 与 DistributeVolumeReal 两个例程。Random_Price 从 1 循环到 m_Marks.iMax-1,对 high/low 之间做 NormalizeDouble(RandomLimit(...), m_NDigits) 取随机价,再交给 MountPrice 挂到 tick 数组;同时用三元判断 m_IsPriceBID 来比对 bid 或 last 是否触到当日高低,写回 m_Marks.bHigh / bLow 标志。 成交量分配走的是“先均摊、后随机补”的路子:DistributeVolumeReal 先把 0 到 iMax 每笔 volume 和 volume_real 都置 1,若 (iMax+1) 小于 tick_volume,就随机挑位置把整数字段累加;real_volume 超出部分同理以 1.0 步长散到随机下标。这样造出的 tick 流在成交量分布上更接近真实撮合而非均匀假数据。 RandomWalk 是总调度入口,入参带 Open/Close/High/Low/Spread 与 iMode、iDesloc,内部先声明 vStep、vNext、price 及局部高低 vH、vL,char i0 作步进计数器。外汇与贵金属品种用这类合成 tick 做回测,需清醒认识:历史重放和蒙特卡洛式随机重建都只是概率近似,实盘滑点与流动性断裂风险远高于离线环境。
class="num">061. tick[iPos].last = NormalizeDouble(price, m_NDigits); class="num">062. } class="num">063. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">064. class="kw">inline class="type">void Random_Price(const class="type">MqlRates &rate, class="type">MqlTick &tick[]) class="num">065. { class="num">066. for (class="type">int c0 = class="num">1; c0 < m_Marks.iMax; c0++) class="num">067. { class="num">068. MountPrice(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((class="type">int)(rate.spread | (m_Marks.iMax & 0xF)), class="num">0)), tick); class="num">069. m_Marks.bHigh = (rate.high == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bHigh; class="num">070. m_Marks.bLow = (rate.low == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bLow; class="num">071. } class="num">072. } class="num">073. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">074. class="kw">inline class="type">void DistributeVolumeReal(const class="type">MqlRates &rate, class="type">MqlTick &tick[]) class="num">075. { class="num">076. for (class="type">int c0 = class="num">0; c0 <= m_Marks.iMax; c0++) class="num">077. { class="num">078. tick[c0].volume_real = class="num">1.0; class="num">079. tick[c0].volume = class="num">1; class="num">080. } class="num">081. if ((m_Marks.iMax + class="num">1) < rate.tick_volume) for (class="type">int c0 = (class="type">int)(rate.tick_volume - m_Marks.iMax); c0 > class="num">0; c0--) class="num">082. tick[RandomLimit(class="num">0, m_Marks.iMax - class="num">1)].volume += class="num">1; class="num">083. for (class="type">int c0 = (class="type">int)(rate.real_volume - m_Marks.iMax); c0 > class="num">0; c0--) class="num">084. tick[RandomLimit(class="num">0, m_Marks.iMax)].volume_real += class="num">1.0; class="num">085. } class="num">086. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">087. 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="num">088. { class="num">089. class="type">class="kw">double vStep, vNext, price, vH = High, vL = Low; class="num">090. class="type">char i0 = class="num">0;
「逐tick重排时边界与步进的收口逻辑」
这段循环负责在 In 到 Out 的 tick 区间内,按 m_TickSize 粒度重排价格并同步更新高低边界。vStep 由 (Out - In) / ((High - Low) / m_TickSize) 算得,意味着每推进约 vStep 个 tick,理论价格才跨过一个 tick 价位,这是控制重排密度的核心参数。 第95行先取前一 tick 的 bid 或 last(由 m_IsPriceBID 决定),再叠加减 iDesloc 到 +iDesloc 的随机偏移;第96行把价格夹在 vH 与 vL 之间,防止越界。第97行调用 MountPrice 写入重排后的报价与伪 spread。 iMode 分支里,mode 1 用 i0 的 0x01/0x02 位标记是否触到 High/Low,当 i0==3 时把 vH、vL 钉死为原 High/Low;mode 0 则在 price==Close 时直接返回当前索引 c0,提前结束重排。 第111行是节流阀:若 floor(vNext) 仍大于等于已循环计数 c1 就 continue,否则 c2 自增,最多容忍连续 3 次不推进价位。越过阈值后 vNext += vStep,并按 iMode 与 c2 奇偶性微调 vL、vH(第113–114行),使边界随 Close 位置缓慢收缩或扩张。外汇与贵金属tick重排属高风险模拟,实盘前务必在 MT5 策略测试器用历史数据校验边界行为。
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 ? -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; } 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))); } class="kw">return Out;
回放引擎里的逐笔 tick 合成逻辑
这段 C_Simulation 构造器只做三件事:记下报价精度 nDigits、判断回放品种用的是 BID 还是 LAST 图(m_IsPriceBID)、抓一次 SYMBOL_TRADE_TICK_SIZE 存进 m_TickSize。后面 Simulation() 每次吃一根 MqlRates 蜡烛,吐出一组 MqlTick 数组,本质是把 1 根 K 线拆成若干假 tick 供 EA 回测。 iMax 先被夹在 1 到 def_MaxTicksVolume 之间,再用当前 tick_volume-1 封顶——也就是说一根成交量只有 5 的柱,最多拆 4 个 tick。bHigh / bLow 标记开盘或收盘是否触到极值,决定后续游走方向偏好。 当 iMax 超过 10 时走细分分支:前段占 i0 = min(iMax/3, iMax*0.2) 个 tick,中段补到 i1,步长 i2 由 (high-low)/TickSize 除以 i0 得到,最小为 1。若 iMax>=1000 用 rand() 抛硬币选先冲高还是先探低,否则比较 (high-open) 与 (open-low) 谁大就反向先走。RandomWalk 两次调用把 open→极值→另一极值→close 的路径填进 tick 数组。 外汇与贵金属 tick 回放纯属历史重采样,合成路径不代表真实流动性;MT5 里把 def_MaxTicksVolume 调小可加速回测但会丢失微观结构,调大更拟真但耗时陡增,按品种成交量分布试两次就有体感。
class="num">118. } class="num">119. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">120. class="kw">public : class="num">121. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">122. C_Simulation(const class="type">int nDigits) class="num">123. { class="num">124. m_NDigits = nDigits; class="num">125. m_IsPriceBID = (SymbolInfoInteger(def_SymbolReplay, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_BID); class="num">126. m_TickSize = SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE); class="num">127. } class="num">128. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">129. class="kw">inline class="type">int Simulation(const class="type">MqlRates &rate, class="type">MqlTick &tick[], const class="type">int MaxTickVolume = def_MaxTicksVolume) class="num">130. { class="num">131. class="type">int i0, i1, i2; class="num">132. class="type">bool b0; class="num">133. class="num">134. m_Marks.iMax = (MaxTickVolume <= class="num">0 ? class="num">1 : (MaxTickVolume >= def_MaxTicksVolume ? def_MaxTicksVolume : MaxTickVolume)); class="num">135. m_Marks.iMax = ((class="type">int)rate.tick_volume > m_Marks.iMax ? m_Marks.iMax : (class="type">int)rate.tick_volume - class="num">1); class="num">136. m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high); class="num">137. m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low); class="num">138. Simulation_Time(rate, tick); class="num">139. MountPrice(class="num">0, rate.open, rate.spread, tick); class="num">140. if (m_Marks.iMax > class="num">10) class="num">141. { class="num">142. i0 = (class="type">int)(MathMin(m_Marks.iMax / class="num">3.0, m_Marks.iMax * class="num">0.2)); class="num">143. i1 = m_Marks.iMax - i0; class="num">144. i2 = (class="type">int)(((rate.high - rate.low) / m_TickSize) / i0); class="num">145. i2 = (i2 == class="num">0 ? class="num">1 : i2); class="num">146. b0 = (m_Marks.iMax >= class="num">1000 ? ((rand() & class="num">1) == class="num">1) : (rate.high - rate.open) < (rate.open - rate.low)); class="num">147. i0 = RandomWalk(class="num">1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, class="num">0, i2); class="num">148. 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);
◍ 极值标记与价格挂载的执行顺序
这段片段处在 tick 合成函数的尾部,核心动作是把当前 K 线的高低点标记复位,并按条件把价格挂进 tick 序列。第 150 行在 RandomWalk 调用后把 bHigh、bLow 同时置 true,意味着随机游走路径已覆盖极值,后续不再重复挂载。 若走 else 分支,则调用 Random_Price 生成价格;非 BID 模式下还会用 DistributeVolumeReal 分配真实成交量,这是复刻真实报价微观结构的关键一步。 154–156 行用 bLow / bHigh 的布尔状态做守卫:未标记低点时把 Unique(rate.high, tick) 作为低价挂载,未标记高点时反向处理,最后无论怎样都把 iMax 位置的 close 价挂上。
- 行 CorretTime 修正时间戳,159 行返回 iMax 作为本函数写入的末位索引。外汇与贵金属 tick 仿真涉及点差与跳空,实盘验证前务必清楚这类随机合成仅用于回测,真实风险极高。
class="num">149. 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); class="num">150. m_Marks.bHigh = m_Marks.bLow = true; class="num">151. class="num">152. }else Random_Price(rate, tick); class="num">153. if (!m_IsPriceBID) DistributeVolumeReal(rate, tick); class="num">154. if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick); class="num">155. if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick); class="num">156. MountPrice(m_Marks.iMax, rate.close, rate.spread, tick); class="num">157. CorretTime(tick); class="num">158. class="num">159. class="kw">return m_Marks.iMax; class="num">160. } class="num">161. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">162. }; class="num">163. class=class="str">"cmt">//+------------------------------------------------------------------+
「压住回放里的报价洪峰」
真实分时报价灌进回放器时,单位时间内的 tick 数可能冲破系统内部上限。和纯模拟不同,这里卡在一点:加载阶段你根本不知道图表用的是 Bid 还是 Last 报价,也就没法提前按单一市场模型去裁剪。 C_FileTicks 里的 LoadTicks 是切入点。原函数第 10、11 行先暂存已加载分时报价的柱形位置;第 12 行开文件,第 13 行把文件里绝对所有的 tick 一次性读进来。读完后才确定图表类型,所以超限判断只能往后放。 改造思路是在 BuildBar1Min 的循环里动手:一分钟柱形闭合前,若记录的 tick 数超了预设值,就让模拟类用随机游走补运动,并覆盖掉多余的真实 tick。难点是执行顺序——第 21 行若留在原位置,BuildBar1Min 在第 24 行就建不出标准,必须按新片段重排。 第 27 行用双重检查:先确认 ToReplay 为真(避免负索引),再比 tick 数上限;第 29~34 行把位移指针设到待替位置、跑模拟、传数据、销毁模拟器。当前版本第 31 行模拟失败就不释放内存,属测试期小缺陷,下篇再修。 实际在 MT5 里跑这段代码时,把预设上限从默认调小到 200 附近,能明显看到一分钟柱内的报价抖动被熨平,但极端行情下随机游走可能偏离真实微观结构——外汇与贵金属杠杆高,回测结论仅代表历史数据倾向,不构成实盘指引。
class="num">01. class="type">class="kw">datetime LoadTicks(const class="type">class="kw">string szFileNameCSV, const class="type">bool ToReplay = true) class="num">02. { class="num">03. class="type">int MemNRates, class="num">04. MemNTicks; class="num">05. class="type">class="kw">datetime dtRet = TimeCurrent(); class="num">06. class="type">MqlRates RatesLocal[], class="num">07. rate; class="num">08. class="type">bool bNew; class="num">09. class="num">10. MemNRates = (m_Ticks.nRate < class="num">0 ? class="num">0 : m_Ticks.nRate); class="num">11. MemNTicks = m_Ticks.nTicks; class="num">12. if (!Open(szFileNameCSV)) class="kw">return class="num">0; class="num">13. if (!ReadAllsTicks()) class="kw">return class="num">0; class="num">14. rate.time = class="num">0;