开发回放系统(第 41 部分):启动第二阶段(二)·综合运用
🖱️

开发回放系统(第 41 部分):启动第二阶段(二)·综合运用

(3/3)·多数人重复写同质代码却不自知,本篇用C_Mouse类演示如何换思维提效

含代码示例 第 3/3 篇
不少老手在MQL5里反复写功能近似的鼠标交互代码,却从没停下来看自己的重复劳动。把指标当独立进程而非DLL式函数,后期维护成本会悄悄吃掉你的开发效率。

鼠标移动时水平线的实时吸附

这段事件处理核心在 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 即可验证吸附是否跟手。

MQL5 / C++
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 只是清理宏定义,不影响运行逻辑,但能避免多文件编译时的宏污染。

MQL5 / C++
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 开销地携带交互状态,外汇与贵金属图表高频刷新时这种写法能少掉不少类型转换拖累。

MQL5 / C++
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 输出是否跟手。

MQL5 / C++
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 的底层。等真把这套图表交易跑通,回头看就像在海平面走了段路,却以为自己在爬山。外汇和贵金属杠杆高、滑点狠,自己写的下单逻辑更得在策略测试器里反复验,别上线才发现问题。 困难章节已经在后面排队了,到时候连鼠标指针的坐标映射都得抠。现在先把基础打牢,后面才不会在更复杂的结构里迷路。

让小布替你跑这套
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到重复代码模式标记,把重复劳动交给小布,你专注决策。

常见问题

DLL概念强调模块边界与稳定接口,用这种思维写C_Mouse类能减少跨代码重复,提升复用与稳定性,概率上更利于长期维护。
小布盯盘内置的AIGC可扫描品种页关联的诊断信息,标记高频相似逻辑,帮你快速定位该抽离成公共类的部分。
它把底层鼠标坐标与事件转成业务可读的结构,隔离变化,使回放系统调用方不必关心平台指针细节,可能降低耦合。
外汇贵金属属高风险品类,模拟结果仅反映历史逻辑,实盘可能因滑点流动性突变偏离,倾向以概率视角验证而非轻信。