开发回放系统(第 66 部分):玩转服务(七)·进阶篇
「指标初始化与报价回调的衔接点」
这段 MT5 自定义指标代码展示了从 OnInit 收尾到 OnCalculate 实时刷新的关键衔接。第 35–39 行在初始化末尾根据回放状态决定订阅深度簿还是进入重放模式:MarketBookAdd 绑定当前品种的事件源,OnBookEvent 立即触发一次,状态机切到 eCloseMarket 或 eInReplay。 第 40–43 行把指标缓冲区 m_Buff 挂到索引 0,并用 ArrayInitialize 填 EMPTY_VALUE,保证历史柱不画脏数据,最后返回 INIT_SUCCEEDED 才算注册成功。 OnCalculate 里第 50–51 行值得盯:GL_PriceClose 取最新收盘价,GL_TimeAdjust 仅在 spread 小于 60 点(即 spread[rates_total-1] < 60)时保留点差值,否则归零——这意味着点差异常放大时算法会自动放弃时间补偿,回测与实盘偏差可能由此产生。 第 52–55 行把 rates_total 赋给 m_posBuff 并调用 Study->Update(m_Status),返回 rates_total 让 MT5 认为已处理全部柱。外汇与贵金属杠杆高,这类状态切换若在非农前后触发,深度簿订阅失败可能导致图形停滞,开 MT5 用 EURUSD 挂上指标看 35–43 行是否走通即可验证。
class="num">35. MarketBookAdd((*Study).GetInfoTerminal().szSymbol); class="num">36. OnBookEvent((*Study).GetInfoTerminal().szSymbol); class="num">37. m_Status = C_Study::eCloseMarket; class="num">38. }else class="num">39. m_Status = C_Study::eInReplay; class="num">40. SetIndexBuffer(class="num">0, m_Buff, INDICATOR_DATA); class="num">41. ArrayInitialize(m_Buff, EMPTY_VALUE); class="num">42. class="num">43. class="kw">return INIT_SUCCEEDED; class="num">44. } class="num">45. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">46. 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">47. 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">48. class="kw">const class="type">long& volume[], class="kw">const class="type">int& spread[]) class="num">49. { class="num">50. GL_PriceClose = close[rates_total - class="num">1]; class="num">51. GL_TimeAdjust = (spread[rates_total - class="num">1] < class="num">60 ? spread[rates_total - class="num">1] : class="num">0); class="num">52. m_posBuff = rates_total; class="num">53. (*Study).Update(m_Status); class="num">54. class="num">55. class="kw">return rates_total; class="num">56. } class="num">57. class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 鼠标跟随的百分比偏离怎么画
上一段定义了结构体 m_Info,里面塞了颜色对 corP/corN、文字高度、三个布尔开关 bvT/bvD/bvP 以及时间戳。Draw() 就是把这些字段真正写到图表对象上的地方,三个 if 分别对应文字标签、鼠标价偏离、收盘价偏离三种绘制逻辑。 bvT 分支只管把标签文字和纵向位置钉在鼠标上方 18 像素处:ObjectSetInteger 调 YDISTANCE 用鼠标 Y_Adjusted 减 18,ObjectSetString 写 szInfo。这里没有算价格,纯展示当前品种信息。 bvD 分支算的是鼠标价相对收盘价的百分比:((鼠标价 - 收盘价)/收盘价)*100 再 NormalizeDouble 到 2 位小数。负值用 corN 上色、正值用 corP,文本用 StringFormat("%.2f%%", MathAbs(v1)) 只显示绝对值——视觉上红绿看涨跌,数字不带头号。 bvP 分支几乎一样,只是把鼠标价换成全局 GL_PriceClose。两个分支都减 1 像素定位,说明按钮贴着鼠标线。外汇和贵金属杠杆高,这种实时偏离只帮你读图,不预示方向,信号错了就是错了。
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) { 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)));
信息按钮的创建与移除逻辑
这段 inline 函数负责在图表上按事件开关三组研究信息对象:K线时间、每日波动、价格变动。CreateObjInfo 用 switch 分流,分别调用 C_Mouse::CreateObjToStudy 并写入对象名与状态位。 创建时坐标硬编码:显示 BarTime 用 (2,110) 且配色 clrPaleTurquoise,DailyVar 用 (2,53),PriceVar 用 (58,53);对象名都拼了 def_ExpansionPrefix 加 ObjectsTotal(0) 的当前总数,避免重名。 移除侧 RemoveObjInfo 只做两件事:从 m_Info 取回对应按钮名、把 bvT/bvD/bvP 标志翻成 false。实际删除对象动作应在别处用 sz 执行,这里没贴全。 开 MT5 把这段塞进 EA,点一下鼠标事件就能看到图表左上角浮出三块数据;外汇和贵金属波动大,这类叠加层只辅助读图,信号失效概率不低。
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="kw">inline class="type">void RemoveObjInfo(EnumEvents arg) { class="type">class="kw">string sz; class="kw">switch (arg) { case evHideBarTime: sz = m_Info.szBtn1; m_Info.bvT = class="kw">false; class="kw">break; case evHideDailyVar: sz = m_Info.szBtn2; m_Info.bvD = class="kw">false;
「鼠标交互里的对象删除与初始化分支」
在图表对象的交互处理里,evHidePriceVar 这个分支只做一件事:把价格变量按钮的尺寸记到 sz,再把显示标志 m_Info.bvP 置为 false,随后 break 退出 switch。注意 086–088 行没有调用删除,真正的 ObjectDelete 发生在 091 行,且前后用 ChartSetInteger 两次切换 CHART_EVENT_OBJECT_DELETE 状态(先 false 后 true),避免删除动作本身再触发一次对象删除事件。 构造函数 C_Study 从 C_Mouse 继承,入参带图表 ID、短名和三种颜色。100 行先查 _LastError,若非 ERR_SUCCESS 直接 return,防止后续在无效句柄上继续跑。101 行 ZeroMemory(m_Info) 清空结构体,102 行把初始状态锁成 eCloseMarket。
- 行取日线收盘价作为基准:当符号是回放符号,或当前日期与 D1 第 0 根 K 线日期不一致时取 shift 0,否则取 shift 1——这意味着实盘当天未收盘时拿的是前一日收盘价。106–108 行连续创建三个信息对象(BarTime、DailyVar、PriceVar),它们才是之后鼠标悬停时显示的数据层。
打开 MT5 把这段粘进你的指标类,断点打在 091 行,切一下图表上隐藏价格变量的按钮,能看到对象被删且不会递归触发删除事件;外汇与贵金属品种波动大,这类对象刷新逻辑在高波动时可能漏删,建议本地先用 EURUSD 的 M1 回放验证。
case evHidePriceVar: sz = m_Info.szBtn3; m_Info.bvP = class="kw">false; class="kw">break; } ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, class="kw">false); ObjectDelete(GetInfoTerminal().ID, sz); ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ C_Study(class="type">long IdParam, class="type">class="kw">string szShortName, class="type">class="kw">color corH, class="type">class="kw">color corP, class="type">class="kw">color corN) :C_Mouse(IdParam, szShortName, corH, corP, corN) { if (_LastError != ERR_SUCCESS) class="kw">return; ZeroMemory(m_Info); m_Info.Status = eCloseMarket; m_Info.Rate.close = iClose(GetInfoTerminal().szSymbol, PERIOD_D1, ((GetInfoTerminal().szSymbol == def_SymbolReplay) || (macroGetDate(TimeCurrent()) != macroGetDate(iTime(GetInfoTerminal().szSymbol, PERIOD_D1, class="num">0))) ? class="num">0 : class="num">1)); m_Info.corP = corP; m_Info.corN = corN; CreateObjInfo(evShowBarTime); CreateObjInfo(evShowDailyVar); CreateObjInfo(evShowPriceVar); }
◍ 行情状态机里的时间对齐与事件分发
这段逻辑把市场状态分成闭市、回放、实时交易、竞价和异常几类,分别给面板喂不同的提示字符串。闭市直接标 "Closed Market",竞价标 "Auction",兜底的 default 分支则写 "ERROR",方便在 MT5 里一眼看出状态机是否漏了 case。 回放与实时交易共用一套时间对齐:先取 PeriodSeconds() 得到当前图表周期秒数 i0,再按状态选模拟设备时间或 TimeCurrent()。第 125 行把 m_Info.Rate.time 向下取整到周期边界并加一个 i0,保证下一根 K 线的开盘时间戳正确推进;若原时间已大于当前时间则保留不动。 时间差展示用 TimeToString(..., TIME_SECONDS),把 (Rate.time - dt) 换算成距下一根柱的剩余秒数。外汇与贵金属盘面在高波动时段跳空频繁,这种对齐方式只是规避了跨周期错位,实际成交仍受点差与滑点影响,概率上更稳但不保证。 DispatchMessage 重写了自定义事件:evHideBarTime / evShowBarTime 控制时间信息的隐藏与重建,evHideDailyVar 走 RemoveObjInfo 删除日变量对象。复制下面代码到 EA 的鼠标类派生里,挂上 CHARTEVENT_CUSTOM 偏移即可在图表上动态开关这些 HUD 元素。
m_Info.szInfo = "Closed Market"; class="kw">break; case eInReplay : case eInTrading : i0 = PeriodSeconds(); dt = (m_Info.Status == eInReplay ? m_Info.TimeDevice + 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); m_Info.szInfo = TimeToString((class="type">class="kw">datetime)m_Info.Rate.time - dt, TIME_SECONDS); class="kw">break; case eAuction : m_Info.szInfo = "Auction"; class="kw">break; class="kw">default : m_Info.szInfo = "ERROR"; } Draw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">virtual class="type">void DispatchMessage(class="kw">const class="type">int id, class="kw">const class="type">long &lparam, class="kw">const class="type">class="kw">double &dparam, class="kw">const class="type">class="kw">string &sparam) { C_Mouse::DispatchMessage(id, lparam, dparam, sparam); class="kw">switch (id) { case CHARTEVENT_CUSTOM + evHideBarTime: RemoveObjInfo(evHideBarTime); class="kw">break; case CHARTEVENT_CUSTOM + evShowBarTime: CreateObjInfo(evShowBarTime); class="kw">break; case CHARTEVENT_CUSTOM + evHideDailyVar: RemoveObjInfo(evHideDailyVar);
事件分发收口与宏定义收尾
这段收尾代码把图表事件落到具体动作上:自定义事件 evShowDailyVar / evHidePriceVar / evShowPriceVar 分别调 CreateObjInfo 与 RemoveObjInfo 控制信息层显隐,evSetServerTime 把 lparam 强转 datetime 写进 m_Info.TimeDevice,鼠标移动则触发 Draw() 重绘。 每个 case 后都 break,避免穿透;所有分支处理完统一调 ChartRedraw(GetInfoTerminal().ID) 刷新当前图表。外汇与贵金属行情跳变快,这类重绘逻辑若放在高频 OnChartEvent 里可能拖累 MT5 响应,建议你在策略测试器里用 1 分钟周期观察 CPU 占用。 文件末尾用 #undef 撤掉 def_ExpansionPrefix 与 def_MousePrefixName 两个宏,防止与其他 mqh 冲突。随后另起一组宏:macroRemoveSec 用 (A - (A % 60)) 截到分钟,macroGetDate 用 86400 取日期零点,macroGetSec 再取回当前分钟内的秒数——这三个宏在回测里能直接验证时间切片是否对齐。 最后看到 C_FileBars.mqh、C_Simulation.mqh 与 ..\..\Auxiliar\Macros.mqh 三个 include,说明模拟与文件读取模块被拆到外部。你复制这段代码时若报找不到头文件,先确认目录层级和相对路径是否一致。
class="num">150. class="kw">break; class="num">151. case CHARTEVENT_CUSTOM + evShowDailyVar: class="num">152. CreateObjInfo(evShowDailyVar); class="num">153. class="kw">break; class="num">154. case CHARTEVENT_CUSTOM + evHidePriceVar: class="num">155. RemoveObjInfo(evHidePriceVar); class="num">156. class="kw">break; class="num">157. case CHARTEVENT_CUSTOM + evShowPriceVar: class="num">158. CreateObjInfo(evShowPriceVar); class="num">159. class="kw">break; class="num">160. case (CHARTEVENT_CUSTOM + evSetServerTime): class="num">161. m_Info.TimeDevice = (class="type">class="kw">datetime)lparam; class="num">162. class="kw">break; class="num">163. case CHARTEVENT_MOUSE_MOVE: class="num">164. Draw(); class="num">165. class="kw">break; class="num">166. } class="num">167. ChartRedraw(GetInfoTerminal().ID); class="num">168. } class="num">169. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">170. }; class="num">171. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">172. class="macro">#undef def_ExpansionPrefix class="num">173. class="macro">#undef def_MousePrefixName class="num">174. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">1. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">2. class="macro">#class="kw">property copyright "Daniel Jose" class="num">3. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">4. class="macro">#define macroRemoveSec(A) (A - (A % class="num">60)) class="num">5. class="macro">#define macroGetDate(A) (A - (A % class="num">86400)) class="num">6. class="macro">#define macroGetSec(A) (A - (A - (A % class="num">60))) class="num">7. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">01. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">02. class="macro">#class="kw">property copyright "Daniel Jose" class="num">03. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">04. class="macro">#include "C_FileBars.mqh" class="num">05. class="macro">#include "C_Simulation.mqh" class="num">06. class="macro">#include "..\..\Auxiliar\Macros.mqh" class="num">07. class=class="str">"cmt">//+------------------------------------------------------------------+