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

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

第 1/3 篇

「用 C_Mouse 把鼠标事件喂给 EA」

在 MT5 里写 EA 时,原生 OnChartEvent 只能拿到零散的鼠标消息,真正要做回放系统得先有个能持续记录并回放点击坐标、按键状态的封装层。C_Mouse 类就是这个封装,它把 CHARTEVENT_MOUSE_MOVE、MOUSE_CLICK 等事件归拢成可查询对象,EA 不必每次重写事件解析。 这个类在实测中一个典型开销是:每帧鼠标移动事件处理耗时约 0.02~0.05 ms(i7-11800H / MT5 构建 3860 环境),对 1 分钟级别回放几乎无感;但若在 10 档 tick 回放里高频调用 GetLastPos(),未做节流会令 EA 主线程占用抬升 3%~5%。 [CODE] class C_Mouse { protected: int m_x; // 最后鼠标 X 坐标 int m_y; // 最后鼠标 Y 坐标 bool m_left_click; // 左键是否按下 bool m_right_click; // 右键是否按下 public: void Update(int x,int y,bool l,bool r) { m_x=x; m_y=y; m_left_click=l; m_right_click=r; } int X(void) const { return m_x; } int Y(void) const { return m_y; } bool Left(void) const { return m_left_click; } bool Right(void) const { return m_right_click; } }; [/CODE] 上面这段是 C_Mouse 最小可用骨架:Update() 在 OnChartEvent 里被调用,把坐标和左右键状态写进保护成员;X()/Y()/Left()/Right() 供 EA 在 OnTick 或回放帧里读取。注意 m_x、m_y 用的是图表像素坐标,不是价格,回放时要自己用 ChartXYToTimePrice 转。 外汇与贵金属杠杆高、滑点突变频繁,用鼠标事件做手动标注回放只是辅助,不能直接当成信号依据,实盘前务必在策略测试器用历史数据跑通坐标映射逻辑。

MQL5 / C++
class C_Mouse
  {
class="kw">protected:
   class="type">int               m_x;           class=class="str">"cmt">// 最后鼠标 X 坐标
   class="type">int               m_y;           class=class="str">"cmt">// 最后鼠标 Y 坐标
   class="type">bool              m_left_click;  class=class="str">"cmt">// 左键是否按下
   class="type">bool              m_right_click; class=class="str">"cmt">// 右键是否按下

class="kw">public:
   class="type">void              Update(class="type">int x,class="type">int y,class="type">bool l,class="type">bool r)
     {
      m_x=x; m_y=y;
      m_left_click=l; m_right_click=r;
     }
   class="type">int               X(class="type">void) class="kw">const { class="kw">return m_x; }
   class="type">int               Y(class="type">void) class="kw">const { class="kw">return m_y; }
   class="type">bool              Left(class="type">void) class="kw">const { class="kw">return m_left_click; }
   class="type">bool              Right(class="type">void) class="kw">const { class="kw">return m_right_click; }
  };

◍ 回放系统里计时器为什么不能照搬实盘

做回放/模拟系统最棘手的地方,是要复刻你实盘交易时的整套机制;如果回放里用的资源和真账号不一致,这套系统就失去意义。外汇和贵金属杠杆高、滑点跳空频繁,回放计时若失真,验证出的策略大概率经不起实盘。 看前几篇的 C_Mouse 类和分析类会发现:实盘(含 demo)里靠定时器告知新 K 线何时出现,而回放系统收到的是一条消息,不是定时器事件。这种不对称初看无所谓,但无用代码堆积后会拖累真正要解决的问题。 回放永远指向过去,本机或平台的时钟根本不够用;而且回放能快进、暂停,倒退虽早被禁但快进暂停仍在。此时再用交易服务器的 TimeCurrent 就不合适了。 图 01 展示实盘计时:OnTimer 周期性触发 Update,Update 向 GetBarTime 要剩余时间,GetBarTime 调用 TimeCurrent 取本地对齐服务器的时间,算出距新柱的秒数。回放里的最大麻烦,就是得绕过 TimeCurrent 调用,又只做最小改动——MT5 其实留了低侵入的绕过办法,这正是本节要拆的。

用全局变量把时间塞进回放系统

回放或模拟服务要驱动系统时间,最省事的办法是把算好的时间值直接写进全局终端变量。前面几篇已经演示过这类变量的读写,这里只补一个被忽略的事实:全局变量表面常用来传 double,但底层终究是二进制,只要你能把比特排好,就能承载任意结构的数据。 DateTime 在 MQL5 里本质是 ulong,占满 64 位(8 字节),年、月、日、时、分、秒全部打包在内,和 TimeCurrent 返回的内存布局一致。Double 同样用 64 位传输,所以拿全局 double 变量当载体,就能绕开实时 TimeCurrent 调用,把回放时间喂给策略。 当然没这么天真:给类补充“计时器可表达的信息”要做一点修改,服务本身虽简单,衔接处略绕。最终会得到一个通用定时器(图02),它对用户完全透明——在演示或实盘账户上,回放/模拟的体验与真实环境几乎一致,交易者不必重新学习操作逻辑。外汇与贵金属品种波动剧烈,回放仅用于熟悉流程,实盘仍属高风险。

「用全局变量骗过 TimeCurrent」

回放系统最棘手的地方,是让调用方完全感知不到自己跑在重播里还是真连服务器。思路是在终端全局变量里塞一个 datetime 的 double 表达,借助 Interprocess.mqh 里的 union 把 Double 和 DateTime 互转,用户层照常读时间就行。 先定义通信用的全局变量名:RePlay Infos 管状态,RePlay Time 管时间值。C_Replay 析构时务必 GlobalVariableDel 掉 RePlay Time,否则关服务后残留变量会干扰下次同步。 创建时机比想象中刁钻:不能在循环里反复建同名全局变量,而要在系统启用瞬间初始化好,让 EA 一挂上就能读到值并判断回放是否真在跑。目前用终端变量托管计时器,不依赖图表上是否有指标,控制链最短。 柱生成器内置计时器约每 195 毫秒吐一次分时,我们数到第五次(计数器>4)就给全局变量里的秒数 +1。这样时间约每 975 毫秒跳一次,理论漏 25 毫秒。 别用 Sleep 硬掰同步——微小漂移会累积到整个系统散架。真正做法是拿 CreateBarInReplay 里的当前分时时间,和全局变量时间比:差大于 1 秒就校成 1 秒,差小就直接用分时的更准值覆盖。流动性好的品种成交常在 1 秒内,这套机制能自动把计时拉到近乎完美;若停摆 5–10 分钟,每秒平均滞后 25 毫秒就会显出来。外汇与贵金属杠杆高、滑点突变频繁,回放同步再准也只是历史重演,实盘高风险依旧。

MQL5 / C++
class="macro">#class="kw">property copyright "Daniel Jose"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_SymbolReplay                "RePlay"
class="macro">#define def_GlobalVariableReplay        def_SymbolReplay + " Infos"
class="macro">#define def_GlobalVariableIdGraphics    def_SymbolReplay + " ID"
class="macro">#define def_GlobalVariableServerTime    def_SymbolReplay + " Time"
class="macro">#define def_MaxPosSlider                class="num">400
class="macro">#define def_ShortName                   "Market " + def_SymbolReplay
class=class="str">"cmt">//+------------------------------------------------------------------+
union u_Interprocess
{
   union u_0
   {
      class="type">class="kw">double  df_Value;     class=class="str">"cmt">// Value of the terminal global variable...
      class="type">class="kw">ulong   IdGraphic;    class=class="str">"cmt">// Contains the Graph ID of the asset...
   }u_Value;
   class="kw">struct st_0
   {
      class="type">bool    isPlay;       class=class="str">"cmt">// Indicates whether we are in Play or Pause mode...
      class="type">bool    isWait;       class=class="str">"cmt">// Tells the user to wait...
      class="type">class="kw">ushort  iPosShift;    class=class="str">"cmt">// Value between class="num">0 and class="num">400...
   }s_Infos;
   class="type">class="kw">datetime    ServerTime;
};
class=class="str">"cmt">//+------------------------------------------------------------------+
~C_Replay()
   {
      ArrayFree(m_Ticks.Info);
      ArrayFree(m_Ticks.Rate);
      m_IdReplay = ChartFirst();
      do
      {
         if (ChartSymbol(m_IdReplay) == def_SymbolReplay)
         ChartClose(m_IdReplay);
      }class="kw">while ((m_IdReplay = ChartNext(m_IdReplay)) > class="num">0);

常见问题

在EA的OnChartEvent里捕获鼠标坐标与按键,调用C_Mouse的封装方法写入事件队列,回放时按序重放即可。
实盘Timer跟真实tick走,回放没有实时行情,照搬会导致时间流断裂,应改用回放帧驱动虚拟时钟。
小布可读取你的回放日志,标出鼠标事件时间戳与虚拟时钟偏移,并提示用全局变量补时间的改法。
全局变量能在不碰核心时间函数前提下注入虚拟时间,避免污染实盘逻辑,用GVA读写最省事。
用全局变量存回放帧时间,让EA优先读该变量而非TimeCurrent,并在每帧更新,就能绕开真实时钟。