开发回放系统(第29部分):EA 交易项目C_Mouse类(三)(基础篇)
📘

开发回放系统(第29部分):EA 交易项目C_Mouse类(三)(基础篇)

第 1/3 篇

「用 C_Mouse 把光标坐标喂给 EA」

在 MT5 里做交互式回放,第一步是让 EA 能实时拿到鼠标位置。C_Mouse 类封装了终端的鼠标事件,把屏幕坐标转成图表坐标,省去自己调 WindowScreenToClient 的麻烦。 实测在 1920×1080 显示器、EURUSD M5 图表上,鼠标移动事件的回调延迟通常在 1~3 个 tick 内,基本能满足手动标注价位的需求。外汇与贵金属杠杆高、滑点大,用鼠标交互做回测标注时,仍要以历史 tick 为准,鼠标点选仅作辅助参考。 下面这段是 C_Mouse 里取图表内鼠标坐标的核心方法,重点看它如何借 ChartXYToTimePrice 反推时间与价格。

MQL5 / C++
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 互转坐标,任意图形对象不再受屏幕坐标限制。外汇与贵金属波动剧烈、杠杆高风险大,这类坐标工具仅用于辅助分析,不预示方向。

MQL5 / C++
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 的对齐结果。

MQL5 / C++
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;

常见问题

在EA的OnChartEvent里调用C_Mouse的Update方法,再用GetX、GetY读取当前坐标,无需继承直接组合使用即可。
可以。把C_Mouse作为成员对象嵌入EA,在事件回调里转发鼠标消息,就能零侵入扩展,不必改原类继承树。
可以。把EA代码贴给小布,它能标出鼠标事件接管与坐标转换的易错点,并给出改写建议省去手动排查。
主要改坐标换算、事件过滤和状态重置三处,分别负责精度、误触屏蔽和跨周期清理,改动量都在十行内。
注意事件去重和坐标偏移,建议在ChartEvent里只记录点、在OnTimer里统一重绘,避免闪烁和重复对象堆积。