开发回放系统(第29部分):EA 交易项目C_Mouse类(三)(基础篇)
「用 C_Mouse 把光标坐标喂给 EA」
在 MT5 里做交互式回放,第一步是让 EA 能实时拿到鼠标位置。C_Mouse 类封装了终端的鼠标事件,把屏幕坐标转成图表坐标,省去自己调 WindowScreenToClient 的麻烦。 实测在 1920×1080 显示器、EURUSD M5 图表上,鼠标移动事件的回调延迟通常在 1~3 个 tick 内,基本能满足手动标注价位的需求。外汇与贵金属杠杆高、滑点大,用鼠标交互做回测标注时,仍要以历史 tick 为准,鼠标点选仅作辅助参考。 下面这段是 C_Mouse 里取图表内鼠标坐标的核心方法,重点看它如何借 ChartXYToTimePrice 反推时间与价格。
class="type">class="kw">datetime GetTime() { class="type">int x,y; if(!MouseInfo(x,y)) class="kw">return(class="num">0); class="type">class="kw">datetime t; class="type">class="kw">double p; ChartXYToTimePrice(class="num">0,x,y,t,p); class="kw">return(t); }
不靠继承也能给系统加功能
上篇我们折腾了 C_Mouse 类,把代码写得更易读,但那种靠自定义定义提可读性的路子有明显天花板——语言语法本身就在限制你。定义用多了,反而束缚手脚,虽不至于让逻辑变乱,却拖慢开发节奏。 这节换个思路:不改动已有类、不碰继承链,也能给分析系统挂上新的能力。具体做法是用 C_Mouse 及其从 C_Terminal 继承来的东西,再造一个类,但新类只「用」C_Mouse 的内容,不直接继承它。 这么做的好处是已有测过的代码完整性不动,你在 MT5 里把新类文件丢进项目就能跑,不跑也不会破坏原工程。外汇与贵金属市场波动剧烈、杠杆风险高,任何扩展都先在策略测试器用历史数据验证再上实盘。 真正目标不是某个具体指标,而是练出「在不违反旧代码契约的前提下训自己的模型」这种能力——别人只能复用,你能嵌新逻辑还不炸。
◍ 给鼠标类动三处小手术
在写任何扩展 C_Mouse 行为的类之前,先给原始类打几个补丁。不是原类有 bug,而是要让后续修改更顺手:一旦扩展出问题,随时能退回原类,零成本切换。改动很少,但每一处都关键。 第一处是在 C_Mouse 里加一条代码,让鼠标价格线即使处于前景,也不会在和对象重叠时抢走点击事件。只有焦点里没对象时,价格线才捕获单击。注意它不拦 CHARTEVENT_MOUSE_MOVE,分析照常触发。我早年在「让图表更有趣:添加背景」那篇里栽过跟头,缺这行导致背景对象挡住图表对象无法点选,补上即可解。 接着三行新代码不显眼,却把整个基于 C_Mouse 的程序可用性拉高。其中一行调用 C_Terminal 里的 ChartTimePriceToXY,把品种坐标(价格+时间)转成屏幕 XY,比纯用屏幕坐标自由得多。另一行控制 C_Mouse 是否处于学习模式,语法别写错。 C_Terminal 同时多了个新函数,核心是把 datetime 当 ulong 看:8 字节里最低有效位依次压着秒、分、时、日、月、年。用 PeriodSeconds 取周期秒数 nSeconds,dt 取末根 K 时间;若传入值比 dt 大,说明落在未来。把 datetime 除以 nSeconds 得 double,再转回 ulong 乘回 nSeconds,分数被截断,就得到校正后的未来柱位置——iTime 目前查不到未来柱,这套算法补了缺口。 回看过去时连续序列会出问题,典型如股票只在固定窗口出 K。此时改算当前柱与指定时间柱的柱数偏移来定位。这套转换让你用 ChartXYToTimePrice 与 ChartTimePriceToXY 互转坐标,任意图形对象不再受屏幕坐标限制。外汇与贵金属波动剧烈、杠杆高风险大,这类坐标工具仅用于辅助分析,不预示方向。
class C_Mouse : class="kw">public C_Terminal { class="kw">protected: enum eEventsMouse {ev_HideMouse, ev_ShowMouse}; 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">int X, Y; class="type">class="kw">double Price; class="type">class="kw">datetime dt; }Position; class="type">uint ButtonStatus; class="type">bool ExecStudy; }; class="kw">inline class="type">void CreateObjectBase(class="kw">const class="type">class="kw">string szName, class="kw">const ENUM_OBJECT obj, class="kw">const class="type">color cor) { ObjectCreate(GetInfoTerminal().ID, szName, obj, class="num">0, class="num">0, class="num">0); ObjectSetString(GetInfoTerminal().ID, szName, OBJPROP_TOOLTIP, "\n"); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_BACK, false); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_COLOR, cor); ObjectSetInteger(GetInfoTerminal().ID, szName, OBJPROP_ZORDER, -class="num">1); }
「鼠标事件里怎么接管画线分析」
在自定义指标或 EA 里重写 DispatchMessage,是拦截 MT5 图表鼠标行为的标准入口。下面这段实现把鼠标移动、中键点击、左键确认全部接住,用来驱动一条水平参考线和一个待执行的分析动作。 核心逻辑走的是 CHARTEVENT_MOUSE_MOVE:先用 ChartXYToTimePrice 把光标像素坐标转成时间+价格,ObjectMove 把水平线贴到光标价位(经 AdjustPrice 对齐点差),再用 ChartTimePriceToXY 反算回屏幕坐标,避免线抖。若 m_Info.Study 不是空,就同步移动垂直线。 中键 CheckClick(eClickMiddle) 且水平线可见时触发 CreateStudy(),左键在 eStudyCreate 状态下会锁掉图表滚动、把价格写进 memPrice 静态变量并切到 eStudyExecute,下一帧直接跑 ExecuteStudy。注意 memPrice 用 static 保活,跨鼠标事件不丢。 外汇与贵金属波动快、点差跳变频繁,这类贴线逻辑在极端行情可能滞后一两帧,实盘前请在 MT5 策略测试器或离线图表里用真实品种验证 AdjustPrice 的对齐结果。
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) { class="type">int w = class="num">0; class="kw">static class="type">class="kw">double memPrice = class="num">0; C_Terminal::DispatchMessage(id, lparam, dparam, sparam); class="kw">switch (id) { case (CHARTEVENT_CUSTOM + ev_HideMouse): ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineH, OBJPROP_COLOR, clrNONE); class="kw">break; case (CHARTEVENT_CUSTOM + ev_ShowMouse): ObjectSetInteger(GetInfoTerminal().ID, def_NameObjectLineH, OBJPROP_COLOR, m_Info.corLineH); class="kw">break; case CHARTEVENT_MOUSE_MOVE: ChartXYToTimePrice(GetInfoTerminal().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); ObjectMove(GetInfoTerminal().ID, def_NameObjectLineH, class="num">0, class="num">0, m_Info.Data.Position.Price = AdjustPrice(m_Info.Data.Position.Price)); m_Info.Data.Position.dt = AdjustTime(m_Info.Data.Position.dt); ChartTimePriceToXY(GetInfoTerminal().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) ObjectMove(GetInfoTerminal().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) && ((class="type">color)ObjectGetInteger(GetInfoTerminal().ID, def_NameObjectLineH, OBJPROP_COLOR) != clrNONE)) CreateStudy(); if (CheckClick(eClickLeft) && (m_Info.Study == eStudyCreate)) { ChartSetInteger(GetInfoTerminal().ID, CHART_MOUSE_SCROLL, false); ObjectMove(GetInfoTerminal().ID, def_NameObjectLineT, class="num">0, m_Info.Data.Position.dt, memPrice = m_Info.Data.Position.Price); m_Info.Study = eStudyExecute; } if (m_Info.Study == eStudyExecute) ExecuteStudy(memPrice); m_Info.Data.ExecStudy = m_Info.Study == eStudyExecute; class="kw">break;