开发回放系统(第 35 部分):进行调整 (一)·进阶篇
🛠️

开发回放系统(第 35 部分):进行调整 (一)·进阶篇

(2/3)· 一个 CTRL/SHIFT 组合键为何让重放器谎报服务繁忙?本篇用最小改动定位 lparam 传递错位

偏理论进阶 第 2/3 篇
在回放系统里按下 CTRL 或 SHIFT 下单却跳出“服务繁忙”,多数人会以为是后台卡死。其实这只是 EventChartCustom 的 lparam 传值错位造成的误报,改一行就能消掉。

「让 C_Mouse 在指标和控制 EA 里不再打架」

回放或模拟系统里有个隐蔽坑:用户没动鼠标,位置指示器却可能被系统悄悄挪走。光加个「用户是否在拖滑块」的判断不够,根子在于 C_Mouse 类原本只给 EA 用,现在要同时喂给控制指标和 EA,两边不知情地共用同一套对象操控代码,MT5 会被迫对同一个对象函数发两次调用。 解决办法是在类构造阶段就分清「完整模式」还是「轻量模式」。完整模式下 EA 会用到图表图形对象(价格线、趋势线等),控制指标里不该建这些;通过构造函数重载默认颜色参数来识别,比运行时到处写检查更干净,也避开了误用类的风险。 构造函数里用 clrNONE 作默认色,调用时若三个颜色都不是 clrNONE,就置 m_Mem.IsFull = true 并创建水平线;否则不建。析构函数先查 Terminal 指针有效性,无效直接 return,避免对野指针操作。CreateStudy 和 ExecuteStudy 等方法也都加了 if(m_Mem.IsFull) 闸门,非完整模式跳过对象创建与操控。 别小看这层检查。MetaTrader 5 不会因为对象已存在就跳过重复调用,EA 和指标各调一次就是两倍开销。市场回放追求的是和真实行情一致的时序,哪怕单次只多耗几毫秒甚至纳秒,柱形生成也会累积出可见延迟。外汇与贵金属模拟本身高风险,时序失真会让回测结论偏向错误。 目录结构变动只影响 include 路径,EA 侧业务代码暂未动;但控制指标那边要 deeper 改,留到下一节拆。

MQL5 / C++
class="kw">struct st_Mem
{
class="type">bool      CrossHair,
          IsFull;
  class="type">class="kw">datetime dt;
}m_Mem;
C_Mouse(C_Terminal *arg, class="type">color corH = clrNONE, class="type">color corP = clrNONE, class="type">color corN = clrNONE)
{
  Terminal = arg;
  if (CheckPointer(Terminal) == POINTER_INVALID) SetUserError(C_Terminal::ERR_PointerInvalid);
  if (_LastError != ERR_SUCCESS) class="kw">return;
  m_Mem.CrossHair = (class="type">bool)ChartGetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL);
  ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_MOUSE_MOVE, true);
  ChartSetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL, false);
  ZeroMemory(m_Info);
  m_Info.corLineH  = corH;
  m_Info.corTrendP = corP;
  m_Info.corTrendN = corN;
  m_Info.Study = eStudyNull;
  if (m_Mem.IsFull = (corP != clrNONE) && (corH != clrNONE) && (corN != clrNONE))
    def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH);
}
~C_Mouse()
{
  if (CheckPointer(Terminal) == POINTER_INVALID) class="kw">return;
  ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_OBJECT_DELETE, class="num">0, false);
  ChartSetInteger(def_InfoTerminal.ID, CHART_EVENT_MOUSE_MOVE, false);
  ChartSetInteger(def_InfoTerminal.ID, CHART_CROSSHAIR_TOOL, m_Mem.CrossHair);
  ObjectsDeleteAll(def_InfoTerminal.ID, def_MousePrefixName);
}
class="type">void CreateStudy(class="type">void)
{
  if (m_Mem.IsFull)
  {
    def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineV, OBJ_VLINE, m_Info.corLineH);
    def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineT, OBJ_TREND, m_Info.corLineH);
    ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineT, OBJPROP_WIDTH, class="num">2);
    CreateObjectInfo(class="num">0, class="num">0, def_NameObjectStudy);
  }
  m_Info.Study = eStudyCreate;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ExecuteStudy(const class="type">class="kw">double memPrice)
{

◍ 鼠标测距的刷新与对象重绘

这段逻辑处理的是在图表上用鼠标拉出一段价格距离时,标签和连线如何跟着光标实时重算。核心变量 v1 等于当前鼠标价格减去记忆价格 memPrice,正负决定涨跌色:负值走 corTrendN,正值走 corTrendP。 若左键未命中 CheckClick,说明测量中止,Study 置空并恢复图表鼠标滚轮,已存在的临时对象被 ObjectsDeleteAll 按前缀清理。反之当内存数据已满(m_Mem.IsFull),就进入实时标注分支。 标签文本用 StringFormat 拼出「绝对价差 + 隔开的K线数 + 百分比振幅」,其中 K线数由 Bars(PERIOD_CURRENT, m_Mem.dt, 鼠标时间)-1 得出,百分比是 MathAbs(v1/memPrice)*100.0。GetDimensionText 先量出文字宽高 w、h,再写回 OBJPROP_XSIZE / YSIZE,并把标签钉在鼠标左上角(X 减 w,Y 按涨跌偏移 1 或 h)。 连线 ObjectMove 到鼠标位置,颜色同样按 memPrice 与鼠标价大小切换。下方 DispatchMessage 接自定义事件:ev_HideMouse 把水平辅助线设成 clrNONE 隐藏,ev_ShowMouse 则对应显示,静态 memPrice 在跨事件间保留锚定价。外汇与贵金属波动大,这类视觉辅助只作参考,实盘仍以经纪商报价为准,杠杆品种风险偏高。

MQL5 / C++
class="type">class="kw">double v1 = GetInfoMouse().Position.Price - memPrice;
class="type">int w, h;
if (!CheckClick(eClickLeft))
{
   m_Info.Study = eStudyNull;
   ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, true);
   if (m_Mem.IsFull) ObjectsDeleteAll(def_InfoTerminal.ID, def_MousePrefixName + "T");
}else if (m_Mem.IsFull)
{
   class="type">class="kw">string sz1 = StringFormat(" %." + (class="type">class="kw">string)def_InfoTerminal.nDigits + "f [ %d ] %class="num">02.02f%% ",
   MathAbs(v1), Bars(def_InfoTerminal.szSymbol, PERIOD_CURRENT, m_Mem.dt, GetInfoMouse().Position.dt) - class="num">1, MathAbs((v1/ memPrice) * class="num">100.0)));
   GetDimensionText(sz1, w, h);
   ObjectSetString(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_TEXT, sz1);
   ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_BGCOLOR, (v1 < class="num">0 ? m_Info.corTrendN : m_Info.corTrendP));
   ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_XSIZE, w);
   ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_YSIZE, h);
   ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_XDISTANCE, GetInfoMouse().Position.X - w);
   ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectStudy, OBJPROP_YDISTANCE, GetInfoMouse().Position.Y - (v1 < class="num">0 ? class="num">1 : h));
   ObjectMove(def_InfoTerminal.ID, def_NameObjectLineT, class="num">1, GetInfoMouse().Position.dt, GetInfoMouse().Position.Price);
   ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineT, OBJPROP_COLOR, (memPrice > GetInfoMouse().Position.Price ? m_Info.corTrendN : m_Info.corTrendP));
}
m_Info.Data.ButtonStatus = eKeyNull;
}
class="kw">virtual class="type">void DispatchMessage(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, 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;
   class="kw">switch (id)
   {
      case (CHARTEVENT_CUSTOM + ev_HideMouse):
         if (m_Mem.IsFull) ObjectSetInteger(def_InfoTerminal.ID, def_NameObjectLineH, OBJPROP_COLOR, clrNONE);
         break;
      case (CHARTEVENT_CUSTOM + ev_ShowMouse):

鼠标事件驱动的研究对象联动

这段处理集中在 CHARTEVENT_MOUSE_MOVE 与 OBJECT_DELETE 两类事件上,核心是把光标位置实时翻译成价格与时间的图形坐标。 鼠标移动时先用 ChartXYToTimePrice 把 X/Y 像素转成 dt 与 Price,再用 AdjustPrice、AdjustTime 做步长吸附,避免落在非法报价上。若内存标记 m_Mem.IsFull 为真,就通过 ObjectMove 把水平线推到光标价、把垂直线推到光标时间。 中键点击会触发 CreateStudy,条件是未满载或水平线颜色不是 clrNONE;左键在 eStudyCreate 状态下则关掉图表滚动、把参考线钉在鼠标处并切到 eStudyExecute,随后 ExecuteStudy 跑计算。 OBJECT_DELETE 分支只防一种情况:水平线被手动删了,且当前处于满载状态,就立刻用 CreateObjectGraphics 按原色重建,保证后续研究不丢基准。外汇与贵金属波动剧烈,这类交互对象在高波动时段可能频繁重绘,建议在 MT5 策略测试器外挂 EA 手动拖一下水平线验证吸附逻辑。

MQL5 / C++
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">color)ObjectGetInteger(def_InfoTerminal.ID, def_NameObjectLineH, OBJPROP_COLOR) != clrNONE)) CreateStudy();
        if (CheckClick(eClickLeft) && (m_Info.Study == eStudyCreate))
          {
            ChartSetInteger(def_InfoTerminal.ID, CHART_MOUSE_SCROLL, false);
            if (m_Mem.IsFull) ObjectMove(def_InfoTerminal.ID, def_NameObjectLineT, class="num">0, m_Mem.dt = GetInfoMouse().Position.dt, memPrice = GetInfoMouse().Position.Price);
            m_Info.Study = eStudyExecute;
          }
        if (m_Info.Study == eStudyExecute) ExecuteStudy(memPrice);
        m_Info.Data.ExecStudy = m_Info.Study == eStudyExecute;
        break;
      case CHARTEVENT_OBJECT_DELETE:
        if ((m_Mem.IsFull) && (sparam == def_NameObjectLineH)) def_AcessTerminal.CreateObjectGraphics(def_NameObjectLineH, OBJ_HLINE, m_Info.corLineH);
        break;
    }
}

「给控制指示器动刀:C_Control 类重构要点」

改控制指示器,核心是先动它依赖的 C_Control 类。现在头两行 include 换成了 C_Terminal 和 C_Mouse 两个类文件,而不是沿用继承自 C_Mouse 的 C_Study 类——因为控制指示器本就不负责分析,那是 EA 的活儿,现阶段让它直接挂 C_Mouse 更干净。 C_Control 原先有个私有全局变量记图表窗口索引,这版直接删了,改由 C_Terminal 类接管。所有引用旧索引的地方清掉,只在真需要时用宏定义绕到 C_Terminal 去取。构造函数现在收一个 C_Terminal 指针,透传给 C_Mouse,但 C_Mouse 不再以完整模式起对象,纯当代码复用底座。 指针有效性检查被加进构造和析构:构造若失败,析构靠标志位知道原因,避免乱摸无效内存。原来在类里触发鼠标移动、通知平台删对象的几行也删了——C_Mouse 和 C_Terminal 已各自管掉,留着会在某些场景出诡异行为。重复代码在 MT5 里是维护雷区。 消息处理里,光靠 CHARTEVENT_MOUSE_MOVE 不够,得让 C_Mouse 先跑完它的活儿。EA 和指示器虽都用 C_Mouse,但不互换鼠标数据:走 MT5 内部功能传,脱离重放/模拟就易崩;走 DLL 共享内存又绑死平台。所以这里各自独立,控制指示器只问 C_Mouse「刚才发生啥」再决策。 左键按下且滑块对象在图上(暂停模式)时,命中滑块就允许拖拽,体验和以前一致。另加了个方法让类感知「用户是否正在分析」,本地鼠标事件此时全部忽略。这类便利短期不明显,但把重放系统搭起来后,你会发现 MQL5 能玩的比多数人想的深。

MQL5 / C++
class="macro">#include "..\Auxiliar\C_Terminal.mqh"
class="macro">#include "..\Auxiliar\C_Mouse.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_AcessTerminal(*Terminal)
class="macro">#define def_InfoTerminal def_AcessTerminal.GetInfoTerminal()
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_Controls : class="kw">protected C_Mouse
{
			class="kw">protected:
				enum EventCustom {ev_WaitOn, ev_WaitOff};
		class="kw">private :
class=class="str">"cmt">//+------------------------------------------------------------------+
			class="type">class="kw">string	m_szBtnPlay;
			class="type">bool	m_bWait;
			class="kw">struct st_00
			{
				class="type">class="kw">string	szBtnLeft,
						szBtnRight,
						szBtnPin,
						szBarSlider,
						szBarSliderBlock;
				class="type">int		posPinSlider,
						posY,
						Minimal;
			}m_Slider;
			C_Terminal *Terminal;
C_Controls(C_Terminal *arg)
		:C_Mouse(arg),
		 m_bWait(false)
	{
把事件排查交给小布盯盘
这类自定义事件参数错乱,小布盯盘的 AIGC 诊断已内置,打开对应品种页即可看到事件流异常标记,你只需确认改动是否生效。

常见问题

MQL5 运行在终端沙箱内,递归深度与指针语义受限,某些交互会触发保护式终止,需改用类封装与消息传递规避。
可能让图表自定义事件被错误路由,造成鼠标类响应延迟或柱形创建信号丢失,概率随交互复杂度上升。
新交互类型引入后旧代码路径被触发,隐藏的参数字节错位在高频事件下显现,属累积型缺陷。
小布盯盘内置了事件流与参数异常看板,可标记 Service Busy 类误报来源,但具体代码改动仍需你在 IDE 内完成。
控制指示器依赖同一套自定义事件通道,lparam 修正后指示器状态同步更准,避免模拟与实盘视图撕裂。