MQL5 细则手册:在 MQL5 中开发多交易品种波动指标·综合运用
📊

MQL5 细则手册:在 MQL5 中开发多交易品种波动指标·综合运用

(3/3)· 六个品种 ATR 同屏、句柄同步与进度画布,一篇把多品种指标的最后坑填平

案例拆解新手友好 第 3/3 篇
多品种指标最容易被忽略的不是算 ATR,而是别的品种数据和当前图表对不上时间。新手常直接拉句柄取值,结果柱起点错乱、缺数据还不报错。先理清同步逻辑,后面代码才不会越写越歪。

「指标子窗口的画布与尺寸自适配」

在 MT5 自定义指标里,想把图形直接画到独立子窗口而不是主图,第一步是拿到子窗口的几何参数。GetSubwindowGeometry() 用 ChartWindowFind(0, subwindow_shortname) 定位窗口序号,再用 CHART_WIDTH_IN_PIXELS 和 CHART_HEIGHT_IN_PIXELS 取宽高,最后算出中心坐标 subwindow_center_x = chart_width/2、subwindow_center_y = subwindow_height/2,后续所有位图标签都按这个中心铺开。 画布本身用 Canvas 类的 CreateBitmapLabel 建:若 ObjectFind 找不到 canvas_name 就新建一个覆盖整个子窗口的位图标签,Erase(ColorToARGB(...,0)) 把背景擦成全透明,Update() 提交。这样你的绘制层不会挡住 K 线,也不会留下默认灰底。 窗口被用户拖拽缩放时,OnSubwindowChange() 会先调 GetSubwindowGeometry 重新取尺寸,再用 SubwindowSizeChanged() 判断是否有变化;若 subwindow_height<1 或中心算错就直接 return,避免崩在负数像素上。确认变化后才 ResizeCanvas() 并重绘最后一条 ShowCanvasMessage(msg_last)。外汇与贵金属行情跳空频繁,子窗口历史补全后可能触发 OC_prev_calculated==0 的重算分支,这时主动再调一次 OnCalculate 比等新 tick 更稳。

MQL5 / C++
  if(OC_prev_calculated==class="num">0)
  {
    OnCalculate(OC_rates_total,OC_prev_calculated,
                OC_time,OC_open,OC_high,OC_low,OC_close,
                OC_tick_volume,OC_volume,OC_spread);
  }
}
class="type">void GetSubwindowGeometry()
  {
  subwindow_number=ChartWindowFind(class="num">0,subwindow_shortname);
  chart_width=(class="type">int)ChartGetInteger(class="num">0,CHART_WIDTH_IN_PIXELS);
  subwindow_height=(class="type">int)ChartGetInteger(class="num">0,CHART_HEIGHT_IN_PIXELS,subwindow_number);
  subwindow_center_x=chart_width/class="num">2;
  subwindow_center_y=subwindow_height/class="num">2;
  }
class="type">void SetCanvas()
  {
  if(ObjectFind(class="num">0,canvas_name)<class="num">0)
    {
    canvas.CreateBitmapLabel(class="num">0,subwindow_number,canvas_name,class="num">0,class="num">0,chart_width,subwindow_height,clr_format);
    canvas.Erase(ColorToARGB(canvas_background,class="num">0));
    canvas.Update();
    }
  }
class="type">void OnSubwindowChange()
  {
  GetSubwindowGeometry();
  if(!SubwindowSizeChanged())
    class="kw">return;
  if(subwindow_height<class="num">1 || subwindow_center_y<class="num">1)
    class="kw">return;
  ResizeCanvas();
  ShowCanvasMessage(msg_last);
  }
class="type">bool SubwindowSizeChanged()
  {

◍ 画布生命周期与数据加载的底层控制

在 MT5 指标里用 CCanvas 类做自定义绘制,最容易被忽略的是窗口尺寸变化检测。上面这段逻辑先用 last_chart_width / last_subwindow_height 两个静态变量记录上一次的尺寸,若本次 OnCalculate 拿到的 chart_width 和 subwindow_height 都没变,直接 return(false) 跳过重绘,能省掉大量无谓的 GDI 资源消耗。 ResizeCanvas() 只做一件事:当 ObjectFind(0,canvas_name) 返回的窗口序号等于 subwindow_number,就调 canvas.Resize(chart_width,subwindow_height) 把画布拉伸到新尺寸。注意这里依赖 GetSubwindowGeometry() 提前算好坐标,否则 resize 会画到错误区域。 ShowCanvasMessage() 的消失动画思路值得抄:DeleteCanvas() 里用 for(int i=canvas_opacity; i>0; i-=5) 每次把背景透明度降 5 再 Erase+Update,做出淡出效果,最后 canvas.Destroy() 释放资源。步长 5 是硬编码,若你的 canvas_opacity 是 255,完整淡出约 51 帧,在 30fps 图表上接近 1.7 秒。 LoadAndFormData() 开头把 bars_count 写死成 100,意味着每次只重算最近 100 根 K 线。做外汇或贵金属指标时这属于高风险简化——小周期跳空或非农行情里 100 根可能覆盖不到关键波段,建议改成按图表可见柱数动态取。

MQL5 / C++
class=class="str">"cmt">//--- If the subwindow size has not changed, exit
if(last_chart_width==chart_width && last_subwindow_height==subwindow_height)
   class="kw">return(class="kw">false);
class=class="str">"cmt">//--- If the size has changed, save it
else
   {
    last_chart_width=chart_width;
    last_subwindow_height=subwindow_height;
   }
class=class="str">"cmt">//---
   class="kw">return(true);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Resizing canvas                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ResizeCanvas()
  {
class=class="str">"cmt">//--- If the canvas has already been added to the indicator subwindow, set the new size
   if(ObjectFind(class="num">0,canvas_name)==subwindow_number)
      canvas.Resize(chart_width,subwindow_height);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Displaying message on the canvas                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ShowCanvasMessage(class="type">class="kw">string message_text)
  {
   GetSubwindowGeometry();
class=class="str">"cmt">//--- If the canvas has already been added to the indicator subwindow
   if(ObjectFind(class="num">0,canvas_name)==subwindow_number)
     {
      class=class="str">"cmt">//--- If the class="type">class="kw">string passed is not empty and correct coordinates have been obtained, display the message
      if(message_text!="" && subwindow_center_x>class="num">0 && subwindow_center_y>class="num">0)
        {
         canvas.Erase(ColorToARGB(canvas_background,canvas_opacity));
         canvas.TextOut(subwindow_center_x,subwindow_center_y,message_text,ColorToARGB(clrRed),TA_CENTER|TA_VCENTER);
         canvas.Update();
        }
     }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Deleting canvas                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DeleteCanvas()
  {
class=class="str">"cmt">//--- Delete the canvas if it exists
   if(ObjectFind(class="num">0,canvas_name)>class="num">0)
     {
      class=class="str">"cmt">//--- Before deleting, implement the disappearing effect
      for(class="type">int i=canvas_opacity; i>class="num">0; i-=class="num">5)
        {
         canvas.Erase(ColorToARGB(canvas_background,(class="type">uchar)i));
         canvas.Update();
        }
      class=class="str">"cmt">//--- Delete the graphical resource
      canvas.Destroy();
     }
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Loading and generating the necessary/available amount of data       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void LoadAndFormData()
  {
   class="type">int bars_count=class="num">100; class=class="str">"cmt">// Number of loaded bars
class=class="str">"cmt">//---
   for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)

分批拉历史数据的退出条件

多品种回测前要把终端本地缺的历史 bar 补齐,但不能无脑一直向服务器要数据。这段代码用 while 循环控制:只要已拷数组长度小于目标总量 OC_rates_total,且终端首 bar 与服务器首 bar 的时间差仍大于 PeriodSeconds()*bars_count,就继续 CopyTime 往回搬。 循环里有个防卡死逻辑值得注意:如果连续 100 次拷贝后数组大小都没变大(attempts 计数到 100),就强制 break。外汇与贵金属行情在休市时段或桥接延迟下可能拷不回更早数据,这种退出机制能避免 EA 卡在死循环里空耗。 另外当拷回的首根时间减去一个 bars_count 周期已早于图表首根 OC_time[0],也会直接退出——说明本地已有足够覆盖图表范围的样本,不必再向服务器深挖。 逐行看变量定义与判断:attempts 记无效拷贝次数,array_size 记当前数组长,firstdate_server / firstdate_terminal 分别拿服务器与终端首 bar 时间;SeriesInfoInteger 配合 SERIES_FIRSTDATE 与 SERIES_SERVER_FIRSTDATE 取这两个时间点。while 条件把两者时间差和 PeriodSeconds()*bars_count 比,决定要不要继续搬。

MQL5 / C++
   {
      class="type">int      attempts         =class="num">0;   class=class="str">"cmt">// Counter of data copying attempts
      class="type">int      array_size        =class="num">0;   class=class="str">"cmt">// Array size
      class="type">class="kw">datetime firstdate_server  =NULL; class=class="str">"cmt">// Time of the first bar on the server
      class="type">class="kw">datetime firstdate_terminal=NULL; class=class="str">"cmt">// Time of the first bar in the terminal base
      class=class="str">"cmt">//--- Get the first date by the symbol/time frame in the terminal base
      SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE,firstdate_terminal);
      class=class="str">"cmt">//--- Get the first date of the symbol/time frame on the server
      SeriesInfoInteger(symbol_names[s],Period(),SERIES_SERVER_FIRSTDATE,firstdate_server);
      class=class="str">"cmt">//--- Print the message
      msg_last=msg_load_data="Loading and generating data: "+
             symbol_names[s]+"("+(class="type">class="kw">string)(s+class="num">1)+"/"+(class="type">class="kw">string)SYMBOLS_COUNT+") ... ";
      ShowCanvasMessage(msg_load_data);
      class=class="str">"cmt">//--- Load/generate data.
      class=class="str">"cmt">//    If the array size is smaller than the maximum number of bars in the terminal, and if
      class=class="str">"cmt">//    the number of bars between the first date of the series in the terminal and the first date of the series on the server is more than specified
      class="kw">while(array_size<OC_rates_total &&
            firstdate_terminal-firstdate_server>PeriodSeconds()*bars_count)
        {
         class="type">class="kw">datetime copied_time[];
         class=class="str">"cmt">//--- Get the first date by the symbol/time frame in the terminal base
         SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE,firstdate_terminal);
         class=class="str">"cmt">//--- Load/copy the specified number of bars
         if(CopyTime(symbol_names[s],Period(),class="num">0,array_size+bars_count,copied_time)!=-class="num">1)
           {
            class=class="str">"cmt">//--- If the time of the first bar in the array, excluding the number of the bars being loaded, is earlier 
            class=class="str">"cmt">//    than the time of the first bar in the chart, terminate the loop
            if(copied_time[class="num">0]-PeriodSeconds()*bars_count<OC_time[class="num">0])
              class="kw">break;
            class=class="str">"cmt">//--- If the array size hasn&class="macro">#x27;t increased, increase the counter
            if(ArraySize(copied_time)==array_size)
               attempts++;
            class=class="str">"cmt">//--- Otherwise get the current size of the array
            else
               array_size=ArraySize(copied_time);
            class=class="str">"cmt">//--- If the array size hasn&class="macro">#x27;t increased over class="num">100 attempts, terminate the loop
            if(attempts==class="num">100)

「多品种数据就绪前的重试逻辑」

在 MT5 里跑跨品种指标时,最容易被忽略的是历史数据不一定即时到位。上面这段逻辑在遍历 SYMBOLS_COUNT 个品种时,先通过 BarsCalculated 拿指标已计算值,再用 SeriesInfoInteger 取终端当前周期首根数据日期,最后用 Bars 算出从首日期到当下的可用 bar 数。 针对 bar 时间和指标数据,代码各给了 5 次拉取重试:CopyTime 拷贝时间数组,若 ArraySize(time) 大于等于 available_bars 就 break 退出;指标数据同理循环 5 次尝试。这个 5 次上限是硬约束,意味着若服务端历史缺失严重,超过 5 次仍拿不全就会带着残缺数据继续跑。 子窗口尺寸检查放在每 2000 根 bar 触发一次(array_size%2000 为 0 时调 OnSubwindowChange),避免逐 bar 重算画布拖慢回测。外汇与贵金属多品种叠加本就高波动高杠杆,数据缺口会让信号偏移,实盘前应在 MT5 用「打开数据窗口」核对各品种首日期是否一致。

MQL5 / C++
{
   attempts=class="num">0;
   class="kw">break;
   }
  }
  class=class="str">"cmt">//--- Check the subwindow size once every class="num">2000 bars 
  class=class="str">"cmt">//    and if the size has changed, adjust the canvas size to it
  if(!(array_size%class="num">2000))
    OnSubwindowChange();
  }
  }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Checking the amount of available data for all symbols              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CheckAvailableData()
  {
  for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
    {
    class=class="str">"cmt">//--- If this symbol is available
    if(symbol_names[s]!=empty_symbol)
      {
      class="type">class="kw">double   data[]; class=class="str">"cmt">// Array for checking the amount of indicator data
      class="type">class="kw">datetime time[]; class=class="str">"cmt">// Array for checking the number of bars
      class="type">int      calculated_values =class="num">0;      class=class="str">"cmt">// Amount of indicator data
      class="type">int      available_bars    =class="num">0;      class=class="str">"cmt">// Number of bars of the current period
      class="type">class="kw">datetime firstdate_terminal=NULL; class=class="str">"cmt">// First date of the current time frame data available in the terminal
      class=class="str">"cmt">//--- Get the number of calculated values of the indicator
      calculated_values=BarsCalculated(symbol_handles[s]);
      class=class="str">"cmt">//--- Get the first date of the current time frame data in the terminal
      firstdate_terminal=(class="type">class="kw">datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_TERMINAL_FIRSTDATE);
      class=class="str">"cmt">//--- Get the number of available bars from the date specified
      available_bars=Bars(symbol_names[s],Period(),firstdate_terminal,TimeCurrent());
      class=class="str">"cmt">//--- Check the readiness of bar data: class="num">5 attempts to get values
      for(class="type">int i=class="num">0; i<class="num">5; i++)
        {
        class=class="str">"cmt">//--- Copy the specified amount of data
        if(CopyTime(symbol_names[s],Period(),class="num">0,available_bars,time)!=-class="num">1)
          {
          class=class="str">"cmt">//--- If the required amount has been copied, terminate the loop
          if(ArraySize(time)>=available_bars)
            class="kw">break;
          }
        }
      class=class="str">"cmt">//--- Check the readiness of indicator data: class="num">5 attempts to get values
      for(class="type">int i=class="num">0; i<class="num">5; i++)

◍ 历史深度不够时如何优雅退出重采

多品种指标在 OnInit 或 OnCalculate 里最怕一件事:终端还没把足够 K 线塞进内存,你就去算跨品种相关性,结果数组长度对不上,画出来的线直接断头。上面这段逻辑就是专门兜底——先 CopyBuffer 拉数据,拉到了比对 ArraySize 是否达到 calculated_values,够数才 break 跳出循环。 如果 time 或 data 数组尺寸仍小于 available_bars / calculated_values,说明这一轮历史没备齐。代码不硬算,而是把 OC_prev_calculated 置 0、弹一条画布提示并返回 false,等下一 tick 再试。外汇与贵金属行情在跳空或换周时可能临时缺柱,这种重试机制能降低误报概率。 CheckLoadedHistory 则盯着 SERIES_FIRSTDATE:首次跑把首根日期存进 series_first_date_last,之后每次重新取,若新日期比记忆里的更早,就判定「 deeper history 已加载」。实战里你改 SYMBOLS_COUNT 或多开小周期,首次加载常常要等几秒,盯这个函数返回值比盲等更靠谱。

MQL5 / C++
  {
      class=class="str">"cmt">//--- Copy the specified amount of data
      if(CopyBuffer(symbol_handles[s],class="num">0,class="num">0,calculated_values,data)!=-class="num">1)
        {
         class=class="str">"cmt">//--- If the required amount has been copied, terminate the loop
         if(ArraySize(data)>=calculated_values)
            class="kw">break;
        }
      }
      class=class="str">"cmt">//--- If the amount of data copied is not sufficient, one more attempt is required
      if(ArraySize(time)<available_bars || ArraySize(data)<calculated_values)
        {
         msg_last=msg_prepare_data;
         ShowCanvasMessage(msg_prepare_data);
         OC_prev_calculated=class="num">0;
         class="kw">return(class="kw">false);
        }
     }
   }
class=class="str">"cmt">//---
   class="kw">return(true);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Checking the event of loading a deeper history                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CheckLoadedHistory()
  {
   class="type">bool loaded=class="kw">false;
class=class="str">"cmt">//---
   for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
     {
      class=class="str">"cmt">//--- If this symbol is available
      if(symbol_names[s]!=empty_symbol)
        {
         class=class="str">"cmt">//--- If the series need to be updated
         if(OC_prev_calculated==class="num">0)
           {
            class=class="str">"cmt">//--- Get the first date by the symbol/time frame
            series_first_date[s]=(class="type">class="kw">datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE);
            class=class="str">"cmt">//--- If this is the first time(no value is available), then
            if(series_first_date_last[s]==NULL)
               class=class="str">"cmt">//--- Store the first date by the symbol/time frame for further comparison 
               class=class="str">"cmt">//    in order to determine if a deeper history has been loaded
               series_first_date_last[s]=series_first_date[s];
           }
         else
           {
            class=class="str">"cmt">//--- Get the first date by the symbol/time frame
            series_first_date[s]=(class="type">class="kw">datetime)SeriesInfoInteger(symbol_names[s],Period(),SERIES_FIRSTDATE);
            class=class="str">"cmt">//--- If the dates are different, i.e. the date in the memory is later than the one we have just obtained,
            class=class="str">"cmt">//    this means that a deeper history has been loaded

深历史加载与同步校验的返回逻辑

指标在刷新前需要确认各交易品种的历史数据是否比上一次更深。代码里用 series_first_date_last[s] 与 series_first_date[s] 做比较,若前者大于后者,说明服务端又补了更早的棒线或本地生成了更深的缓存,此时向日志打印具体品种、周期和日期,并把新日期存回数组,同时将 loaded 置为 true。 一旦发现任意品种加载了更深历史,函数直接 return(false),告诉调用层先别画图,等下一轮再刷新;否则 return(true) 继续后续绘制。这一机制能避免用残缺历史算出偏移的缓冲曲线。 紧接着的 CheckSymbolIsSynchronized() 只做一件事:在 TERMINAL_CONNECTED 为真时,逐个扫 symbol_names 里非空的品种,用 SeriesInfoInteger 查 SERIES_SYNCHRONIZED。只要有一个不同步,就通过画布提示并 return(false),全部通过才 return(true)。外汇与贵金属行情在跳空或重连后常出现不同步,这套检查能降低误绘概率。 TimeframeToString() 则是把 ENUM_TIMEFRAMES 转成 "M1" 这类短串,遇到 WRONG_VALUE 或 NULL 会回退到当前图表 Period(),方便上面日志输出时一眼看清周期。

MQL5 / C++
if(series_first_date_last[s]>series_first_date[s])
  {
   class=class="str">"cmt">//--- Print the relevant message to the log
   Print("(",symbol_names[s],",",TimeframeToString(Period()),
         ") > A deeper history has been loaded/generated: ",
         series_first_date_last[s],"> ",series_first_date[s]);
   class=class="str">"cmt">//--- Store the date
   series_first_date_last[s]=series_first_date[s];
   loaded=true;
   }
  }
 }
}
class=class="str">"cmt">//--- If a deeper history has been loaded/generated, then
class=class="str">"cmt">//    send the command to refresh the plotting series of the indicator
 if(loaded)
   class="kw">return(class="kw">false);
class=class="str">"cmt">//---
 class="kw">return(true);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Checking synchronization by symbol/time frame                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CheckSymbolIsSynchronized()
 {
class=class="str">"cmt">//--- If the connection to the server is established, check the data synchronization
 if(TerminalInfoInteger(TERMINAL_CONNECTED))
   {
    for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
      {
       class=class="str">"cmt">//--- If the symbol is available
       if(symbol_names[s]!=empty_symbol)
         {
          class=class="str">"cmt">//--- If the data are not synchronized, print the relevant message and try again
          if(!SeriesInfoInteger(symbol_names[s],Period(),SERIES_SYNCHRONIZED))
            {
             msg_last=msg_not_synchronized;
             ShowCanvasMessage(msg_not_synchronized);
             class="kw">return(class="kw">false);
             }
          }
       }
    }
class=class="str">"cmt">//---
 class="kw">return(true);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Converting time frame to a class="type">class="kw">string                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string TimeframeToString(ENUM_TIMEFRAMES timeframe)
 {
  class="type">class="kw">string str="";
class=class="str">"cmt">//--- If the value passed is incorrect, take the current chart time frame
  if(timeframe==WRONG_VALUE || timeframe== NULL)
     timeframe= Period();
  class="kw">switch(timeframe)
    {
     case PERIOD_M1  : str="M1";  class="kw">break;

「周期映射与首根有效棒判定」

把 ENUM_TIMEFRAMMES 枚举翻译成短字符串,是面板里显示周期标签的基础活。下面这段 switch 把 M2 到 MN1 共 21 个周期常量逐一对应成 "M2""H4" 这类可读文本,缺一个都会在界面上漏标。 周期字符串映射只是表象,真正影响绘制起点的是 DetermineFirstTrueBar()。它按 SYMBOLS_COUNT 遍历品种,遇到 empty_symbol 直接 continue 跳过,再用 Bars() 取当前图表周期的总棒数,随后用 CopyTime() 把时间数组搬进内存。 CopyTime 的返回值若小于 available_bars,说明本次拷贝不完整,函数会进入重试逻辑。外汇与贵金属品种在跨周期拉取时常常因流动性断层导致拷贝失败,属于高风险环境下的常见异常,需留足重试余量。 只要任一品种成功取到首根时间,后续绘制就以该时间为基准对齐;多品种不同步时,首根有效棒决定了整体图形的左边界。

MQL5 / C++
   case PERIOD_M2  : str="M2";  class="kw">break;
   case PERIOD_M3  : str="M3";  class="kw">break;
   case PERIOD_M4  : str="M4";  class="kw">break;
   case PERIOD_M5  : str="M5";  class="kw">break;
   case PERIOD_M6  : str="M6";  class="kw">break;
   case PERIOD_M10 : str="M10"; class="kw">break;
   case PERIOD_M12 : str="M12"; class="kw">break;
   case PERIOD_M15 : str="M15"; class="kw">break;
   case PERIOD_M20 : str="M20"; class="kw">break;
   case PERIOD_M30 : str="M30"; class="kw">break;
   case PERIOD_H1  : str="H1";  class="kw">break;
   case PERIOD_H2  : str="H2";  class="kw">break;
   case PERIOD_H3  : str="H3";  class="kw">break;
   case PERIOD_H4  : str="H4";  class="kw">break;
   case PERIOD_H6  : str="H6";  class="kw">break;
   case PERIOD_H8  : str="H8";  class="kw">break;
   case PERIOD_H12 : str="H12"; class="kw">break;
   case PERIOD_D1  : str="D1";  class="kw">break;
   case PERIOD_W1  : str="W1";  class="kw">break;
   case PERIOD_MN1 : str="MN1"; class="kw">break;
   }
class=class="str">"cmt">//---
   class="kw">return(str);
   }
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class=class="str">"cmt">//| Determining the time of the first true bar for the purpose of drawing |
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class="type">bool DetermineFirstTrueBar()
  {
   for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
     {
      class="type">class="kw">datetime time[];                class=class="str">"cmt">// Bar time array
      class="type">int      available_bars=class="num">0; class=class="str">"cmt">// Number of bars
      class=class="str">"cmt">//--- If this symbol is not available, move to the next one
      if(symbol_names[s]==empty_symbol)
         class="kw">continue;
      class=class="str">"cmt">//--- Get the total number of bars for the symbol
      available_bars=Bars(symbol_names[s],Period());
      class=class="str">"cmt">//--- Copy the bar time array. If this action failed, try again.
      if(CopyTime(symbol_names[s],Period(),class="num">0,available_bars,time)<available_bars)

◍ 用相邻棒差定位首根真实K线

多周期脚本里常遇到「合成棒」和「真实棒」混排。要画时间轴起点,得先知道当前时间框架下第一根货真价实的柱子从哪根开始。 思路很直接:遍历时间数组,比较相邻两根的秒差是否等于 PeriodSeconds() 返回的当前周期秒数。一旦相等,那根就是首个真实棒,记下时间并 break。 下面这段 GetFirstTrueBarTime 就是干这个的。注意 ArraySetAsSeries(time,false) 把序列方向转正,i 从 1 起跳,用 time[i]-time[i-1] 判断间隔。 拿到时间后,CreateVerticalLine 在 0 号图表、0 号窗口按 2 像素宽、实线、指定颜色画竖线,文本标出 begin time series 和具体时间。外汇与贵金属波动剧烈,这类标记仅辅助复盘,实际下单仍属高风险行为。

MQL5 / C++
class="type">class="kw">datetime GetFirstTrueBarTime(class="type">class="kw">datetime &time[])
  {
  class="type">class="kw">datetime true_period =NULL; class=class="str">"cmt">// Time of the first true bar
  class="type">int      array_size  =class="num">0;    class=class="str">"cmt">// Array size
class=class="str">"cmt">//--- Get the array size
  array_size=ArraySize(time);
  ArraySetAsSeries(time,class="kw">false);
class=class="str">"cmt">//--- Check each bar one by one
  for(class="type">int i=class="num">1; i<array_size; i++)
    {
    class=class="str">"cmt">//--- If the bar corresponds to the current time frame
    if(time[i]-time[i-class="num">1]==PeriodSeconds())
      {
      class=class="str">"cmt">//--- Save it and terminate the loop
      true_period=time[i];
      class="kw">break;
      }
    }
class=class="str">"cmt">//--- Return the time of the first true bar
  class="kw">return(true_period);
  }

class="type">void CreateVerticalLine(class="type">long          chart_id,       class=class="str">"cmt">// chart id
                         class="type">int           window_number,  class=class="str">"cmt">// window number
                         class="type">class="kw">datetime      time,           class=class="str">"cmt">// time
                         class="type">class="kw">string        object_name,    class=class="str">"cmt">// object name
                         class="type">int           line_width,     class=class="str">"cmt">// line width
                         ENUM_LINE_STYLE line_style,    class=class="str">"cmt">// line style
                         class="type">class="kw">color         line_color      class=class="str">"cmt">// line class="type">class="kw">color

垂直线对象的创建与首帧初始化

在 MT5 自定义指标里画一条可交互的竖向参考线,核心是先调用 ObjectCreate 用 OBJ_VLINE 类型把对象挂到指定图表和子窗口,时间坐标由 time 参数锁定、纵向忽略(传 0)。下面这段函数尾部参数表给出了几个容易被忽略的控制项:selectable 为 false 时鼠标无法选中该线,tooltip 传 "\n" 则强制不显示悬浮提示,description_text 才是对象描述正文。

MQL5 / C++
class="type">bool selectable, class=class="str">"cmt">// cannot select the object if FALSE
class="type">class="kw">string description_text, class=class="str">"cmt">// text of the description
class="type">class="kw">string tooltip) class=class="str">"cmt">// no tooltip if "\n"
{
class=class="str">"cmt">//--- If the object has been created successfully
 if(ObjectCreate(chart_id,object_name,OBJ_VLINE,window_number,time,class="num">0))
   {
   class=class="str">"cmt">//--- set its properties
   ObjectSetInteger(chart_id,object_name,OBJPROP_TIME,time);
   ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable);
   ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style);
   ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width);
   ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color);
   ObjectSetString(chart_id,object_name,OBJPROP_TEXT,description_text);
   ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip);
   }
}
逐行看:ObjectCreate 成功后才进分支设属性;OBJPROP_TIME 重复写一次是为了兼容某些重绘场景;线型、粗细、颜色全用 ObjectSetInteger 整型接口,文字类和提示类才用 ObjectSetString。外汇与贵金属市场波动剧烈、滑点频繁,这类标记线仅作视觉辅助,不代表任何方向预判。 首帧(prev_calculated==0)的初始化顺序很关键:先 ZeroCalculatedArrays 和 ZeroIndicatorBuffers 清空自有数组与缓冲,再 GetSubwindowGeometry 取子窗口几何、SetCanvas 挂画布,随后 LoadAndFormData 装载并成型数据。任一步骤如 GetIndicatorHandles、CheckAvailableData、CheckLoadedHistory、CheckSymbolIsSynchronized、DetermineFirstTrueBar 返回失败就直接 return(RESET),避免脏数据上屏。 实测若跳过 DetermineFirstTrueBar 直接全量绘制,在 28 个货币对多周期同屏时可能触发子窗口重算卡顿;按上面顺序走,首次计算返回 rates_total 并存入 OC_prev_calculated,后续帧才走增量逻辑。

MQL5 / C++
class="type">bool selectable, class=class="str">"cmt">// cannot select the object if FALSE
class="type">class="kw">string description_text, class=class="str">"cmt">// text of the description
class="type">class="kw">string tooltip) class=class="str">"cmt">// no tooltip if "\n"
{
class=class="str">"cmt">//--- If the object has been created successfully
 if(ObjectCreate(chart_id,object_name,OBJ_VLINE,window_number,time,class="num">0))
   {
   class=class="str">"cmt">//--- set its properties
   ObjectSetInteger(chart_id,object_name,OBJPROP_TIME,time);
   ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable);
   ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style);
   ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width);
   ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color);
   ObjectSetString(chart_id,object_name,OBJPROP_TEXT,description_text);
   ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip);
   }
}

class=class="str">"cmt">//--- If this is the first calculation or if a deeper history has been loaded or gaps in the history have been filled
 if(prev_calculated==class="num">0)
   {
   class=class="str">"cmt">//--- Zero out arrays for data preparation
   ZeroCalculatedArrays();
   class=class="str">"cmt">//--- Zero out indicator buffers
   ZeroIndicatorBuffers();
   class=class="str">"cmt">//--- Get subwindow properties
   GetSubwindowGeometry();
   class=class="str">"cmt">//--- Add the canvas
   SetCanvas();
   class=class="str">"cmt">//--- Load and generate the necessary/available amount of data
   LoadAndFormData();
   class=class="str">"cmt">//--- If there is an invalid handle, try to get it again
   if(!GetIndicatorHandles())
     class="kw">return(RESET);
   class=class="str">"cmt">//--- Check the amount of data available for all symbols
   if(!CheckAvailableData())
     class="kw">return(RESET);
   class=class="str">"cmt">//--- Check if a deeper history has been loaded
   if(!CheckLoadedHistory())
     class="kw">return(RESET);
   class=class="str">"cmt">//--- Check synchronization by symbol/time frame at the current moment
   if(!CheckSymbolIsSynchronized())
     class="kw">return(RESET);
   class=class="str">"cmt">//--- For each symbol, determine the bar from which we should start drawing
   if(!DetermineFirstTrueBar())
     class="kw">return(RESET);
   class=class="str">"cmt">//--- If you reached this point, it means that OnCalculate() will class="kw">return non-zero value and this needs to be saved
   OC_prev_calculated=rates_total;
   }

「定时器驱动的多品种数据回填」

MT5 指标若依赖 OnTimer 做异步装载,得防历史数据中途补全导致计算断片。上面这段逻辑里,一旦 CheckLoadedHistory() 返回假,就把 OC_prev_calculated 清零,随即不等新 tick 直接重跑 OnCalculate 做补救。 真正耗时的活是后面那层双重循环:外层扫 SYMBOLS_COUNT 个品种,内层从 limitrates_total 逐根 bar 调 PrepareData。每 1000 根刷新一次画布进度提示,每 2000 根顺手校一次子窗口尺寸,避免后台图表缩放后画布错位。 注意 PrepareData 里写了 attempts=100 的拷贝重试上限,说明跨品种抓价可能因 broker 端短暂无响应而失败。外汇与贵金属行情在高波动时段易丢包,这种重试机制能降低指标空白的概率,但依旧不保证百分百同步。

MQL5 / C++
class="type">void OnTimer()
  {
class=class="str">"cmt">//--- If a deeper history has been loaded
   if(!CheckLoadedHistory())
      OC_prev_calculated=class="num">0;
class=class="str">"cmt">//--- If for some reason calculations have not been completed or
class=class="str">"cmt">//    a deeper history has been loaded or
class=class="str">"cmt">//    gaps in the history have been filled, 
class=class="str">"cmt">//    then make another attempt without waiting for the new tick
   if(OC_prev_calculated==class="num">0)
     {
      OnCalculate(OC_rates_total,OC_prev_calculated,
                  OC_time,OC_open,OC_high,OC_low,OC_close,
                  OC_tick_volume,OC_volume,OC_spread);
     }
class=class="str">"cmt">//--- Prepare data for drawing
   for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
     {
      class=class="str">"cmt">//--- If the symbol is available
      if(symbol_names[s]!=empty_symbol)
        {
         class="type">class="kw">double percent=class="num">0.0; class=class="str">"cmt">// For the purpose of calculating the progress percentage
         msg_last=msg_sync_update="Preparing data("+IntegerToString(rates_total)+" bars) : "+
                                  symbol_names[s]+"("+(class="type">class="kw">string)(s+class="num">1)+"/"+(class="type">class="kw">string)(SYMBOLS_COUNT)+") - class="num">00% ... ";
         class=class="str">"cmt">//--- Print the message
         ShowCanvasMessage(msg_sync_update);
         class=class="str">"cmt">//--- Control every value of the array
         for(class="type">int i=limit; i<rates_total; i++)
           {
            PrepareData(i,s,time);
            class=class="str">"cmt">//--- Refresh the message once every class="num">1000 bars
            if(i%class="num">1000==class="num">0)
              {
               class=class="str">"cmt">//--- Progress percentage
               ProgressPercentage(i,s,percent);
               class=class="str">"cmt">//--- Print the message
               ShowCanvasMessage(msg_sync_update);
              }
            class=class="str">"cmt">//--- Check the subwindow size once every class="num">2000 bars
            class=class="str">"cmt">//    and if the size has changed, adjust the canvas size to it
            if(i%class="num">2000==class="num">0)
               OnSubwindowChange();
           }
        }
     }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Preparing data before drawing                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PrepareData(class="type">int bar_index,class="type">int symbol_number,class="type">class="kw">datetime class="kw">const &time[])
  {
   class="type">int attempts=class="num">100; class=class="str">"cmt">// Number of copying attempts
代码逐行拆解:void OnTimer() 是定时器回调入口;if(!CheckLoadedHistory()) OC_prev_calculated=0; 检测深层历史是否载入,未载全则重置计算标记。if(OC_prev_calculated==0) 内直接调 OnCalculate 补算,跳过等 tick。for(int s=0; s<SYMBOLS_COUNT; s++) 遍历品种,if(symbol_names[s]!=empty_symbol) 跳过空品种。double percent=0.0 仅用于进度百分比;msg_sync_update 拼出“Preparing data (N bars) : 品种名(序号/总数) - 00%”的提示串。ShowCanvasMessage 推到画布。内层 for(int i=limit; i<rates_total; i++) 逐 bar 跑 PrepareData(i,s,time)if(i%1000==0) 每千根算进度并刷新;if(i%2000==0) 每两千根调 OnSubwindowChange() 适配子窗。尾部 PrepareData 函数头定义里 int attempts=100 是拷贝重试次数上限。

MQL5 / C++
class="type">void OnTimer()
  {
class=class="str">"cmt">//--- If a deeper history has been loaded
   if(!CheckLoadedHistory())
      OC_prev_calculated=class="num">0;
class=class="str">"cmt">//--- If for some reason calculations have not been completed or
class=class="str">"cmt">//    a deeper history has been loaded or
class=class="str">"cmt">//    gaps in the history have been filled, 
class=class="str">"cmt">//    then make another attempt without waiting for the new tick
   if(OC_prev_calculated==class="num">0)
     {
      OnCalculate(OC_rates_total,OC_prev_calculated,
                  OC_time,OC_open,OC_high,OC_low,OC_close,
                  OC_tick_volume,OC_volume,OC_spread);
     }
class=class="str">"cmt">//--- Prepare data for drawing
   for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
     {
      class=class="str">"cmt">//--- If the symbol is available
      if(symbol_names[s]!=empty_symbol)
        {
         class="type">class="kw">double percent=class="num">0.0; class=class="str">"cmt">// For the purpose of calculating the progress percentage
         msg_last=msg_sync_update="Preparing data("+IntegerToString(rates_total)+" bars) : "+
                                  symbol_names[s]+"("+(class="type">class="kw">string)(s+class="num">1)+"/"+(class="type">class="kw">string)(SYMBOLS_COUNT)+") - class="num">00% ... ";
         class=class="str">"cmt">//--- Print the message
         ShowCanvasMessage(msg_sync_update);
         class=class="str">"cmt">//--- Control every value of the array
         for(class="type">int i=limit; i<rates_total; i++)
           {
            PrepareData(i,s,time);
            class=class="str">"cmt">//--- Refresh the message once every class="num">1000 bars
            if(i%class="num">1000==class="num">0)
              {
               class=class="str">"cmt">//--- Progress percentage
               ProgressPercentage(i,s,percent);
               class=class="str">"cmt">//--- Print the message
               ShowCanvasMessage(msg_sync_update);
              }
            class=class="str">"cmt">//--- Check the subwindow size once every class="num">2000 bars
            class=class="str">"cmt">//    and if the size has changed, adjust the canvas size to it
            if(i%class="num">2000==class="num">0)
               OnSubwindowChange();
           }
        }
     }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Preparing data before drawing                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PrepareData(class="type">int bar_index,class="type">int symbol_number,class="type">class="kw">datetime class="kw">const &time[])
  {
   class="type">int attempts=class="num">100; class=class="str">"cmt">// Number of copying attempts

◍ 跨周期取数时的边界与重试处理

在多品种监控里,当前图表时间帧之外的 K 线不应强行取数。代码用 time[bar_index]>=limit_time[symbol_number] 做判断:落在当前时间帧范围内的才去 CopyTime 和 CopyBuffer,否则直接把 ATR 缓冲写成 EMPTY_VALUE,避免把错位数据画进指标。 CopyTime 与 CopyBuffer 都不是百分百一次成功,尤其跨品种喊单时服务器偶尔丢包。原文用 for(i=0;i<attempts;i++) 套一层重试,返回值为 1 才写进 tmp 数组并 break,attempts 设小了可能漏数据,设大了拖慢刷新,实盘建议从 3 起步在 MT5 里试。 进度函数 ProgressPercentage 把 bar_index 除以 OC_rates_total 再乘 100,percent<=9.99 时补前导零,让日志显示 '01%' 而非 '1%'。OC_rates_total 是总预处理 bar 数,若它因品种节假日缺失而偏小,进度百分比会虚高,这是外汇贵金属多符号回算的高风险点,需自己打印校验。 填充阶段对 empty_symbol 直接 ArrayInitialize 清零,存在的品种才走更新消息,这样终端不会因某个平台下架的品种卡死整个缓冲写入。

MQL5 / C++
class=class="str">"cmt">//--- Time of the bar of the specified symbol and time frame
  class="type">class="kw">datetime symbol_time[];
class=class="str">"cmt">//--- Array for copying indicator values
  class="type">class="kw">double atr_values[];
class=class="str">"cmt">//--- If within the area of the current time frame bars
  if(time[bar_index]>=limit_time[symbol_number])
    {
      class=class="str">"cmt">//--- Copy the time
      for(class="type">int i=class="num">0; i<attempts; i++)
        {
         if(CopyTime(symbol_names[symbol_number],class="num">0,time[bar_index],class="num">1,symbol_time)==class="num">1)
            {
              tmp_symbol_time[symbol_number].time[bar_index]=symbol_time[class="num">0];
              class="kw">break;
            }
        }
      class=class="str">"cmt">//--- Copy the indicator value
      for(class="type">int i=class="num">0; i<attempts; i++)
        {
         if(CopyBuffer(symbol_handles[symbol_number],class="num">0,time[bar_index],class="num">1,atr_values)==class="num">1)
            {
              tmp_atr_values[symbol_number].value[bar_index]=atr_values[class="num">0];
              class="kw">break;
            }
        }
    }
class=class="str">"cmt">//--- If outside the area of the current time frame bars, set an empty value
   else
      tmp_atr_values[symbol_number].value[bar_index]=EMPTY_VALUE;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculating progress percentage                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ProgressPercentage(class="type">int bar_index,class="type">int symbol_number,class="type">class="kw">double &percent)
  {
   class="type">class="kw">string message_text="";
   percent=(class="type">class="kw">double(bar_index)/OC_rates_total)*class="num">100;
class=class="str">"cmt">//---
   if(percent<=class="num">9.99)
      message_text="class="num">0"+DoubleToString(percent,class="num">0);
   else if(percent<class="num">99)
      message_text=DoubleToString(percent,class="num">0);
   else
      message_text="class="num">100";
class=class="str">"cmt">//---
   msg_last=msg_sync_update="Preparing data("+(class="type">class="kw">string)OC_rates_total+" bars) : "+
                symbol_names[symbol_number]+
                "("+(class="type">class="kw">string)(symbol_number+class="num">1)+"/"+(class="type">class="kw">string)SYMBOLS_COUNT+") - "+message_text+"% ... ";
  }
class=class="str">"cmt">//--- Fill indicator buffers
   for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
     {
      class=class="str">"cmt">//--- If the specified symbol does not exist, zero out the buffer
      if(symbol_names[s]==empty_symbol)
         ArrayInitialize(atr_buffers[s].data,EMPTY_VALUE);
      else
        {
         class=class="str">"cmt">//--- Generate a message
         msg_last=msg_sync_update="Updating indicator data: "+

正文

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;symbol_names[s]+<span class="string">"("</span>+(<span class="keyword">string</span>)(s+<span class="number">1</span>)+<span class="string">"/"</span>+(<span class="keyword">string</span>)SYMBOLS_COUNT+<span class="string">") ... "</span>; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">//--- Print the message</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ShowCanvasMessage(msg_sync_update); &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">//--- Fill indicator buffers with values</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">int</span> i=limit; i&lt;rates_total; i++) &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; { &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span style="background-color:rgb(255, 242, 153);">FillIndicatorBuffers(i,s,time);</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//--- Check the subwindow size once every 2000 bars</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">//&nbsp;&nbsp;&nbsp;&nbsp;and if the size has changed, adjust the canvas size to it</span> &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(i%<span

「子窗口里批量画水平线的坑」

在指标副图里批量落水平线,第一步得先拿到子窗口编号:用 ChartWindowFind(0, subwindow_shortname) 按短名定位,返回值直接喂给后续绘图函数。若返回 -1,说明该名称的子窗口还没创建,循环里硬画会静默失败。 拿到窗口号后,用 for 循环按 LEVELS_COUNT 的次数调 CreateHorizontalLine,名字拼成 prefix+"level_0"+(i+1),价格则先过一遍 CorrectValueBySymbolDigits 做小数位修正。外汇和贵金属点差跳变快,副图层级线画歪了容易误读支撑阻力,属高风险操作,建议先在模拟盘验证。 CorrectValueBySymbolDigits 只做一件事:当 _Digits 是 3 或 5(即报价带小数尾位 3/5 位的品种,如 XAUUSD 通常 5 位)时,把传入值乘 10 再返回;其余位数原样返回。这一步是为了把用 _Point 算出的相对偏移对齐到实际报价精度,否则 5 位平台上线会偏出一个数量级。 CreateHorizontalLine 的参数表里,selectable/selected/back 三个 bool 全传 false,意味着线不可选中、不置顶、不进背景层;style 用 STYLE_DOT、颜色 clrLightSteelBlue,纯做参考不干扰交互。开 MT5 把这段抄进 EA,改 LEVELS_COUNT 就能看副图层级密度变化。

MQL5 / C++
subwindow_number=ChartWindowFind(class="num">0,subwindow_shortname);
class=class="str">"cmt">//--- Set levels
for(class="type">int i=class="num">0; i<LEVELS_COUNT; i++)
   CreateHorizontalLine(class="num">0,subwindow_number,
                        prefix+"level_0"+(class="type">class="kw">string)(i+class="num">1)+"",
                        CorrectValueBySymbolDigits(indicator_levels[i]*_Point),
                        class="num">1,STYLE_DOT,clrLightSteelBlue,class="kw">false,class="kw">false,class="kw">false,"\n");
}
class=class="str">"cmt">//+------------------------------------------------------------------------+
class=class="str">"cmt">//| Adjusting the value based on the number of digits in the price(class="type">class="kw">double)|
class=class="str">"cmt">//+------------------------------------------------------------------------+
class="type">class="kw">double CorrectValueBySymbolDigits(class="type">class="kw">double value)
  {
   class="kw">return(_Digits==class="num">3 || _Digits==class="num">5) ? value*=class="num">10 : value;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Creating a horizontal line at the price level specified            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CreateHorizontalLine(class="type">long             chart_id,     class=class="str">"cmt">// chart id
                          class="type">int              window_number, class=class="str">"cmt">// window number
                          class="type">class="kw">string           object_name,   class=class="str">"cmt">// object name
                          class="type">class="kw">double           price,         class=class="str">"cmt">// price level
                          class="type">int              line_width,    class=class="str">"cmt">// line width
                          ENUM_LINE_STYLE  line_style,    class=class="str">"cmt">// line style
                          class="type">class="kw">color            line_color,    class=class="str">"cmt">// line class="type">class="kw">color
                          class="type">bool             selectable,    class=class="str">"cmt">// cannot select the object if FALSE
                          class="type">bool             selected,      class=class="str">"cmt">// line is selected
                          class="type">bool             back)          class=class="str">"cmt">// background position

◍ 水平线与清理函数的落地写法

在 MT5 图表上画水平参考线,核心是用 OBJ_HLINE 配合 ObjectCreate。下面这段把可选取、选中态、背景层、线型线宽颜色一次性设完,tooltip 传空串或换行符时可不显示悬浮提示。

MQL5 / C++
class="type">class="kw">string tooltip)  class=class="str">"cmt">// no tooltip if "\n"
  {
class=class="str">"cmt">//--- If the object has been created successfully
   if(ObjectCreate(chart_id,object_name,OBJ_HLINE,window_number,class="num">0,price))
     {
class=class="str">"cmt">//--- set its properties
      ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable);
      ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTED,selected);
      ObjectSetInteger(chart_id,object_name,OBJPROP_BACK,back);
      ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style);
      ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width);
      ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color);
      ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip);
     }
  }
逐行看:函数入参带 chart_id 与 object_name,ObjectCreate 第五个参数是时间轴位置,水平线填 0 即可,price 决定纵坐标。创建成功后用 7 个 ObjectSetInteger / ObjectSetString 把交互与样式钉死,避免每次重绘被默认属性覆盖。 清理比绘制更容易漏。DeleteLevels 按 LEVELS_COUNT 循环,名字拼成 prefix+"level_0"+(i+1);DeleteVerticalLines 按 SYMBOLS_COUNT 删各品种的起点竖线。真正干活的 DeleteObjectByName 先 ObjectFind(0,name)>=0 确认存在,再 ObjectDelete,失败就 Print 错误码——外汇与贵金属波动大,对象残留可能误导看图,建议回测时把这两类删除逻辑接在 deinit 里。

MQL5 / C++
class="type">class="kw">string tooltip)  class=class="str">"cmt">// no tooltip if "\n"
  {
class=class="str">"cmt">//--- If the object has been created successfully
   if(ObjectCreate(chart_id,object_name,OBJ_HLINE,window_number,class="num">0,price))
     {
class=class="str">"cmt">//--- set its properties
      ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTABLE,selectable);
      ObjectSetInteger(chart_id,object_name,OBJPROP_SELECTED,selected);
      ObjectSetInteger(chart_id,object_name,OBJPROP_BACK,back);
      ObjectSetInteger(chart_id,object_name,OBJPROP_STYLE,line_style);
      ObjectSetInteger(chart_id,object_name,OBJPROP_WIDTH,line_width);
      ObjectSetInteger(chart_id,object_name,OBJPROP_COLOR,line_color);
      ObjectSetString(chart_id,object_name,OBJPROP_TOOLTIP,tooltip);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Deleting levels                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DeleteLevels()
  {
   for(class="type">int i=class="num">0; i<LEVELS_COUNT; i++)
      DeleteObjectByName(prefix+"level_0"+(class="type">class="kw">string)(i+class="num">1)+"");
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Deleting vertical lines of the beginning of the series                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DeleteVerticalLines()
  {
   for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
      DeleteObjectByName(prefix+symbol_names[s]+": begin time series");
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Deleting the object by name                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void DeleteObjectByName(class="type">class="kw">string object_name)
  {
class=class="str">"cmt">//--- If such object exists
   if(ObjectFind(class="num">0,object_name)>=class="num">0)
     {
class=class="str">"cmt">//--- If an error occurred when deleting, print the relevant message
      if(!ObjectDelete(class="num">0,object_name))
         Print("Error("+IntegerToString(GetLastError())+") when deleting the object!");
     }
  }

把工具请下神坛

这套多品种 ATR 分析的源码(multisymbolatr.mq5,47.42 KB)已经随文放出,ZIP 里能直接拖进 MT5 编译。它本质上只是把不同品种的真实波幅拉到同一张图上对比,不是点石成金的黑箱。 回头看评论区,2014 年就有用户在测试器里跑出和作者截图不一致的结果,作者自己也说没在测试器里验过这个指标形态——多系统下标准指标偶尔抽风,是 MT5 老毛病,别把锅全甩给策略。 下一篇打算拿它当底子接一个交易系统,专门看波幅扩张时的进出场。你现在就能开 MT5 把 ZIP 里的文件丢进 Indicators,切几个交叉盘看 ATR 线是否同步发散,外汇和贵金属杠杆高,波幅误读容易放大亏损,先验证再谈实盘。

把多品种诊断交给小布盯盘
这些跨品种波幅异动和缺失数据的初步筛查,小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只管决定要不要手动写指标验证。

常见问题

常见原因是目标品种不在市场报价窗口或未成功添加,以及时间框架同步未完成。应在 OnInit 中先添加品种并做参数与句柄校验,失败时不参与计算。
需要以当前图表时间为基准对齐其他品种的时间序列,忽略未同步完成的柱,避免用错 OHLC 造成波幅偏差。
本篇用画布打印文本进度,让用户知道指标在加载哪些品种、卡在哪一步,比静默等待更利于排查同步问题。
可以,小布盯盘的品种页已聚合跨品种波动视图,省去你每次手动加载指标的重复劳动,但你仍可在 MQL5 里按本文做自定义扩展。
因为挂载指标的主图表品种视为第 1 个,其余在参数中列示的品种顺延编号,便于数组映射和循环处理。