开发回放系统(第 68 部分):取得正确的时间(一)·进阶篇
(2/3)· 当资产每秒仅几笔成交或突入竞价,OnCalculate 失灵后剩余时间该怎么算准
◍ 回放引擎里的逐笔注入与暂停逻辑
这段片段来自一个 MT5 历史 tick 回放器的核心循环。它先把单笔 tick 通过 CustomTicksAdd 塞进自定义回放品种 def_SymbolReplay,再用 CustomRatesUpdate 刷新 0 号柱的报价序列,使图表看起来像在实时走。 当 bViewTick 为真时,每来一个新 tick 就发一个 EventChartCustom 事件(evSetServerTime),把 m_Infos.Rate[0].time 推给图表控件;同时把 spread 字段写成该 tick 秒数,属于用现成字段做标记的小技巧。 LoopEventOnTime 是回放的主驱动:只要没在播放态(ePlay),就每 200 毫秒 Sleep 一次并刷新控件,避免界面卡死。进入播放后,只要 CountReplay 小于总 tick 数 nTicks 且服务标志有效,就持续 CreateBarInReplay(true) 造棒。 暂停判定很直接——若模式切到 ePause 立即 return true 退出循环;而 iPos 累加相邻 tick 的 time_msc 差,超过 200 就进内层等待,相当于用毫秒差模拟原始节奏。外汇与贵金属回放仅用于策略验证,实盘滑点与断线风险仍可能让结果偏离。 让小布替你跑这套 把 def_SymbolReplay 换成你自己的虚拟品种名,在 MT5 策略测试器外挂 EA 里贴入这段循环,能直观看到历史 tick 重播而非普通 K 线回测。
if (bViewTick) { CustomTicksAdd(def_SymbolReplay, m_Infos.tick); if (bNew) EventChartCustom(m_Infos.IdReplay, evSetServerTime, (class="type">long)m_Infos.Rate[class="num">0].time, class="num">0, ""); } m_Infos.Rate[class="num">0].spread = (class="type">int)macroGetSec(m_MemoryData.Info[m_Infos.CountReplay].time); CustomRatesUpdate(def_SymbolReplay, m_Infos.Rate); class="type">bool LoopEventOnTime(class="type">void) { class="type">int iPos; class="kw">while ((def_CheckLoopService) && (m_IndControl.Mode != C_Controls::ePlay)) { UpdateIndicatorControl(); Sleep(class="num">200); } m_MemoryData = GetInfoTicks(); AdjustPositionToReplay(); EventChartCustom(m_Infos.IdReplay, evSetServerTime, (class="type">long)macroRemoveSec(m_MemoryData.Info[m_Infos.CountReplay].time), class="num">0, ""); iPos = 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)) {
回放循环的步进与终止判定
这段回放控制逻辑里,每次循环先 Sleep(195) 让出 195 毫秒 CPU,再把索引 iPos 回退 200 个单位,相当于以约 5 帧/秒的节奏倒推历史 tick 流。 滑块位置按已回放 tick 数占总量比例实时换算:Position = (CountReplay * def_MaxSlider) / nTicks,随后调用 UpdateIndicatorControl() 刷新控件,使图表指示与回放进度同步。 循环退出条件由 return 语句给出:当 CountReplay 等于 nTicks 且 def_CheckLoopService 为真时返回 true,否则继续。外汇与贵金属回测属高风险验证,实盘参数须以 MT5 策略测试器复跑确认。
class="num">228. Sleep(class="num">195); class="num">229. iPos -= class="num">200; class="num">230. m_IndControl.Position = (class="type">class="kw">ushort)((m_Infos.CountReplay * def_MaxSlider) / m_MemoryData.nTicks); class="num">231. UpdateIndicatorControl(); class="num">232. } class="num">233. } class="num">234. class="num">235. class="kw">return ((m_Infos.CountReplay == m_MemoryData.nTicks) && (def_CheckLoopService)); class="num">236. } class="num">237. }; class="num">238. class=class="str">"cmt">//+------------------------------------------------------------------+
「头文件改完,鼠标指标怎么跟着动」
先动 Defines.mqh:第 36 行旧的事件更新计时器定义被删掉,第 15 行新增了掩码 def_MaskTimeService 值为 0xFED00000,用来在消息处理里更舒服地做位运算。光删行不够,后续代码得配合新消息策略改,否则编译都过不去。 Macros.mqh 只加了一行(第 7 行),宏负责从 datetime 变量里抠出时间值,纯增量改动,不影响别的。C_Study.mqh 里要把原文件中标删的行全去掉,重点看第 124 与 125 行对比——m_Info.TimeDevice 变量被干掉了,它以前就是用来跟计时器同步的,现在不需要了。 鼠标指标本体改动更狠:第 47–49 行整段删,换成第 50 行,OnCalculate 不再接收那堆参数;第 52 行变 53 行,54 行变 55–56 行;另外插了第 12 行指标属性。服务推 RATE 时 MT5 触发 Calculate,但若图表不是 M1 周期,rates_total 在新柱形成前不更新,所以回放里只用 iSpread 取点差,并核对交易品种是否匹配预期。 因为改用 iSpread 拿值,OnCalculate 没必要带旧参数,第 53 行抓价格即可。抓哪个价格?第 12 行定的:用每根 bar 的收盘价,这样鼠标指标和图表已有显示值能对上。外汇与贵金属杠杆高,回放模拟和实盘点差逻辑差异大,验证前先开 MT5 用 M1 以外的周期跑一遍再信。
class="macro">#define def_MaskTimeService 0xFED00000◍ 鼠标事件枚举与底层宏定义拆解
在 MT5 自定义指标里做交互控制,第一步是把用户动作抽象成枚举。下面这段枚举把鼠标价格线、K线时间、日内波幅、瞬时波幅的显隐,以及回放系统的计时器初始化都列成了独立事件常量,方便后续用 switch 分发。 evSetServerTime 这一项专门对接回放/模拟的系统计时器,evCtrlReplayInit 则负责把回放控制器置初值;外汇与贵金属行情回放涉及杠杆与滑点,属于高风险验证环境,参数错了可能画出完全失真的价格行为。 紧跟在枚举后的宏才是真正干活的部分。macroRemoveSec 用取模 60 把秒清掉,macroGetDate 用取模 86400 拿到当日 0 点时间戳,macroTransparency 把 0~100 的透明度百分比转成 0~1 的 alpha 系数(100 对应完全不透明)。 开 MT5 把这段直接贴进 mqh 头文件,编译后挂到 EURUSD 的 M1 图,改 macroTransparency 的入参就能肉眼看到画布透明度随数值线性变化。
enum EnumEvents { evHideMouse, class=class="str">"cmt">//Hide mouse price line evShowMouse, class=class="str">"cmt">//Show mouse price line evHideBarTime, class=class="str">"cmt">//Hide bar time evShowBarTime, class=class="str">"cmt">//Show bar time evHideDailyVar, class=class="str">"cmt">//Hide daily variation evShowDailyVar, class=class="str">"cmt">//Show daily variation evHidePriceVar, class=class="str">"cmt">//Hide instantaneous variation evShowPriceVar, class=class="str">"cmt">//Show instantaneous variation evSetServerTime, class=class="str">"cmt">//Replay/simulation system timer evCtrlReplayInit, class=class="str">"cmt">//Initialize replay control }; class="macro">#define macroRemoveSec(A) (A - (A % class="num">60)) class="macro">#define macroGetDate(A) (A - (A % class="num">86400)) class="macro">#define macroGetSec(A) (A - (A - (A % class="num">60))) class="macro">#define macroGetTime(A) (A % class="num">86400) class="macro">#define macroColorRGBA(A, B) ((class="type">uint)((B << class="num">24) | (A & 0x00FF00) | ((A & 0xFF0000) >> class="num">16) | ((A & 0x0000FF) << class="num">16))) class="macro">#define macroTransparency(A) (((A > class="num">100 ? class="num">100 : (class="num">100 - A)) * class="num">2.55) / class="num">255.0) class="macro">#include "..\C_Mouse.mqh" class="macro">#define def_ExpansionPrefix def_MousePrefixName + "Expansion_" class C_Study : class="kw">public C_Mouse { class="kw">private :
鼠标测距面板的结构体与绘制逻辑
做浮动测距面板,先得把状态和数据打包进一个结构体。下面这段 st00 把市场状态、K线、按钮文本、正负颜色、文本高度和三个可见开关全塞进 m_Info,外部只要改结构体字段就能驱动界面,不用动绘制主逻辑。 [CODE]struct st00 { eStatusMarket Status; MqlRates Rate; string szInfo, szBtn1, szBtn2, szBtn3; color corP, corN; int HeightText; bool bvT, bvD, bvP; datetime TimeDevice; }m_Info;[/CODE] Draw() 里分三个开关走不同分支。bvT 为真时,把 szBtn1 的 Y 距离设为鼠标修正坐标减 18,并写入 szInfo 文本——这是顶部信息行。 bvD 分支算的是鼠标价相对收盘价的百分比:((鼠标价 - 收盘价)/收盘价)*100,用 NormalizeDouble 保留两位小数。按钮背景色按正负切换,corN 表跌、corP 表涨,文本只显示绝对值加 %。外汇和贵金属点差大,这个读数在美盘开盘前后可能跳 0.3% 以上,属正常高频波动,杠杆品种高风险,别拿它当方向确认。 bvP 分支代码在 044 行截断,可见面板还预留了第三块内容位,具体画什么由后续小节补。现在你开 MT5 建个 EA,把这段结构体和 Draw 骨架贴进去,挂上鼠标事件就能看到前两块浮动出来了。
class="kw">struct st00 { eStatusMarket Status; class="type">MqlRates Rate; class="type">class="kw">string szInfo, szBtn1, szBtn2, szBtn3; class="type">class="kw">color corP, corN; class="type">int HeightText; class="type">bool bvT, bvD, bvP; class="type">class="kw">datetime TimeDevice; }m_Info; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Draw(class="type">void) { class="type">class="kw">double v1; if (m_Info.bvT) { ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">18); ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn1, OBJPROP_TEXT, m_Info.szInfo); } if (m_Info.bvD) { v1 = NormalizeDouble((((GetInfoMouse().Position.Price - m_Info.Rate.close) / m_Info.Rate.close) * class="num">100.0), class="num">2); ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">1); ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP)); ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); } if (m_Info.bvP) {
「按钮对象的创建与差异标记」
上面这段把鼠标悬浮时弹出的三个信息按钮落到了代码层:涨跌幅标签跟着光标走,颜色由正负决定。045 行先算 GL_PriceClose 相对当前收盘的百分数,NormalizeDouble 保留两位小数;047 行用三元表达式在 corN(负)和 corP(正)之间切背景色,视觉上红绿一眼可分。 CreateObjInfo 按事件分支建对象:evShowBarTime 用 110 像素宽、浅绿松石色,evShowDailyVar 与 evShowPriceVar 分别用 53 和 58 像素宽。每个按钮名用 def_ExpansionPrefix 拼上 ObjectsTotal(0) 的实时整数,避免重名覆盖。 RemoveObjInfo 只开了头:用 string sz 承接待删对象名,switch(arg) 还没列完分支。真要跑起来,得把三个 case 的 ObjectDelete 补上,否则 MT5 图表会留下僵尸标签,切周期时可能叠一堆。外汇与贵金属波动剧烈,这类悬浮控件只作辅助参考,信号误读会带来真实亏损风险。
v1 = NormalizeDouble((((GL_PriceClose - m_Info.Rate.close) / m_Info.Rate.close) * class="num">100.0), class="num">2); ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">1); ObjectSetInteger(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP)); ObjectSetString(GetInfoTerminal().ID, m_Info.szBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void CreateObjInfo(EnumEvents arg) { class="kw">switch (arg) { case evShowBarTime: C_Mouse::CreateObjToStudy(class="num">2, class="num">110, m_Info.szBtn1 = (def_ExpansionPrefix + (class="type">class="kw">string)ObjectsTotal(class="num">0)), clrPaleTurquoise); m_Info.bvT = true; class="kw">break; case evShowDailyVar: C_Mouse::CreateObjToStudy(class="num">2, class="num">53, m_Info.szBtn2 = (def_ExpansionPrefix + (class="type">class="kw">string)ObjectsTotal(class="num">0))); m_Info.bvD = true; class="kw">break; case evShowPriceVar: C_Mouse::CreateObjToStudy(class="num">58, class="num">53, m_Info.szBtn3 = (def_ExpansionPrefix + (class="type">class="kw">string)ObjectsTotal(class="num">0))); m_Info.bvP = true; class="kw">break; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void RemoveObjInfo(EnumEvents arg) { class="type">class="kw">string sz; class="kw">switch (arg)