开发回放系统(第 69 部分):取得正确的时间(二)·综合运用
状态机里的市场时钟对齐
这段 Update 函数把 MT5 里的市场状态分成收盘、回放、实时交易和竞价几类,靠 switch 分支写信息字符串并触发 Draw()。做面板或 HUD 时,这种写法能让你一眼看出当前会话处于哪种模式,而不是去轮询一大堆标志位。 关键在第 122–124 行:先取 PeriodSeconds() 得到当前图表周期秒数(比如 M5 就是 300),再把当前时间 dt 按周期向下取整后加一个周期,算出下一根 K 线的开盘时间。若 m_Info.Rate.time 已小于等于 dt,就更新为这个值,否则保留。这套对齐逻辑在回放(GL_TimeAdjust)和实盘(TimeCurrent())下都能用。 第 125 行把 dt 直接转成秒级字符串塞进 szInfo,原代码里减 Rate.time 的注释被划掉,说明作者最终选择显示当前时间而非剩余秒数。你在 MT5 里改这一行,就能在面板上切出「距下根 K 线倒计时」或「服务器时间」两种视图。 外汇与贵金属杠杆高、跳空频繁,状态切换瞬间可能漏掉一根 Bar,拿去跑 EA 前建议在策略测试器用不同周期回测确认对齐无偏移。
class="type">void Update(class="kw">const eStatusMarket arg) { class="type">int i0; class="type">class="kw">datetime dt; class="kw">switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) { case eCloseMarket : m_Info.szInfo = "Closed Market"; class="kw">break; case eInReplay : case eInTrading : i0 = PeriodSeconds(); dt = (m_Info.Status == eInReplay ? (class="type">class="kw">datetime) GL_TimeAdjust : TimeCurrent()); m_Info.Rate.time = (m_Info.Rate.time <= dt ? (class="type">class="kw">datetime)(((class="type">class="kw">ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); if (dt > class="num">0) m_Info.szInfo = TimeToString((class="type">class="kw">datetime) dt, TIME_SECONDS); class="kw">break; case eAuction : m_Info.szInfo = "Auction"; class="kw">break; class="kw">default : m_Info.szInfo = "ERROR"; } Draw(); }
◍ 指标主循环怎么把报价喂给状态机
这段是自定义指标里 OnCalculate 的实际落点,负责把每一根 K 的收盘价和 spread 低位掩码写进全局变量,再触发研究对象的 Update。 第 52 行取 close[rates_total-1],也就是当前未闭合 K 线的收盘价赋给 GL_PriceClose;第 54 行用 spread[rates_total-1] 和 ~def_MaskTimeService 做位与,把点差里的服务时间位剥掉后存进 GL_TimeAdjust,这种写法在回放券符号下常被注释掉的 iSpread 方案替代。 m_posBuff 在第 57 行被设为 rates_total,相当于告诉后续模块当前缓冲长度;第 58 行通过指针 (*Study).Update(m_Status) 把状态抛给上层逻辑,最后 return rates_total 让 MT5 认为已处理全部柱数。 前面 129–134 行是某个 switch 的收尾:default 分支把 m_Info.szInfo 标成 "ERROR" 后统一调用 Draw(),说明异常态也要重绘界面而不静默跳过。复制这段代码到 MT5 指标里,改掉 def_MaskTimeService 的宏定义,就能在 EURUSD 的 M1 上观察到 GL_TimeAdjust 随点差跳变——外汇和贵金属杠杆品种点差突变频繁,实盘验证需自担高风险。
class="num">129. class="kw">break; class="num">130. class="kw">default : class="num">131. m_Info.szInfo = "ERROR"; class="num">132. } class="num">133. Draw(); class="num">134. } class="num">135. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">46. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">47. class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime& time[], class="kw">const class="type">class="kw">double& open[], class="num">48. class="kw">const class="type">class="kw">double& high[], class="kw">const class="type">class="kw">double& low[], class="kw">const class="type">class="kw">double& close[], class="kw">const class="type">long& tick_volume[], class="num">49. class="kw">const class="type">long& volume[], class="kw">const class="type">int& spread[]) class="num">50. class=class="str">"cmt">//class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double& price[]) class="num">51. { class="num">52. GL_PriceClose = close[rates_total - class="num">1]; class="num">53. class=class="str">"cmt">// GL_PriceClose = price[rates_total - class="num">1]; class="num">54. GL_TimeAdjust = (spread[rates_total - class="num">1] & (~def_MaskTimeService); class="num">55. class=class="str">"cmt">// if (_Symbol == def_SymbolReplay) class="num">56. class=class="str">"cmt">// GL_TimeAdjust = iSpread(NULL, PERIOD_M1, class="num">0) & (~def_MaskTimeService); class="num">57. m_posBuff = rates_total; class="num">58. (*Study).Update(m_Status); class="num">59. class="num">60. class="kw">return rates_total; class="num">61. } class="num">62. class=class="str">"cmt">//+------------------------------------------------------------------+
「回放卡死在长间隔等待里怎么破」
做 MT5 市场回放服务时有个隐蔽坑:原本把等待下一笔分时报价的循环放在柱形创建程序里,在流动性高的品种上一切正常,因为报价间隔通常小于 1 秒。但一旦切到外汇日盘开盘或贵金属竞价时段,两笔报价可能隔 40 秒甚至更久,服务会卡死在第 230~236 行的等待循环里——这时你点暂停、拖控制滑块再播放,系统完全不理你,直到那 40 秒跑完。 根因是旧逻辑没检查用户是否暂停。修复思路不是单独打补丁,而是顺手把「低流动性剩余时间反馈」一起做了。新代码在第 221 行加了计数器变量,第 230 行初始化,第 242 行让它从 0 数到 4 后调用 RateUpdate(false):这对应前面说的「每秒约 5 个周期」,用近似计时让鼠标指标感知 1 秒流逝,精度不求严丝合缝,只给用户一个「还要等多久」的大致概念。 RateUpdate 函数从第 69 行起,专门隔离时间更新逻辑,避免直接改时间导致漏掉报价(关联第 110 行)。第 73 行是关键:真实报价由 CreateBarInReplay 接管时 st_Spread 用真实时间戳;LoopEventOnTime 调用时 st_Spread 只 +1,相当于每秒走一步。等真实报价一来,st_Spread 立刻被修正回实时值。所以若流动性枯竭、报价隔了 50 秒,计时器可能略微领先或滞后,指标显示的数值跳变不是 bug,反而带来好处——你暂停再立刻恢复,就能跳过那段漫长等待。外汇与贵金属市场的高风险在于此类流动性断层很常见,回测前务必在 MT5 用真实稀疏数据验证该逻辑。 看第 223 行的新循环:它替代了原来卡死式的等待,允许用户中途重定位控制指标。第 236 行校正后,检测到暂停就退出内层循环,函数返回主控制流重新调用,体验在低密度资产上顺滑很多。打开 C_Replay.mqh 把第 230 行附近改成这种轮询结构,是你今天就能在 MT5 测的一件事。
<span class="number">class="num">001</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">002</span>. <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Daniel Jose"</span> <span class="number">class="num">003</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">004</span>. <span class="preprocessor">class="macro">#include </span><span class="class="type">class="kw">string">"C_ConfigService.mqh"</span> <span class="number">class="num">005</span>. <span class="preprocessor">class="macro">#include </span><span class="class="type">class="kw">string">"C_Controls.mqh"</span> <span class="number">class="num">006</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">007</span>. <span class="preprocessor">class="macro">#define </span>def_IndicatorControl <span class="class="type">class="kw">string">"Indicators\\Market Replay.ex5"</span> <span class="number">class="num">008</span>. <span class="preprocessor">class="macro">#resource </span><span class="class="type">class="kw">string">"\\"</span> + def_IndicatorControl <span class="number">class="num">009</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">010</span>. <span class="preprocessor">class="macro">#define </span>def_CheckLoopService((!<span class="predefines">_StopFlag</span>) && (<span class="functions">ChartSymbol</span>(m_Infos.IdReplay) != <span class="class="type">class="kw">string">""</span>)) <span class="number">class="num">011</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">012</span>. <span class="preprocessor">class="macro">#define </span>def_ShortNameIndControl <span class="class="type">class="kw">string">"Market Replay Control"</span> <span class="number">class="num">013</span>. <span class="preprocessor">class="macro">#define </span>def_MaxSlider (def_MaxPosSlider + <span class="number">class="num">1</span>) <span class="number">class="num">014</span>. <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="number">class="num">015</span>. <span class="keyword">class</span> C_Replay : <span class="keyword">class="kw">public</span> C_ConfigService <span class="number">class="num">016</span>. { <span class="number">class="num">017</span>. <span class="keyword">class="kw">private</span> : <span class="number">class="num">018</span>. <span class="keyword">class="kw">struct</span> st00 <span class="number">class="num">019</span>. { <span class="number">class="num">020</span>. C_Controls::eObjectControl Mode; <span class="number">class="num">021</span>. uCast_Double Memory; <span class="number">class="num">022</span>. <span class="keyword">class="type">class="kw">ushort</span> Position; <span class="number">class="num">023</span>. <span class="keyword">class="type">int</span> Handle; <span class="number">class="num">024</span>. }m_IndControl; <span class="number">class="num">025</span>. <span class="keyword">class="kw">struct</span> st01 <span class="number">class="num">026</span>. { <span class="number">class="num">027</span>. <span class="keyword">class="type">long</span> IdReplay;
控制面板的双向同步机制
回放系统的指标控制块用一段共享内存与控制面板通信,EA 侧通过 UpdateIndicatorControl 读状态、写位置,避免每帧都重建句柄。 当 m_IndControl.Handle 为 INVALID_HANDLE 时直接 return,说明指标未加载完就别碰缓冲区,否则 CopyBuffer 会吞掉一帧。 若面板内存里的 eCtrlPosition 与当前 Position 一致,则只做两件事:CopyBuffer 取 1 根数据写进 dValue(成功返回值为 1),以及当状态字节被置为 ePlay 时把 Position 同步回内存。 位置不一致的分支里,代码把 'D' 'M' 写进 _8b[7]、_8b[6],再发 EventChartCustom 通知图表初始化回放——这两个 ASCII 标记是调试时区分本 EA 控制块的依据。 外汇与贵金属回放涉及杠杆与滑点,实盘验证前请在 MT5 策略测试器用历史数据跑通事件链路,高波动品种可能丢自定义事件。
class="type">int CountReplay; class="type">class="kw">double PointsPerTick; class="type">MqlTick tick[class="num">1]; class="type">MqlRates Rate[class="num">1]; } m_Infos; stInfoTicks m_MemoryData; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">bool MsgError(class="type">class="kw">string sz0) { Print(sz0); class="kw">return class="kw">false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void UpdateIndicatorControl(class="type">void) { class="type">class="kw">double Buff[]; if (m_IndControl.Handle == INVALID_HANDLE) class="kw">return; if (m_IndControl.Memory._16b[C_Controls::eCtrlPosition] == m_IndControl.Position) { if (CopyBuffer(m_IndControl.Handle, class="num">0, class="num">0, class="num">1, Buff) == class="num">1) m_IndControl.Memory.dValue = Buff[class="num">0]; if ((m_IndControl.Mode = (C_Controls::eObjectControl)m_IndControl.Memory._16b[C_Controls::eCtrlStatus]) == C_Controls::ePlay) m_IndControl.Position = m_IndControl.Memory._16b[C_Controls::eCtrlPosition]; } else { m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = m_IndControl.Position; m_IndControl.Memory._16b[C_Controls::eCtrlStatus] = (class="type">class="kw">ushort)m_IndControl.Mode; m_IndControl.Memory._8b[class="num">7] = &class="macro">#x27;D&class="macro">#x27;; m_IndControl.Memory._8b[class="num">6] = &class="macro">#x27;M&class="macro">#x27;; EventChartCustom(m_Infos.IdReplay, evCtrlReplayInit, class="num">0, m_IndControl.Memory.dValue, ""); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void SweepAndCloseChart(class="type">void) { class="type">long id;
◍ 回放图表清理与价差注入的实现细节
在回放引擎初始化阶段,需要先扫掉同名回放图表,避免旧窗口堆积。下面这段用 ChartFirst/ChartNext 遍历所有图表,遇到 Symbol 等于 def_SymbolReplay 的就 ChartClose 掉,逻辑直白但容易漏写 while 条件导致死循环。 [CODE] if ((id = ChartFirst()) > 0) do { if (ChartSymbol(id) == def_SymbolReplay) ChartClose(id); }while ((id = ChartNext(id)) > 0); [/CODE] RateUpdate 负责把合成价差写进自定义品种。st_Spread 用 static 保持状态:bCheck 为真时按宏取时间重置,否则自增 1;最终通过 CustomRatesUpdate 推给 def_SymbolReplay。注意 def_MaskTimeService 是按位或进去的,别当成普通加法。 CreateBarInReplay 里用 rand() 做微扰:当 iRand 落在 29080 到 32767 之间,有概率给 dSpread 加一个 PointsPerTick。这个区间约占 rand() 全正值域(0~32767)的 11.2%,意味着大约每九根 tick 才可能出现一次加点差,回测时价差分布会偏稀疏。外汇与贵金属回放带杠杆,实测价差模型偏差会放大滑点风险,上 MT5 跑前先确认 PointsPerTick 取值。 别把正态当圣经 rand() 的区间切割是人为硬阈值,不是统计分布拟合;直接用真实点差样本替换 iRand 判断,回放才接近实盘跳动。
if ((id = ChartFirst()) > class="num">0) do { if (ChartSymbol(id) == def_SymbolReplay) ChartClose(id); }class="kw">while ((id = ChartNext(id)) > class="num">0); class="kw">inline class="type">int RateUpdate(class="type">bool bCheck) { class="kw">static class="type">int st_Spread = class="num">0; st_Spread = (bCheck ? (class="type">int)macroGetTime(m_MemoryData.Info[m_Infos.CountReplay].time) : st_Spread + class="num">1); m_Infos.Rate[class="num">0].spread = (class="type">int)(def_MaskTimeService | st_Spread); CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); class="kw">return class="num">0; } class="kw">inline class="type">void CreateBarInReplay(class="type">bool bViewTick) { class="type">bool bNew; class="type">class="kw">double dSpread; class="type">int iRand = rand(); class="kw">static class="type">int st_Spread = class="num">0; if (BuildBar1Min(m_Infos.CountReplay, m_Infos.Rate[class="num">0], bNew)) { m_Infos.tick[class="num">0] = m_MemoryData.Info[m_Infos.CountReplay]; if (m_MemoryData.ModePlot == PRICE_EXCHANGE) { dSpread = m_Infos.PointsPerTick + ((iRand > class="num">29080) && (iRand < class="num">32767) ? ((iRand & class="num">1) == class="num">1 ? m_Infos.PointsPerTick : class="num">0 ) : class="num">0 ); if (m_Infos.tick[class="num">0].last > m_Infos.tick[class="num">0].ask) { m_Infos.tick[class="num">0].ask = m_Infos.tick[class="num">0].last;
「回放引擎里的报价重建与图表线切换」
在自定义回放品种上,tick 的 bid/ask 并非直接来自历史,而是围绕 last 价格按固定点差 dSpread 反推。当 last 高于当前 bid 时,代码把 bid 设为 last 减点差、ask 维持 last;当 last 低于 bid 时,ask 设为 last 加点差、bid 取 last 本身。 [CODE] m_Infos.tick[0].bid = m_Infos.tick[0].last - dSpread; } else if (m_Infos.tick[0].last < m_Infos.tick[0].bid) { m_Infos.tick[0].ask = m_Infos.tick[0].last + dSpread; m_Infos.tick[0].bid = m_Infos.tick[0].last; } [/CODE] 这段逻辑意味着:回放流里的买卖边界完全由 last 相对位置决定,点差恒定,不反映真实流动性扰动。 重建完 tick 后,若开启可视开关,CustomTicksAdd 把 tick 推入 def_SymbolReplay,RateUpdate(true) 强制刷新。随后用 macroGetTime 取时间戳做掩码写进 spread 字段,CustomRatesUpdate 更新 M1 柱。 AdjustViewDetails 负责按品种模式切图表线:外汇模式(PRICE_FOREX)才显示 ask/bid 线,交易所模式(PRICE_EXCHANGE)显示 last 线。PointsPerTick 取自 SYMBOL_TRADE_TICK_SIZE,CopyRates 拉 1 根 M1 柱;若交易所模式且首根量为 0,则循环跳过前置零量 tick。外汇与贵金属回放属高风险验证,真实点差跳变不会被这套静态模型捕获。
m_Infos.tick[class="num">0].bid = m_Infos.tick[class="num">0].last - dSpread; } else if (m_Infos.tick[class="num">0].last < m_Infos.tick[class="num">0].bid) { m_Infos.tick[class="num">0].ask = m_Infos.tick[class="num">0].last + dSpread; m_Infos.tick[class="num">0].bid = m_Infos.tick[class="num">0].last; }
回放引擎里的报价与仓位对齐逻辑
这段逻辑干了两件事:把当前 K 线收价塞进自定义回放品种的 tick 结构,以及按滑动条位置把历史速率数据推回图表。外汇与贵金属回放属高风险模拟,实盘参数偏差可能导致走势错位。 127~136 行处理 tick 赋值。若是最后一笔(index 0)且走 else 分支,就用 close 当 bid,ask 在 close 上加 spread 乘 PointsPerTick;time_msc 直接拿 time 乘 1000 毫秒。若不是实时合成,则从 GetInfoTicks 取历史 tick 再 CustomTicksAdd 写入 def_SymbolReplay。 140~150 行的 AdjustPositionToReplay 是滑动条联动核心。144 行先比对新旧位置,相等就 return;145 行把滑动条值按 def_MaxSlider 换算成 nPos 索引;146 行正向扫 Rate 数组,用 spread 字段累进 CountReplay;147 行若扫到数据就 CustomRatesUpdate 局部刷新。 148~149 行用 while 循环在 nPos 超过已回放数量时持续 CreateBarInReplay(false),直到追上滑动条。def_CheckLoopService 为假会跳出,避免卡死。开 MT5 把 def_MaxSlider 调到 1000 以上,拖滑动条能明显感到 147 行刷新粒度变化。
class="num">127. m_Infos.tick[class="num">0].last = rate[class="num">0].close; class="num">128. else class="num">129. { class="num">130. m_Infos.tick[class="num">0].bid = rate[class="num">0].close; class="num">131. m_Infos.tick[class="num">0].ask = rate[class="num">0].close + (rate[class="num">0].spread * m_Infos.PointsPerTick); class="num">132. } class="num">133. m_Infos.tick[class="num">0].time = rate[class="num">0].time; class="num">134. m_Infos.tick[class="num">0].time_msc = rate[class="num">0].time * class="num">1000; class="num">135. }else class="num">136. m_Infos.tick[class="num">0] = GetInfoTicks().Info[m_Infos.CountReplay]; class="num">137. CustomTicksAdd(def_SymbolReplay, m_Infos.tick); class="num">138. } class="num">139. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">140. class="type">void AdjustPositionToReplay(class="type">void) class="num">141. { class="num">142. class="type">int nPos, nCount; class="num">143. class="num">144. if (m_IndControl.Position == (class="type">int)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks)) class="kw">return; class="num">145. nPos = (class="type">int)((m_MemoryData.nTicks * m_IndControl.Position) / def_MaxSlider); class="num">146. for (nCount = class="num">0; m_MemoryData.Rate[nCount].spread < nPos; m_Infos.CountReplay = m_MemoryData.Rate[nCount++].spread); class="num">147. if (nCount > class="num">0) CustomRatesUpdate(def_SymbolReplay, m_MemoryData.Rate, nCount - class="num">1); class="num">148. class="kw">while ((nPos > m_Infos.CountReplay) && def_CheckLoopService) class="num">149. CreateBarInReplay(class="kw">false); class="num">150. } class="num">151. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">152. class="kw">public : class="num">153. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">154. C_Replay() class="num">155. :C_ConfigService() class="num">156. { class="num">157. Print("************** Market Replay Service **************"); class="num">158. srand(GetTickCount()); class="num">159. SymbolSelect(def_SymbolReplay, class="kw">false);
◍ 自定义回放品种的清理与重建逻辑
在 MT5 里做历史回放,第一步往往是先把上一次残留的自定义品种清掉,再按固定规格重建。下面这段析构与初始化衔接的代码,把删除、创建、参数归零的动作串在了一起,读者可以直接对照开 MT5 的自定义品种管理器验证。 CustomSymbolDelete 先删掉名为 def_SymbolReplay 的旧品种;随后 CustomSymbolCreate 用 StringFormat 拼出 "Custom\\%s" 路径重建。紧接着连续三个 CustomSymbolSetDouble 把 tick size、tick value、volume step 全设为 0,说明这个回放品种在创建初期不绑定任何真实报价尺度,等后续 OpenChartReplay 再补。 重建后还设了 DIGITS=8、描述串 "Symbol for replay / simulation",并用 SymbolSelect 置为可见。控制结构体里 CountReplay 清零、指标句柄标 INVALID_HANDLE、回放模式锁成 ePause,相当于把状态机复位到待命点。 析构函数 ~C_Replay 反向操作:先 SweepAndCloseChart 关图,IndicatorRelease 放指标句柄,SymbolSelect(false) 取消可见,最后 CustomSymbolDelete 真删除,并打印 "Finished replay service..."。这套对称生命周期在外接 EA 反复调试时,能避免自定义品种越堆越多把终端拖慢。 OpenChartReplay 开场两道闸:若 SYMBOL_TRADE_TICK_SIZE 或 SYMBOL_TRADE_TICK_VALUE 仍为 0,直接 MsgError 返回,提示资产配置没补全。也就是说前面那三个 SetDouble 里至少后两个得在开图前补上非零值,否则回放图打不开。外汇与贵金属回测涉及高杠杆与滑点风险,自定义品种仅用于本地模拟,不代表实盘可能收益。
CustomSymbolDelete(def_SymbolReplay); CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, class="num">0); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, class="num">0); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP, class="num">0); CustomSymbolSetString(def_SymbolReplay, SYMBOL_DESCRIPTION, "Symbol for replay / simulation"); CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_DIGITS, class="num">8); SymbolSelect(def_SymbolReplay, true); m_Infos.CountReplay = class="num">0; m_IndControl.Handle = INVALID_HANDLE; m_IndControl.Mode = C_Controls::ePause; m_IndControl.Position = class="num">0; m_IndControl.Memory._16b[C_Controls::eCtrlPosition] = C_Controls::eTriState; } class=class="str">"cmt">//+------------------------------------------------------------------+ ~C_Replay() { SweepAndCloseChart(); IndicatorRelease(m_IndControl.Handle); SymbolSelect(def_SymbolReplay, class="kw">false); CustomSymbolDelete(def_SymbolReplay); Print("Finished replay service..."); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool OpenChartReplay(class="kw">const ENUM_TIMEFRAMES arg1, class="kw">const class="type">class="kw">string szNameTemplate) { if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE) == class="num">0) class="kw">return MsgError("Asset configuration is not complete, it remains to declare the size of the ticket."); if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE) == class="num">0) class="kw">return MsgError("Asset configuration is not complete, need to declare the ticket value.");
「回放图表初始化与控件挂载」
做历史回放前,EA 会先确认交易品种的最小成交量步长。若 SymbolInfoDouble 读到的 SYMBOL_VOLUME_STEP 为 0,说明资产配置没填完,直接弹错退出,不往下走。 通过校验后,脚本先清掉旧图、用 ChartOpen 开一张回放专用图,再套用同名 .tpl 模板;套用失败会在日志打印提示并退回 default.tpl,成功则打印确认。这套动作确保回放界面和指标布局一致。 InitBaseControl 里有个 1000 毫秒的等待,然后以 200 毫秒轮询去抓"Indicator Mouse Study"。抓不到句柄就一直睡,直到拿到或开关被关。接着挂自定义控制指标(iCustom 指向 def_IndicatorControl,并把回放图 ID 当参数传进去),再 ChartIndicatorAdd 到主窗口。 外汇与贵金属回放测试属高概率验证手段,但不代表实盘信号有效,实盘仍可能因点差滑点失效。
if (SymbolInfoDouble(def_SymbolReplay, SYMBOL_VOLUME_STEP) == class="num">0) class="kw">return MsgError("Asset configuration not complete, need to declare the minimum volume."); SweepAndCloseChart(); m_Infos.IdReplay = ChartOpen(def_SymbolReplay, arg1); if (!ChartApplyTemplate(m_Infos.IdReplay, szNameTemplate + ".tpl")) Print("Failed apply class="kw">template: ", szNameTemplate, ".tpl Using class="kw">template class="kw">default.tpl"); else Print("Apply class="kw">template: ", szNameTemplate, ".tpl"); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool InitBaseControl(class="kw">const class="type">class="kw">ushort wait = class="num">1000) { Print("Waiting for Mouse Indicator..."); Sleep(wait); class="kw">while ((def_CheckLoopService) && (ChartIndicatorGet(m_Infos.IdReplay, class="num">0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(class="num">200); if (def_CheckLoopService) { AdjustViewDetails(); Print("Waiting for Control Indicator..."); if ((m_IndControl.Handle = iCustom(ChartSymbol(m_Infos.IdReplay), ChartPeriod(m_Infos.IdReplay), "::" + def_IndicatorControl, m_Infos.IdReplay)) == INVALID_HANDLE) class="kw">return class="kw">false; ChartIndicatorAdd(m_Infos.IdReplay, class="num">0, m_IndControl.Handle); UpdateIndicatorControl(); } class="kw">return def_CheckLoopService; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool LoopEventOnTime(class="type">void) {
回放引擎里的节拍控制逻辑
这段回放核心循环干的事很直接:先卡住等用户把控制状态切到播放(ePlay),没切就每 200 毫秒刷一次控件并 Sleep(200),避免空转吃 CPU。 真正跑数据时在 231 行起第二个 while:只要已回放 tick 数小于内存里的总 tick 数(m_MemoryData.nTicks)且服务标记有效,就逐 tick 推进。每次用相邻两 tick 的 time_msc 差值累加进 iPos,再调用 CreateBarInReplay(true) 生成棒线。 节奏节流点藏在 236 行:iPos 超过 200 毫秒当量后,内层循环每 195 毫秒 Sleep 一次、扣减 200,同时把滑块位置按 (已回放/总tick)*def_MaxSlider 映射到 ushort,并每 4 个周期调一次 RateUpdate(false) 刷新速率显示。外汇与贵金属回测属高风险场景,这类重播仅用于策略验证,不预示实盘盈亏。 末行返回判定很硬:只有回放 tick 数等于总 tick 数且服务标记仍有效,才视为正常结束;中途 ePause 会在 233 行直接 return true 交出控制权。
class="type">int iPos, iCycles; class="kw">while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) { UpdateIndicatorControl(); Sleep(class="num">200); } m_MemoryData = GetInfoTicks(); AdjustPositionToReplay(); iPos = iCycles = class="num">0; class="kw">while ((m_Infos.CountReplay < m_MemoryData.nTicks) && (def_CheckLoopService)) { if (m_IndControl.Mode == C_Controls::ePause) class="kw">return true; iPos += (class="type">int)(m_Infos.CountReplay < (m_MemoryData.nTicks - class="num">1) ? m_MemoryData.Info[m_Infos.CountReplay + class="num">1].time_msc - m_MemoryData.Info[m_Infos.CountReplay].time_msc : class="num">0); CreateBarInReplay(true); class="kw">while ((iPos > class="num">200) && (def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePause)) { Sleep(class="num">195); iPos -= class="num">200; m_IndControl.Position = (class="type">class="kw">ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); UpdateIndicatorControl(); iCycles = (iCycles == class="num">4 ? RateUpdate(class="kw">false) : iCycles + class="num">1); } } class="kw">return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); }; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#undef def_SymbolReplay class="macro">#undef def_CheckLoopService class="macro">#undef def_MaxSlider class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 鼠标指标怎么识别竞价状态
想直观搞懂前面那套回放逻辑,可以对低流动性品种跑模拟分时报价,省掉实盘干等的耗时。配套的暂停播放演示视频里,已经把跳过空白时段的手法过了一遍。 还剩一个没拆干净的点:鼠标指标靠什么提示品种切进竞价模式。这事儿不简单,光讲清楚就够单独开一篇。指标在实盘挂实时数据时会明确标出竞价起点,但回放用的是自定义合成品种,信号判定就卡住了。 具体卡在哪个环节、回放里怎么绕过,留到下一篇接着说。外汇和贵金属本身高波动高杠杆,这类模拟结论只供验证思路,别直接当实盘依据。