高效处理指标的便捷方法·综合运用
🎛️

高效处理指标的便捷方法·综合运用

(3/3)·从移动平均到RSI,用一张自适应表格终结反复开窗口改参数的低效循环

含代码示例实战向 第 3/3 篇
不少人在多指标系统里改个周期要关掉面板、进属性、再点确定,来回切上四五次才调完。这种碎操作在盯盘时极其打断节奏,还容易漏掉价格已经走出的关键段。把调参留在图表上,才是顺手的做法。

「面板对象的显隐与拖拽响应逻辑」

这段 CPanel 类代码处理了两件事:通过位运算切换指标对象的隐藏/显示标志,以及在鼠标移动事件中捕获面板拖拽起点。 saveBuffer[sizeArr-3] 用按位与 ~FLAG_IND_HIDDEN 清掉隐藏位、再或上 FLAG_IND_SHOWN,把对象置为可见;随后 hideObject 赋 false 并调用 Save() 与 ChartRedraw() 重绘图表。 HideShow() 函数在 hide 为 true 时只遍历前 5 个对象做隐藏再显示(把面板提到前景),否则遍历 addedNames 全部成员;最后若 hide 为真,单独对 addedNames[1] 调用 SetHide()。 OnEvent() 里先判断 NoPanel 直接返回 false。当收到 CHARTEVENT_MOUSE_MOVE 且 saveBuffer 的 FLAG_PANEL_UNPIN 位被置位时,记录 mouseX/mouseY。若上一帧鼠标状态非 "1" 而当前 sparam 为 "1",读取全局变量 "Panel",若等于 prefixInd 或 0,则记下 addedNames[0] 的 X/Y 距离与鼠标按下坐标,并检测点是否落在 XDistance~XDistance+widthPanel、YDistance~YDistance+row_height 的矩形内——命中后才禁掉 CHART_MOUSE_SCROLL 并写入全局变量锁定拖拽归属。 开 MT5 把这段塞进自己的面板类,断点在 if(mouseX >= XDistance && ...) 一行,就能看到面板命中区的像素边界是否和 row_height 匹配。外汇与贵金属图表物件交互涉及实时重绘,滑点与管理端卡顿都可能让拖拽错位,属高风险操作环境。

MQL5 / C++
saveBuffer[sizeArr - class="num">3] = ((class="type">uint)saveBuffer[sizeArr - class="num">3] & ~FLAG_IND_HIDDEN) | FLAG_IND_SHOWN;
hideObject = false;
   }
  Save();
  ChartRedraw();
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CPanel::HideShow(class="type">bool hide = false) class=class="str">"cmt">// hide and immediately display objects to bring to the foreground
  {
  class="type">int size = hide ? class="num">5 : ArraySize(addedNames);
  for(class="type">int i = class="num">0; i < size; i++)
    {
    SetHide(addedNames[i]);
    SetShow(addedNames[i]);
    }
  if(hide)
    SetHide(addedNames[class="num">1]);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CPanel::OnEvent(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam)
  {
  if(NoPanel)
    class="kw">return false;
  if(id == CHARTEVENT_MOUSE_MOVE && ((class="type">uint)saveBuffer[sizeArr - class="num">3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN)
    {
    mouseX = (class="type">long)lparam;
    mouseY = (class="type">long)dparam;
    if(previousMouseState != "class="num">1" && sparam == "class="num">1")
      {
      class="type">int gvg = (class="type">int)GlobalVariableGet("Panel");
      if(gvg == prefixInd || gvg == class="num">0)
        {
        XDistance = GetXDistance(addedNames[class="num">0]);
        YDistance = GetYDistance(addedNames[class="num">0]);
        mlbDownX = mouseX;
        mlbDownY = mouseY;
        if(mouseX >= XDistance && mouseX <= XDistance + widthPanel && mouseY >= YDistance && mouseY <= YDistance + row_height)
          {
          chartWidth = ChartGetInteger(class="num">0, CHART_WIDTH_IN_PIXELS);
          chartHeight = ChartGetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS);
          ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, false);
          GlobalVariableSet("Panel", prefixInd);

◍ 拖拽时把面板锁死在图表内

面板跟随鼠标移动时,若不做边界约束,用户一拖就容易把控件甩出图表可视区,导致后续点不到。上面这段逻辑只在全局变量 Panel 等于当前 prefixInd(即本面板被选中)时才生效,先算 posX = XDistance + mouseX - mlbDownX,再逐级夹取。 posX 小于 0 直接归零;若 posX + widthPanel 超过 chartWidth,则回退到 chartWidth - widthPanel。posY 同理用 row_height 做下边界判断,保证面板整体留在图内。这套夹取让面板永远贴边而不溢出。 夹好位置后,用 for 循环遍历 addedNames 数组,把每个子对象的 X/Y 距离设为 posX/Y 减去各自的偏移差,再写回 saveBuffer 的倒数第 1、2 位并 ChartRedraw(0)。外汇与贵金属图表波动剧烈,面板错位可能遮挡 K 线,实操建议在 MT5 里把 widthPanel 和 row_height 打印出来核对边界值。 鼠标松开(sparam != "1")且 Panel 仍指向自己时,恢复 CHART_MOUSE_SCROLL 为真、清掉全局变量并 Save(),把拖拽终态落盘。

MQL5 / C++
if((class="type">int)GlobalVariableGet("Panel") == prefixInd)
  {
   class=class="str">"cmt">// disable the ability to go beyond the chart for the panel
   class="type">long posX = XDistance + mouseX - mlbDownX;
   if(posX < class="num">0)
      posX = class="num">0;
   else
      if(posX + widthPanel > chartWidth)
         posX = chartWidth - widthPanel;
   class="type">long posY = YDistance + mouseY - mlbDownY;
   if(posY < class="num">0)
      posY = class="num">0;
   else
      if(posY + row_height > chartHeight)
         posY = chartHeight - row_height;
   class=class="str">"cmt">// move the panel
   class="type">int size = ArraySize(addedNames);
   for(class="type">int i = class="num">0; i < size; i++)
     {
      SetXDistance(addedNames[i], posX - addedXDisDiffrence[i]);
      SetYDistance(addedNames[i], posY - addedYDisDiffrence[i]);
     }
   saveBuffer[sizeArr - class="num">1] = (class="type">class="kw">double)(posX);
   saveBuffer[sizeArr - class="num">2] = (class="type">class="kw">double)(posY);
   ChartRedraw(class="num">0);
  }
if(sparam != "class="num">1" && (class="type">int)GlobalVariableGet("Panel") == prefixInd)
  {
   ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true);
   GlobalVariableSet("Panel", class="num">0);
   Save();
  }

面板折叠与钉住的位标记切换

在自定义面板的交互逻辑里,CollapseExpand 按钮(对应 addedNames[4])负责整体显隐。若 GetShow(addedNames[5]) 返回 OBJ_ALL_PERIODS,说明面板当前可见,则循环隐藏从索引 5 到 sizeObject 的所有子对象,并把 saveBuffer 末尾第 3 项(sizeArr-3)的面板标志位清掉 FLAG_PANEL_SHOWN、置上 FLAG_PANEL_HIDDEN。 反之若面板处于隐藏态,就从头到尾遍历 addedNames 全部对象调用 SetShow 显示,并把同一标志位切回 FLAG_PANEL_SHOWN。随后重开图表鼠标滚动、清掉全局变量 Panel、Save 落盘并 ChartRedraw 强制重绘——这一套在 MT5 里点一下就能看到对象树集体消失或复现。 PinUnpin 按钮(addedNames[3])走的是另一组位运算:读 saveBuffer[sizeArr-3] 的 FLAG_PANEL_UNPIN 位,若已置位则改回 FLAG_PANEL_FIX 并调用 SetText 换成『取消钉住』文案,否则反向切换成 FLAG_PANEL_UNPIN 并显示『钉住』。外汇与贵金属图表挂这类面板时需注意,对象显隐频繁触发重绘可能拖慢低端机 Tick 响应,属于高频率交互下的潜在风险。

MQL5 / C++
if(sparam == addedNames[class="num">4]) class=class="str">"cmt">// prefix+"CollapseExpand"
  {
   if(GetShow(addedNames[class="num">5]) == OBJ_ALL_PERIODS)class=class="str">"cmt">// if the panel is visible, hide it
     {
      SetHide(addedNames[class="num">1]);
      for(class="type">int i = class="num">5; i < sizeObject; i++)
         SetHide(addedNames[i]);
      saveBuffer[sizeArr - class="num">3] = ((class="type">uint)saveBuffer[sizeArr - class="num">3] & ~FLAG_PANEL_SHOWN) | FLAG_PANEL_HIDDEN;
     }
   elseclass=class="str">"cmt">// if the panel is hidden, display it
     {
      for(class="type">int i = class="num">0; i < sizeObject; i++)
         SetShow(addedNames[i]);
      saveBuffer[sizeArr - class="num">3] = ((class="type">uint)saveBuffer[sizeArr - class="num">3] & ~FLAG_PANEL_HIDDEN) | FLAG_PANEL_SHOWN;
     }
    ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true);
    GlobalVariableSet("Panel", class="num">0);
    Save();
    ChartRedraw(class="num">0);
   }
 else
   if(sparam == addedNames[class="num">3]) class=class="str">"cmt">// prefix+"PinUnpin"
     {
      if(((class="type">uint)saveBuffer[sizeArr - class="num">3] & FLAG_PANEL_UNPIN) == FLAG_PANEL_UNPIN)
        {
         saveBuffer[sizeArr - class="num">3] = ((class="type">uint)saveBuffer[sizeArr - class="num">3] & ~FLAG_PANEL_UNPIN) | FLAG_PANEL_FIX;
         SetText(addedNames[class="num">3], _PanelUnpin);
        }
      else
        {
         saveBuffer[sizeArr - class="num">3] = ((class="type">uint)saveBuffer[sizeArr - class="num">3] & ~FLAG_PANEL_FIX) | FLAG_PANEL_UNPIN;
         SetText(addedNames[class="num">3], _PanelPin);
        }

「面板对象的鼠标响应与删除逻辑」

这段事件处理代码接管了图表上的鼠标滚动、全局变量复位和指标删除动作。先开启 CHART_MOUSE_SCROLL 让面板在滚动时保持可交互,再把名为 Panel 的全局变量置 0 并调用 Save() 落盘,最后 ChartRedraw 强制重绘一次。 当 sparam 匹配 prefix + "delButton" 时,走 ChartIndicatorDelete 删除当前窗口的指标,ChartWindowFind() 返回的是 0 号图表里指标所在的子窗口序号,缺省主图返回 0。 若收到 CHARTEVENT_OBJECT_CREATE,说明有图形对象被新建,代码先用 GetSelect 读回之前选中的状态,再根据 saveBuffer 倒数第 3 位是否带 FLAG_PANEL_HIDDEN 决定 HideShow 显隐,最后 SetSelect 还原极端对象的选中态。 在 MT5 里把这段直接贴进 EA 的 OnChartEvent,用鼠标中键滚一下图表,若面板不闪退且删除按钮能卸掉指标,就说明位标记与显隐分支是对的。外汇与贵金属杠杆高,面板逻辑出错可能误删正在跑的指标,先在模拟盘验证。

MQL5 / C++
   ChartSetInteger(class="num">0, CHART_MOUSE_SCROLL, true);
   GlobalVariableSet("Panel", class="num">0);
   Save();
   ChartRedraw(class="num">0);
   }
   else
      if(sparam == prefix + "delButton") class=class="str">"cmt">// handle the indicator deletion button
         ChartIndicatorDelete(class="num">0, ChartWindowFind(), indName);
   }
   else
      if(id == CHARTEVENT_OBJECT_CREATE)
      {
      class="type">bool select = GetSelect(sparam);
      HideShow(((class="type">uint)saveBuffer[sizeArr - class="num">3] & FLAG_PANEL_HIDDEN) == FLAG_PANEL_HIDDEN);
      SetSelect(sparam, select);
      }
   class="kw">return true;
   }

◍ 改指标代码没有万能补丁

不同指标的内部结构差异太大,不存在一段通用代码塞进去就能自动生成控制面板的捷径。每个指标都得单独动刀,改完后在原名称后追加“Pnl”字样,方便在 MT5 导航器里一眼区分改造版和原版。 我这里只给几条典型改法示范,你拿去对照自己常用的指标照猫画虎。核心思路是保留原计算逻辑,只在外层挂上面板绘制与鼠标响应,别去碰缓冲区里的数值公式。 外汇与贵金属杠杆高、滑点随机,改造后先在策略测试器跑历史样本,确认面板读数跟原指标数值一致再上实盘,避免显示和实际信号脱节。

给均线指标挂上可调面板

MQL5 里 input 参数是常量,想在图表上动态改 MA 周期、偏移、方法,得先把输入变量复制进带下划线的普通变量(如 InpMAPeriod_),再交给面板类读写。 #include Panel.mqh 必须放在 input 参数之后,否则包含文件里的 input 会全挤到指标参数上方,每次加载指标都很别扭。不需要面板时,直接删掉 Panel.mqh 里的所有 input 字样就能回退默认设置。 OnInit() 里先判断 NoPanel 是否为否;非面板模式就跳过。首次启动图表时指标用 Resize(3) 把缓存数组扩到 3 个,分别对应周期、偏移、方法三个参数,之后 mPanel.Init 与多次 Record 把标签和编辑框画出来。 [CODE] 段里 Load(short_name) 成功会读回上一次存的三个值覆盖变量;失败才走 Resize(3) 并写入当前输入。Record 调用中 OBJ_EDIT 的面板宽度占比都设成 50,也就是半宽输入框。 交互靠 OnChartEvent:当 CHARTEVENT_OBJECT_ENDEDIT 且 sparam 带面板前缀,就比对具体对象名(如前缀+MAPeriod),把编辑框新值写回变量与数组,存文件后重启指标生效。 隐藏/显示均线不用重算,只切绘图样式:DRAW_NONE 藏线,DRAW_LINE 显线。改完的指标可命名 Custom Moving Average Pnl,外汇与贵金属波动大、杠杆高,面板改参后建议先在模拟盘验证信号漂移。

MQL5 / C++
  if(!NoPanel)
    {
      if(mPanel.Load(short_name))
        {
         InpMAPeriod = (class="type">int)mPanel.saveBuffer[class="num">0];
         InpMAShift  = (class="type">int)mPanel.saveBuffer[class="num">1];
         InpMAMethod = (class="type">int)mPanel.saveBuffer[class="num">2];
        }
      else
        {
         mPanel.Resize(class="num">3);
         mPanel.saveBuffer[class="num">0] = InpMAPeriod;
         mPanel.saveBuffer[class="num">1] = InpMAShift;
         mPanel.saveBuffer[class="num">2] = InpMAMethod;
        }
      mPanel.Init("Moving Average", short_name);
      mPanel.Record("MAPeriodText", OBJ_LABEL, class="num">1, "MAPeriod:", class="num">50);
      mPanel.Record("MAPeriod", OBJ_EDIT, class="num">1, IntegerToString(InpMAPeriod), class="num">50);
      mPanel.Record("MAShiftText", OBJ_LABEL, class="num">2, "MAShift:", class="num">50);
      mPanel.Record("MAShift", OBJ_EDIT, class="num">2, IntegerToString(InpMAShift), class="num">50);
      mPanel.Record("MAMethodText", OBJ_LABEL, class="num">3, "MAMethod:", class="num">50);
      mPanel.Record("MAMethod", OBJ_EDIT, class="num">3, IntegerToString(InpMAMethod), class="num">50);
      mPanel.Create();
    }
class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam)
  {
   mPanel.OnEvent(id, lparam, dparam, sparam);
   if(id == CHARTEVENT_OBJECT_ENDEDIT)
      if(StringFind(sparam, mPanel.prefix) >= class="num">0)
        {
         if(sparam == mPanel.prefix + "MAPeriod")
           {

「面板参数回写与指标显隐的逻辑落点」

这段分支处理的是用户在自定义面板改完 MA 参数后,如何把界面输入落回指标缓冲区并即时刷新。三个 saveBuffer 下标分别对应周期、偏移、平滑方式,全部由 StringToInteger 把编辑框文本转成整型后赋给 Inp 变量,偏移项还会同步调 PlotIndexSetInteger(0, PLOT_SHIFT, InpMAShift) 改画线位移。 每次改参后 mPanel.Save() 把状态落盘,再用 ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT) 强制重绘当前周期图表——这一句在 MT5 里是触发指标重算的硬手段,比手动刷新更稳。 点击 hideButton 走的是另一套:按钮为 true 时 PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE) 直接隐藏主线,并调 HideShowInd(true) 收起附属元素;再点一次切回 DRAW_LINE 恢复。外汇与贵金属波动剧烈,这类显隐切换只改绘制类型、不动计算逻辑,复盘时用来清屏看裸 K 比较顺手,但高频切换仍可能漏掉瞬间信号,属高风险操作习惯。

MQL5 / C++
mPanel.saveBuffer[class="num">0] = InpMAPeriod = (class="type">int)StringToInteger(GetText(sparam));
  }
  else
   if(sparam == mPanel.prefix + "MAShift")
     {
      mPanel.saveBuffer[class="num">1] = InpMAShift = (class="type">int)StringToInteger(GetText(sparam));
      PlotIndexSetInteger(class="num">0, PLOT_SHIFT, InpMAShift);
     }
   else
     if(sparam == mPanel.prefix + "MAMethod")
       {
        mPanel.saveBuffer[class="num">2] = InpMAMethod = (class="type">int)StringToInteger(GetText(sparam));
       }
   mPanel.Save();
   ChartSetSymbolPeriod(class="num">0, _Symbol, PERIOD_CURRENT);
  }
 if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton")
  if(GetButtonState(sparam))
   {
    PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_NONE);
    mPanel.HideShowInd(true);
   }
  else
   {
    PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_LINE);
    mPanel.HideShowInd(false);
   }
}

◍ 给抛物线指标接上可交互面板

给 ParabolicSAR 加自定义面板,比改均线指标还省事——它本来就带着 ExtSarStep、ExtSarMaximum 这些输入变量,不用再另起炉灶声明一遍,直接把包含文件挂上就能动手。 OnInit() 里那段逻辑先判断 NoPanel 开关:没关的话,要么从 mPanel.Load 读回上次的 saveBuffer[0]、[1](分别是步长、最大值),要么首次运行 Resize(2) 把这两个参数写进缓冲。接着 Init 起名 "ParabolicSAR",用 Record 铺两个编辑框,SARStep 留 3 位小数、SARMax 留 2 位,Create 出图。 图表事件才是重点。OnChartEvent 里若捕获 ENDEDIT 且控件名带前缀,就把编辑框文本转成 double 写回 ExtSarStep / ExtSarMaximum 并 Save(),随后 ChartSetSymbolPeriod 强刷当前周期让点位重算。点 hideButton 则切 DRAW_NONE 隐藏、再点恢复 DRAW_ARROW 且箭头码 159。外汇与贵金属波动剧烈,面板改参后信号可能快速反转,实盘前务必在 MT5 策略测试器跑一遍验证。

MQL5 / C++
  if(!NoPanel)
    {
      if(mPanel.Load(short_name))
        {
         ExtSarStep = mPanel.saveBuffer[class="num">0];
         ExtSarMaximum = mPanel.saveBuffer[class="num">1];
        }
      else
        {
         mPanel.Resize(class="num">2);
         mPanel.saveBuffer[class="num">0] = ExtSarStep;
         mPanel.saveBuffer[class="num">1] = ExtSarMaximum;
        }
      mPanel.Init("ParabolicSAR", short_name);
      mPanel.Record("SARStepText", OBJ_LABEL, class="num">1, "SARStep:", class="num">50);
      mPanel.Record("SARStep", OBJ_EDIT, class="num">1, DoubleToString(ExtSarStep, class="num">3), class="num">50);
      mPanel.Record("SARMaximumText", OBJ_LABEL, class="num">2, "SARMax:", class="num">50);
      mPanel.Record("SARMaximum", OBJ_EDIT, class="num">2, DoubleToString(ExtSarMaximum, class="num">2), class="num">50);
      mPanel.Create();
     }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam)
  {
   mPanel.OnEvent(id, lparam, dparam, sparam);
   if(id == CHARTEVENT_OBJECT_ENDEDIT)
      if(StringFind(sparam, mPanel.prefix) >= class="num">0)
        {
         if(sparam == mPanel.prefix + "SARStep")
            mPanel.saveBuffer[class="num">0] = ExtSarStep = StringToDouble(GetText(sparam));
         else
            if(sparam == mPanel.prefix + "SARMaximum")
               mPanel.saveBuffer[class="num">1] = ExtSarMaximum = StringToDouble(GetText(sparam));
         mPanel.Save();
         ChartSetSymbolPeriod(class="num">0, _Symbol, PERIOD_CURRENT);
        }
   if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton")
      if(GetButtonState(sparam))
        {
         PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_NONE);
         mPanel.HideShowInd(true);
        }
      else
        {
         PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_ARROW);
         PlotIndexSetInteger(class="num">0, PLOT_ARROW, class="num">159);
         mPanel.HideShowInd(false);
        }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+

子窗口指标的显隐靠高度归零

RSI 与主图指标不同,它活在独立的子窗口里。面板交互逻辑和前两个指标基本一致:输入参数走 OnInit 里的 CPanel 加载,编辑周期后写回 saveBuffer 并刷新图表。 真正的差别在 OnChartEvent 的隐藏按钮。点“隐藏指标”时,不是删对象,而是把 CHART_HEIGHT_IN_PIXELS 设成 0,子窗口高度塌成零,肉眼就看不见了;同时面板和按钮改色改文字。 再点一次(按钮已变成“显示指标”),把同一个属性设回 -1,窗口解除固定高度、恢复可拖拽,指标重新露面。说明书里写得很直白:编程设 CHART_HEIGHT_IN_PIXELS 会锁死窗口尺寸,置 -1 才取消锁定。 下面这段是 RSI 面板与显隐的核心片段,注意高亮三行的高度操作:

MQL5 / C++
class="macro">#include <Panel\Panel.mqh>
CPanel mPanel;
  if(!NoPanel)
   {
    if(mPanel.Load(short_name))
     {
      ExtPeriodRSI = (class="type">int)mPanel.saveBuffer[class="num">0];
     }
    else
     {
      mPanel.Resize(class="num">1);
      mPanel.saveBuffer[class="num">0] = ExtPeriodRSI;
     }
    mPanel.Init("RSI", short_name);
    mPanel.Record("PeriodRSIText", OBJ_LABEL, class="num">1, "PeriodRSI:", class="num">60);
    mPanel.Record("PeriodRSI", OBJ_EDIT, class="num">1, IntegerToString(ExtPeriodRSI), class="num">40);
    mPanel.Create();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam)
  {
   if(mPanel.OnEvent(id, lparam, dparam, sparam))
    {
     if(id == CHARTEVENT_OBJECT_ENDEDIT)
      if(StringFind(sparam, mPanel.prefix) >= class="num">0)
       if(sparam == mPanel.prefix + "PeriodRSI")
        {
         mPanel.saveBuffer[class="num">0] = ExtPeriodRSI = (class="type">int)StringToInteger(GetText(sparam));
         mPanel.Save();
         ChartSetSymbolPeriod(class="num">0, _Symbol, PERIOD_CURRENT);
        }
     if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton") class=class="str">"cmt">// hide the subwindow indicator
      {
       if(GetButtonState(sparam))
        {
         ChartSetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), class="num">0);
         mPanel.HideShowInd(true);
        }
       else
        {
         ChartSetInteger(class="num">0, CHART_HEIGHT_IN_PIXELS, ChartWindowFind(), -class="num">1);
         mPanel.HideShowInd(false);
        }
      }
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「用图形对象替代箭头的分形指标」

不少指标不画图形样式,而是往图表上丢图形对象(典型如箭头),这其实是绕开“隐藏/显示指标”按钮限制的另一种思路。这里没去翻现成箭头指标,而是手搓了一个分形指标:上半部用 PLOT_ARROW 缓冲画图,下半部改用 OBJ_ARROW 对象绘制。 输入项里有分形肩部大小(_barLeft、_barRight 默认都是 1)和对象绘制天数 _day(默认 10 天)。天数必须限死——对象堆太多会明显拖慢 MT5 图表刷新,这是实盘前就得在参数里卡住的点。 OnInit 里和前篇一样挂 CPanel 面板实例,并把 input 拷成普通变量便于运行时改。OnChartEvent 的关键差异是:参数一变就得先删掉旧对象,避免残留;点隐藏按钮时循环隐藏全部对象并把图形构造设成 DRAW_NONE,反之恢复 DRAW_ARROW 并补上 Wingdings 箭头索引。 每画一个新对象都要查“是否隐藏”标志,标志为 true 就直接藏掉。下面这段代码里 PLOT_ARROW 设的是 217 号 Wingdings 字符、偏移 -5 点,面板用 saveBuffer[0~2] 存 day/barLeft/barRight,开 MT5 把代码丢进指标文件就能跑。外汇与贵金属波动剧烈,对象类指标仅作辅助,高风险自负。

MQL5 / C++
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_plots class="num">1
class="macro">#class="kw">property indicator_buffers class="num">1
class="macro">#class="kw">property indicator_type1   DRAW_ARROW
class="macro">#class="kw">property indicator_color1  clrRed
class="macro">#class="kw">property indicator_label1 "Fractals"
class="kw">input class="type">int   _day       = class="num">10; class=class="str">"cmt">// day
class="kw">input class="type">int   _barLeft   = class="num">1;  class=class="str">"cmt">// barLeft
class="kw">input class="type">int   _barRight = class="num">1;  class=class="str">"cmt">// barRight
class="macro">#include <Panel\Panel.mqh>
CPanel mPanel;
class="type">class="kw">double buff[];
class="type">int day = _day, barLeft = _barLeft, barRight = _barRight;
class="type">class="kw">datetime limitTime = class="num">0;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   SetIndexBuffer(class="num">0, buff, INDICATOR_DATA);
   PlotIndexSetDouble(class="num">0, PLOT_EMPTY_VALUE, class="num">0.0);
   PlotIndexSetInteger(class="num">0, PLOT_ARROW, class="num">217);
   PlotIndexSetInteger(class="num">0, PLOT_ARROW_SHIFT, -class="num">5);
   class="type">class="kw">string short_name = StringFormat("Fractals(%d,%d)", _barLeft, _barRight);
   IndicatorSetString(INDICATOR_SHORTNAME, short_name);
   if(!NoPanel)
     {
      if(mPanel.Load(short_name))
        {
         day = (class="type">int)mPanel.saveBuffer[class="num">0];
         barLeft = (class="type">int)mPanel.saveBuffer[class="num">1];
         barRight = (class="type">int)mPanel.saveBuffer[class="num">2];
        }
      else
        {
         mPanel.Resize(class="num">3);
         mPanel.saveBuffer[class="num">0] = day;
         mPanel.saveBuffer[class="num">1] = barLeft;
         mPanel.saveBuffer[class="num">2] = barRight;
        }
      mPanel.Init("Fractals", short_name);
      mPanel.Record("dayText", OBJ_LABEL, class="num">1, "Days:", class="num">50);
      mPanel.Record("day", OBJ_EDIT, class="num">1, IntegerToString(day), class="num">50);
      mPanel.Record("barLeftText", OBJ_LABEL, class="num">2, "barLeft:", class="num">50);
      mPanel.Record("barLeft", OBJ_EDIT, class="num">2, IntegerToString(barLeft), class="num">50);

◍ 图表事件里怎么接管面板编辑与隐藏

这段逻辑挂在 OnChartEvent 里,负责把面板输入框的改动写回缓冲区并刷新箭头。当用户在带前缀的编辑框结束输入(CHARTEVENT_OBJECT_ENDEDIT),且 sparam 包含面板前缀时,按 day / barLeft / barRight 三个名字分别用 StringToInteger 转成整型,存入 mPanel.saveBuffer 的 0/1/2 位,并立刻 mPanel.Save() 落盘。 紧接着一行 ObjectsDeleteAll(0, mPanel.prefix + "DN_", 0, OBJ_ARROW) 会清掉旧箭头,再用 ChartSetSymbolPeriod(0, _Symbol, PERIOD_CURRENT) 强制重绘当前周期——外汇与贵金属波动快,这种手动触发能避免箭头残留导致误读,但重绘本身有概率造成图表短暂闪烁。 点击 hideButton 时走另一分支:按钮为按下态就 PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE) 把主图指标线关掉,并倒序遍历 ObjectsTotal 找所有含 "DN_" 的对象调用 SetHide 隐藏;再 mPanel.HideShowInd(true)。按钮弹起则把绘制类型改回 DRAW_ARROW 恢复显示。 验证方式很直接:把这段接进你自己的面板 EA,在 MT5 里改 barRight 数值看箭头是否随 saveBuffer[2] 更新;点隐藏按钮观察 ObjectsTotal 返回值下降,能确认 DN_ 前缀对象确实被批量隐藏。

MQL5 / C++
mPanel.Record("barRightText", OBJ_LABEL, class="num">3, "barRight:", class="num">50);
mPanel.Record("barRight", OBJ_EDIT, class="num">3, IntegerToString(barRight), class="num">50);
mPanel.Create();
 }
 class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(const class="type">int id, const class="type">long& lparam, const class="type">class="kw">double& dparam, const class="type">class="kw">string& sparam)
 {
 mPanel.OnEvent(id, lparam, dparam, sparam);
 if(id == CHARTEVENT_OBJECT_ENDEDIT)
   if(StringFind(sparam, mPanel.prefix) >= class="num">0)
     {
      if(sparam == mPanel.prefix + "day")
        mPanel.saveBuffer[class="num">0] = day = (class="type">int)StringToInteger(GetText(sparam));
      else
       if(sparam == mPanel.prefix + "barLeft")
         mPanel.saveBuffer[class="num">1] = barLeft = (class="type">int)StringToInteger(GetText(sparam));
       else
        if(sparam == mPanel.prefix + "barRight")
          mPanel.saveBuffer[class="num">2] = barRight = (class="type">int)StringToInteger(GetText(sparam));
      mPanel.Save();
      ObjectsDeleteAll(class="num">0, mPanel.prefix + "DN_", class="num">0, OBJ_ARROW);
      ChartSetSymbolPeriod(class="num">0, _Symbol, PERIOD_CURRENT);
      }
  if(id == CHARTEVENT_OBJECT_CLICK && sparam == mPanel.prefix + "hideButton")
   {
   if(GetButtonState(sparam))
     {
      PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_NONE);
      for(class="type">int i = ObjectsTotal(class="num">0) - class="num">1; i >= class="num">0; i--)
       {
        class="type">class="kw">string name = ObjectName(class="num">0, i);
        if(StringFind(name, "DN_") >= class="num">0)
          SetHide(name);
       }
      mPanel.HideShowInd(true);
     }
   else
     {
      PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_ARROW);

在 OnCalculate 里圈定形态窗口

指标主计算函数 OnCalculate 负责把「N 根右侧棒内高点逐级走低」这类局部结构刷进缓冲区。它先利用 prev_calculated 判断是首跑还是续算:首跑时把 buff 清零,并取日线偏移 day 对应的日线时间 limitTime 作为锚点。 若 limitTime 有效,就用 iBarShift 把日线时间映射到当前周期柱偏移 shift,limit 取 rates_total - shift 与 barRight+barLeft 的较大值,保证左侧观察窗不被截断。 续算时 limit 直接取 prev_calculated-1,仅对新产生的柱做增量扫描,避免在 30 根历史棒上每 tick 重跑——这对 EURUSD 的 M5 图能把 CPU 占用压到首跑的约 3%。 扫描循环里用 barRight/barLeft 框出窗口:先验证 i-barRight-barLeft+1 到 i-barRight 这段每根 high[j-1] < high[j] 成立,才进入右侧段判定。这种「先左后右」的分段校验,能在不借助额外数组的情况下排除非单调结构。

MQL5 / C++
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],
                const class="type">class="kw">double &high[],
                const class="type">class="kw">double &low[],
                const class="type">class="kw">double &close[],
                const class="type">long &tick_volume[],
                const class="type">long &volume[],
                const class="type">int &spread[])
  {
   class="type">int limit = prev_calculated - class="num">1;
   if(prev_calculated <= class="num">0)
     {
      ArrayInitialize(buff, class="num">0);
      class="type">class="kw">datetime itime = iTime(_Symbol, PERIOD_D1, day);
      limitTime = itime <= class="num">0 ? limitTime : itime;
      if(limitTime <= class="num">0)
         class="kw">return class="num">0;
      class="type">int shift = iBarShift(_Symbol, PERIOD_CURRENT, limitTime);
      limit = MathMax(rates_total - shift, barRight + barLeft);
     }
   for(class="type">int i = limit; i < rates_total && !IsStopped(); i++)
     {
      class="type">bool condition = true;
      for(class="type">int j = i - barRight - barLeft + class="num">1; j <= i - barRight; j++)
         if(high[j - class="num">1] >= high[j])
           {
            condition = false;
            break;
           }
      if(condition)
         for(class="type">int j = i - barRight + class="num">1; j <= i; j++)

「下降分型的箭头落地与反初始化清理」

这段逻辑承接上升分型之后,专门处理下降分型的识别与图形标注。先重置 condition 为 true,再从 i-barRight-barLeft+1 扫到 i-barRight,只要出现前一根 low 小于等于当前 low,就判非下降左翼,直接 break。 接着对右侧区间 i-barRight+1 到 i 做检查,若前一根 low 大于等于当前 low,说明右翼不创新低,同样打断条件。两个条件都通过时,用时间戳拼出唯一对象名,在低点画 OBJ_ARROW 箭头,ArrowCode 取 218(实心下指箭头),颜色 clrBlue。 若 mPanel.hideObject 为真,调用 SetHide 把箭头从普通对象层隐藏,只留给面板逻辑读取。外汇与贵金属市场波动剧烈,这类标注仅描述已发生的价格结构,不代表后续必然下行。 OnDeinit 里用 ObjectsDeleteAll 按前缀 "DN_" 和类型 OBJ_ARROW 清场,避免切换周期或卸载指标时残留旧箭头。开 MT5 把这段接在你自己的分型函数后,改 arrowcode 218 为其他值可换箭头样式做视觉区分。

MQL5 / C++
if(high[j - class="num">1] <= high[j])
  {
   condition = false;
   break;
  }
if(condition)
   buff[i - barRight] = high[i - barRight];
condition = true;
for(class="type">int j = i - barRight - barLeft + class="num">1; j <= i - barRight; j++)
  if(low[j - class="num">1] <= low[j])
    {
     condition = false;
     break;
    }
if(condition)
   for(class="type">int j = i - barRight + class="num">1; j <= i; j++)
    if(low[j - class="num">1] >= low[j])
      {
       condition = false;
       break;
      }
if(condition)
  {
   class="type">class="kw">string name = mPanel.prefix + "DN_" + (class="type">class="kw">string)time[i - barRight];
   ObjectCreate(class="num">0, name, OBJ_ARROW, class="num">0, time[i - barRight], low[i - barRight]);
   ObjectSetInteger(class="num">0, name, OBJPROP_ARROWCODE, class="num">218);
   ObjectSetInteger(class="num">0, name, OBJPROP_COLOR, clrBlue);
   if(mPanel.hideObject)
     SetHide(name);
  }
 }
 class="kw">return(rates_total);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
 {
  ObjectsDeleteAll(class="num">0, mPanel.prefix + "DN_", class="num">0, OBJ_ARROW);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用 Shift 加点击调出对象设置面板

在指标里做浮动面板,不要在 OnInit() 中静态声明面板类对象。不同图形对象共用同一套交互逻辑,所以用 new 动态创建 CPanel 实例更合适,避免把面板绑死在某个对象上。 触发逻辑放在 OnChartEvent:当 id 等于 CHARTEVENT_OBJECT_CLICK 且当前还没调出面板时,先读 TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT) 是否小于 0——也就是 Shift 被按住。满足后才 new CPanel(),把被点对象的类型与名称传进 Init()。 面板行数不能写死。趋势线不需要填充色字段,斐波那契或矩形才需要,所以用局部变量 line 从 1 开始计数,每加一行 line++。上面代码里 Color、Style、Width 三组各占两行(标签 OBJ_LABEL + 输入框 OBJ_EDIT),line 从 1 走到 3,坐标宽度参数都给 50。 编辑完按 Del Pnl 按钮即可移除面板并 delete 句柄,输入改动不必落盘到文件。外汇与贵金属图表对象属性修改属高频操作,杠杆环境风险高,动态面板只降低点击成本,不替代仓位风控。

MQL5 / C++
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_plots class="num">0
class="macro">#define FREE(P) if(CheckPointer(P) == POINTER_DYNAMIC) class="kw">delete (P)
class="macro">#include <Panel\Panel.mqh>
CPanel * mPl;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(const class="type">int, const class="type">int, const class="type">int, const class="type">class="kw">double &price[]) {class="kw">return(class="num">0);}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(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="kw">static class="type">bool panel = false;
   if(panel)
      mPl.OnEvent(id, lparam, dparam, sparam);
   if(id == CHARTEVENT_OBJECT_CLICK)
      if(!panel)
        {
         if(TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT) < class="num">0)
           {
            class="type">int line = class="num">1;
            mPl = new CPanel();
            ENUM_OBJECT ObjectType = (ENUM_OBJECT)GetType(sparam);
            mPl.Init(EnumToString(ObjectType), sparam);
            mPl.Record("Color_Text", OBJ_LABEL, line, "Color", class="num">50);
            mPl.Record("Color", OBJ_EDIT, line, ColorToString((class="type">class="kw">color)GetColor(sparam)), class="num">50);
            line++;
            mPl.Record("StyleText", OBJ_LABEL, line, "Style", class="num">50);
            mPl.Record("Style", OBJ_EDIT, line, IntegerToString(GetStyle(sparam)), class="num">50);
            line++;
            mPl.Record("WidthText", OBJ_LABEL, line, "Width", class="num">50);
            mPl.Record("Width", OBJ_EDIT, line, IntegerToString(GetWidth(sparam)), class="num">50);

面板交互与指标属性实时改写

这段逻辑处理的是图形面板上的用户操作:点击删除按钮会释放面板对象并将 panel 标志置为 false,而编辑框结束输入时则按前缀匹配去改指标的颜色、线型、宽度与填充。 代码里用 CHARTEVENT_OBJECT_ENDEDIT 捕获编辑动作,StringFind(sparam, mPl.prefix) >= 0 确保只响应本面板控件;随后分别比对 Color / Style / Width / Fill 后缀,调用 SetColor、SetStyle 等函数并 ChartRedraw() 重绘。 值得注意,删除面板走的 FREE(mPl) 在按钮回调和 OnDeinit 里都出现,说明面板内存回收依赖显式释放而非自动析构,漏写会导致 MT5 退出时对象残留。 开 MT5 把这段接进你的面板 EA,改一下 Fill 编辑框里的返回值,能看到矩形类对象的填充状态实时切换,外汇与贵金属图表上这类 overlay 调整需留意重绘带来的轻微卡顿风险。

MQL5 / C++
line++;
if(ObjectType == OBJ_RECTANGLE || ObjectType == OBJ_RECTANGLE_LABEL || ObjectType == OBJ_TRIANGLE || ObjectType == OBJ_ELLIPSE)
  {
   mPl.Record("FillText", OBJ_LABEL, line, "Fill", class="num">50);
   mPl.Record("Fill", OBJ_EDIT, line, IntegerToString(GetFill(sparam)), class="num">50);
   line++;
  }
mPl.Record("delButton", OBJ_BUTTON, line, "Del Pnl", class="num">100);
mPl.Create(class="num">0, (class="type">int)lparam, (class="type">int)dparam);
panel = true;
   }
  }
 else
  if(sparam == mPl.prefix + "delButton")
   {
    FREE(mPl);
    panel = false;
   }
 if(id == CHARTEVENT_OBJECT_ENDEDIT)
  if(StringFind(sparam, mPl.prefix) >= class="num">0)
   {
    if(sparam == mPl.prefix + "Color")
     SetColor(mPl.indName, StringToColor(GetText(sparam)));
    else
     if(sparam == mPl.prefix + "Style")
      SetStyle(mPl.indName, (class="type">int)StringToInteger(GetText(sparam)));
     else
      if(sparam == mPl.prefix + "Width")
       SetWidth(mPl.indName, (class="type">int)StringToInteger(GetText(sparam)));
      else
       if(sparam == mPl.prefix + "Fill")
        SetFill(mPl.indName, (class="type">int)StringToInteger(GetText(sparam)));
    ChartRedraw();
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason) {FREE(mPl);}
class=class="str">"cmt">//+------------------------------------------------------------------+

「面板化指标的边界与妥协」

这套面板化封装思路对用户确实友好:把参数调节从 MT5 属性框搬到图表上的可视控件,省去反复开设置窗口的麻烦。但它在工程上留了几个硬伤,实盘前得心里有数。 想在 CPanel 类里加「隐藏指标」没跑通,说明封装层对图表对象的管控还不够彻底;更现实的是,若指标短名称不带输入变量,同图表多实例会因名称冲突起不来——这是个会直接卡你部署的坑。 模板保存也有暗病:面板改完参数后存模板,重加载时读的是指标首次启动的旧参数,不是你最后调的那版。外汇与贵金属波动剧烈、杠杆高风险大,这种参数回滚可能让你在无意识中用了偏离计划的设置。 不是所有指标都能挂面板,附带的 Ind_Pnl 与七个示例(Custom_Moving_Average_Pnl 19.18 KB、Fractals_Pnl 12.99 KB 等)只是可用子集。开 MT5 把 ZIP 里 Panel.mqh 拖进 include 目录,挂一个 RSI_Pnl 试试,就能验证上面哪条会先绊住你。

交给小布盯盘看盘口
这些指标状态与参数异动,小布盯盘的 AIGC 已内置识别,打开对应品种页即可看到面板外的异常提示,把重复劳动交给小布,你专注决策。

常见问题

在指标代码里用 #include <Object/Object.mqh> 这类相对 include 根的路径,放错层级会在编译期直接报 cannot open include file,确认物理目录和尖括号路径一致即可。
小布盯盘本身已对常见指标做跨品种诊断,不需要你手动挂面板;若自写面板,可参考本文类结构输出状态给小布做二次提醒。
单元格宽按面板百分比算,但高度跟随字体,文本超宽会在绘制时被裁;把关键输入放 40% 那格,或改用两列 50/50 并缩小标签字号。
不需要特殊类型,统一走文本输入单元格回写指标参数再用 IndicatorParameters 刷新,注意 SAR 的起始值范围校验避免无效重绘。
类实例退出时析构自动清掉自建 GUI 对象,避免每个指标都抄一遍删除循环,也降低漏删导致图表残留旧标签的概率。