开发回放系统(第 41 部分):启动第二阶段(二)·综合运用
(3/3)·多数人重复写同质代码却不自知,本篇用C_Mouse类演示如何换思维提效
鼠标移动时水平线的实时吸附
这段事件处理核心在 CHARTEVENT_MOUSE_MOVE:每当光标在图表内移动,先用 ChartXYToTimePrice 把像素坐标 (lparam, dparam) 反算成时间 dt 与价格 Price,并写回 m_Info.Data.Position。 若内存区 m_Mem.IsFull 已就绪,ObjectMove 会把水平线 def_NameObjectLineH 的 Y 值直接钉在 AdjustPrice 修正过的 Price 上,保证价格对齐到当前品种的最小报价单位。 随后 AdjustTime 修正时间轴,再用 ChartTimePriceToXY 把修正后的 dt/Price 算回屏幕 XY,更新 Position.X/Y 缓存;若已选了分析对象(m_Info.Study != eStudyNull)且内存满,垂直参考线也同步移到该时间。 中键点击由 CheckClick(eClickMiddle) 拦截:当内存未满,或水平线颜色不是 clrNONE 时,触发 CreateStudy() 新建分析。外汇与贵金属波动大、点差跳变频繁,这类鼠标联动在高波动时段可能出明显延迟,开 MT5 把这段塞进 EA 的 OnChartEvent 即可验证吸附是否跟手。
case (CHARTEVENT_CUSTOM + ev_ShowMouse): if (m_Mem.IsFull) ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineH, OBJPROP_COLOR, m_Info.corLineH); break; case CHARTEVENT_MOUSE_MOVE: ChartXYToTimePrice(def_InfoTerminal.ID, m_Info.Data.Position.X = (class="type">int)lparam, m_Info.Data.Position.Y = (class="type">int)dparam, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price); if (m_Mem.IsFull) ObjectMove(def_InfoTerminal.ID, def_NameObjectLineH, class="num">0, class="num">0, m_Info.Data.Position.Price = def_AcessTerminal.AdjustPrice(m_Info.Data.Position.Price)); m_Info.Data.Position.dt = def_AcessTerminal.AdjustTime(m_Info.Data.Position.dt); ChartTimePriceToXY(def_InfoTerminal.ID, w, m_Info.Data.Position.dt, m_Info.Data.Position.Price, m_Info.Data.Position.X, m_Info.Data.Position.Y); if ((m_Info.Study != eStudyNull) && (m_Mem.IsFull)) ObjectMove(def_InfoTerminal.ID, def_NameObjectLineV, class="num">0, m_Info.Data.Position.dt, class="num">0); m_Info.Data.ButtonStatus = (class="type">uint) sparam; if (CheckClick(eClickMiddle)) if ((!m_Mem.IsFull) || ((class="type">class="kw">color)ObjectGetInteger(def_InfoTerminal.ID, def_NameObjectLineH, OBJPROP_COLOR) != clrNONE)) CreateStudy();
◍ 左键落点如何接管图表交互
在自定义指标或 EA 的 OnChartEvent 里,左键点击是启动手动测距的关键触发点。代码先用 CheckClick(eClickLeft) 判定左键动作,且要求当前状态处于 eStudyCreate,才允许进入绘制分支。 进入分支后立刻用 ChartSetInteger 把 CHART_MOUSE_SCROLL 设为 false,这一步很硬核:不关掉滚轮缩放,鼠标定位价就会被图表滚动干扰,后续 ObjectMove 挂上去的线会漂。 若记忆区 m_Mem.IsFull 已就绪,直接把水平线 def_NameObjectLineT 移到鼠标所在的 datetime 与 Price(GetInfoMouse().Position 取值),同时把状态切到 eStudyExecute 并调用 ExecuteStudy(memPrice) 跑计算。外汇与贵金属波动剧烈,这种手动取价方式仅作局部测算,实盘仍属高风险。 OBJECT_DELETE 事件里还有个防误删回补:如果用户删了 def_NameObjectLineH 这条水平线,且 m_Mem.IsFull 为真,就立刻用 CreateObjectGraphics 以 OBJ_HLINE 重建,颜色取 m_Info.corLineH。末尾一堆 #undef 只是清理宏定义,不影响运行逻辑,但能避免多文件编译时的宏污染。
class="num">211. if (CheckClick(eClickLeft) && (m_Info.Study == eStudyCreate)) class="num">212. { class="num">213. ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, class="kw">false); class="num">214. if (m_Mem.IsFull) ObjectMove(def_InfoTerminal.ID, def_NameObjectLineT, class="num">0, m_Mem.dt = GetInfoMouse().Position.dt, memPrice = GetInfoMouse().Position.Price); class="num">215. m_Info.Study = eStudyExecute; class="num">216. } class="num">217. if (m_Info.Study == eStudyExecute) ExecuteStudy(memPrice); class="num">218. m_Info.Data.ExecStudy = m_Info.Study == eStudyExecute; class="num">219. break; class="num">220. case CHARTEVENT_OBJECT_DELETE: class="num">221. if ((m_Mem.IsFull) && (sparam == def_NameObjectLineH)) def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH); class="num">222. break; class="num">223. } class="num">224. } class="num">225. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">226. }; class="num">227. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">228. class="macro">#undef def_AcessTerminal class="num">229. class="macro">#undef def_InfoTerminal class="num">230. class=class="str">"cmt">//+------------------------------------------------------------------+ class="num">231. class="macro">#undef def_MousePrefixName class="num">232. class="macro">#undef def_NameObjectLineV class="num">233. class="macro">#undef def_NameObjectLineH
「用联合体把鼠标状态塞进指标缓冲区」
MQL5 里指标缓冲区只能存 double,但要记录鼠标在图表上的时间、坐标、按键状态,就得借联合体(union)做类型重解释。上面这段 SetBuffer 就是典型做法:先防越界把 m_posBuff 钳到 0 以下归零,再把鼠标价格写进缓冲区第 0 槽。 随后的 uCast_Double 联合体把 datetime、两个整数坐标、一个字符状态分别赋值,再取 .dValue 写进 m_Buff 的 +1、+2、+3 位置。也就是说一个 4 倍长度的 double 数组,实际装了 1 个价格 + 3 组被 reinterpret 的鼠标信息。 注意第 113 行:只有当 ExecStudy 为 eStudyNull 时才把 ButtonStatus 写进字符位,否则填 0。这样在 MT5 里用鼠标移动触发 OnChartEvent 时,缓冲区能无 GC 开销地携带交互状态,外汇与贵金属图表高频刷新时这种写法能少掉不少类型转换拖累。
class="macro">#undef def_NameObjectLineT class="macro">#undef def_NameObjectStudy class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void SetBuffer(class="type">void) { uCast_Double Info; m_posBuff = (m_posBuff < class="num">0 ? class="num">0 : m_posBuff); m_Buff[m_posBuff + class="num">0] = (*Study).GetInfoMouse().Position.Price; Info._datetime = (*Study).GetInfoMouse().Position.dt; m_Buff[m_posBuff + class="num">1] = Info.dValue; Info._int[class="num">0] = (*Study).GetInfoMouse().Position.X; Info._int[class="num">1] = (*Study).GetInfoMouse().Position.Y; m_Buff[m_posBuff + class="num">2] = Info.dValue; Info._char[class="num">0] = ((*Study).GetInfoMouse().ExecStudy == C_Mouse::eStudyNull ? (class="type">char)(*Study).GetInfoMouse().ButtonStatus : class="num">0); m_Buff[m_posBuff + class="num">3] = Info.dValue; }
把鼠标类当翻译器使
这段 EA 代码看着比之前那篇精简不少,但实际跑起来行为一致,只是写法更干净、更不容易在后续扩展时出岔子。核心思路是:不在 EA 里手搓鼠标逻辑,而是把 C_Mouse 当成一个‘翻译器’,它替你解析图表上的鼠标状态,你只管拿结果。 第 16 行 new 出 C_Mouse 时传了短名称 "Indicator Mouse Study",这名字会显示在图表上;第 39 行 OnDeinit 里 delete 掉指针,避免内存泄漏。真正干活的是第 31、32 行——在 CHARTEVENT_MOUSE_MOVE 事件里调用 GetInfoMouse() 取结构体,再用 Comment 把价格和时间戳打印到图表左上角。 如果你在第 16 行把类声明成指标模式,只需在 OnChartEvent 里加一句 DispatchMessage 调用,就能获得和图形指标一致的鼠标响应,主程序结构完全不用改。换句话说,EA 负责分析‘鼠标在哪、在干嘛’,C_Mouse 负责准确汇报,二者分工像翻译官和本地人。 外汇与贵金属市场杠杆高、滑点频繁,用鼠标发单的回放系统只在模拟环境验证过逻辑;上真实账户前务必自己开 MT5 挂这个 EA 看 Comment 输出是否跟手。
class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#include <Market Replay\Auxiliar\C_Mouse.mqh> C_Mouse *mouse = NULL; class="type">int OnInit() { mouse = new C_Mouse("Indicator Mouse Study"); class="kw">return INIT_SUCCEEDED; } class="type">void OnTick() { } 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) { C_Mouse::st_Mouse Infos; class="kw">switch (id) { case CHARTEVENT_MOUSE_MOVE: Infos = (*mouse).GetInfoMouse(); Comment((class="type">class="kw">string)Infos.Position.Price + " :: [" + TimeToString(Infos.Position.dt), "]"); break; } } class="type">void OnDeinit(class="kw">const class="type">int reason) { class="kw">delete mouse; }
◍ 把工具请下神坛
前面几篇铺垫的图表对象、事件循环那些东西,说白了都是给 Chart Trader 铺路。下一步要在系统里把这个能在图上直接发单的应用做出来,而不是接着用 MT5 自带的订单面板——现有那套在自定义逻辑里不够灵活,自己写一套才可控。 你现在觉得前两篇难,多半是因为还没沉下去读 MQL5 的底层。等真把这套图表交易跑通,回头看就像在海平面走了段路,却以为自己在爬山。外汇和贵金属杠杆高、滑点狠,自己写的下单逻辑更得在策略测试器里反复验,别上线才发现问题。 困难章节已经在后面排队了,到时候连鼠标指针的坐标映射都得抠。现在先把基础打牢,后面才不会在更复杂的结构里迷路。