多交易品种多周期指标中的 DRAW_ARROW 绘图类型·进阶篇
🎯

多交易品种多周期指标中的 DRAW_ARROW 绘图类型·进阶篇

(2/3)· 为什么 M15 图表上的分形箭头总被空值抹掉?跨周期复制的间隙处理才是关键

偏理论 第 2/3 篇
把低周期箭头指标直接塞进高周期图表,空值缓冲区会悄悄覆盖已画好的箭头。很多人只复制零号柱和一号柱,结果箭头根本不显示——它们其实挂在更后面的柱上。

「缓冲复制失败时的日志与双缓冲落点」

指标在 MT5 里首次加载常碰到一个现象:CopyBuffer 返回 -1(即 WRONG_VALUE),代表历史数据还在后台下载,此时不能当错误处理,只能等下一 tick 再试。代码里用 PrintFormat 把品种、周期和函数名打进日志,方便你开 MT5 专家面板直接看卡在哪一根。 若返回值不是 -1 但 copied 小于预期,说明数据没拷全。日志会给出 m_rates_total 与 copied 两个具体数字,比如 Data available: 500, total copied: 480,这种差值在切换小周期(如 M1 跨品种)时概率偏高,属于正常限速而非 bug。 双缓冲场景(DRAW_HISTOGRAM2 / DRAW_FILLING 等)下,to_copy==2 时会先建 double array[2],再把 array[1] 塞进倒数第一根、array[0] 塞进倒数第二根。注意索引用 DataTotal(buff_num,0)-1 和 -2,若你自写指标漏了这层偏移,图形会出现整体右移一根的视觉错位。 外汇与贵金属行情高波动,历史数据补全时机受经纪商服务器限制,上述等待逻辑可能延长指标就绪时间,实盘前建议在策略测试器用不同品种跑一遍确认。

MQL5 / C++
if(copied==WRONG_VALUE)
   ::PrintFormat("%s::%s: Start downloading data by %s/%s. Waiting for the next tick...",__FUNCTION__,this.Title(),this.m_symbol,this.TimeframeDescription());
else
   ::PrintFormat("%s::%s: Not all data was copied. Data available: %lu, total copied: %ld",__FUNCTION__,this.Title(),this.m_rates_total,copied);
class="kw">return false;
}

class="type">bool CIndMSTF::CopyArrays(const class="type">uint buff_num,const class="type">int to_copy)
  {
   class="type">bool res=true;
   class="type">class="kw">double array[class="num">2];
   if(to_copy==class="num">2)
     {
      class="kw">switch(this.BufferDrawType(buff_num))
        {
         case DRAW_LINE         :
         case DRAW_HISTOGRAM    :
         case DRAW_ARROW        :
         case DRAW_SECTION      :
            res=this.CopyArray(buff_num,class="num">0,to_copy,array);
            if(res)
              {
               this.m_buffers[buff_num].array0[this.DataTotal(buff_num,class="num">0)-class="num">1]=array[class="num">1];
               this.m_buffers[buff_num].array0[this.DataTotal(buff_num,class="num">0)-class="num">2]=array[class="num">0];
              }
            class="kw">return res;
         case DRAW_HISTOGRAM2   :
         case DRAW_ZIGZAG       :
         case DRAW_FILLING      :

单缓冲与双缓冲的拷贝分叉

自定义指标在 OnCalculate 里拿到 to_copy 等于自身 m_bars_to_calculate 时,意味着要全量刷新缓冲区。这时先按 BufferDrawType 判断图形类别,再决定走单缓冲还是双缓冲的拷贝路径。 单缓冲分支覆盖 DRAW_LINE、DRAW_HISTOGRAM、DRAW_ARROW、DRAW_SECTION 四类。逻辑是先 CopyArray 把数据塞进 m_array_data,再用一个从后往前的循环把数组倒贴进 buffer 的 array0,索引靠 DataTotal(buff_num,0)-(total-i) 定位,避免新数据覆盖旧柱。 双缓冲分支对应 DRAW_HISTOGRAM2、DRAW_ZIGZAG、DRAW_FILLING,需要两组数组。第一次 CopyArray 取索引 0,循环贴完后再用 res &= CopyArray(...,1,...) 取第二维,任意一维失败 res 即变 false,整段直接 return。 在 MT5 里改这类指标时,若你画的 ZigZag 只显示一半,优先查第二维 CopyArray 是否被 res &= 短路掉,而非怀疑图形类型枚举。外汇与贵金属行情跳空频繁,这类缓冲错位可能放大重绘偏差,实盘前务必用历史数据回放验证。

MQL5 / C++
class="type">bool res=true;
if(to_copy==this.m_bars_to_calculate)
  {
   class="type">int total=(class="type">int)this.m_array_data.Size();
   class="kw">switch(this.BufferDrawType(buff_num))
     {
      class=class="str">"cmt">//--- One buffer
      case DRAW_LINE         :
      case DRAW_HISTOGRAM    :
      case DRAW_ARROW        :
      case DRAW_SECTION      :
        res=this.CopyArray(buff_num,class="num">0,to_copy,this.m_array_data);
        if(res)
          {
           for(class="type">int i=class="num">0;i<total;i++)
             this.m_buffers[buff_num].array0[this.DataTotal(buff_num,class="num">0)-(total-i)]=this.m_array_data[i];
          }
        class="kw">return res;
      class=class="str">"cmt">//--- Two buffers
      case DRAW_HISTOGRAM2   :
      case DRAW_ZIGZAG       :
      case DRAW_FILLING      :
        res=this.CopyArray(buff_num,class="num">0,to_copy,this.m_array_data);
        if(res)
          {
           for(class="type">int i=class="num">0;i<total;i++)
             this.m_buffers[buff_num].array0[this.DataTotal(buff_num,class="num">0)-(total-i)]=this.m_array_data[i];
          }
        res &=this.CopyArray(buff_num,class="num">1,to_copy,this.m_array_data);
        if(res)
          {
           for(class="type">int i=class="num">0;i<total;i++)

◍ K线类缓冲区的四数组回填逻辑

当自定义图形类型落到 DRAW_BARS 或 DRAW_CANDLES 时,底层要同时维护四个数据序列:开盘、最高、最低、收盘。代码里用同一个 m_array_data 做中转,分四次调用 CopyArray 把目标缓冲拉进来,再按倒序写回各 array0~array3。 关键在索引计算:DataTotal(buff_num, n) 返回该缓冲已有总长度,减去 (total-i) 后定位到本次写入的末端位置。i 从 0 走到 total-1,于是最早的数据落在缓冲尾部、最新的贴近前端,跟 MT5 内置时序保持一致。 res &= CopyArray(...) 这种写法把四次拷贝的成功标志做了按位与,只要任意一次失败 res 就置 0,后续 for 循环靠 if(res) 拦截,避免半截数据污染图形。开 MT5 写个继承类断点跟一下 DataTotal 返回值,能直观看到 total=500 时末端索引从 499 倒数填起。 别把缓冲总长度当静态值 DataTotal 在每个缓冲上独立计数,DRAW_CANDLES 的四个子缓冲若历史加载不一致,-(total-i) 算出的下标会错位,图形可能只画一半。

MQL5 / C++
res=this.CopyArray(buff_num,class="num">0,to_copy,this.m_array_data);
if(res)
  {
   for(class="type">int i=class="num">0;i<total;i++)
      this.m_buffers[buff_num].array0[this.DataTotal(buff_num,class="num">0)-(total-i)]=this.m_array_data[i];
  }
res &=this.CopyArray(buff_num,class="num">1,to_copy,this.m_array_data);
if(res)
  {
   for(class="type">int i=class="num">0;i<total;i++)
      this.m_buffers[buff_num].array1[this.DataTotal(buff_num,class="num">1)-(total-i)]=this.m_array_data[i];
  }
res &=this.CopyArray(buff_num,class="num">2,to_copy,this.m_array_data);
if(res)
  {
   for(class="type">int i=class="num">0;i<total;i++)
      this.m_buffers[buff_num].array2[this.DataTotal(buff_num,class="num">2)-(total-i)]=this.m_array_data[i];
  }
res &=this.CopyArray(buff_num,class="num">3,to_copy,this.m_array_data);
if(res)
  {
   for(class="type">int i=class="num">0;i<total;i++)

「彩色指标缓冲的回填写法」

在自定义指标里处理 DRAW_COLOR_LINE、DRAW_COLOR_HISTOGRAM 这类带颜色缓冲的绘图类型时,不能只拷主数据,还得单独把颜色索引数组补齐,否则 MT5 图表上线画得出、色却不对。 下面这段是单缓冲+颜色缓冲分支的核心回填逻辑:先 CopyArray 到主数组(偏移 0),再对 color_indexes(偏移 4)做第二次拷贝,两次都成功才返回 res。 [CODE] res=this.CopyArray(buff_num,0,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array0[this.DataTotal(buff_num,0)-(total-i)]=this.m_array_data[i]; } res &=this.CopyArray(buff_num,4,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].color_indexes[this.DataTotal(buff_num,4)-(total-i)]=this.m_array_data[i]; } return res; [/CODE] 逐行拆解:第 1 行把 buff_num 号缓冲的主数据(编号 0)复制到临时成员 m_array_data;第 2~6 行若成功,用 DataTotal 算出现有总长度,把 m_array_data[i] 倒序填进 array0 尾部,保证新柱在右。第 7 行用 &= 再拷颜色缓冲(编号 4),失败则 res 归零;第 8~12 行把颜色索引同样倒序写进 color_indexes。 双缓冲类(DRAW_COLOR_HISTOGRAM2、DRAW_COLOR_ZIGZAG)只是多拷一个 array1,结构一致。开 MT5 把这段塞进你的 CI 类 Refresh 函数,改 buff_num 和 to_copy 就能验证颜色是否随 K 线走。外汇与贵金属行情波动剧烈,指标仅作辅助,实盘高风险。

MQL5 / C++
res=this.CopyArray(buff_num,class="num">0,to_copy,this.m_array_data);
if(res)
  {
   for(class="type">int i=class="num">0;i<total;i++)
     this.m_buffers[buff_num].array0[this.DataTotal(buff_num,class="num">0)-(total-i)]=this.m_array_data[i];
  }
res &=this.CopyArray(buff_num,class="num">4,to_copy,this.m_array_data);
if(res)
  {
   for(class="type">int i=class="num">0;i<total;i++)
     this.m_buffers[buff_num].color_indexes[this.DataTotal(buff_num,class="num">4)-(total-i)]=this.m_array_data[i];
  }
class="kw">return res;

彩色K线与色带缓冲的回填写法

自定义指标里用到 DRAW_COLOR_CANDLES 或 DRAW_COLOR_BARS 时,每个缓冲编号要同时维护数值数组和颜色索引数组,漏掉哪一个都会在 MT5 里出现画不出或变色失效。下面这段逻辑先按缓冲号 1、4 回填普通数组与颜色索引,再走四缓冲带颜色的分支。 res &=this.CopyArray(buff_num,1,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].array1[this.DataTotal(buff_num,1)-(total-i)]=this.m_array_data[i]; } res &=this.CopyArray(buff_num,4,to_copy,this.m_array_data); if(res) { for(int i=0;i<total;i++) this.m_buffers[buff_num].color_indexes[this.DataTotal(buff_num,4)-(total-i)]=this.m_array_data[i]; } return res; 注意下标用的是 DataTotal(buff_num,1)-(total-i),也就是从已有数据末端往前推 total 根对齐,避免历史重绘错位。若 CopyArray 返回 false,res 会被按位与清零,整段直接返回失败,调用方拿不到半截数据。 碰到 DRAW_COLOR_BARS 和 DRAW_COLOR_CANDLES,代码继续把缓冲 0、1 都填一遍:缓冲 0 通常对应开盘/低位基准,缓冲 1 对应收盘/高位基准,颜色索引另算。外汇与贵金属市场波动剧烈、杠杆风险高,这类彩色图形只辅助读结构,不能单独作为入场依据。

MQL5 / C++
res &=this.CopyArray(buff_num,class="num">1,to_copy,this.m_array_data);
if(res)
  {
   for(class="type">int i=class="num">0;i<total;i++)
     this.m_buffers[buff_num].array1[this.DataTotal(buff_num,class="num">1)-(total-i)]=this.m_array_data[i];
  }
res &=this.CopyArray(buff_num,class="num">4,to_copy,this.m_array_data);
if(res)
  {
   for(class="type">int i=class="num">0;i<total;i++)
     this.m_buffers[buff_num].color_indexes[this.DataTotal(buff_num,class="num">4)-(total-i)]=this.m_array_data[i];
  }
class="kw">return res;
class=class="str">"cmt">//--- Four buffers + class="type">class="kw">color buffer
case DRAW_COLOR_BARS       :
case DRAW_COLOR_CANDLES    :
   res=this.CopyArray(buff_num,class="num">0,to_copy,this.m_array_data);
   if(res)
     {
      for(class="type">int i=class="num">0;i<total;i++)
        this.m_buffers[buff_num].array0[this.DataTotal(buff_num,class="num">0)-(total-i)]=this.m_array_data[i];
     }
   res &=this.CopyArray(buff_num,class="num">1,to_copy,this.m_array_data);
   if(res)
     {
      for(class="type">int i=class="num">0;i<total;i++)
        this.m_buffers[buff_num].array1[this.DataTotal(buff_num,class="num">1)-(total-i)]=this.m_array_data[i];
     }

◍ 多缓冲区的回写定位逻辑

当指标缓冲区类型需要多组数据(如含颜色索引的画线或柱状)时,CopyArray 要按缓冲槽位 2、3、4 分别拉取,再反向填回对应的 array2、array3 与 color_indexes。每段拷贝都先判断 res 是否为真,避免前一步失败还继续写脏数据。 循环里下标用 DataTotal(buff_num,槽号)-(total-i) 而不是从头覆盖,意味着新拷贝的 total 根 K 线总是接在已有数据尾部。若你改过 DataTotal 的计数逻辑,这里会直接错位,MT5 上表现为图形右侧突然断点。 单缓冲类型(DRAW_LINE / DRAW_HISTOGRAM / DRAW_ARROW / DRAW_SECTION)则省掉上述折腾,直接 CopyArray(buff_num,0,to_copy,m_array_data) 返回。写自定义指标时先确认 BufferDrawType,能少绕一半弯路。

MQL5 / C++
res &=this.CopyArray(buff_num,class="num">2,to_copy,this.m_array_data);
if(res)
  {
   for(class="type">int i=class="num">0;i<total;i++)
     this.m_buffers[buff_num].array2[this.DataTotal(buff_num,class="num">2)-(total-i)]=this.m_array_data[i];
  }
res &=this.CopyArray(buff_num,class="num">3,to_copy,this.m_array_data);
if(res)
  {
   for(class="type">int i=class="num">0;i<total;i++)
     this.m_buffers[buff_num].array3[this.DataTotal(buff_num,class="num">3)-(total-i)]=this.m_array_data[i];
  }
res &=this.CopyArray(buff_num,class="num">4,to_copy,this.m_array_data);
if(res)
  {
   for(class="type">int i=class="num">0;i<total;i++)
     this.m_buffers[buff_num].color_indexes[this.DataTotal(buff_num,class="num">4)-(total-i)]=this.m_array_data[i];
  }
class="kw">return res;
class=class="str">"cmt">//---DRAW_NONE
class="kw">default:
  break;
   }
  }
 else
  {
   class="kw">switch(this.BufferDrawType(buff_num))
     {
      class=class="str">"cmt">//--- One buffer
      case DRAW_LINE         :
      case DRAW_HISTOGRAM    :
      case DRAW_ARROW        :
      case DRAW_SECTION      :
        class="kw">return this.CopyArray(buff_num,class="num">0,to_copy,this.m_array_data);
      class=class="str">"cmt">//--- Two buffers
交给小布盯盘看跨周期箭头
这些跨周期箭头对齐的异常诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到哪些柱被空值擦除,你只需判断信号有效性。

常见问题

高周期柱复制低周期缓冲时若不加区分地把空值写进去,原先非空箭头值会被覆盖,需判断目标柱是否已有非空值再决定复制。
倾向取所属低周期柱中的最后一个分形值,因为它更接近当前时间,信号时效性更强。
箭头可能绘制在二号柱及更后方,若类只处理前两根柱,后续索引的箭头根本不会被计算到。
可以,小布盯盘的品种页会高亮那些被低周期空值覆盖的高周期柱,省去手动比对缓冲区的麻烦。
能,PlotIndexSetInteger 配合 PLOT_EMPTY_VALUE 可设任意不绘制的空值,但复制逻辑都要按该值做判断。