📘

多指标同图的初次验证思路

多指标同图的初次验证思路

在 MT5 里把多个指标叠到同一张图表,是确认信号共振最直接的第一步。很多交易者习惯单指标裸看,但价格行为本身噪声大,两个独立指标同时给方向,过滤假突破的概率会明显高一些。 以 2022 年 5 月 6 日发布的示例为例,该多指标同图实验帖在站内获得 1981 次查看、30 条讨论,说明这类基础叠加玩法仍有高频实战需求。你可以直接开 MT5 新建图表,先拖一个 MA 再拖一个 RSI 上去,观察两者在 H1 级别是否经常反向打架。 外汇与贵金属杠杆高、滑点突发行情多,叠加指标只是降噪手段,不等于信号必胜;任何共振都只是「倾向」而非确定。

「从概念验证到代码落地」

上一篇文章只讲了多指标同图的数据层构想,没给一行可运行的系统代码,这次开始把骨架补上。 当前版本先把“建库—取数—绘制”的最小闭环跑通,后续几篇会逐步把系统做得更通用,比如支持动态指标列表和跨周期读取。 外汇与贵金属市场高杠杆、高波动,这类多指标聚合工具只用于辅助观察,任何信号都只是概率倾向,不能直接当作下单依据。

◍ 用指标外壳搭可扩展子窗口框架

把多资产监视系统拆成两个文件,主逻辑走 OOP,指标文件只负责在图表上开一个子窗口。选指标而非脚本或 EA,是因为指标自带创建子窗口的内部机制,省掉自己写窗口逻辑的麻烦,开发更快也更稳。 指标头部两行就能让 MT5 在品种图下方挂出独立子窗口: indicator_plots 0 —— 告诉编译器不跟踪任何绘图数据,消除警告; indicator_separate_window —— 指令编译器注入创建子窗口的必要逻辑。 不熟悉 MQL5 的人看源码会觉得怪,但它沿用了 C++ 社区广泛接受的协议,所以能用 C 风格 include 引入头文件。 include 路径用尖括号是绝对路径,用引号是相对路径;MQL5 默认从头文件目录找,所以可省略 Includes\ 前缀。指标里留了 user01、user02 输入串,可预填 "RSI:3;MACD:2" 这样的默认命令,打开即带好线宽配置。 对象类里真正公开给指标调用的只有 Resize 和 AddThese 两个函数,其余 SetBase、decode、AddTemplate 等都被 private 隐藏,外部改不动,可靠性来自这种封装。类内 #define 最大指针数定为 6,这是你最可能想改的唯一关键点——它动态限制子窗口内图表数量,适配多数显示器。 SetBase 里用宏替换简化重复代码,创建 CHART 对象并绑定资产:若填的资产不存在就链到当前品种;OBJPROP_CHART_SCALE 取 0–5 的缩放级,OBJPROP_PERIOD 默认跟随主图周期,想锁 PERIOD_M10 也可手动改。最后那个捕获 OBJ_CHART 句柄的赋值行最关键,它让后续设置能真正落到图表对象上。 AddThese 先检查能否加指标,若是 SYMBOL 则校验必须存在于市场观察窗口,通过后才建对象并按设置文件套模板。Resize 负责把所有内容约束在子窗口区域内不溢出。剩下 decode 等私有函数只做命令行语法解码与数据完整性校验,不报错,但拦住不一致数据进对象。

MQL5 / C++
class="macro">#class="kw">property indicator_plots class="num">0
class="macro">#class="kw">property indicator_separate_window
class="macro">#include <Auxiliary\C_TemplateChart.mqh>
class="kw">input class="type">class="kw">string user01 = "" ;      class=class="str">"cmt">//Used indicators
class="kw">input class="type">class="kw">string user02 = "" ;      class=class="str">"cmt">//Assets to follow
class="kw">input class="type">class="kw">string user01 = "RSI:class="num">3;MACD:class="num">2" ;  class=class="str">"cmt">//Used indicators
class="kw">input class="type">class="kw">string user02 = "" ;      class=class="str">"cmt">//       Assets to follow
C_TemplateChart SubWin;
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{
    SubWin.AddThese(C_TemplateChart::INDICATOR, user01);
    SubWin.AddThese(C_TemplateChart::SYMBOL, user02);
    class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//...... other lines are of no interest to us ......
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnChartEvent(class="kw">const class="type">int id,

子窗口里并排塞多个图表对象

想在 MT5 的自定义子窗口里同时摆好几张图表(比如主图品种加几个指标模板),靠 OBJ_CHART 对象就能办到。上面这段逻辑把图表对象建出来,再用宏批量设属性,核心是 ObjectCreate 建 OBJ_CHART 后绑定符号与周期。 #define def_MaxTemplates 6 这条硬上限意味着子窗口最多并排 6 个图表对象,超了 AddTemplate 直接 return 不建。默认缩放比例走 def_ScaleDefault 4,可在 Decode 里改。 Resize 按子窗口像素宽除以计数器均分每张图表的 X 距离和 X 尺寸;CHARTEVENT_CHART_CHANGE 触发时重排,避免拉伸变形。外汇与贵金属图表叠加多看盘维度有帮助,但多对象实时重绘会增加 CPU 占用,高频行情下可能卡顿。

MQL5 / C++
class="kw">const class="type">long &lparam,
class="kw">const class="type">class="kw">double &dparam,
class="kw">const class="type">class="kw">string &sparam)
{
   if(id==CHARTEVENT_CHART_CHANGE) SubWin.Resize();
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define def_MaxTemplates        class="num">6
enum eTypeChart {INDICATOR, SYMBOL};
class="kw">private :
class="type">void SetBase(class="kw">const class="type">class="kw">string szSymbol, class="type">int scale)
{
class="macro">#define macro_SetInteger(A, B) ObjectSetInteger(m_Id, m_szObjName[m_Counter], A, B)
...
   ObjectCreate(m_Id, m_szObjName[m_Counter], OBJ_CHART, m_IdSubWin, class="num">0, class="num">0);
   ObjectSetString(m_Id, m_szObjName[m_Counter], OBJPROP_SYMBOL, szSymbol);
   macro_SetInteger(OBJPROP_CHART_SCALE, scale);
...
   macro_SetInteger(OBJPROP_PERIOD, _Period);
   m_handle = ObjectGetInteger(m_Id, m_szObjName[m_Counter], OBJPROP_CHART_ID);
   m_Counter++;
class="macro">#undef macro_SetInteger
};
m_handle = ObjectGetInteger(m_Id, m_szObjName[m_Counter], OBJPROP_CHART_ID);
class="type">void AddTemplate(class="kw">const eTypeChart type, class="kw">const class="type">class="kw">string szTemplate, class="type">int scale)
{
   if(m_Counter >= def_MaxTemplates) class="kw">return;
   if(type == SYMBOL) SymbolSelect(szTemplate, true);
   SetBase((type == INDICATOR ? _Symbol : szTemplate), scale);
   ChartApplyTemplate(m_handle, szTemplate + ".tpl");
   ChartRedraw(m_handle);
}
class="type">void Resize(class="type">void)
{
   class="type">int x0 = class="num">0, x1 = (class="type">int)(ChartGetInteger(m_Id, CHART_WIDTH_IN_PIXELS, m_IdSubWin) / (m_Counter > class="num">0 ? m_Counter : class="num">1));
   for(class="type">char c0 = class="num">0; c0 < m_Counter; c0++, x0 += x1)
   {
      ObjectSetInteger(m_Id, m_szObjName[c0], OBJPROP_XDISTANCE, x0);
      ObjectSetInteger(m_Id, m_szObjName[c0], OBJPROP_XSIZE, x1);
      ObjectSetInteger(m_Id, m_szObjName[c0], OBJPROP_YSIZE, ChartGetInteger(m_Id, CHART_HEIGHT_IN_PIXELS, m_IdSubWin));
   }
   ChartRedraw();
}
class="kw">public :
class="type">void Decode(class="type">class="kw">string &szArg, class="type">int &iScale)
{
class="macro">#define def_ScaleDefault class="num">4
   StringToUpper(szArg);
   iScale = def_ScaleDefault;
   for(class="type">int c0 = class="num">0, c1 = class="num">0, max = StringLen(szArg); c0 < max; c0++) class="kw">switch(szArg[c0])
   {
      case &class="macro">#x27;:&class="macro">#x27; :

「从字符串里抠出小数位与模板名」

这段逻辑干的事很直接:把外部传进来的字符串参数拆开,从中解析出图表模板名称以及附带的小数精度(scale)。解析入口在 AddThese,它先把参数转大写并尾部补分号,再用一个 for+switch 逐字符扫描。 扫描碰到 ';' 就切一段子串,调用 Decode 提取精度 i0,再丢给 AddTemplate 注册。注意 c1 和 c2 分别标记片段起止,若二者不等才认为捞到了有效片段——空段直接跳过。 Decode 内部那段 for 循环专门跳过非数字字符('0'~'9' 之外),随后把紧接的数字字符转成 int 作为 iScale。若读出的值大于 5 或小于 0,则回退到宏 def_ScaleDefault,说明脚本默认只认 0~5 位小数,超出就当没写。 空格在 switch 里被当作分隔符:在 AddThese 中若 c1>=c2 就把 c1 推到下一字符,避免连续空格制造空片段;在 Decode 中空格直接 break 不处理。外汇与贵金属图表加载模板时精度误设可能让指标显示错位,实盘前建议在 MT5 用 Print 把 Decode 后的 i0 打出来核对。

MQL5 / C++
for (; (c0 < max) && ((szArg[c0] < &class="macro">#x27;class="num">0&class="macro">#x27; ) || (szArg[c0] > &class="macro">#x27;class="num">9&class="macro">#x27; )); c0++);
      iScale = ( class="type">int )(szArg[c0] - &class="macro">#x27;class="num">0&class="macro">#x27; );
      iScale = ((iScale > class="num">5 ) || (iScale < class="num">0 ) ? def_ScaleDefault : iScale);
      szArg = StringSubstr(szArg, class="num">0 , c1 + class="num">1 );
      class="kw">return ;
      case &class="macro">#x27; &class="macro">#x27; :
         class="kw">break ;
      class="kw">default :
         c1 = c0;
         class="kw">break ;
class="macro">#undef def_ScaleDefault
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// ... Codes not related to this part...
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void AddThese( class="kw">const eTypeChart type, class="type">class="kw">string szArg)
{
      class="type">class="kw">string szLoc;
      class="type">int i0;
      StringToUpper(szArg);
      StringAdd(szArg, ";" );
      for ( class="type">int c0 = class="num">0 , c1 = class="num">0 , c2 = class="num">0 , max = StringLen(szArg); c0 < max; c0++) class="kw">switch (szArg[c0])
      {
         case &class="macro">#x27;;&class="macro">#x27; :
            if (c1 != c2)
            {
               szLoc = StringSubstr(szArg, c1, c2 - c1 + class="num">1 );
               Decode(szLoc, i0);
               AddTemplate(type, szLoc, i0);
            }
            c1 = c2 = (c0 + class="num">1 );
            class="kw">break ;
         case &class="macro">#x27; &class="macro">#x27; :
            c1 = (c1 >= c2 ? c0 + class="num">1 : c1);
            class="kw">break ;
         class="kw">default :
            c2 = c0;

◍ 循环退出的临界写法

在 MQL5 的嵌套遍历里,最里层用 break 只打断当前这一轮最内层循环,外层遍历仍会继续推进,这点容易被误读成整段逻辑中止。 写指标或 EA 的 K 线扫描时,若只想在命中某个条件后跳出全部循环,单靠一个 break 不够,需要配合标志位或改为函数 return。 下面这段是原文里最里层退出的核心片段,贴在 MT5 里单步跑一遍就能看清作用域边界。

MQL5 / C++
              class="kw">break ;
        }
}

把子窗口指标跑起来再说

附件里的 Chart_In_SubWindow_k_Version_1.0a.zip 只有 3.08 KB,解压后丢进 MQL5 目录用 MetaEditor 编译即可,但编译通过不等于能跑。有用户在 2023 年 10 月反馈,装好后同窗口多资产就是不显示,作者回得很直接:按文章步骤一步步来,少一步都不行。 实际踩坑集中在子窗口加载环节——应用三个指标后画面闪一下变空白,资产消失。作者点明原因不是编译问题,而是跳过了前文里关于指标挂载顺序和参数配置的说明。外汇与贵金属市场波动剧烈、杠杆风险高,这类多周期同屏工具只帮你省排版时间,不替你判断方向。 想验证就现在开 MT5,把 ZIP 里指标编译挂上,主图放 2 分钟周期,子窗塞 5/15/60 分钟,看是否真能稳定同屏。跑通了,再谈怎么往里加 RSI、MACD 这类东西。

常见问题

先搭一个可扩展的子窗口框架,用指标外壳承载多个图表对象,并排显示即可快速看相关性,不必反复切换窗口。
卡在子窗口对象的坐标对齐和循环退出条件,建议先用字符串解析出小数位与模板名,再写临界退出避免死循环。
可以,小布能按你给的品种自动生成副图对比框架并标注异常窗口,你只需确认参数,不用手写底层代码。
用分隔符切分后转数值,保留原始模板名做键,这样既不会错位也能在循环里精准匹配对应子图。
把上限设为对象总数减一并在末尾补一次绘制,可避免最后一项被跳过,外汇贵金属波动大更该这么写。