MQL5 Cookbook: 开发多品种指标分析价格偏离·综合运用
📘

MQL5 Cookbook: 开发多品种指标分析价格偏离·综合运用

第 3/3 篇

「历史数据拉取的重试与兜底」

MT5 自定义指标在 OnInit 或 OnCalculate 早期常遇到历史数据尚未就绪的情况,直接算缓冲区会得到空值或错位结果。上面这段逻辑用 attempts=100 次循环去拿 SERIES_BARS_COUNT 与 CopyTime 的时间数组,就是给服务端同步留余量。 具体看第一个 for 循环:用 SeriesInfoInteger 取指定周期的已加载 bar 数,只要大于 0 就 break;第二个 for 循环用 CopyTime 从终端首个日期加周期秒数到当前时间拉时间轴,同样成功才跳出。若两次都没拿到(ArraySize(time)<=0 或 total_period_bars<=0),就清掉上次计算量并返回 false,避免在残缺数据上画图。 FillIndicatorBuffers 里也沿用了同一思路:对单个 bar 用 CopyRates 最多试 100 次,成功一次就 ResetLastError 并 break;若 rates 数组不足或 GetLastError 非 0 直接 return。外汇与贵金属行情在高波动时段可能延迟推送,这种重试机制能降低指示器首屏空白的概率,但无法消除报价中断风险,实盘前应在 MT5 策略测试器用不同品种验证。

MQL5 / C++
for(class="type">int i=class="num">0; i<attempts; i++)
  if((total_period_bars=(class="type">int)SeriesInfoInteger(Symbol(),timeframe_start_point,SERIES_BARS_COUNT))>class="num">0)
    class="kw">break;
class=class="str">"cmt">//--- Check the readiness of bar data
for(class="type">int i=class="num">0; i<attempts; i++)
  class=class="str">"cmt">//--- Copy the specified amount of data
  if(CopyTime(Symbol(),timeframe_start_point,
    terminal_first_date+PeriodSeconds(timeframe_start_point),TimeCurrent(),time)>class="num">0)
    class="kw">break;
class=class="str">"cmt">//--- If the amount of data copied is not sufficient, one more attempt is required
if(ArraySize(time)<=class="num">0 || total_period_bars<=class="num">0)
  {
   msg_last=msg_prepare_data;
   ShowCanvasMessage(msg_prepare_data);
   OC_prev_calculated=class="num">0;
   class="kw">return(false);
  }
 }
class=class="str">"cmt">//---
  class="kw">return(true);
 }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Fills indicator buffers                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void FillIndicatorBuffers(class="type">int i,class="type">int s,class="type">class="kw">datetime const &time[])
  {
  class="type">MqlRates   rates[];            class=class="str">"cmt">// Data structure
  class="type">class="kw">double     period_open[];      class=class="str">"cmt">// Opening price for bar at the price divergence starting point
  class="type">class="kw">datetime   period_time[];      class=class="str">"cmt">// Time of the price divergence starting point
  class="type">int        attempts=class="num">100;       class=class="str">"cmt">// Number of copying attempts
  class="type">class="kw">datetime   high_tf_time=NULL;  class=class="str">"cmt">// Time of higher timeframe&class="macro">#x27;s bar
class=class="str">"cmt">//--- Exit if we are out of "true" bars zone
  if(time[i]<limit_time[s])
    class="kw">return;
class=class="str">"cmt">//--- Reset the last error
  ResetLastError();
class=class="str">"cmt">//--- Get data of current bar for the specified symbol
  for(class="type">int j=class="num">0; j<attempts; j++)
    if(CopyRates(symbol_names[s],Period(),time[i],class="num">1,rates)==class="num">1)
      { ResetLastError(); class="kw">break; }
class=class="str">"cmt">//--- Exit if failed to get data
  if(ArraySize(rates)<class="num">1 || GetLastError()!=class="num">0)
    class="kw">return;
class=class="str">"cmt">//--- If the current time is before the first timeframe&class="macro">#x27;s time or
class=class="str">"cmt">//    bar time is not equal to the bar time of the current symbol or
class=class="str">"cmt">//    empty values are fetched
  if(rates[class="num">0].time==NULL ||

◍ 背离起点在高周期上的时间锚定

在指标计算里,先要判断当前处理的柱是否合法:若时间不匹配、早于高周期首根时间,或高周期 OHLC 任一为 EMPTY_VALUE,就直接把缓冲写空并 return,避免脏数据污染图形。 当背离起点模式设为 VERTICAL_LINE 时,逻辑很简单——用 ObjectGetInteger 取画线对象的 OBJPROP_TIME 作为 divergence_time,再把当前图表 time[0] 记成 first_period_time,垂直线就锚在这两点。 非垂直线模式下,首次进入会把高周期首根开盘时间通过 CopyTime 拿到(最多 attempts 次重试,失败即退出),之后按当前柱时间是否早于该首根,决定 high_tf_time 取首根还是当前柱,再用 CopyOpen 取对应高周期开盘价。外汇与贵金属波动剧烈,高周期数据偶尔取不到,这类重试与空值守卫是必须保留的。

MQL5 / C++
time[i]!=rates[class="num">0].time ||
time[i]<first_period_time ||
rates[class="num">0].low==EMPTY_VALUE ||
rates[class="num">0].open==EMPTY_VALUE ||
rates[class="num">0].high==EMPTY_VALUE ||
rates[class="num">0].close==EMPTY_VALUE)
  {
   class=class="str">"cmt">//--- Write empty value
   if(DrawType!=LINE)
     {
      buffer_data[s].low[i]   =EMPTY_VALUE;
      buffer_data[s].open[i]  =EMPTY_VALUE;
      buffer_data[s].high[i]  =EMPTY_VALUE;
     }
   buffer_data[s].close[i]=EMPTY_VALUE;
   class="kw">return;
  }
class=class="str">"cmt">//--- If current mode is vertical line of the price divergence starting point
  if(StartPriceDivergence==VERTICAL_LINE)
   {
    class=class="str">"cmt">//--- Get the time of the line
    divergence_time=(class="type">class="kw">datetime)ObjectGetInteger(class="num">0,start_price_divergence,OBJPROP_TIME);
    class=class="str">"cmt">//--- Get the time of the first bar
    first_period_time=time[class="num">0];
   }
class=class="str">"cmt">//--- For all other modes, we will keep track the beginning of period
  else
   {
    class=class="str">"cmt">//--- If we are here for the first time, store data of the first bar of higher timeframe
    if(divergence_time==NULL)
      {
       ResetLastError();
       class=class="str">"cmt">//--- Get opening time of the first bar of higher timeframe
       for(class="type">int j=class="num">0; j<attempts; j++)
       if(CopyTime(Symbol(),timeframe_start_point,time[class="num">0]+PeriodSeconds(timeframe_start_point),class="num">1,period_time)==class="num">1)
         { ResetLastError(); class="kw">break; }
       class=class="str">"cmt">//--- Exit if failed to get price/time
       if(ArraySize(period_time)<class="num">1 || GetLastError()!=class="num">0)
         class="kw">return;
       class=class="str">"cmt">//--- Otherwise store time of the first bar of higher timeframe
       else
         first_period_time=period_time[class="num">0];
      }
    class=class="str">"cmt">//--- If current bar&class="macro">#x27;s time on the current timeframe is before the first bar&class="macro">#x27;s time on higher timeframe
    if(time[i]<first_period_time)
      high_tf_time=first_period_time;
    class=class="str">"cmt">//--- Otherwise we will receive data of the last bar of the higher timeframe with respect to the current bar on the current timeframe
    else
      high_tf_time=time[i];
    class=class="str">"cmt">//--- Reset the last error
    ResetLastError();
    class=class="str">"cmt">//--- Get the opening price of the first bar of the higher timeframe
    for(class="type">int j=class="num">0; j<attempts; j++)
      if(CopyOpen(Symbol(),timeframe_start_point,high_tf_time,class="num">1,period_open)==class="num">1)

背离起点锚定与垂直画线逻辑

在跨周期取数失败时必须及时退出,否则后续数组越界会让整个指标崩在 MT5 的某根 K 线上。代码用 ArraySize(period_open)<1 或 ArraySize(period_time)<1 配合 GetLastError()!=0 做双重保险,只要较高周期的开盘时间或价格没取到就直接 return。 当当前图表时间早于背离起点时间,或内存里记录的 divergence_time 与本次取到的 period_time[0] 不一致时,说明价格背离区间重新开启了。此时把 symbol_difference 和 inverse_difference 清零,并把 divergence_time、divergence_price 重设为高周期第一根 K 线的对应值,相当于把锚点往前挪。 锚点重设后会调用 CreateVerticalLine 在 divergence_time 处画一条白色实线(样式 STYLE_SOLID、宽度 2、不可选中但可后台隐藏),名字拼了 start_price_divergence + 下划线 + TimeToString(divergence_time),方便在对象列表里一眼定位。 如果模式是 VERTICAL_LINE 且 time[i]<divergence_time,说明还在背离起点左侧,差值保持 0。LINE 模式只改 close 缓冲(rates[0].close - symbol_difference[s]);其余绘制模式则对 low/open/high/close 四个缓冲同步减去差值,并调用 SetBufferColorIndex 给当前柱子上色。外汇与贵金属波动剧烈,这类跨周期锚点在高波动时段可能频繁重画,实盘前务必在策略测试器里跑至少 3 个月 tick 数据验证重画频率。

MQL5 / C++
    { ResetLastError(); class="kw">break; }
  class=class="str">"cmt">//--- Get opening time of the first bar of higher timeframe
  for(class="type">int j=class="num">0;j<attempts;j++)
    if(CopyTime(Symbol(),timeframe_start_point,high_tf_time,class="num">1,period_time)==class="num">1)
      { ResetLastError(); class="kw">break; }
  class=class="str">"cmt">//--- Exit if failed to get price/time
  if(ArraySize(period_open)<class="num">1 || ArraySize(period_time)<class="num">1 || GetLastError()!=class="num">0)
    class="kw">return;
  class=class="str">"cmt">//--- If the current timeframe&class="macro">#x27;s time is before the first period&class="macro">#x27;s time or
  class=class="str">"cmt">//   time of specified period is not equal to the one in memory
  if(time[i]<first_period_time || divergence_time!=period_time[class="num">0])
    {
     symbol_difference[s]  =class="num">0.0; class=class="str">"cmt">// Zero out difference in symbol prices
     inverse_difference[s] =class="num">0.0; class=class="str">"cmt">// Zero our difference of inversion
     class=class="str">"cmt">//--- Store time of the price divergence starting point
     divergence_time=period_time[class="num">0];
     class=class="str">"cmt">//--- Store price of the price divergence starting point
     divergence_price=period_open[class="num">0];
     class=class="str">"cmt">//--- Set vertical line in the beginning of the price divergence start
     CreateVerticalLine(class="num">0,class="num">0,period_time[class="num">0],start_price_divergence+"_"+TimeToString(divergence_time),
                        class="num">2,STYLE_SOLID,clrWhite,false,false,true,TimeToString(divergence_time),"\n");
    }
  }
class=class="str">"cmt">//--- If current mode is &class="macro">#x27;Vertical Line&class="macro">#x27; and bar&class="macro">#x27;s time is less than line&class="macro">#x27;s time
  if(StartPriceDivergence==VERTICAL_LINE && time[i]<divergence_time)
   {
    class=class="str">"cmt">//--- Keep zero values of difference
    symbol_difference[s]  =class="num">0.0;
    inverse_difference[s] =class="num">0.0;
    class=class="str">"cmt">//--- For the &class="macro">#x27;Line&class="macro">#x27; drawing mode only opening price is used
    if(DrawType==LINE)
      buffer_data[s].close[i]=rates[class="num">0].close-symbol_difference[s];
    class=class="str">"cmt">//--- For all other modes all prices are used
    else
     {
      buffer_data[s].low[i]  =rates[class="num">0].low-symbol_difference[s];
      buffer_data[s].open[i] =rates[class="num">0].open-symbol_difference[s];
      buffer_data[s].high[i] =rates[class="num">0].high-symbol_difference[s];
      buffer_data[s].close[i]=rates[class="num">0].close-symbol_difference[s];
      class=class="str">"cmt">//--- Set class="type">color for the current element of indicator buffer
      SetBufferColorIndex(i,s,rates[class="num">0].close,rates[class="num">0].open);
     }
   }
class=class="str">"cmt">//--- For all other modes
  else
   {

「镜像品种的价差锚定与缓冲写入」

做跨品种背离指标时,若勾选了镜像反转(inverse[s] 为真),就得先把基准价差算清楚,否则后面画出来的线会整体漂移。新周期刚启动的瞬间,symbol_difference[s] 还是 0.0,这时才允许重算锚点,避免每根 K 线都重设基准导致图形抖动。 垂直基线模式(VERTICAL_LINE)和其他模式取价不同:前者用 OC_open[i] 作参照,后者用 divergence_price。镜像场景下还要额外算 inverse_difference[s],它等于参照价减去其相反数,本质是把坐标轴对称翻转到负半轴。 LINE 模式只写 close 缓冲,其他模式则把 low/open/high/close 全部按「负(实时价-锚差)+镜像差」重写,并调用 SetBufferColorIndex 给每根 K 上色。若不镜像,逻辑砍掉一半:仅在新周期补一次 symbol_difference[s],后续直接拿实时价减锚差绘图,省掉翻轴计算。 开 MT5 把下面片段塞进你的指标 OnCalculate,把 VERTICAL_LINE 和 LINE 两个宏值对调,能直观看到 XAUUSD 相对镜像基准的背离带如何整体上下翻转。外汇与贵金属品种点差跳变频繁,镜像基准在重大数据行情中可能瞬间失准,仅作概率参考。

MQL5 / C++
class=class="str">"cmt">//--- If inversion of symbol data is required
if(inverse[s])
  {
   class=class="str">"cmt">//--- If new period has started, recalculate variables
   if(symbol_difference[s]==class="num">0.0)
     {
      class=class="str">"cmt">//--- For the &class="macro">#x27;Vertical Line&class="macro">#x27; mode
      if(StartPriceDivergence==VERTICAL_LINE)
        {
         class=class="str">"cmt">//--- Calculate the difference
         symbol_difference[s]  =rates[class="num">0].open-OC_open[i];
         inverse_difference[s] =OC_open[i]-(-OC_open[i]);
        }
      class=class="str">"cmt">//--- For all other modes
      else
        {
         class=class="str">"cmt">//--- Calculate the difference
         symbol_difference[s]  =rates[class="num">0].open-divergence_price;
         inverse_difference[s] =divergence_price-(-divergence_price);
        }
     }
   class=class="str">"cmt">//--- In the &class="macro">#x27;Line&class="macro">#x27; mode only opening price is used
   if(DrawType==LINE)
     buffer_data[s].close[i]=-(rates[class="num">0].close-symbol_difference[s])+inverse_difference[s];
   class=class="str">"cmt">//--- For all other modes all prices are used
   else
     {
      buffer_data[s].low[i]   =-(rates[class="num">0].low-symbol_difference[s])+inverse_difference[s];
      buffer_data[s].open[i]  =-(rates[class="num">0].open-symbol_difference[s])+inverse_difference[s];
      buffer_data[s].high[i]  =-(rates[class="num">0].high-symbol_difference[s])+inverse_difference[s];
      buffer_data[s].close[i] =-(rates[class="num">0].close-symbol_difference[s])+inverse_difference[s];
      class=class="str">"cmt">//--- Set class="type">color for the current element of indicator buffer
      SetBufferColorIndex(i,s,rates[class="num">0].close,rates[class="num">0].open);
     }
  }
class=class="str">"cmt">//--- If inversion is not used, then we need to calculate only the difference between symbol prices at the beginning of period
else
  {
   class=class="str">"cmt">//--- If new period has started
   if(symbol_difference[s]==class="num">0.0)
     {
      class=class="str">"cmt">//--- For the &class="macro">#x27;Vertical Line&class="macro">#x27; mode
      if(StartPriceDivergence==VERTICAL_LINE)
        symbol_difference[s]=rates[class="num">0].open-OC_open[i];
      class=class="str">"cmt">//--- For all other modes
      else
        symbol_difference[s]=rates[class="num">0].open-divergence_price;
     }
   class=class="str">"cmt">//--- For the &class="macro">#x27;Line&class="macro">#x27; drawing mode only opening price is used

◍ 差值缓冲的空值校验与双色判定

多符号叠加指标里,算完相对价差缓冲后必须做一轮合法性过滤,否则老符号在跳空或加载初期会画出悬空线段。LINE 模式只用了 close 缓冲,因此只要当前柱时间对不上基准周期时间、或早于首根有效周期,就直接写 EMPTY_VALUE。 非 LINE 模式(含 OHLC 全部价格)的判定更严:除时间错位外,还要排查 rates[0].time==NULL 以及 low/open/high/close 中任一为 EMPTY_VALUE 的情况。只要命中任一条件,四个缓冲位全置空,避免半截 K 线误导读图。 双色渲染靠 SetBufferColorIndex() 落地:传入柱序 i、符号序号、close 与 open,当 close>open 时 icolor[i] 记 0(涨柱色),否则落第二种颜色。实盘加载 EURUSD+XAUUSD 这类跨品种组合时,建议在 MT5 里临时把 TwoColor 关掉对比,能直观看到收口逻辑是否吞掉了早期柱。 外汇与贵金属跨品种叠加受点差和节假日影响大,缓冲置空仅是防错手段,不代表信号概率提升,验证时请控制仓位。

MQL5 / C++
if(DrawType==LINE)
   buffer_data[s].close[i]=rates[class="num">0].close-symbol_difference[s];
   class=class="str">"cmt">//--- For all other modes all prices are used
   else
     {
     buffer_data[s].low[i]   =rates[class="num">0].low-symbol_difference[s];
     buffer_data[s].open[i]  =rates[class="num">0].open-symbol_difference[s];
     buffer_data[s].high[i]  =rates[class="num">0].high-symbol_difference[s];
     buffer_data[s].close[i] =rates[class="num">0].close-symbol_difference[s];
     class=class="str">"cmt">//--- Set class="type">color for the current element of indicator buffer
     SetBufferColorIndex(i,s,rates[class="num">0].close,rates[class="num">0].open);
     }
   }
 }
class=class="str">"cmt">//--- Verification of the calculated values
class=class="str">"cmt">//    In the &class="macro">#x27;Line&class="macro">#x27; mode only opening price is used
  if(DrawType==LINE)
   {
   class=class="str">"cmt">//--- If the current time is before the first timeframe&class="macro">#x27;s time or
   class=class="str">"cmt">//    bar time is not equal to the bar time, write empty value
   if(time[i]!=rates[class="num">0].time || time[i]<first_period_time)
      buffer_data[s].close[i]=EMPTY_VALUE;
   }
class=class="str">"cmt">//--- For all other modes all prices are used
  else
   {
   class=class="str">"cmt">//--- If the current time is before the first timeframe&class="macro">#x27;s time or
   class=class="str">"cmt">//    bar time is not equal to the bar time of the current symbol or
   class=class="str">"cmt">//    empty values are fetched
   if(rates[class="num">0].time==NULL ||
       time[i]!=rates[class="num">0].time ||
       time[i]<first_period_time ||
       rates[class="num">0].low==EMPTY_VALUE ||
       rates[class="num">0].open==EMPTY_VALUE ||
       rates[class="num">0].high==EMPTY_VALUE ||
       rates[class="num">0].close==EMPTY_VALUE)
     {
     class=class="str">"cmt">//--- Write empty value
     buffer_data[s].low[i]   =EMPTY_VALUE;
     buffer_data[s].open[i]  =EMPTY_VALUE;
     buffer_data[s].high[i]  =EMPTY_VALUE;
     buffer_data[s].close[i] =EMPTY_VALUE;
     }
   }
 }
class="type">void SetBufferColorIndex(class="type">int i,class="type">int symbol_number,class="type">class="kw">double close,class="type">class="kw">double open)
  {
class=class="str">"cmt">//--- For two-class="type">color mode, check condition
   if(TwoColor)
     {
     class=class="str">"cmt">//--- If the closing price is more than the opening price, this is up bar, so we use the first class="type">color
     if(close>open)
        buffer_data[symbol_number].icolor[i]=class="num">0;
     class=class="str">"cmt">//--- otherwise it is down bar, so we use the second class="type">color

多品种图表高低点重算的容错写法

在把多个交易品种叠到同一张 MT5 图表上时,主图自身的 HIGH/LOW 往往只认当前品种,叠加上去的副图缓冲容易被裁掉上下边缘。CorrectChartMaxMin 这个函数就是专门按可见区域把所有缓冲的高低价拉通重算的。 函数开头先声明 low[]、high[] 两个数组,并给 attempts 写死为 10——意思是复制高低价时最多重试 10 次,避免行情跳动瞬间 CopyHigh / CopyLow 返回根数不足直接废掉整轮绘制。 可见根数用 ChartGetInteger(0, CHART_VISIBLE_BARS) 拿,首根可见 bar 用 CHART_FIRST_VISIBLE_BAR,末根则等于首根减可见根数;若算出来小于 0 就强制归 0,防止左侧越界。 高低价获取分两个 for 循环各跑最多 10 次,只要某次 CopyHigh / CopyLow 返回根数等于 visible_bars 就 break。若最终数组尺寸 ≤0 直接 return,不往下走;否则用 ArrayMinimum、ArrayMaximum 从数组里抠出 min_price 和 max_price,后面再拿去扩边。 外汇与贵金属杠杆高、跳空频繁,这种重试 + 边界修正的写法在实盘多品种监控里能明显降低图形撕裂概率,建议直接把 attempts=10 抄进你自己的指标骨架里跑一下。

MQL5 / C++
   else
      buffer_data[symbol_number].icolor[i]=class="num">1;
   }
class=class="str">"cmt">//--- For one-class="type">color mode we use the first class="type">color for all bars/candlesticks
   else
      buffer_data[symbol_number].icolor[i]=class="num">0;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Corrects chart&class="macro">#x27;s high/low with respect to all buffers             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CorrectChartMaxMin()
  {
   class="type">class="kw">double low[];                 class=class="str">"cmt">// Array of lows
   class="type">class="kw">double high[];                class=class="str">"cmt">// Array of highs
   class="type">int    attempts       =class="num">10;    class=class="str">"cmt">// Number of attempts
   class="type">int    array_size     =class="num">0;     class=class="str">"cmt">// Array size for drawing
   class="type">int    visible_bars   =class="num">0;     class=class="str">"cmt">// Number of visible bars
   class="type">int    first_visible_bar =class="num">0;  class=class="str">"cmt">// Number of the first visible bar
   class="type">int    last_visible_bar  =class="num">0;  class=class="str">"cmt">// Number of the last visible bar
   class="type">class="kw">double max_price          =class="num">0.0; class=class="str">"cmt">// Highest price
   class="type">class="kw">double min_price          =class="num">0.0; class=class="str">"cmt">// Lowest price
   class="type">class="kw">double offset_max_min     =class="num">0.0; class=class="str">"cmt">// Offset from chart&class="macro">#x27;s high/low
class=class="str">"cmt">//--- Reset the last error
   ResetLastError();
class=class="str">"cmt">//--- Number of visible bars
   visible_bars=(class="type">int)ChartGetInteger(class="num">0,CHART_VISIBLE_BARS);
class=class="str">"cmt">//--- Number of the first visible bar
   first_visible_bar=(class="type">int)ChartGetInteger(class="num">0,CHART_FIRST_VISIBLE_BAR);
class=class="str">"cmt">//--- Number of the last visible bar
   last_visible_bar=first_visible_bar-visible_bars;
class=class="str">"cmt">//--- Exit in case of error
   if(GetLastError()!=class="num">0)
      class="kw">return;
class=class="str">"cmt">//--- Fix incorrect value
   if(last_visible_bar<class="num">0)
      last_visible_bar=class="num">0;
class=class="str">"cmt">//--- Get the current symbol high/low in visible area of chart
   for(class="type">int i=class="num">0; i<attempts; i++)
      if(CopyHigh(Symbol(),Period(),last_visible_bar,visible_bars,high)==visible_bars)
         class="kw">break;
   for(class="type">int i=class="num">0; i<attempts; i++)
      if(CopyLow(Symbol(),Period(),last_visible_bar,visible_bars,low)==visible_bars)
         class="kw">break;
class=class="str">"cmt">//--- Exit if failed to get data
   if(ArraySize(high)<=class="num">0 || ArraySize(low)<=class="num">0)
      class="kw">return;
class=class="str">"cmt">//--- If succeeded to get data, identify high and low in the current symbol arrays
   else
     {
      min_price=low[ArrayMinimum(low)];
      max_price=high[ArrayMaximum(high)];
     }
class=class="str">"cmt">//--- Get high and low prices in all price arrays

「多品种遍历里的数据对齐坑点」

在 MT5 里做跨品种指标绘制时,外层 for 循环按 SYMBOLS_COUNT 逐个处理品种,但 symbol_names[s]==empty_symbol 的分支直接 continue,说明监控列表里可能存在空槽位——这是从配置数组初始化时留下的,实盘前务必确认填充逻辑没漏填,否则会静默跳过部分品种。 时间数组的获取用了 attempts 次重试去 CopyTime,只有成功拿到 visible_bars 根才 break;若 ArraySize(time) 仍小于 visible_bars 就直接 return,意味着图表可视 K 线数不足时整个刷新中止。外汇与贵金属品种在跳空或流动性中断时,可能拿不到完整序列,属正常高风险现象。 limit_time[s] 与 time[0] 的比较决定了 bars_count 取法:若限定时间晚于首根可见 bar 时间,用 Bars() 反推真实可用根数;否则直接取 visible_bars。LINE 模式和非 LINE 模式对 low/high 缓冲的拷贝源不同(前者用 close 填充两者),调试时若发现线模式高低位重合,就是这个分支在起作用。 ArraySetAsSeries 把 low/high 设为时间序列,保证索引 0 是最新 bar;随后 ArrayCopy 把各品种缓冲数据搬进主数组,array_size 取自 high,循环填空值避免参与极值计算。开 MT5 把这段塞进你的多品种 EA 初始化后跑一遍,观察 limit_time 边界附近的 bars_count 跳变。

MQL5 / C++
for(class="type">int s=class="num">0; s<SYMBOLS_COUNT; s++)
  {
  class=class="str">"cmt">//--- If current symbol is not present, go to the next one
  if(symbol_names[s]==empty_symbol)
     class="kw">continue;
  class=class="str">"cmt">//---
  class="type">class="kw">datetime time[];           class=class="str">"cmt">// Time array
  class="type">int     bars_count=class="num">0;      class=class="str">"cmt">// Number of bars for calculation
  class=class="str">"cmt">//--- Set zero size for arrays
  ArrayResize(high,class="num">0);
  ArrayResize(low,class="num">0);
  class=class="str">"cmt">//--- Get the time of the first bar visible on chart
  for(class="type">int i=class="num">0; i<attempts; i++)
     if(CopyTime(Symbol(),Period(),last_visible_bar,visible_bars,time)==visible_bars)
        class="kw">break;
  class=class="str">"cmt">//--- Exit if the amount of data is less than number of visible bars on chart
  if(ArraySize(time)<visible_bars)
     class="kw">return;
  class=class="str">"cmt">//--- If time of the first "true" bar is greater than
  class=class="str">"cmt">//    time of the first visible bar on the chart, then
  class=class="str">"cmt">//    get available number of bars of the current symbol in loop
  if(limit_time[s]>time[class="num">0])
    {
    class=class="str">"cmt">//--- Get the array size
    array_size=ArraySize(time);
    class=class="str">"cmt">//--- Get the number of bars from the first "true" one
    if((bars_count=Bars(Symbol(),Period(),limit_time[s],time[array_size-class="num">1]))<=class="num">0)
       class="kw">return;
    }
  class=class="str">"cmt">//--- Else get number of visible bars on chart
  else
     bars_count=visible_bars;
  class=class="str">"cmt">//--- Index elements in indicator buffers as timeseries
  ArraySetAsSeries(low,true);
  ArraySetAsSeries(high,true);
  class=class="str">"cmt">//--- Copy data from the indicator buffer
  class=class="str">"cmt">//    All modes except &class="macro">#x27;Line&class="macro">#x27;
  if(DrawType!=LINE)
    {
    ArrayCopy(low,buffer_data[s].low);
    ArrayCopy(high,buffer_data[s].high);
    }
  class=class="str">"cmt">//--- For the &class="macro">#x27;Line&class="macro">#x27; mode
  else
    {
    ArrayCopy(low,buffer_data[s].close);
    ArrayCopy(high,buffer_data[s].close);
    }
  class=class="str">"cmt">//--- Get the array size
  array_size=ArraySize(high);
  class=class="str">"cmt">//--- Fill empty values,
  class=class="str">"cmt">//    so they are not considered when calculating high/low
  for(class="type">int i=class="num">0; i<array_size; i++)
    {

◍ 反转型图表里的极值抓取与固定比例尺

在价格背离指标里,若某一子图被标记为 inverted(inverse[s]=true),高低点的取法要反过来:用 low 数组里的极大值去推 max_price,用 high 数组里的极小值去推 min_price,否则仍按常规取 low 的最小值与 high 的最大值。 代码片段里靠 ArrayMaximum / ArrayMinimum 在 last_visible_bar 起的 bars_count 根 K 线内定位极值下标,再用 fmax / fmin 把历史极值与当前窗口极值合并,避免缩放时上下边界跳变。 极值差会先乘 3 再除 100,也就是在图表顶底各留 3% 的空白偏移;随后 ChartSetInteger(0,CHART_SCALEFIX,true) 锁死比例,并用 ChartSetDouble 写死 CHART_FIXED_MAX / CHART_FIXED_MIN,最后 ChartRedraw 刷新。 拖拽起点对象(CHARTEVENT_OBJECT_DRAG)且模式为 VERTICAL_LINE 时,直接重算 OnCalculate,让背离起点随手动拖线实时更新——外汇与贵金属波动大,固定比例尺下若点错起点,视觉背离可能完全失真,建议开 MT5 用 EURUSD 15 分钟图手动拖一次验证。

MQL5 / C++
if(high[i]==EMPTY_VALUE)
   high[i]=max_price;
if(low[i]==EMPTY_VALUE)
   low[i]=min_price;
   }
   class=class="str">"cmt">//--- Get high/low with respect to inversion
   if(inverse[s])
     {
     class=class="str">"cmt">//--- If no errors occur, store values
     if(ArrayMaximum(high,last_visible_bar,bars_count)>=class="num">0 &&
        ArrayMinimum(low,last_visible_bar,bars_count)>=class="num">0)
       {
       max_price=fmax(max_price,low[ArrayMaximum(low,last_visible_bar,bars_count)]);
       min_price=fmin(min_price,high[ArrayMinimum(high,last_visible_bar,bars_count)]);
       }
     }
   else
     {
     class=class="str">"cmt">//--- If no errors occur, store values
     if(ArrayMinimum(low,last_visible_bar,bars_count)>=class="num">0 &&
        ArrayMaximum(high,last_visible_bar,bars_count)>=class="num">0)
       {
       min_price=fmin(min_price,low[ArrayMinimum(low,last_visible_bar,bars_count)]);
       max_price=fmax(max_price,high[ArrayMaximum(high,last_visible_bar,bars_count)]);
       }
     }
     }
class=class="str">"cmt">//--- Calculate offset(class="num">3%) form chart&class="macro">#x27;s top and bottom
   offset_max_min=((max_price-min_price)*class="num">3)/class="num">100;
class=class="str">"cmt">//--- Turn on the fixed chart scale mode.
   ChartSetInteger(class="num">0,CHART_SCALEFIX,true);
class=class="str">"cmt">//--- Set high/low
   ChartSetDouble(class="num">0,CHART_FIXED_MAX,max_price+offset_max_min);
   ChartSetDouble(class="num">0,CHART_FIXED_MIN,min_price-offset_max_min);
class=class="str">"cmt">//--- Refresh the chart
   ChartRedraw();
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| ChartEvent function                                              |
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=class="str">"cmt">//--- Event of dragging a graphical object
  if(id==CHARTEVENT_OBJECT_DRAG)
    {
    class=class="str">"cmt">//--- If current mode is vertical line for the price divergence starting point, then update indicator buffers
    if(StartPriceDivergence==VERTICAL_LINE)
      OnCalculate(OC_rates_total,
                  class="num">0,

用结构体收口K线字段与图表变更响应

上面这段落 closing 括号里,把单根蜡烛的八个维度一次性塞进 OC_ 开头的结构体成员:时间、开高低收、Tick 成交量、真实成交量和点差。这样在后续循环里按索引取用,比散着写八个数组更不容易在下标上出错。 CHARTEVENT_CHART_CHANGE 这个事件在用户拖拽缩放、或改了属性对话框里的坐标范围时触发。里面只调了 CorrectChartMaxMin(),意思是按当前指标缓冲区的值重新校正纵轴最大最小,避免价格跑出可视区。 在 MT5 里挂这段逻辑时,若你的指标画的是离差带而非主图价格,不重写 MaxMin 就可能看到图形被压在底部。外汇与贵金属波动大、点差跳变频繁,这类校正最好加上,否则复盘时容易误判信号位置。

MQL5 / C++
 OC_time,
 OC_open,
 OC_high,
 OC_low,
 OC_close,
 OC_tick_volume,
 OC_volume,
 OC_spread);
  }
class=class="str">"cmt">//--- Event of resizing the chart or modifying the chart properties using the properties dialog window.
  if(id==CHARTEVENT_CHART_CHANGE)
   class=class="str">"cmt">//--- Correct the maximum and minimum of chart with respect to the indicator buffers&class="macro">#x27; values
   CorrectChartMaxMin();
 }

「画得少,看得清」

这套多币种价格偏离指标的核心价值不在堆参数,而在把跨品种背离压缩成一张可横向比对的图。读者在 MT5 里加载后,优先看偏离带宽度而不是绝对值,宽度突变往往早于单边行情。 有用户反馈 2020 年后在 Checks.mqh 编译挂起、2024 年指标装图报错,说明旧代码在新 build 下需要重编译并核对头文件引用。外汇与贵金属波动受杠杆和跳空影响,跨币种信号仅作概率参考,实盘前务必在策略测试器跑一遍样本外数据。 真要落地,就开 MT5 把原文指标拖上 EURUSD 与 XAUUSD 的 15 分钟图,调一次偏离周期参数,比读十篇综述都实在。

常见问题

在请求历史数据后检查返回长度,若不足则延时数秒并重试,超过设定次数就置空缓冲并打日志,避免指标卡死。
用 iTime 取高周期对应序号的时间戳,存为全局变量,后续画线和缓冲写入都以此时间为锚,防止错位。
可以,小布能读取你的指标逻辑并提示差值缓冲空值校验缺失处,还能标出双色判定可能误染的K线区间。
需要留一个微小缓冲带再写入,比如正负0.5点容差,防止 tick 跳动导致写入失败或频繁重算。
对每个品种单独包 try 容错,缺数据就跳过该品种继续算其余,并在末尾补一次全量校验重算。