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

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

第 3/3 篇

◍ 解析对象节点时顺手锚定浮动窗坐标

在读取图表对象配置文本时,循环会以 </object> 作为结束标记逐行扫描。遇到 name 字段就把值转大写,再去和预设的 IDE 消息枚举(从 eRESULT 到 eEDIT_STOP)比对,找不到且当前处于浮动模式就直接返回 true,相当于跳过不匹配的对象。 这段逻辑里有个容易忽略的落点:当匹配索引正好等于 m_IDEStruct.Index 时,代码会把窗口坐标加 3 像素偏移写回结构体,Y 方向还额外叠了 def_SizeBarCaption + 3。这说明浮动面板的锚点不是窗口左上角,而是避开标题栏后的内缩位置。 struct IDE_Struct 只存 X、Y、Index 和 IsMaximized 四个字段,非常轻量。你在 MT5 里接这套解析时,若发现浮动窗总是偏出预期几像素,优先查 def_SizeBarCaption 的实际值,外汇与贵金属图表多屏布局下这种偏移会被放大,属高风险调试项。

MQL5 / C++
class="kw">while (m_szLine != "</object>") if (!FileReadLine()) class="kw">return class="kw">false; else
{
  if (m_szLine == "name")
  {
    b0 = class="kw">false;
    StringToUpper(m_szValue);
    for(c0 = eRESULT; (c0 <= eEDIT_STOP) && (!(b0 = (m_szValue == szMsgIDE[c0]))); c0++);
    if (!b0 && m_IsFloating) class="kw">return true; else c0 = (b0 ? c0 : m_CountObject);
    m_ArrObject[c0].szName = StringFormat("%s%04s>%s", def_HeaderMSG, sz0, m_szValue);
class=class="str">"cmt">//... The rest of the function...
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">struct IDE_Struct
{
  class="type">int     X,
          Y,
          Index;
  class="type">bool    IsMaximized;
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class C_ChartFloating
{
class=class="str">"cmt">// ... Class code ...
class=class="str">"cmt">//+------------------------------------------------------------------+
  class="type">void SetPosition(class="kw">const class="type">int X, class="kw">const class="type">int Y, class="kw">const class="type">int c0)
  {
class=class="str">"cmt">// ... Function code ...
    if (c0 == m_IDEStruct.Index)
    {
      m_IDEStruct.X = m_Win[c0].PosX + class="num">3;
      m_IDEStruct.Y = m_Win[c0].PosY + def_SizeBarCaption + class="num">3;
      m_IDEStruct.IsMaximized = m_Win[c0].IsMaximized;
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// ... Class code ...
class=class="str">"cmt">//+------------------------------------------------------------------+

浮动面板的锚定与重绘逻辑

在 MT5 自建 IDE 式面板时,先通过 GetIDE_Struct() 把当前窗口的几何状态(坐标、是否最大化)缓存进 m_IDEStruct,后续所有子对象定位都读这份快照,避免每次重绘都去查系统。 Add_RAD_IDE() 负责开一个子窗口:宽高任一 ≤0 直接返回 false,StageLocal01 建图失败也返回 false,只有都过了才套用 \Files\Chart Trade\IDE.tpl 模板,并把 m_IDEStruct.Index 记为 m_MaxCounter。 Resize(int x) 有两种写法。第一版里,浮动对象用 GetIDE_Struct().X/Y 加偏移量设 OBJPROP_XDISTANCE / YDISTANCE;非浮动则只用传入的 x。第二版多了一句判断:IsMaximized 为 false 时,X/Y 偏移还要叠上 Terminal.GetWidth() / GetHeight(),否则面板会黏在屏幕左上角之外。 开 MT5 把第二段 Resize 抄进 EA,故意让 IsMaximized=false 且不给 Terminal.GetWidth() 补偿,能直观看到对象飞出可视区——这就是锚点算漏的典型坑。外汇与贵金属图表上跑这类界面属高风险操作,参数误写可能让对象丢失且难回溯。

MQL5 / C++
class="kw">inline IDE_Struct GetIDE_Struct(class="type">void) class="kw">const { class="kw">return m_IDEStruct; }
class=class="str">"cmt">//+------------------------------------------------------------------+
                        class="type">bool Add_RAD_IDE(class="type">class="kw">string sz0, class="type">int x, class="type">int y, class="type">int w, class="type">int h)
                        {
                                if ((w <= class="num">0) || (h <= class="num">0)) class="kw">return class="kw">false;
                                if (!StageLocal01(sz0, PERIOD_CURRENT, -class="num">1)) class="kw">return class="kw">false;
                                ChartApplyTemplate(m_Win[m_MaxCounter].handle, "\\Files\\Chart Trade\\IDE.tpl");
                                m_IDEStruct.Index = m_MaxCounter;
                                StageLocal02(x, y, w, h);
                                class="kw">return true;
                        }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//... The rest of the class code 
}
class="type">void Resize(class="type">int x)
{
        for (class="type">int c0 = class="num">0; c0 < m_CountObject; c0++) if (m_IsFloating)
        {
                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[c0].szName, OBJPROP_XDISTANCE, GetIDE_Struct().X + m_ArrObject[c0].iPosX);
                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[c0].szName, OBJPROP_YDISTANCE, GetIDE_Struct().Y + m_ArrObject[c0].iPosY);
        }else   ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[c0].szName, OBJPROP_XDISTANCE, x + m_ArrObject[c0].iPosX);
};
class="type">void Resize(class="type">int x)
{
        for (class="type">int c0 = class="num">0; c0 < m_CountObject; c0++) if (m_IsFloating)
        {
                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[c0].szName, OBJPROP_XDISTANCE, GetIDE_Struct().X + m_ArrObject[c0].iPosX + (GetIDE_Struct().IsMaximized ? class="num">0 : Terminal.GetWidth()));
                ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[c0].szName, OBJPROP_YDISTANCE, GetIDE_Struct().Y + m_ArrObject[c0].iPosY + (GetIDE_Struct().IsMaximized ? class="num">0 : Terminal.GetHeight()));
        }else   ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[c0].szName, OBJPROP_XDISTANCE, x + m_ArrObject[c0].iPosX);
};
class="type">void DispatchMessage(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam)
{

「用鼠标位移触发浮动面板重绘」

在 MT5 自定义图表类里,鼠标移动事件是最廉价的「脏检查」入口。下面这段把上一次光标坐标存进 static 变量,只在像素真正变化时调 Resize(-1),避免每帧无谓重排。 [CODE] static double AccumulatedRoof = 0.0; bool b0; double d0; static int px = -1, py = -1; C_ChartFloating::DispatchMessage(id, lparam, dparam, sparam); if (m_CountObject < eEDIT_STOP) return; switch (id) { case CHARTEVENT_MOUSE_MOVE:

if ((GetIDE_Struct().X != px)(GetIDE_Struct().Y != py))

{ px = GetIDE_Struct().X; py = GetIDE_Struct().Y; Resize(-1); } break; //... The rest of the function ... [/CODE] 逐行拆解:第1行 static double AccumulatedRoof 是跨调用保留的累加器,初值 0.0;b0、d0 为局部占位变量。static int px=-1, py=-1 记录「上一帧」鼠标 x/y,初始化为 -1 保证首帧必触发。 DispatchMessage 先透传事件给基类;若编辑对象数不足 eEDIT_STOP 直接 return,不往下走。switch 只抓 CHARTEVENT_MOUSE_MOVE:当新坐标与 px/py 任一不同,就刷新坐标并 Resize(-1) 重算面板尺寸,否则跳过。 实盘注意:外汇、贵金属杠杆高,这类浮动 UI 只影响交互,不改变订单逻辑;在 EURUSD 的 1 分钟图上快速拖鼠标,若每帧都 Resize 可能吃掉 3~5% 的 OnChartEvent 耗时,用此差分法可压到接近 0。开 MT5 把这段塞进你的 C_ChartFloating 派生类,跑一下看像素级位移会不会触发重绘。

MQL5 / C++
class="kw">static class="type">class="kw">double AccumulatedRoof = class="num">0.0;
class="type">bool b0;
class="type">class="kw">double d0;
class="kw">static class="type">int px = -class="num">1, py = -class="num">1;

C_ChartFloating::DispatchMessage(id, lparam, dparam, sparam);
if (m_CountObject < eEDIT_STOP) class="kw">return;
class="kw">switch (id)
{
 case CHARTEVENT_MOUSE_MOVE:
  if ((GetIDE_Struct().X != px) || (GetIDE_Struct().Y != py))
  {
   px = GetIDE_Struct().X;
   py = GetIDE_Struct().Y;
   Resize(-class="num">1);
  }
  class="kw">break;
class=class="str">"cmt">//... The rest of the function ...

◍ 别急着下结论

这套从问题出发、靠小步迭代收敛到精简可用代码的思路,在 MT5 里确实能少走弯路,但落到外汇和贵金属实盘,价格行为受宏观事件扰动,历史回测顺滑不代表未来概率一致,属于高风险博弈。 另有一个坑得记牢:MT5 更新 3228 之后,用到 clrNONE 颜色的对象会莫名不透明,图表上出现怪异遮罩时,优先用附带的 IDE.tpl(约 10.13 KB)覆盖本地模板修复,而非改 EA 逻辑。 代码能跑通只是起点,真要用于黄金或欧美盘,建议先在策略测试器用 2023 年至今的 tick 数据跑一遍,观察滑点和点差放大时的最小成交单位是否还稳。

常见问题

解析对象节点时直接把坐标字段读出来存进结构体,重绘时复用即可,不必每次重新获取屏幕位置。
在位移事件里触发重绘并锁定锚点,避免每帧全量刷新,只更新被拖动的面板区域。
可以,小布能对照你的面板代码指出锚点丢失或重绘死循环的风险,并给出简化建议。
周期切换会清空部分对象属性,需在初始化时重新锚定坐标并强制重绘一次。
先确认是否在非位移事件里也调了重绘,再把重绘范围收窄到单个面板,通常能立刻降负载。