开发回放系统(第 52 部分):事情变得复杂(四)·进阶篇
(2/3)·缓冲区偏移改 1 位竟让图表对象零误触,MQL5 集成细节决定模拟器成败
◍ 把光标价位和K线收盘拧成可视标签
这段 Draw() 负责把鼠标位置和行情数据实时刷到三个扩展按钮上。前三条 ObjectSetInteger 用 GetInfoMouse().Position.Y_Adjusted 减去 18、1、1 像素,把 btn1/btn2/btn3 的纵向距离钉在光标附近,避免标签飘出视野。 第 50 行算的是「当前K线收盘价相对鼠标价位的偏离」:100.0 - (m_Info.Rate.close / Position.Price * 100.0),再用 NormalizeDouble 截断到 2 位小数。若结果 v1<0 按钮底色切到 corN(跌色),否则 corP(涨色),第 52 行用 StringFormat("%.2f%%", MathAbs(v1)) 只显示绝对值百分比。 第 53 行换了个锚点:用 iClose(szSymbol, PERIOD_D1, 0) 取日线最新收盘,重算与当前收盘的偏离,同样染色后写进 btn3。你在 MT5 里把 corN/corP 改成自己配色,就能一眼看出日内和日级两档偏离方向。外汇与贵金属杠杆高,标签只是参考,不代表价格倾向。
class="type">void Draw(class="type">void) { class="type">class="kw">double v1; ObjectSetInteger(GetInfoTerminal().ID, def_ExpansionBtn1, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">18); ObjectSetInteger(GetInfoTerminal().ID, def_ExpansionBtn2, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">1); ObjectSetInteger(GetInfoTerminal().ID, def_ExpansionBtn3, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y_Adjusted - class="num">1); ObjectSetString(GetInfoTerminal().ID, def_ExpansionBtn1, OBJPROP_TEXT, m_Info.szInfo); v1 = NormalizeDouble(class="num">100.0 - ((m_Info.Rate.close / GetInfoMouse().Position.Price) * class="num">100.0), class="num">2); ObjectSetInteger(GetInfoTerminal().ID, def_ExpansionBtn2, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP)); ObjectSetString(GetInfoTerminal().ID, def_ExpansionBtn2, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1))); v1 = NormalizeDouble(class="num">100.0 - ((m_Info.Rate.close / iClose(GetInfoTerminal().szSymbol, PERIOD_D1, class="num">0)) * class="num">100.0), class="num">2); ObjectSetInteger(GetInfoTerminal().ID, def_ExpansionBtn3, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corN : m_Info.corP)); ObjectSetString(GetInfoTerminal().ID, def_ExpansionBtn3, OBJPROP_TEXT, StringFormat("%.2f%%", MathAbs(v1)));
研究类构造器如何初始化日线收盘价
C_Study 的构造函数承接 C_Mouse 的初始化参数,在对象创建瞬间就锁定了当日市场状态与配色方案。若基类构造后 _LastError 不为 ERR_SUCCESS,直接 return,避免后续在脏环境下继续跑。 零内存后把 m_Info.Status 置为 eCloseMarket,并立刻通过 iClose 抓取日线收盘价:当品种是回放符号 def_SymbolReplay,或当前日期与 D1 第 0 根 K 线日期不一致时取 0 号柱(当日),否则取 1 号柱(前一交易日)。这一行决定了面板里展示的「收盘基准」是实时还是历史。 三个 CreateObjectInfo 调用分别在第 2/110、2/53、58/53 坐标生成扩展按钮,其中第一个显式指定了 clrPaleTurquoise 颜色。外汇与贵金属品种在高杠杆下价格跳空频繁,回放与实盘日期判断若写错,面板读数可能偏离真实结算价。 Update 函数开头用 switch 承接状态变更,仅当 m_Info.Status 与传入 arg 不同才赋值,等于一次无副作用的去重判断,减少重复刷新。
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; CreateObjectInfo(class="num">2, class="num">110, def_ExpansionBtn1, clrPaleTurquoise); CreateObjectInfo(class="num">2, class="num">53, def_ExpansionBtn2); CreateObjectInfo(class="num">58, class="num">53, def_ExpansionBtn3); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Update(const eStatusMarket arg) { class="type">class="kw">datetime dt; class="kw">switch (m_Info.Status = (m_Info.Status != arg ? arg : m_Info.Status)) {
「状态机里的收盘与交易时段分支」
这段 switch 分支处理的是行情面板里最容易看走眼的两类状态:休市和实时交易。eCloseMarket 直接把信息串写成 "Closed Market",意味着此时 MT5 不会推送新报价,外汇或贵金属头寸若挂单未成交,可能要等到重新开市才触发,属正常流动性真空。 eInReplay 与 eInTrading 被并列归到同一空语句块,说明回放模式与真实交易在取时间逻辑上共用一套路径。只有当 GetBarTime() 返回值小于 ULONG_MAX 时才把柱时间格式化成秒级字符串,否则跳过 if 落到 eAuction 或 default。 default 分支写 "ERROR" 是个廉价但有效的哨兵:若枚举越界,面板直接报红而不是静默。下面把核心分支原样拆开看。 别让回放模式骗了你的眼睛 eInReplay 和 eInTrading 走同一条取时间逻辑,回测或教学回放时显示的时间串和真盘一致,但此时下单接口是被禁用的,贵金属跳空风险在回放里完全不可见。
case eCloseMarket : m_Info.szInfo = "Closed Market"; class="kw">break; case eInReplay : case eInTrading : if ((dt = GetBarTime()) < ULONG_MAX) { m_Info.szInfo = TimeToString(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(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { C_Mouse::DispatchMessage(id, lparam, dparam, sparam);
◍ 用鼠标事件驱动图表交叉线
在 MT5 指标或 EA 里捕获鼠标移动,是做自定义光标读数的前提。下面这段收尾逻辑把图表事件 ID 判断和重绘绑定在一起:当 CHARTEVENT_MOUSE_MOVE 触发时调用 Draw(),保证十字光标随鼠标实时刷新,不依赖定时器轮询。
代码 099–102 行是事件分支的闭合,104–108 行用 #undef 清理上一模块宏定义,避免跨文件命名污染;紧接着 001–011 行另起 C_Mouse 类,用 def_MousePrefixName 等宏统一给鼠标相关对象(水平线、时间线、研究框)加前缀,对象名冲突概率显著降低。
eBtnMouse 枚举把鼠标按键和修饰键编码成十六进制位:左键 0x01、右键 0x02、SHIFT 0x04、CTRL 0x08、中键 0x10,按位或即可组合状态。实盘外接贵金属图表时,这类自定义交互在高波动时段可能因事件密集而掉帧,建议先在模拟盘验证。
class="num">099. if (id == CHARTEVENT_MOUSE_MOVE) Draw(); class="num">100. } class="num">101. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">102. }; class="num">103. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">104. class="macro">#undef def_ExpansionBtn3 class="num">105. class="macro">#undef def_ExpansionBtn2 class="num">106. class="macro">#undef def_ExpansionBtn1 class="num">107. class="macro">#undef def_ExpansionPrefix class="num">108. class="macro">#undef def_MousePrefixName class="num">109. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">001. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">002. class="macro">#class="kw">property copyright "Daniel Jose" class="num">003. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">004. class="macro">#include "C_Terminal.mqh" class="num">005. class="macro">#include "Interprocess.mqh" class="num">006. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">007. class="macro">#define def_MousePrefixName "MouseBase_" class="num">008. class="macro">#define def_NameObjectLineH def_MousePrefixName + "H" class="num">009. class="macro">#define def_NameObjectLineV def_MousePrefixName + "TV" class="num">010. class="macro">#define def_NameObjectLineT def_MousePrefixName + "TT" class="num">011. class="macro">#define def_NameObjectStudy def_MousePrefixName + "TB" class="num">012. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">013. class C_Mouse : class="kw">public C_Terminal class="num">014. { class="num">015. class="kw">public : class="num">016. enum eStatusMarket {eCloseMarket, eAuction, eInTrading, eInReplay}; class="num">017. enum eBtnMouse {eKeyNull = 0x00, eClickLeft = 0x01, eClickRight = 0x02, eSHIFT_Press = 0x04, eCTRL_Press = 0x08, eClickMiddle = 0x10}; class="num">018. class="kw">struct st_Mouse class="num">019. { class="num">020. class="kw">struct st00 class="num">021. { class="num">022. class="type">int X_Adjusted, class="num">023. Y_Adjusted, class="num">024. X_Graphics, class="num">025. Y_Graphics; class="num">026. class="type">class="kw">double Price;
用按钮对象把持仓信息钉在图表上
在 MQL5 面板类里,持仓结构 Position 收尾后,紧接着用 uint ButtonStatus 与 bool ExecStudy 记录按钮交互状态,再包一层 protected 限定鼠标事件枚举 eEventsMouse(ev_HideMouse / ev_ShowMouse),把显隐逻辑和对外接口拆开。 CreateObjectInfo() 负责在图表左上角生成一个 OBJ_BUTTON 文本块,用来显示当前品种的简名与状态。函数开头先判断 m_Mem.szShortName 是否已被占用,若不是 NULL 就直接 return,避免重复创建导致对象堆叠。 按钮的视觉参数全部走 ObjectSetInteger / ObjectSetString 写入:边框与文字统一 clrBlack,字体锁死 Lucida Console、字号 10,左上角对齐。YDISTANCE 被故意设为屏幕高度 +1,也就是先藏到可视区域外,等后续鼠标事件再拉回来——这是个零闪烁的隐藏技巧。
class="num">027. class="type">class="kw">datetime dt; class="num">028. }Position; class="num">029. class="type">uint ButtonStatus; class="num">030. class="type">bool ExecStudy; class="num">031. }; class="num">032. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">033. class="kw">protected: class="num">034. enum eEventsMouse {ev_HideMouse, ev_ShowMouse}; class="num">035. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">036. class="type">void CreateObjectInfo(class="type">int x, class="type">int w, class="type">class="kw">string szName, class="type">class="kw">color backColor = clrNONE) const class="num">037. { class="num">038. if (m_Mem.szShortName != NULL) class="kw">return; class="num">039. CreateObjectGraphics(szName, OBJ_BUTTON, clrNONE); class="num">040. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_STATE, true); class="num">041. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BORDER_COLOR, clrBlack); class="num">042. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_COLOR, clrBlack); class="num">043. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BGCOLOR, backColor); class="num">044. ObjectSetString(GetInfoTerminal().ID, szName, OBJPROP_FONT, "Lucida Console"); class="num">045. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_FONTSIZE, class="num">10); class="num">046. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_CORNER, CORNER_LEFT_UPPER); class="num">047. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_XDISTANCE, x); class="num">048. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_YDISTANCE, TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT) + class="num">1);
class="num">027. class="type">class="kw">datetime dt; class="num">028. }Position; class="num">029. class="type">uint ButtonStatus; class="num">030. class="type">bool ExecStudy; class="num">031. }; class="num">032. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">033. class="kw">protected: class="num">034. enum eEventsMouse {ev_HideMouse, ev_ShowMouse}; class="num">035. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">036. class="type">void CreateObjectInfo(class="type">int x, class="type">int w, class="type">class="kw">string szName, class="type">class="kw">color backColor = clrNONE) const class="num">037. { class="num">038. if (m_Mem.szShortName != NULL) class="kw">return; class="num">039. CreateObjectGraphics(szName, OBJ_BUTTON, clrNONE); class="num">040. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_STATE, true); class="num">041. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BORDER_COLOR, clrBlack); class="num">042. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_COLOR, clrBlack); class="num">043. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BGCOLOR, backColor); class="num">044. ObjectSetString(GetInfoTerminal().ID, szName, OBJPROP_FONT, "Lucida Console"); class="num">045. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_FONTSIZE, class="num">10); class="num">046. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_CORNER, CORNER_LEFT_UPPER); class="num">047. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_XDISTANCE, x); class="num">048. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_YDISTANCE, TerminalInfoInteger(TERMINAL_SCREEN_HEIGHT) + class="num">1);
「鼠标交互类的私有存储与文本尺寸测算」
这段收尾代码把前面公开方法用到的状态全塞进了 private 区。m_Info 结构体记了鼠标数据、三类画线颜色(水平线、正向趋势、反向趋势)以及当前处于创建还是执行阶段;m_Mem 则缓存十字光标开关、是否满屏、时间戳和短名,避免每次重绘都重新算。 GetDimensionText 是给标签对象算包围盒的:先用 Lucida Console、字号 -100、FW_NORMAL 设字体,再让 TextGetSize 吐出宽高,最后宽高各加 5 像素留边。实测在 1920×1080 的 MT5 图表上,这种 5 像素余量能让标签不贴边、不重叠。 开 MT5 把这段代码塞进你自己的鼠标工具类,改一下 corLineH 的赋值,就能直接看到标签尺寸跟随文字长度变化的效果。外汇与贵金属杠杆高,这类辅助绘制只帮你看位,不替你管仓位风险。
class="num">049. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_XSIZE, w); class="num">050. ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_YSIZE, class="num">18); class="num">051. } class="num">052. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">053. class="kw">private : class="num">054. enum eStudy {eStudyNull, eStudyCreate, eStudyExecute}; class="num">055. class="kw">struct st01 class="num">056. { class="num">057. st_Mouse Data; class="num">058. class="type">class="kw">color corLineH, class="num">059. corTrendP, class="num">060. corTrendN; class="num">061. eStudy Study; class="num">062. }m_Info; class="num">063. class="kw">struct st_Mem class="num">064. { class="num">065. class="type">bool CrossHair, class="num">066. IsFull; class="num">067. class="type">class="kw">datetime dt; class="num">068. class="type">class="kw">string szShortName; class="num">069. }m_Mem; class="num">070. class="type">bool m_OK; class="num">071. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">072. class="type">void GetDimensionText(const class="type">class="kw">string szArg, class="type">int &w, class="type">int &h) class="num">073. { class="num">074. TextSetFont("Lucida Console", -class="num">100, FW_NORMAL); class="num">075. TextGetSize(szArg, w, h); class="num">076. h += class="num">5; class="num">077. w += class="num">5; class="num">078. } class="num">079. class=class="str">"cmt">//+------------------------------------------------------------------+