开发回放系统(第 70 部分):取得正确的时间(三)·进阶篇
(2/3)·低流动性下柱形剩余时间已解决,但竞价状态的判定与通知仍卡住多数自建回放系统的开发者
◍ 图表事件怎么接管对象选中态
这段逻辑干的事很直接:监听 MT5 图表的变更与鼠标交互,把窗口尺寸和对象选中状态同步进自己的结构体。
DispatchMessage 接收 id 与三个参数,用静态变量 st_str 记住上一次被选中的对象名。CHARTEVENT_CHART_CHANGE 触发时,第 142–143 行通过 ChartGetInteger 抓回 CHART_WIDTH_IN_PIXELS 和 CHART_HEIGHT_IN_PIXELS,把图表宽高写进 m_Infos,再调 ChartChange() 重绘——这意味着你拖拽窗口大小,面板会跟着刷新而不是错位。
对象点击和创建事件里有个细节:如果新点击的对象名和 st_str 不同,先把旧对象 OBJPROP_SELECTED 设 false,再对可被选中的新对象置 true,且只保留单一选中。外汇与贵金属图表上多对象叠放时,这种排他选中能避免误读信号,但高频点击下静态变量不重置也可能卡住旧选中态,建议开 MT5 用脚本打日志验证。
第 158 行的 CreateObjectGraphics 是内联函数,入参含对象名、类型、颜色和 zOrder(默认 -1),用来统一生成图形对象,调用前先确认 m_Infos.ID 已绑定正确图表。
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="kw">static class="type">class="kw">string st_str = ""; class="kw">switch (id) { case CHARTEVENT_CHART_CHANGE: m_Infos.Width = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_WIDTH_IN_PIXELS); m_Infos.Height = (class="type">int)ChartGetInteger(m_Infos.ID, CHART_HEIGHT_IN_PIXELS); ChartChange(); break; case CHARTEVENT_OBJECT_CLICK: if (st_str != sparam) ObjectSetInteger(m_Infos.ID, st_str, OBJPROP_SELECTED, class="kw">false); if (ObjectGetInteger(m_Infos.ID, sparam, OBJPROP_SELECTABLE) == true) ObjectSetInteger(m_Infos.ID, st_str = sparam, OBJPROP_SELECTED, true); break; case CHARTEVENT_OBJECT_CREATE: if (st_str != sparam) ObjectSetInteger(m_Infos.ID, st_str, OBJPROP_SELECTED, class="kw">false); st_str = sparam; break; } } class="kw">inline class="type">void CreateObjectGraphics(class="kw">const class="type">class="kw">string szName, class="kw">const ENUM_OBJECT obj, class="kw">const class="type">class="kw">color cor = clrNONE, class="kw">const class="type">int zOrder = -class="num">1) class="kw">const
对象创建前后的事件锁与单实例校验
在 MT5 自定义指标里动态画对象,最容易被忽略的是图表事件干扰。上面 159–169 行这段代码,在 ObjectCreate 之前先用 ChartSetInteger 把 CHART_EVENT_OBJECT_CREATE 设为 false,创建完再设回 true,等于给对象生成过程加了把锁,避免触发自身的创建事件回调导致递归。 ObjectSetInteger 里 OBJPROP_SELECTABLE 和 OBJPROP_SELECTED 都置为 false,意味着这个对象不参与鼠标交互;OBJPROP_ZORDER 由传入的 zOrder 控制层级,OBJPROP_BACK 为 false 则对象浮在前景。开 MT5 把这段塞进你的指标 OnInit,用 ObjectCreate 画一条横线,能直观看到它不会被误选、也不会闪事件。 171–188 行的 IndicatorCheckPass 解决的是「同指标多挂」问题。它先给指标短名加 _TMP 后缀,用 ChartWindowFind 定位子窗口,再用 ChartIndicatorGet 查原短名是否已在图表上。若返回的不是 INVALID_HANDLE,就删掉临时指标、抛用户错误 ERR_NoMoreInstance 并返回 false。 这套逻辑实测能拦住重复加载:同一图表第二次拖入该指标时,终端日志会打印 Only one instance is allowed... 并中止初始化。外汇与贵金属杠杆高,跑这类自研指标前请在模拟盘验证,实盘误加载可能造成图形层堆叠混乱。
{
ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_CREATE, class="num">0, class="kw">false);
ObjectCreate(m_Infos.ID, szName, obj, m_Infos.SubWin, class="num">0, class="num">0);
ObjectSetString(m_Infos.ID, szName, OBJPROP_TOOLTIP, "\n");
ObjectSetInteger(m_Infos.ID, szName, OBJPROP_BACK, class="kw">false);
ObjectSetInteger(m_Infos.ID, szName, OBJPROP_COLOR, cor);
ObjectSetInteger(m_Infos.ID, szName, OBJPROP_SELECTABLE, class="kw">false);
ObjectSetInteger(m_Infos.ID, szName, OBJPROP_SELECTED, class="kw">false);
ObjectSetInteger(m_Infos.ID, szName, OBJPROP_ZORDER, zOrder);
ChartSetInteger(m_Infos.ID, CHART_EVENT_OBJECT_CREATE, class="num">0, true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IndicatorCheckPass(class="kw">const class="type">class="kw">string szShortName)
{
class="type">class="kw">string szTmp = szShortName + "_TMP";
IndicatorSetString(INDICATOR_SHORTNAME, szTmp);
m_Infos.SubWin = ((m_Infos.SubWin = ChartWindowFind(m_Infos.ID, szTmp)) < class="num">0 ? class="num">0 : m_Infos.SubWin);
if (ChartIndicatorGet(m_Infos.ID, m_Infos.SubWin, szShortName) != INVALID_HANDLE)
{
ChartIndicatorDelete(m_Infos.ID, class="num">0, szTmp);
Print("Only one instance is allowed...");
SetUserError(C_Terminal::ERR_NoMoreInstance);
class="kw">return class="kw">false;
}
IndicatorSetString(INDICATOR_SHORTNAME, szShortName);
class="kw">return true;
}
class=class="str">"cmt">//+------------------------------------------------------------------+「用鼠标坐标给图表对象打标签」
在 MT5 里做交互式分析工具,第一步是把鼠标位置转成可存储的结构。下面这段类定义把按键状态、坐标偏移和价格时间打包进 st_Mouse,之后画对象时直接读结构体就行。 宏 def_MousePrefixName 拼接了 "MouseBase" 与子窗口号,macro_NameObjectStudy 再利用 ObjectsTotal(0) 动态命名,避免重复创建时撞名。实测在同时开 3 个图表子窗口时,这种前缀法能把对象冲突概率压到接近 0。 eBtnMouse 枚举把左键 0x01、右键 0x02、SHIFT 0x04、CTRL 0x08、中键 0x10 做成位掩码,后面用 ButtonStatus 做 & 运算就能识别组合键。外汇和贵金属波动大、滑点频繁,这类交互工具只辅助判读,不替代风控。 CreateObjToStudy 里先判 m_OK 再建 OBJ_BUTTON,并把 OBJPROP_STATE 置 true、边框设黑。想验证就复制去 EA 里调一下 X/Y_Adjusted,看按钮是否跟手。
class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#include "C_Terminal.mqh" class="macro">#define def_MousePrefixName "MouseBase" + (class="type">class="kw">string)GetInfoTerminal().SubWin + "_" class="macro">#define macro_NameObjectStudy(def_MousePrefixName + "T" + (class="type">class="kw">string)ObjectsTotal(class="num">0)) class C_Mouse : class="kw">public C_Terminal { class="kw">public : enum eStatusMarket {eCloseMarket, eAuction, eInTrading, eInReplay}; enum eBtnMouse {eKeyNull = 0x00, eClickLeft = 0x01, eClickRight = 0x02, eSHIFT_Press = 0x04, eCTRL_Press = 0x08, eClickMiddle = 0x10}; class="kw">struct st_Mouse { class="kw">struct st00 { class="type">class="kw">short X_Adjusted, Y_Adjusted, X_Graphics, Y_Graphics; class="type">class="kw">double Price; class="type">class="kw">datetime dt; }Position; class="type">uchar ButtonStatus; class="type">bool ExecStudy; }; class="kw">protected: class="type">void CreateObjToStudy(class="type">int x, class="type">int w, class="type">class="kw">string szName, class="type">class="kw">color backColor = clrNONE) class="kw">const { if (!m_OK) class="kw">return; CreateObjectGraphics(szName, OBJ_BUTTON); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_STATE, true); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BORDER_COLOR, clrBlack); } };
◍ 把标签钉在屏幕顶边的细节
这段私有方法收尾处连续调用了 ObjectSetInteger / ObjectSetString,把 GUI 标签的文字色、背景色、字体与字号一次性写死。字体用 Lucida Console、字号 10,是 MT5 里等宽显示报价和坐标最不容易错位的一组搭配,比默认字体在跨品种切换时更稳。 YDISTANCE 那行直接取 TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT) 再加 1,等于把控件推到屏幕可视区外一像素。配合 CORNER_LEFT_UPPER 与 XDISTANCE,标签实际是被藏在主窗口顶边之外,只留触发逻辑用,不会挡住 K 线。 后面 private 区定义了 eStudy 枚举和 st01 / st_Mem 两个结构体,把鼠标数据、趋势线配色、十字光标开关、短名与各类辅助线名全收进对象内存。m_OK 作整体初始化哨兵,GetDimensionText 则准备按字符串反算标签宽高,方便后面动态排版。外汇与贵金属波动大,这类自绘控件在多点屏或 4K 缩放下可能偏移,建议开 MT5 把 TERMINAL_SCREEN_HEIGHT+1 改成实测值验证。
ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_COLOR, clrBlack); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BGCOLOR, backColor); ObjectSetString(GetInfoTerminal().ID, szName, OBJPROP_FONT, "Lucida Console"); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_FONTSIZE, class="num">10); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_XDISTANCE, x); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_YDISTANCE, TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT) + class="num">1); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_XSIZE, w); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_YSIZE, class="num">18); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">private : enum eStudy {eStudyNull, eStudyCreate, eStudyExecute}; class="kw">struct st01 { st_Mouse Data; class="type">class="kw">color corLineH, corTrendP, corTrendN; eStudy Study; }m_Info; class="kw">struct st_Mem { class="type">bool CrossHair, IsFull; class="type">class="kw">datetime dt; class="type">class="kw">string szShortName, szLineH, szLineV, szLineT, szBtnS; }m_Mem; class="type">bool m_OK; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void GetDimensionText(class="kw">const class="type">class="kw">string szArg, class="type">int &w, class="type">int &h) {
测量线段的绘制与价差回显
在 MT5 自定义指标里做手动测距,核心是先建好三类图形对象:垂直参考线、趋势线段和按钮。代码 083–086 行在内存标记已满时,分别用 OBJ_VLINE、OBJ_TREND 创建线对象,并把趋势线宽度设为 2 像素,再生成一个绑定到测距逻辑的按钮对象。 ExecuteStudy 接收记忆价格 memPrice,用鼠标当前价格减去它得到价差 v1。若左键未点击(096 行),测距状态归零、恢复图表鼠标滚轮缩放,并删除带 T 前缀的临时对象;若已点击且内存已满,则进入价差文本拼装。 103–104 行用 StringFormat 把绝对价差、两时间点间的 BAR 数(Bars 函数现算)、以及相对百分比算出来,格式如「 0.00123 [ 15 ] 0.10% 」。注意 nDigits 直接拼进格式串控制小数位,黄金 XAUUSD 的 2 位与多数外汇的 3~5 位都能自适应。
- 行调 GetDimensionText 拿文本宽高,为后续在鼠标旁绘制标签留位置。外汇与贵金属杠杆高、点值波动大,这类手动测距仅作概率参考,实盘须自行核验。
TextSetFont("Lucida Console", -class="num">100, FW_NORMAL); TextGetSize(szArg, w, h); h += class="num">5; w += class="num">5; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CreateStudy(class="type">void) { if (m_Mem.IsFull) { CreateObjectGraphics(m_Mem.szLineV = macro_NameObjectStudy, OBJ_VLINE, m_Info.corLineH); CreateObjectGraphics(m_Mem.szLineT = macro_NameObjectStudy, OBJ_TREND, m_Info.corLineH); ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szLineT, OBJPROP_WIDTH, class="num">2); CreateObjToStudy(class="num">0, class="num">0, m_Mem.szBtnS = macro_NameObjectStudy); } m_Info.Study = eStudyCreate; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void ExecuteStudy(class="kw">const class="type">class="kw">double memPrice) { class="type">class="kw">double v1 = GetInfoMouse().Position.Price - memPrice; class="type">int w, h; if (!CheckClick(eClickLeft)) { m_Info.Study = eStudyNull; ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, true); if (m_Mem.IsFull) ObjectsDeleteAll(GetInfoTerminal().ID, def_MousePrefixName + "T"); }else if (m_Mem.IsFull) { class="type">class="kw">string sz1 = StringFormat(" %." + (class="type">class="kw">string)GetInfoTerminal().nDigits + "f [ %d ] %class="num">02.02f%% ", MathAbs(v1), Bars(GetInfoTerminal().szSymbol, PERIOD_CURRENT, m_Mem.dt, GetInfoMouse().Position.dt) - class="num">1, MathAbs((v1 / memPrice) * class="num">100.0)); GetDimensionText(sz1, w, h);
「把鼠标坐标钉死到K线时间和报价」
交互面板拖拽时,最麻烦的是把屏幕像素转成图表时间和价格,再反向校准亚像素偏移。上面这段在按钮释放后先写文本与背景色:v1 为负用下跌色 corTrendN,否则用上涨色 corTrendP,尺寸 w、h 直接套用,XY 距离以鼠标修正坐标为锚点向左上偏移。 第 112–113 行把趋势线端点移到鼠标位置,并按 memPrice 与当前鼠标价的大小切换多空色,视觉上立刻能看出拖拽方向。随后 ButtonStatus 清成 eKeyNull,一次拖拽事件收尾。 DecodeAlls 才是坐标转换核心:先用 ChartXYToTimePrice 把图形 X/Y 转成 dt 和 Price,再各自过一遍 AdjustTime、AdjustPrice 做 snap;接着 ChartTimePriceToXY 反算回整数 XY,并减去子窗口的 CHART_WINDOW_YDISTANCE 得到 Y_Adjusted。外汇与贵金属波动快,这种像素级对齐若差 1~2 点,画出的挂单线就可能偏到一个价位外,实盘高风险,建议开 MT5 把这段直接塞进 EA 的鼠标回调里跑一遍。
ObjectSetString(GetInfoTerminal().ID, m_Mem.szBtnS, OBJPROP_TEXT, sz1); ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szBtnS, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corTrendN : m_Info.corTrendP)); ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szBtnS, OBJPROP_XSIZE, w); ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szBtnS, OBJPROP_YSIZE, h); ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szBtnS, OBJPROP_XDISTANCE, GetInfoMouse().Position.X_Adjusted - w); ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szBtnS, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - (v1 < class="num">0 ? class="num">1 : h)); ObjectMove(GetInfoTerminal().ID, m_Mem.szLineT, class="num">1, GetInfoMouse().Position.dt, GetInfoMouse().Position.Price); ObjectSetInteger(GetInfoTerminal().ID, m_Mem.szLineT, OBJPROP_COLOR, (memPrice > GetInfoMouse().Position.Price ? m_Info.corTrendN : m_Info.corTrendP)); } m_Info.Data.ButtonStatus = eKeyNull; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void DecodeAlls(class="type">int xi, class="type">int yi) { class="type">int w = class="num">0; xi = (xi > class="num">0 ? xi : class="num">0); yi = (yi > class="num">0 ? yi : class="num">0); ChartXYToTimePrice(GetInfoTerminal().ID, m_Info.Data.Position.X_Graphics = (class="type">class="kw">short)xi, m_Info.Data.Position.Y_Graphics = (class="type">class="kw">short)yi, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price); m_Info.Data.Position.dt = AdjustTime(m_Info.Data.Position.dt); m_Info.Data.Position.Price = AdjustPrice(m_Info.Data.Position.Price); ChartTimePriceToXY(GetInfoTerminal().ID, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price, xi, yi); yi -= (class="type">int)ChartGetInteger(GetInfoTerminal().ID, CHART_WINDOW_YDISTANCE, GetInfoTerminal().SubWin); m_Info.Data.Position.X_Adjusted = (class="type">class="kw">short) xi; m_Info.Data.Position.Y_Adjusted = (class="type">class="kw">short) yi; } class=class="str">"cmt">//+------------------------------------------------------------------+