开发回放系统(第 70 部分):取得正确的时间(三)·综合运用
「鼠标追踪类的构造与析构细节」
在 MT5 自定义指标或 EA 里封装鼠标交互,C_Mouse 类给出了两个重载构造函数。第一个(135–140 行)仅接收终端 id 与短名,把 m_OK 置为 false,不做图形初始化,相当于占位。 第二个(142–157 行)才是实战入口:先调 IndicatorCheckPass 校验同名指标是否已加载,失败则 m_OK 保持 false 直接 return;通过后读取当前图表十字线状态并存到 m_Mem.CrossHair,随后强制开启 CHART_EVENT_MOUSE_MOVE、临时关闭 CHART_CROSSHAIR_TOOL,避免系统十字线干扰自绘水平线。 颜色三元组(corH/corP/corN)全部非 clrNONE 时,m_Mem.IsFull 为真,代码用 def_MousePrefixName 拼接 ObjectsTotal(0) 生成唯一水平线名并创建 OBJ_HLINE。这一判断决定了后续是否能画辅助线,复制时别把 clrNONE 传成有效色。 析构函数(159–165 行)负责还原环境:恢复 CHART_CROSSHAIR_TOOL 到构造前状态,按短名窗口是否存在来决定是否保留鼠标移动事件,最后 ObjectsDeleteAll 清掉带前缀的临时对象。外汇与贵金属图表高频刷新,不还原事件开关可能拖慢其他 EA,属高风险操作须自测。
class="kw">public : class=class="str">"cmt">//+------------------------------------------------------------------+ C_Mouse(class="kw">const class="type">long id, class="kw">const class="type">class="kw">string szShortName) :C_Terminal(id), m_OK(class="kw">false) { m_Mem.szShortName = szShortName; } class=class="str">"cmt">//+------------------------------------------------------------------+ C_Mouse(class="kw">const class="type">long id, class="kw">const 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_Terminal(id) { if (!(m_OK = IndicatorCheckPass(m_Mem.szShortName = szShortName))) class="kw">return; m_Mem.CrossHair = (class="type">bool)ChartGetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL); ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, class="kw">false); ZeroMemory(m_Info); m_Info.corLineH = corH; m_Info.corTrendP = corP; m_Info.corTrendN = corN; m_Info.Study = eStudyNull; if (m_Mem.IsFull = (corP != clrNONE) && (corH != clrNONE) && (corN != clrNONE)) CreateObjectGraphics(m_Mem.szLineH = (def_MousePrefixName + (class="type">class="kw">string)ObjectsTotal(class="num">0)), OBJ_HLINE, m_Info.corLineH); ChartRedraw(GetInfoTerminal().ID); } class=class="str">"cmt">//+------------------------------------------------------------------+ ~C_Mouse() { if (!m_OK) class="kw">return; ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, class="kw">false); ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_MOUSE_MOVE, ChartWindowFind(GetInfoTerminal().ID, m_Mem.szShortName) != -class="num">1); ChartSetInteger(GetInfoTerminal().ID, CHART_CROSSHAIR_TOOL, m_Mem.CrossHair); ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName);
用指标缓冲区偷渡鼠标状态
想把图表上的鼠标按键和坐标传给 EA,又不想走笨重的全局变量,一个取巧办法是把信息塞进自定义指标的缓冲区。下方代码就演示了怎么在指标侧读、写这组隐藏数据。 CheckClick 用位与直接比对按键掩码,返回当前鼠标按键是否包含指定键。GetInfoMouse 则在指标未初始化时,从 ChartIndicatorGet 拿句柄、CopyBuffer 只读最新 1 根柱(第182行要求返回值严格等于 1 才解析),把 double 按字节拆成按键状态与坐标。 SetBuffer 反向操作:把按键字节、X/Y 图形坐标压回一个 double,写进最后一柱缓冲区。这样 EA 侧只要 CopyBuffer 同指标就能拿到鼠标现场,外汇与贵金属市场波动剧烈、杠杆风险高,这类交互仅作辅助,实盘前请在 MT5 策略测试器验证缓冲区同步延迟。
class="kw">inline class="type">bool CheckClick(class="kw">const eBtnMouse value) { class="kw">return (GetInfoMouse().ButtonStatus & value) == value; } class="kw">inline class="kw">const st_Mouse GetInfoMouse(class="type">void) { if (!m_OK) { class="type">class="kw">double Buff[]; uCast_Double loc; class="type">int handle = ChartIndicatorGet(GetInfoTerminal().ID, class="num">0, m_Mem.szShortName); ZeroMemory(m_Info.Data); if (CopyBuffer(handle, class="num">0, class="num">0, class="num">1, Buff) == class="num">1) { loc.dValue = Buff[class="num">0]; m_Info.Data.ButtonStatus = loc._8b[class="num">0]; DecodeAlls((class="type">int)loc._16b[class="num">1], (class="type">int)loc._16b[class="num">2]); } IndicatorRelease(handle); } class="kw">return m_Info.Data; } class="kw">inline class="type">void SetBuffer(class="kw">const class="type">int rates_total, class="type">class="kw">double &Buff[]) { uCast_Double info; info._8b[class="num">0] = (class="type">uchar)(m_Info.Study == C_Mouse::eStudyNull ? m_Info.Data.ButtonStatus : class="num">0); info._16b[class="num">1] = (class="type">class="kw">ushort) m_Info.Data.Position.X_Graphics; info._16b[class="num">2] = (class="type">class="kw">ushort) m_Info.Data.Position.Y_Graphics; Buff[rates_total - class="num">1] = info.dValue; }
◍ 鼠标事件怎么驱动图表对象
在 MT5 自定义指标或 EA 里,图表交互靠 DispatchMessage 接管系统事件。下面这段处理函数把自定义消息和鼠标移动都收口到一处,决定水平线、垂直线和研究的显隐与生成。 先看一下核心代码骨架: void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam) { int w = 0; static double memPrice = 0; if (m_OK) { C_Terminal::DispatchMessage(id, lparam, dparam, sparam); switch (id) { case (CHARTEVENT_CUSTOM + evHideMouse): if (m_Mem.IsFull) ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szLineH, OBJPROP_COLOR, clrNONE); break; case (CHARTEVENT_CUSTOM + evShowMouse): if (m_Mem.IsFull) ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szLineH, OBJPROP_COLOR, m_Info.corLineH); break; case CHARTEVENT_MOUSE_MOVE: DecodeAlls((int)lparam, (int)dparam); if (m_Mem.IsFull) ObjectMove(GetInfoTerminal().ID, m_Mem.szLineH, 0, 0, m_Info.Data.Position.Price); if ((m_Info.Study != eStudyNull) && (m_Mem.IsFull)) ObjectMove(GetInfoTerminal().ID, m_Mem.szLineV, 0, m_Info.Data.Position.dt, 0); m_Info.Data.ButtonStatus = (uchar) sparam; if (CheckClick(eClickMiddle))
| if ((!m_Mem.IsFull) | (((color)ObjectGetInteger(GetInfoTerminal().ID, m_Mem.szLineH, OBJPROP_COLOR)) != clrNONE)) CreateStudy(); |
|---|
if (CheckClick(eClickLeft) && (m_Info.Study == eStudyCreate)) { 逐行拆一遍:函数入口拿 id 和三类参数(整型、双精度、字符串),分别对应事件类型与坐标/状态。w 和静态 memPrice 在这里只是占位与记忆,真正逻辑在 m_OK 为真时才跑。 自定义事件 evHideMouse 把水平线颜色设成 clrNONE 实现隐藏;evShowMouse 则还原成 m_Info.corLineH 设定的颜色。CHARTEVENT_MOUSE_MOVE 里先用 DecodeAlls 解包鼠标坐标,再把水平线移到当前 Price、垂直线移到当前时间 dt;中键点击可能触发 CreateStudy,左键在创建态下继续流程。 开 MT5 把这段塞进你的面板类,中键画线、移动鼠标跟价的反馈立刻能验。外汇和贵金属图表上这类交互对滑点敏感,实操前先在模拟盘确认事件频率,高频鼠标移动可能拖累 EA 性能。
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) { class="type">int w = class="num">0; class="kw">static class="type">class="kw">double memPrice = class="num">0; if (m_OK) { C_Terminal::DispatchMessage(id, lparam, dparam, sparam); class="kw">switch (id) { case (CHARTEVENT_CUSTOM + evHideMouse): if (m_Mem.IsFull) ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szLineH, OBJPROP_COLOR, clrNONE); break; case (CHARTEVENT_CUSTOM + evShowMouse): if (m_Mem.IsFull) ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szLineH, OBJPROP_COLOR, m_Info.corLineH); break; case CHARTEVENT_MOUSE_MOVE: DecodeAlls((class="type">int)lparam, (class="type">int)dparam); if (m_Mem.IsFull) ObjectMove(GetInfoTerminal().ID, m_Mem.szLineH, class="num">0, class="num">0, m_Info.Data.Position.Price); if ((m_Info.Study != eStudyNull) && (m_Mem.IsFull)) ObjectMove(GetInfoTerminal().ID, m_Mem.szLineV, class="num">0, m_Info.Data.Position.dt, class="num">0); m_Info.Data.ButtonStatus = (class="type">uchar) sparam; if (CheckClick(eClickMiddle)) if ((!m_Mem.IsFull) || (((class="type">class="kw">color)ObjectGetInteger(GetInfoTerminal().ID, m_Mem.szLineH, OBJPROP_COLOR)) != clrNONE)) CreateStudy(); if (CheckClick(eClickLeft) && (m_Info.Study == eStudyCreate)) {
「鼠标拖线后如何触发研究并防误删」
在图表事件回调里,先用 ChartSetInteger 关掉 CHART_MOUSE_SCROLL,避免滚轮缩放干扰手动画线。随后若内存标记 m_Mem.IsFull 为真,就把水平线按鼠标当前的 dt 与 Price 移动到对应位置,并将状态切到 eStudyExecute。 关键在执行分支:当 m_Info.Study == eStudyExecute 时调用 ExecuteStudy(memPrice),同时把 m_Info.Data.ExecStudy 写成布尔结果。注意第233行与234行之间没有 break,事件会自然落到后续逻辑。 遇到 CHARTEVENT_OBJECT_DELETE 且被删的是记忆水平线 m_Mem.szLineH 时,立刻用 CreateObjectGraphics 以 OBJ_HLINE 重建,颜色取 m_Info.corLineH。这样用户手滑删线也不会丢失分析基准。 外汇与贵金属图表上这类交互脚本波动剧烈、滑点风险高,任何自动重画都只是降低操作成本,不代表信号更有效。
class="num">229. ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, class="kw">false); class="num">230. if (m_Mem.IsFull) ObjectMove(GetInfoTerminal().ID, m_Mem.szLineT, class="num">0, m_Mem.dt = GetInfoMouse().Position.dt, memPrice = GetInfoMouse().Position.Price); class="num">231. m_Info.Study = eStudyExecute; class="num">232. } class="num">233. if (m_Info.Study == eStudyExecute) ExecuteStudy(memPrice); class="num">234. m_Info.Data.ExecStudy = m_Info.Study == eStudyExecute; class="num">235. break; class="num">236. case CHARTEVENT_OBJECT_DELETE: class="num">237. if ((m_Mem.IsFull) && (sparam == m_Mem.szLineH)) class="num">238. CreateObjectGraphics(m_Mem.szLineH, OBJ_HLINE, m_Info.corLineH); class="num">239. break; class="num">240. } class="num">241. } class="num">242. } class="num">243. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">244. }; class="num">245. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">246. class="macro">#undef macro_NameObjectStudy class="num">247. class=class="str">"cmt">//+------------------------------------------------------------------+
让鼠标指标接管任意品种的订单簿事件
过去鼠标指标只在连真实服务器时处理订单簿,回放/模拟环境被隔离在外。现在改成了无论真实账户、模拟账户还是自定义回放工具,都持续监听订单簿事件——这一步靠 OnInit 里主动告知 MetaTrader 5 接收事件实现,不再依赖图表是否绑定交易所数据。 C_Study 头文件里有个隐蔽坑:原构造函数中初始化 Rate.close,当指标先放进模板再拖上图表时,偶尔会拿到错误值。作者没完全查明原因,但把初始化挪到代码第 114 行做「值为 0 才取收盘价」的判断后,随机不稳定初始化的概率明显下降。外汇与贵金属品种叠加这类自定义回放本就高风险,初始化错乱会直接带歪后续所有逻辑。 鼠标指标的 OnBookEvent 从第 62 行起只干一件事:判断品种是交易模式、竞价模式还是市场关闭。第 69 行先测订单簿数组是否为空——空即收盘;非空再扫 BOOK_TYPE_BUY_MARKET / BOOK_TYPE_SELL_MARKET,命中即代表竞价模式。注意 MT5 不会按品种过滤订单簿事件,所以第 67 行必须用 symbol 过滤掉无关图表,否则多图同时收事件纯属浪费。 OnInit 第 31 行改用 SetUserError 替代原先的 INIT_FAILED 硬报错,因为构造函数无法返回值,旧办法靠 _LastError 不实用。第 34 行从「市场关闭」值起步,35–36 行初始化缓冲区,随后强制开启订单簿监听。OnDeInit 第 77 行关闭监听、79 行 delete 释放内存,避免残留。 OnCalculate 不再调用 iSpread 却也不再冻结,和上一版表现相反。作者直说找不到根因,只确认现在同段代码跑起来不卡。这种只有真去推 MT5 极限才会撞见的怪现象,恰恰说明自定义工具开发没有标准答案。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "..\C_Mouse.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_ExpansionPrefix def_MousePrefixName + "Expansion_" class=class="str">"cmt">//+------------------------------------------------------------------+ class C_Study : class="kw">public C_Mouse { class="kw">private : class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct st00 { eStatusMarket Status; class="type">MqlRates Rate; class="type">class="kw">string szInfo, szBtn1,
◍ 鼠标跟随的百分比偏离绘制逻辑
这段结构体收尾后紧接着是 Draw() 方法,负责把鼠标位置和K线收盘价之间的偏离度实时画到图表对象上。它分三个开关控制:bvT 管信息标签、bvD 管鼠标实时偏离、bvP 管固定收盘价偏离,三者独立触发。 bvD 分支里先算偏离百分比:用鼠标价格减收盘价再除以收盘价乘 100,NormalizeDouble 到 2 位小数。例如收盘价 1.0850、鼠标价 1.0870,算出来约 0.18%,标签背景按正负切换 corN/corP 两色。 Y 距离统一用鼠标 Y_Adjusted 减 18 或减 1 像素定位,避免文字压住光标。外汇与贵金属波动快,这类浮标在高杠杆下误读可能放大亏损风险,上 MT5 把 corP/corN 设成对比色更容易辨向。
}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));
「按钮对象的创建与销毁分支」
在 MT5 图表上做鼠标交互面板时,常用 Expansion 对象承载信息按钮。下面这段逻辑用 switch 按事件类型决定建哪个按钮:evShowBarTime 在坐标 (2,110) 建按钮并染成 clrPaleTurquoise,evShowDailyVar 在 (2,53) 建,evShowPriceVar 在 (58,53) 建,名称都拼了 def_ExpansionPrefix 加 ObjectsTotal(0) 保证不重名。 建完立刻把对应的 bvT / bvD / bvP 布尔标记置 true,后续刷新文字或判定显隐就靠这几个旗标。注意 CreateObjToStudy 的前两个参数是图表坐标偏移,不是价格,调布局时改这两个数即可。 销毁侧对称处理:收到 evHide* 事件就把存好的 szBtnX 名字取出来删对象,同时把布尔标记翻回 false。这样面板状态机不会残留幽灵按钮。 外汇与贵金属品种波动大,这类悬浮按钮只做信息辅助,任何数值都不构成方向暗示,实盘请自担高风险。
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; 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; 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; break; } } class=class="str">"cmt">//+------------------------------------------------------------------+ 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; break; case evHideDailyVar:
按钮删除与指标初始化的事件分支
这段逻辑处理两类事:一是图表上三个切换按钮(BarTime、DailyVar、PriceVar)被删除时的事件响应,二是 C_Study 构造时把内存结构和按钮对象一次性建好。 在 switch 的 evHideBarTime、evHideDailyVar、evHidePriceVar 分支里,代码先取对应按钮名 sz,再把 m_Info 里的显隐标志 bvD / bvP 置 false,随后 break。注意 089–091 行:删除对象前先用 ChartSetInteger 把 CHART_EVENT_OBJECT_DELETE 关掉,删完再开回来,避免 ObjectDelete 触发自身递归事件。 C_Study 构造函数(096–107)先判 _LastError 是否大于等于 ERR_USER_ERROR_FIRST,若是直接 return;否则 ZeroMemory(m_Info) 清空结构,写入 corP、corN 颜色,并连续 CreateObjInfo 三个事件按钮,最后 ResetLastError。 Update 函数开头(109–115)做了个日线收盘价缓存判断:当 m_Info.Rate.close 为 0 时才用 iClose 取 PERIOD_D1 数据,且对回放符号或跨日情形取 0 号柱、否则取 1 号柱。外汇与贵金属市场杠杆高、跳空频繁,这类基于日线 close 的缓存若在周末或重大数据后首根 bar 跑,可能拿到非预期值,建议开 MT5 在 EURUSD 日线挂一段打印 Log 验证。
class="num">081. sz = m_Info.szBtn2; class="num">082. m_Info.bvD = class="kw">false; class="num">083. break; class="num">084. case evHidePriceVar: class="num">085. sz = m_Info.szBtn3; class="num">086. m_Info.bvP = class="kw">false; class="num">087. break; class="num">088. } class="num">089. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, class="kw">false); class="num">090. ObjectDelete(GetInfoTerminal().ID, sz); class="num">091. ChartSetInteger(GetInfoTerminal().ID, CHART_EVENT_OBJECT_DELETE, true); class="num">092. } class="num">093. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">094. class="kw">public : class="num">095. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">096. 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) class="num">097. :C_Mouse(IdParam, szShortName, corH, corP, corN) class="num">098. { class="num">099. if (_LastError >= ERR_USER_ERROR_FIRST) class="kw">return; class="num">100. ZeroMemory(m_Info); class="num">101. m_Info.corP = corP; class="num">102. m_Info.corN = corN; class="num">103. CreateObjInfo(evShowBarTime); class="num">104. CreateObjInfo(evShowDailyVar); class="num">105. CreateObjInfo(evShowPriceVar); class="num">106. ResetLastError(); class="num">107. } class="num">108. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">109. class="type">void Update(class="kw">const eStatusMarket arg) class="num">110. { class="num">111. class="type">int i0; class="num">112. class="type">class="kw">datetime dt; class="num">113. class="num">114. if (m_Info.Rate.close == class="num">0) class="num">115. m_Info.Rate.close = iClose(NULL, PERIOD_D1, ((_Symbol == def_SymbolReplay) || (macroGetDate(TimeCurrent()) != macroGetDate(iTime(NULL, PERIOD_D1, class="num">0))) ? class="num">0 : class="num">1));
◍ 状态机里的时间对齐与回放切换
这段逻辑把行情状态分成收盘、回放、交易、竞价几类,用 switch 直接驱动信息字符串和后续绘制。状态只在传入参数与当前不同时才更新,避免无谓重绘。 switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) { case eCloseMarket : m_Info.szInfo = "Closed Market"; break; case eInReplay : case eInTrading : i0 = PeriodSeconds(); dt = (m_Info.Status == eInReplay ? (datetime) GL_TimeAdjust : TimeCurrent()); m_Info.Rate.time = (m_Info.Rate.time <= dt ? (datetime)(((ulong) dt / i0) * i0) + i0 : m_Info.Rate.time); if (dt > 0) m_Info.szInfo = TimeToString((datetime)m_Info.Rate.time - dt, TIME_SECONDS); break; case eAuction : m_Info.szInfo = "Auction"; break; default : m_Info.szInfo = "ERROR"; } Draw(); 回放与实盘的分叉点在 dt 的赋值:回放取 GL_TimeAdjust 伪时间,实盘取 TimeCurrent()。之后用 PeriodSeconds() 把时间向下取整到周期边界并加一个周期,得到下一根 K 线开盘时间。若当前 Rate.time 已落后,就推到该边界;否则保留原值。 TimeToString 算出距下一根 K 线的剩余秒数并显示,这是盯盘时判断休市倒计时或开盘临近的直接依据。外汇与贵金属市场高杠杆、跳空频繁,回放测试的结论只代表历史环境,实盘可能偏离。 DispatchMessage 里对 CHARTEVENT_CUSTOM 的扩展事件做对象显隐:evHideBarTime 调 RemoveObjInfo 清掉时间标签,evShowBarTime 调 CreateObjInfo 重建。自己写面板时,可照这个结构挂自定义事件,鼠标或定时器触发后只动对应图形资源。
class="kw">switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) { case eCloseMarket : m_Info.szInfo = "Closed Market"; 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)m_Info.Rate.time - dt, TIME_SECONDS); break; case eAuction : m_Info.szInfo = "Auction"; break; class="kw">default : m_Info.szInfo = "ERROR"; } Draw(); 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); break; case CHARTEVENT_CUSTOM + evShowBarTime: CreateObjInfo(evShowBarTime);
「图表事件如何驱动信息层显隐」
这段 switch 结构处理的是自定义图表事件与鼠标移动事件,核心逻辑是把外部按钮或交互信号映射到信息对象的创建与销毁。 case CHARTEVENT_CUSTOM + evHideDailyVar 到 evShowPriceVar 这 12 行(149–158),每一对隐藏/显示事件都对应 RemoveObjInfo 与 CreateObjInfo 的调用,说明日变量与价格变量的图层是独立开关的,互不干扰。 CHARTEVENT_MOUSE_MOVE 分支(160–162)只做一件事:调用 Draw() 然后 break,意味着鼠标每移动一次就重绘一次图形层,实时性靠事件触发而非定时器轮询。 事件处理末尾的 ChartRedraw(GetInfoTerminal().ID)(164)强制刷新指定图表窗口,避免自定义对象残留。下面这段是原文的事件分发骨架,逐行看: case CHARTEVENT_CUSTOM + evHideDailyVar: // 收到隐藏日变量的自定义事件 RemoveObjInfo(evHideDailyVar); // 删除对应信息对象 break; // 跳出分支 case CHARTEVENT_CUSTOM + evShowDailyVar: // 收到显示日变量的自定义事件 CreateObjInfo(evShowDailyVar); // 创建对应信息对象 break; case CHARTEVENT_CUSTOM + evHidePriceVar: // 隐藏价格变量事件 RemoveObjInfo(evHidePriceVar); break; case CHARTEVENT_CUSTOM + evShowPriceVar: // 显示价格变量事件 CreateObjInfo(evShowPriceVar); break; case CHARTEVENT_MOUSE_MOVE: // 鼠标移动事件 Draw(); // 重绘图形 break; 在 MT5 里开个空白指标把这段塞进 OnChartEvent,配合自定义事件 ID 宏,就能验证图层开关是否如预期。外汇与贵金属图表上做这类叠加层,需注意滑点与时延带来的高风险,事件驱动不代表成交确定。
break; case CHARTEVENT_CUSTOM + evHideDailyVar: RemoveObjInfo(evHideDailyVar); break; case CHARTEVENT_CUSTOM + evShowDailyVar: CreateObjInfo(evShowDailyVar); break; case CHARTEVENT_CUSTOM + evHidePriceVar: RemoveObjInfo(evHidePriceVar); break; case CHARTEVENT_CUSTOM + evShowPriceVar: CreateObjInfo(evShowPriceVar); break; case CHARTEVENT_MOUSE_MOVE: Draw(); break; } ChartRedraw(GetInfoTerminal().ID); } };
回放指标里的盘口订阅与缓冲初始化
这段声明把‘鼠标研究’指标的骨架搭起来了:用 C_Study 类封装绘制,外部输入三组颜色分别控制价格线、正向研究和负向研究,默认是黑、苍绿、浅珊瑚。直接在 MT5 里改 user02~user04 的输入值,就能让重放界面的多空标记换色,不用碰核心逻辑。 [CODE] datetime GL_TimeAdjust; //+------------------------------------------------------------------+ #include <Market Replay\Auxiliar\Study\C_Study.mqh> //+------------------------------------------------------------------+ C_Study *Study = NULL; //+------------------------------------------------------------------+ input color user02 = clrBlack; //Price Line input color user03 = clrPaleGreen; //Positive Study input color user04 = clrLightCoral; //Negative Study //+------------------------------------------------------------------+ C_Study::eStatusMarket m_Status; int m_posBuff = 0; double m_Buff[]; //+------------------------------------------------------------------+ int OnInit() { Study = new C_Study(0, "Indicator Mouse Study", user02, user03, user04); if (_LastError >= ERR_USER_ERROR_FIRST) return INIT_FAILED; MarketBookAdd((*Study).GetInfoTerminal().szSymbol); OnBookEvent((*Study).GetInfoTerminal().szSymbol); m_Status = C_Study::eCloseMarket; SetIndexBuffer(0, m_Buff, INDICATOR_DATA); ArrayInitialize(m_Buff, EMPTY_VALUE); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int& spread[]) { GL_PriceClose = close[rates_total - 1]; if (_Symbol == def_SymbolReplay) GL_TimeAdjust = spread[rates_total - 1] & (~def_MaskTimeService); m_posBuff = rates_total; (*Study).Update(m_Status); return rates_total; } [/CODE] OnInit 里第 32 行 MarketBookAdd 订阅了 Study 对象拿到的交易品种盘口事件,意味着重放时深度报价变动会推给本指标;若漏了这行,Level II 刷新不会触发绘制。第 35~36 行把 m_Buff 绑成指标数据缓冲并填 EMPTY_VALUE,rates_total 根 K 线以下都不会画出脏值。 OnCalculate 每次被调都用 close[rates_total-1] 抓最新收盘价赋给 GL_PriceClose,并把 m_posBuff 同步成 rates_total。若当前品种等于 def_SymbolReplay,还从 spread 尾项剥掉服务时间掩码存进 GL_TimeAdjust——这是回放引擎对齐历史时间戳的关键一步,实盘品种下该变量保持 0。外汇与贵金属重放带杠杆,盘口仿真和实盘有偏差,验证结论仅作概率参考。
class="type">class="kw">datetime GL_TimeAdjust; class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Market Replay\Auxiliar\Study\C_Study.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ C_Study *Study = NULL; class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">input class="type">class="kw">color user02 = clrBlack; class=class="str">"cmt">//Price Line class="kw">input class="type">class="kw">color user03 = clrPaleGreen; class=class="str">"cmt">//Positive Study class="kw">input class="type">class="kw">color user04 = clrLightCoral; class=class="str">"cmt">//Negative Study class=class="str">"cmt">//+------------------------------------------------------------------+ C_Study::eStatusMarket m_Status; class="type">int m_posBuff = class="num">0; class="type">class="kw">double m_Buff[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { Study = new C_Study(class="num">0, "Indicator Mouse Study", user02, user03, user04); if (_LastError >= ERR_USER_ERROR_FIRST) class="kw">return INIT_FAILED; MarketBookAdd((*Study).GetInfoTerminal().szSymbol); OnBookEvent((*Study).GetInfoTerminal().szSymbol); m_Status = C_Study::eCloseMarket; SetIndexBuffer(class="num">0, m_Buff, INDICATOR_DATA); ArrayInitialize(m_Buff, EMPTY_VALUE); class="kw">return INIT_SUCCEEDED; } 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">class="kw">datetime& time[], class="kw">const class="type">class="kw">double& open[], 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="kw">const class="type">long& volume[], class="kw">const class="type">int& spread[]) { GL_PriceClose = close[rates_total - class="num">1]; if (_Symbol == def_SymbolReplay) GL_TimeAdjust = spread[rates_total - class="num">1] & (~def_MaskTimeService); m_posBuff = rates_total; (*Study).Update(m_Status); class="kw">return rates_total; }
◍ 把深度簿事件接进图表重绘
MT5 的 OnBookEvent 是捕捉市场深度(DOM)变动的主入口,只在当前品种与指标监听品种一致时才处理,避免跨品种误触发。 下面这段把图表事件、深度簿事件和卸载事件三块拼起来,能直接拷进自定义指标骨架里跑。 [CODE] //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { (*Study).DispatchMessage(id, lparam, dparam, sparam); (*Study).SetBuffer(m_posBuff, m_Buff); ChartRedraw((*Study).GetInfoTerminal().ID); } //+------------------------------------------------------------------+ void OnBookEvent(const string &symbol) { MqlBookInfo book[]; C_Study::eStatusMarket loc = m_Status; if (symbol != (*Study).GetInfoTerminal().szSymbol) return; MarketBookGet((*Study).GetInfoTerminal().szSymbol, book); m_Status = (ArraySize(book) == 0 ? C_Study::eCloseMarket : (symbol == def_SymbolReplay ? C_Study::eInReplay : C_Study::eInTrading)); for (int c0 = 0; (c0 < ArraySize(book)) && (m_Status != C_Study::eAuction); c0++)
| if ((book[c0].type == BOOK_TYPE_BUY_MARKET) | (book[c0].type == BOOK_TYPE_SELL_MARKET)) m_Status = C_Study::eAuction; |
|---|
if (loc != m_Status) (*Study).Update(m_Status); } //+------------------------------------------------------------------+ void OnDeinit(const int reason) { MarketBookRelease((*Study).GetInfoTerminal().szSymbol); delete Study; } //+------------------------------------------------------------------+ [/CODE] 逐行拆一下关键点:OnChartEvent 里先让 Study 对象分发消息、写缓冲,再 ChartRedraw 强制重绘,保证 DOM 标注实时跟手。OnBookEvent 先用 MarketBookGet 拉当前簿,ArraySize(book)==0 判定为休市(eCloseMarket);若簿里出现 BOOK_TYPE_BUY_MARKET 或 BOOK_TYPE_SELL_MARKET 这类市价单,状态切到 eAuction,表示集合竞价或流动性吞噬可能发生。状态真变了才调 Update,省掉无谓计算。 OnDeinit 必须 MarketBookRelease 释放订阅,否则切周期会残留 DOM 钩子;最后 delete Study 防内存泄漏。外汇与贵金属属高风险品种,DOM 信号只在流动性正常时可靠,非农前后薄簿可能空数组误报休市。
class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnChartEvent(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) { (*Study).DispatchMessage(id, lparam, dparam, sparam); (*Study).SetBuffer(m_posBuff, m_Buff); ChartRedraw((*Study).GetInfoTerminal().ID); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnBookEvent(class="kw">const class="type">class="kw">string &symbol) { MqlBookInfo book[]; C_Study::eStatusMarket loc = m_Status; if (symbol != (*Study).GetInfoTerminal().szSymbol) class="kw">return; MarketBookGet((*Study).GetInfoTerminal().szSymbol, book); m_Status = (ArraySize(book) == class="num">0 ? C_Study::eCloseMarket : (symbol == def_SymbolReplay ? C_Study::eInReplay : C_Study::eInTrading)); for (class="type">int c0 = class="num">0; (c0 < ArraySize(book)) && (m_Status != C_Study::eAuction); c0++) if ((book[c0].type == BOOK_TYPE_BUY_MARKET) || (book[c0].type == BOOK_TYPE_SELL_MARKET)) m_Status = C_Study::eAuction; if (loc != m_Status) (*Study).Update(m_Status); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(class="kw">const class="type">int reason) { MarketBookRelease((*Study).GetInfoTerminal().szSymbol); class="kw">delete Study; } class=class="str">"cmt">//+------------------------------------------------------------------+
「自定义品种订单簿为什么先要设 bookdepth」
很多人在 MT5 里写自定义交易品种时卡在订单簿上,反复调用 CustomBookAdd 却什么都收不到。根子不在函数本身,而在事件顺序:不先给 SYMBOL_TICKS_BOOKDEPTH 赋值,CustomBookAdd 发了也白发。 源码第 24 行就是这道门槛。这里把 bookdepth 设成 1,因为目的只是驱动一个鼠标指标去提示自定义品种状态,一级深度足够。若你想造一个用于回放研究的人工盘口,把那个 1 改成你需要的档数即可。文档只提了 SYMBOL_TICKS_BOOKDEPTH 这个整数属性,却没明说它和后续能塞进 MqlBookInfo 结构是直接绑定的——这是不少人踩坑的地方。 第 44–47 行先定义一档盘口,再通知终端把数据当订单簿事件推出去。time 变量初值 32400 秒(第 34 行定),第 56 行每轮加 1 秒,第 55 行还插了 0.25 秒延迟,所以实际节奏不是整秒。满 30 秒走第 62 行发竞价模式值,再满 30 秒走第 64–66 行把时间显示复位给鼠标指标。 别把 CustomBookAdd 当万能写入口 它和 Tick、Rate 的发送接口看似同类,但前置条件更隐蔽。开 MT5 新建 service,把下面代码第 24 行注释掉跑一遍,终端订单簿大概率静默;补回那行再跑,盘口事件才进得去。外汇与贵金属自定义品种测试同样属高风险操作,先在模拟环境验证。
class="macro">#class="kw">property service class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#class="kw">property description "Data synchronization demo service." class="macro">#include <Market Replay\Defines.mqh> class="macro">#include <Market Replay\Auxiliar\Macros.mqh> class="macro">#define def_Loop((!_StopFlag) && (ChartSymbol(id) != "")) class="type">void OnStart() { class="type">long id; class="type">int time; class="type">MqlRates Rate[class="num">1]; MqlBookInfo book[class="num">1]; Print("Starting Test Service..."); SymbolSelect(def_SymbolReplay, class="kw">false); CustomSymbolDelete(def_SymbolReplay); CustomSymbolCreate(def_SymbolReplay, StringFormat("Custom\\%s", def_SymbolReplay)); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_SIZE, class="num">0.5); CustomSymbolSetDouble(def_SymbolReplay, SYMBOL_TRADE_TICK_VALUE, class="num">5); CustomSymbolSetInteger(def_SymbolReplay, SYMBOL_TICKS_BOOKDEPTH, class="num">1); Rate[class="num">0].close = class="num">105; Rate[class="num">0].open = class="num">100; Rate[class="num">0].high = class="num">110; Rate[class="num">0].low = class="num">95; Rate[class="num">0].tick_volume = class="num">5; Rate[class="num">0].spread = class="num">1; Rate[class="num">0].real_volume = class="num">10; Rate[class="num">0].time = D&class="macro">#x27;class="num">14.03.class="num">2023 class="num">08:class="num">30&class="macro">#x27;;
用自定义品种回放挂单与市价单切换
这段脚本把回放符号的行情推送、图表打开和订单簿构造串成了一条可复现的测试链路。先在 2023.03.14 09:00 把一根 M30 柱的时间写死并两次调用 CustomRatesUpdate,确保回放符号拿到初始 tick,再 SymbolSelect 显式上架并 ChartOpen 打开 PERIOD_M30 图表。 开图后 Sleep(1000) 留渲染缓冲,随后用 while 轮询 ChartIndicatorGet 等待名为 Indicator Mouse Study 的指标句柄出现,轮询间隔 200 毫秒;拿不到 INVALID_HANDLE 才继续,避免指标未加载就跑后续逻辑。 订单簿先用 BOOK_TYPE_BUY 在收盘价挂一单,CustomBookAdd 注入深度。主循环里每 250 毫秒把 time 自增 1 并刷新 spread 掩码,当 time 走到 32430 时把挂单改成的 BOOK_TYPE_BUY_MARKET 模拟市价吃单,过 30 秒(32460)又退回普通限价买,借此观察鼠标指标对成交流的反应。 收尾直接 ChartClose 关图、SymbolSelect(false) 下架并 CustomSymbolDelete 删自定义品种,终端打印 Finished Test Service。外汇与贵金属自定义回放属高杠杆环境,这段代码只验证接口行为,不构成任何方向判断。
class="num">33. CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1); class="num">34. Rate[class="num">0].time = D&class="macro">#x27;class="num">14.03.class="num">2023 class="num">09:class="num">00&class="macro">#x27;; class="num">35. CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1); class="num">36. SymbolSelect(def_SymbolReplay, true); class="num">37. id = ChartOpen(def_SymbolReplay, PERIOD_M30); class="num">38. class="num">39. Sleep(class="num">1000); class="num">40. class="num">41. Print("Waiting for Mouse Indicator..."); class="num">42. while ((def_Loop) && (ChartIndicatorGet(id, class="num">0, "Indicator Mouse Study") == INVALID_HANDLE)) Sleep(class="num">200); class="num">43. class="num">44. book[class="num">0].type = BOOK_TYPE_BUY; class="num">45. book[class="num">0].price = Rate[class="num">0].close; class="num">46. book[class="num">0].volume = class="num">1; class="num">47. CustomBookAdd(def_SymbolReplay, book, class="num">1); class="num">48. class="num">49. Print(TimeToString(Rate[class="num">0].time, TIME_DATE | TIME_SECONDS)); class="num">50. time = (class="type">int)macroGetTime(Rate[class="num">0].time); class="num">51. while (def_Loop) class="num">52. { class="num">53. Rate[class="num">0].spread = (class="type">int)(def_MaskTimeService | time); class="num">54. CustomRatesUpdate(def_SymbolReplay, Rate, class="num">1); class="num">55. Sleep(class="num">250); class="num">56. time++; class="num">57. Print(TimeToString(time, TIME_SECONDS), " >> (class="type">int):", time); class="num">58. class="kw">switch (time) class="num">59. { class="num">60. case class="num">32430: class="num">61. book[class="num">0].type = BOOK_TYPE_BUY_MARKET; class="num">62. CustomBookAdd(def_SymbolReplay, book, class="num">1); class="num">63. break; class="num">64. case class="num">32460: class="num">65. book[class="num">0].type = BOOK_TYPE_BUY; class="num">66. CustomBookAdd(def_SymbolReplay, book, class="num">1); class="num">67. break; class="num">68. } class="num">69. } class="num">70. ChartClose(id); class="num">71. SymbolSelect(def_SymbolReplay, class="kw">false); class="num">72. CustomSymbolDelete(def_SymbolReplay); class="num">73. Print("Finished Test Service..."); class="num">74. } class="num">75. class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 订单簿传值的代价与下篇预告
前面已经把自定义交易品种订单簿的写值流程跑通了,核心结论很直接:服务和 EA、指标之间,除了全局变量和文件,还能借订单簿做数据中转。这套办法在测试服务里验证过,传递过程稳定、读取延迟低,适合需要高频互通的小模块。 但订单簿不是免费通道。每一次无差别写入都会占用深度档位资源,行情刷新密集时可能拖慢终端响应,实盘里这属于必须计量的隐性成本。外汇与贵金属本身杠杆高、滑点随机,任何跨模块通信方案都要先在策略测试器里压一轮再上真仓。 下一篇会换思路,用回放/模拟器去判断品种是否处于竞价模式,和本文的传值实验不是同一件事。订单簿这招可以留着,但别随手就写,量一下代价再决定。