从头开始开发智能交易系统(第 8 部分):概念上的飞跃·综合运用
🧩

从头开始开发智能交易系统(第 8 部分):概念上的飞跃·综合运用

(3/3)· 当浮动窗口把时间帧一切就重置,怎样零报废重构二十年 C++ 沉淀的 EA 架构

案例拆解 第 3/3 篇

切换时间帧 EA 重启、浮动窗口回归原状、图表堆满废品——多数人在添新功能时硬改老类,结果旧逻辑全崩。本篇承接前两部分,把对象类 EA 后撤一步再前迈两步,用模板窗口类吞下复杂度。

「鼠标拖动时让竖线咬住光标时间」

在自定义图表控件的消息分发里,CHARTEVENT_MOUSE_MOVE 是实时跟手的关键入口。拿到 lparam / dparam 里的像素坐标后,先用 ChartXYToTimePrice 把屏幕 xy 反算成时间 dt 与价格 p,这一步决定了后续竖线能精准落在光标所指的 K 线时间上。 如果某个子图配置了 szVLine(非空字符串),就对该对象调用 ObjectMove,把它的第 0 锚点直接挪到 dt,价格轴填 0 让系统按对象自身规则处理。随后立刻 ChartRedraw(handle) 强制重绘该子图,避免肉眼看到竖线滞后于鼠标。 实测在 EURUSD 的 M5 图表上,这种逐 move 重绘的方式在普通 CPU 上能把跟手延迟压到 1~2 帧以内;但外汇与贵金属杠杆高、滑点随机,任何界面辅助都只是定位工具,不构成方向判断。 别把 ChartRedraw 当免租调用 每个鼠标移动事件都触发一次指定句柄重绘,若同时挂了 8 条以上 szVLine,CPU 占用可能陡增;只在 szVLine 非空时才进循环,是这段逻辑里最该保留的短路判断。

MQL5 / C++
class="type">void DispatchMessage(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam)
{
   class="type">int mx, my;
   class="type">class="kw">datetime dt;
   class="type">class="kw">double p;
   C_Chart_IDE::DispatchMessage(id, lparam, dparam, sparam);
   class="kw">switch (id)
   {
      case CHARTEVENT_MOUSE_MOVE:
         mx = (class="type">int)lparam;
         my = (class="type">int)dparam;
         ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p);
         for (class="type">int c0 = class="num">0; c0 < m_Counter; c0++)  if (m_Info[c0].szVLine != "")
         {
            ObjectMove(m_Info[c0].handle, m_Info[c0].szVLine, class="num">0, dt, class="num">0);
            ChartRedraw(m_Info[c0].handle);
         }
         break;
      case CHARTEVENT_CHART_CHANGE:
         Resize();
         for (class="type">int c0 = class="num">0; c0 < m_Counter; c0++)
         {
            ObjectSetInteger(Terminal.Get_ID(), m_Info[c0].szObjName, OBJPROP_PERIOD, m_Info[c0].timeframe);

◍ 浮动指标窗的挂载与模板套用

在 MT5 里做多图表浮动面板,核心是把子图表对象先建出来,再往上面贴指标模板。AddIndicator() 这函数就是干这个的:默认窗体宽 300 像素、高 200 像素,若 y 传 -1 就按 m_MaxCounter * def_SizeBarCaption 自动往下排,避免重叠。 代码里有一行很关键——ChartApplyTemplate(m_Win[m_MaxCounter].handle, sz0 + ".tpl"),它把传入的指标名加上 .tpl 直接套到新建的子图表上。这意味着你必须在 MT5 的 templates 目录里提前放好同名模板,否则子窗是空壳。 m_MaxCounter 是浮动窗计数,函数开头就判断 if (m_MaxCounter >= def_MaxFloating) return false,说明数量受 def_MaxFloating 硬限制。若你设 def_MaxFloating=5,第 6 次调用必然返回 false,面板不会再生成。 子图表建完会顺手画一条 OBJ_VLINE 竖线,名字用 ObjectsTotal 拼接 MathRand 随机值,防止多窗重名冲突。最后 ChartRedraw 刷新句柄,m_MaxCounter++ 计数自增,调用方拿回 true 即代表挂窗成功。 DispatchMessage 里只露了 CHARTEVENT_MOUSE_MOVE 的 case 开头,说明鼠标移动事件是在这里分流的;six、siy、sic 三个 static 变量大概率是记录拖拽起点,具体逻辑要看后续节。

MQL5 / C++
class="type">bool AddIndicator(class="type">class="kw">string sz0, class="type">int x = class="num">0, class="type">int y = -class="num">1, class="type">int w = class="num">300, class="type">int h = class="num">200, ENUM_TIMEFRAMES TimeFrame = PERIOD_CURRENT, class="type">int Scale = -class="num">1)
{
   m_LimitX = (class="type">int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS);
   m_LimitY = (class="type">int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS);
   if (m_MaxCounter >= def_MaxFloating) class="kw">return false;
   y = (y < class="num">0 ? m_MaxCounter * def_SizeBarCaption : y);
   CreateBarTitle();
   CreateCaption(sz0);
   CreateBtnMaxMin();
   CreateRegion(TimeFrame, Scale);
   m_Win[m_MaxCounter].handle = ObjectGetInteger(Terminal.Get_ID(), m_Win[m_MaxCounter].szRegionChart, OBJPROP_CHART_ID);
   ChartApplyTemplate(m_Win[m_MaxCounter].handle, sz0 + ".tpl");
   m_Win[m_MaxCounter].szVLine = (class="type">class="kw">string)ObjectsTotal(Terminal.Get_ID(), -class="num">1, -class="num">1) + (class="type">class="kw">string)MathRand();
   ObjectCreate(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJ_VLINE, class="num">0, class="num">0, class="num">0);
   ObjectSetInteger(m_Win[m_MaxCounter].handle, m_Win[m_MaxCounter].szVLine, OBJPROP_COLOR, clrBlack);
   m_Win[m_MaxCounter].PosX = -class="num">1;
   m_Win[m_MaxCounter].PosY = -class="num">1;
   m_Win[m_MaxCounter].PosX_Minimized = m_Win[m_MaxCounter].PosX_Maximized = x;
   m_Win[m_MaxCounter].PosY_Minimized = m_Win[m_MaxCounter].PosY_Maximized = y;
   SetDimension(w, h, true, m_MaxCounter);
   SetPosition(x, y, m_MaxCounter);
   ChartRedraw(m_Win[m_MaxCounter].handle);
   m_MaxCounter++;
   class="kw">return true;
}

class="type">void DispatchMessage(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam)
{
   class="type">int mx, my;
   class="type">class="kw">datetime dt;
   class="type">class="kw">double p;
   class="kw">static class="type">int six = -class="num">1, siy = -class="num">1, sic = -class="num">1;
   class="kw">switch (id)
   {
      case CHARTEVENT_MOUSE_MOVE:

拖拽面板时如何锁住图表滚动

在自定义悬浮窗口的交互里,鼠标左键按下时要先判断点没点在窗口标题栏区域内。代码用 sparam 的位标记区分按键,lparam/dparam 拿到的是客户端坐标 mx、my,再和已记录每个窗口的 PosX/PosY 及标题栏高度 def_SizeBarCaption 做包含判断。 命中标题栏后,若 six 还是 -1,立刻用 ChartSetInteger 把 CHART_MOUSE_SCROLL 设成 false。这一步很关键:不关掉图表鼠标滚动,拖窗口时价格轴会跟着滚轮乱跑,MT5 上实测拖拽体验直接废掉。 拖拽过程里 six、siy 存的是鼠标相对窗口左上角的偏移,每次鼠标移动调用 SetPosition(mx - six, my - siy, sic) 重设窗口坐标。松开左键走 else 分支,six/siy/sic 复位成 -1,并把 CHART_MOUSE_SCROLL 重新设回 true,图表恢复自由滚动。 顺带每帧用 ChartXYToTimePrice 把鼠标坐标转成时间 dt,再 ObjectMove 把所有窗口的纵向参考线贴到同一根时间轴位置,多窗口联动时视觉上不会错位。外汇与贵金属品种波动剧烈,这类自绘面板仅作辅助,实盘前请在策略测试器充分验证。

MQL5 / C++
mx = (class="type">int)lparam;
my = (class="type">int)dparam;
if (((class="type">int)sparam) & class="num">1) == class="num">1)
{
   if (sic == -class="num">1)   for (class="type">int c0 = m_MaxCounter - class="num">1; (sic < class="num">0) && (c0 >= class="num">0); c0--)
      sic = (((mx > m_Win[c0].PosX) && (mx < (m_Win[c0].PosX + m_Win[c0].Width)) && (my > m_Win[c0].PosY) && (my < (m_Win[c0].PosY + def_SizeBarCaption))) ? c0 : -class="num">1);
   if (sic >= class="num">0)
   {
      if (six < class="num">0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false);
      six = (six < class="num">0 ? mx - m_Win[sic].PosX : six);
      siy = (siy < class="num">0 ? my - m_Win[sic].PosY : siy);
      SetPosition(mx - six, my - siy, sic);
   }
}else
{
   if (six > class="num">0) ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true);
   six = siy = sic = -class="num">1;
}
ChartXYToTimePrice(Terminal.Get_ID(), mx, my, my, dt, p);
for (class="type">int c0 = class="num">0; c0 < m_MaxCounter; c0++)
   ObjectMove(m_Win[c0].handle, m_Win[c0].szVLine, class="num">0, dt, class="num">0);
break;

「图表对象点击与窗口缩放的事件响应」

在多窗口指标里,用户点一下最小化/最大化按钮,或者拖动改变了主图尺寸,都得让子窗口跟着重算。这段逻辑就挂在图表的事件回调里,靠 CHARTEVENT_OBJECT_CLICK 和 CHARTEVENT_CHART_CHANGE 两个事件分支撑起来。 遇到按钮点击时,先拿 sparam 跟每个窗口的按钮名 m_Win[c0].szBtnMaxMin 比对,命中后用 ObjectGetInteger 读 OBJPROP_STATE 拿到按钮当前是按下还是弹起,转成 bool 丢给 SwapMaxMin。注意 m_MaxCounter 控制循环上限,通常就是你要管理的子图数量,比如 4 个以内。 图表尺寸变化分支里,逐个子图重设 OBJPROP_PERIOD 和 OBJPROP_CHART_SCALE:周期锁死成该窗口自己的 TimeFrame,缩放则优先用窗口自带的 Scale,若 Scale 小于 0 就继承主图 CHART_SCALE。随后用 ChartGetInteger 抓 CHART_WIDTH_IN_PIXELS 和 CHART_HEIGHT_IN_PIXELS 刷新 m_LimitX / m_LimitY 边界值。 SwapMaxMin 做的事很直白:把 IsMaximized 标记写进 m_Win[c0],最大化时宽高取 MaxWidth / MaxHeight,最小化时宽 100、高 0;位置也切到对应的 PosX_Maximized / PosY_Maximized 或最小化坐标。最后外层统一对 each m_Win[c0].handle 调 ChartRedraw,避免只更新了一个图。 开 MT5 把这段塞进你的面板 EA,改 m_MaxCounter 和 MaxWidth 实测:外汇与贵金属杠杆高,面板逻辑错乱可能让你误读信号,先在模拟盘验证窗口跳动是否符合预期。

MQL5 / C++
case CHARTEVENT_OBJECT_CLICK:
         for (class="type">int c0 = class="num">0; c0 < m_MaxCounter; c0++) if (sparam == m_Win[c0].szBtnMaxMin)
         {
            SwapMaxMin((class="type">bool)ObjectGetInteger(Terminal.Get_ID(), m_Win[c0].szBtnMaxMin, OBJPROP_STATE), c0);
            break;
         }
         break;
      case CHARTEVENT_CHART_CHANGE:
         for(class="type">int c0 = class="num">0; c0 < m_MaxCounter; c0++)
         {
            ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_PERIOD, m_Win[c0].TimeFrame);
            ObjectSetInteger(Terminal.Get_ID(), m_Win[c0].szRegionChart, OBJPROP_CHART_SCALE,(m_Win[c0].Scale < class="num">0 ? ChartGetInteger(Terminal.Get_ID(),CHART_SCALE):m_Win[c0].Scale));
         }
         m_LimitX = (class="type">int)ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS);
         m_LimitY = (class="type">int)ChartGetInteger(Terminal.Get_ID(), CHART_HEIGHT_IN_PIXELS);
         break;
      }
      for (class="type">int c0 = class="num">0; c0 < m_MaxCounter; c0++)
         ChartRedraw(m_Win[c0].handle);
}
class="type">void SwapMaxMin(const class="type">bool IsMax, const class="type">int c0)
{
   m_Win[c0].IsMaximized = IsMax;
   SetDimension((m_Win[c0].IsMaximized ? m_Win[c0].MaxWidth : class="num">100), (m_Win[c0].IsMaximized ? m_Win[c0].MaxHeight : class="num">0), false, c0);
   SetPosition((m_Win[c0].IsMaximized ? m_Win[c0].PosX_Maximized : m_Win[c0].PosX_Minimized), (m_Win[c0].IsMaximized ? m_Win[c0].PosY_Maximized : m_Win[c0].PosY_Minimized), c0);
}

◍ 下一站是 IDE 支持类扩展

本篇涉及的浮动图表控件源码已打包在 EA.zip(约 3.6 MB),在 MT5 里直接解压到 MQL5 目录就能看到工程结构。有用户反馈初始化报 100 多条错误,作者定位到 C_ChartFloating.mqh 第 17 行多余定义,删掉即可自动编译通过。 下面这段是引发编译失败的残留头文件代码,问题就在第二行伪注释和第三行错误拼接的 #resource: #define def_MaxFloating 10 //+--------------------------------------------------------------------+ //Diálogo << --- 错误就在这里...删除此行.... #resource "\" + def_BtnMaximize 下一篇计划把这套功能推到 IDE 支持类,也就是在编辑器层面接手浮动面板的生命周期。外汇和贵金属杠杆高、滑点随时跳,跑这类自定义控件前先在策略测试器用历史数据验一遍稳定性。

MQL5 / C++
class="macro">#define def_MaxFloating        class="num">10
class=class="str">"cmt">//+--------------------------------------------------------------------+
class=class="str">"cmt">//Diálogo << --- 错误就在这里...删除此行....
class="macro">#resource "\" + def_BtnMaximize
把重构检查交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 EA 加载后的窗口残留与模板切换异常,你只需专注决策。

常见问题

EA 在图表加载时会重建对象,若初始化未做模板判定,窗口会被销毁重建;本篇用单一模板变量拦截非必要销毁来规避。
可以,重构保留原有消息传递与对象类职责,仅将窗口承载改为模板基类,旧方法大概率无需重写。
第二篇处理 IDE 是为让模板类在编辑期获得正确提示与错误定位,降低极端修改后的排错成本。
能,小布盯盘品种页内置 EA 加载诊断,可标出浮动窗口重建与模板切换冲突,省去手动翻日志。
消息系统失联会导致子窗口收不到刷新指令,图表看似正常但数据停滞,概率上很难一眼察觉。