DoEasy 函数库中的时间序列(第四十六部分):多周期、多品种指标缓冲区·进阶篇
🧩

DoEasy 函数库中的时间序列(第四十六部分):多周期、多品种指标缓冲区·进阶篇

(2/3)· 从单周期到任意品种跨周期取数,缓冲区对象类如何少改代码多扛任务

偏理论 第 2/3 篇
不少人在 MT5 里写跨品种指标时,习惯为每个符号重开一套缓冲区,结果数组索引乱、绘图样式错位。其实函数库早埋了多周期能力,只是缺一步针对任意品种的精练。

缓冲区绘图属性的批量绑定

在自定义指标里,每个绘图缓冲区的外观与空值规则都靠 PlotIndexSet 系列函数落地。下面这段把位移、线型、线宽、颜色、空值、标签一次性从基类属性取出来塞进绘图索引,省得手写重复代码。 ::PlotIndexSetInteger((int)this.GetProperty(BUFFER_PROP_INDEX_PLOT),PLOT_SHIFT,(ENUM_PLOT_PROPERTY_INTEGER)this.GetProperty(BUFFER_PROP_SHIFT)); ::PlotIndexSetInteger((int)this.GetProperty(BUFFER_PROP_INDEX_PLOT),PLOT_LINE_STYLE,(ENUM_PLOT_PROPERTY_INTEGER)this.GetProperty(BUFFER_PROP_LINE_STYLE)); ::PlotIndexSetInteger((int)this.GetProperty(BUFFER_PROP_INDEX_PLOT),PLOT_LINE_WIDTH,(ENUM_PLOT_PROPERTY_INTEGER)this.GetProperty(BUFFER_PROP_LINE_WIDTH)); this.SetColor((color)this.GetProperty(BUFFER_PROP_COLOR)); //--- Set real parameters of the graphical series ::PlotIndexSetDouble((int)this.GetProperty(BUFFER_PROP_INDEX_PLOT),PLOT_EMPTY_VALUE,this.GetProperty(BUFFER_PROP_EMPTY_VALUE)); //--- Set string parameters of the graphical series ::PlotIndexSetString((int)this.GetProperty(BUFFER_PROP_INDEX_PLOT),PLOT_LABEL,this.GetProperty(BUFFER_PROP_LABEL)); 前三条 SetInteger 分别控制 PLOT_SHIFT(水平偏移量,常用于把信号线右移避免未来函数观感)、PLOT_LINE_STYLE(实线/虚线枚举)、PLOT_LINE_WIDTH(像素宽度,MT5 里 1~5 有效)。SetColor 接的是 color 类型属性,空值用 PlotIndexSetDouble 的 PLOT_EMPTY_VALUE 标记,标签用 PlotIndexSetString 的 PLOT_LABEL 决定图表里显示的系列名。 类封装上,CBufferArrow 构造函数写死 BUFFER_STATUS_ARROW 和默认标签 "Arrows",CBufferLine 对应 BUFFER_STATUS_LINE 与 "Line",CBufferSection 同理。注意构造参数里数组维度传了 1 和 1,中间夹的 2 是缓冲区关联的基础数组序号偏移,改这里会直接决定哪条数据线被绘制。外汇与贵金属杠杆高,指标偏移或线宽设错只影响视觉,但信号右移过量可能让你误判入场时机,回测前务必在 MT5 可视化里核对。

MQL5 / C++
::PlotIndexSetInteger((class="type">int)this.GetProperty(BUFFER_PROP_INDEX_PLOT),PLOT_SHIFT,(ENUM_PLOT_PROPERTY_INTEGER)this.GetProperty(BUFFER_PROP_SHIFT));
::PlotIndexSetInteger((class="type">int)this.GetProperty(BUFFER_PROP_INDEX_PLOT),PLOT_LINE_STYLE,(ENUM_PLOT_PROPERTY_INTEGER)this.GetProperty(BUFFER_PROP_LINE_STYLE));
::PlotIndexSetInteger((class="type">int)this.GetProperty(BUFFER_PROP_INDEX_PLOT),PLOT_LINE_WIDTH,(ENUM_PLOT_PROPERTY_INTEGER)this.GetProperty(BUFFER_PROP_LINE_WIDTH));
this.SetColor((class="type">class="kw">color)this.GetProperty(BUFFER_PROP_COLOR));
class=class="str">"cmt">//--- Set real parameters of the graphical series
::PlotIndexSetDouble((class="type">int)this.GetProperty(BUFFER_PROP_INDEX_PLOT),PLOT_EMPTY_VALUE,this.GetProperty(BUFFER_PROP_EMPTY_VALUE));
class=class="str">"cmt">//--- Set class="type">class="kw">string parameters of the graphical series
::PlotIndexSetString((class="type">int)this.GetProperty(BUFFER_PROP_INDEX_PLOT),PLOT_LABEL,this.GetProperty(BUFFER_PROP_LABEL));

「缓冲区绘制风格的子类封装」

在自定义指标里,不同绘图样式对应不同的缓冲区结构。把每种样式写成 CBuffer 的派生类,构造时直接传好状态常量与数组维度,后面调指标就少踩坑。 下面这几个类覆盖了从零轴直方图到 ZigZag、双色填充的常见画法。注意构造参数里第 5、6 个整型值分别代表绘图所需的基础数组数量和内部缓冲数量,比如 CBufferHistogram2 传了 2 和 3,说明它吃两个数据数组、内部开三个缓冲。 外汇与贵金属杠杆高,指标只是概率参考,实盘前务必在 MT5 策略测试器里用历史数据验证缓冲绑定是否正确。

MQL5 / C++
CBuffer(BUFFER_STATUS_SECTION,BUFFER_TYPE_DATA,index_plot,index_base_array,class="num">1,class="num">2,class="num">1,"Section") {}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Buffer of the "Histogram from the zero line" drawing style       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CBufferHistogram : class="kw">public CBuffer
  {
class="kw">private:
class="kw">public:
class=class="str">"cmt">//--- Constructor
                    CBufferHistogram(class="kw">const class="type">uint index_plot,class="kw">const class="type">uint index_base_array) :
                      CBuffer(BUFFER_STATUS_HISTOGRAM,BUFFER_TYPE_DATA,index_plot,index_base_array,class="num">1,class="num">2,class="num">2,"Histogram") {}
class=class="str">"cmt">//+--------------------------------------------------------------------+
class=class="str">"cmt">//|Buffer of the "Histogram on two indicator buffers" drawing style    |
class=class="str">"cmt">//+--------------------------------------------------------------------+
class CBufferHistogram2 : class="kw">public CBuffer
  {
class="kw">private:
class="kw">public:
class=class="str">"cmt">//--- Constructor
                    CBufferHistogram2(class="kw">const class="type">uint index_plot,class="kw">const class="type">uint index_base_array) :
                      CBuffer(BUFFER_STATUS_HISTOGRAM2,BUFFER_TYPE_DATA,index_plot,index_base_array,class="num">2,class="num">3,class="num">8,"Histogram2 class="num">0;Histogram2 class="num">1") {}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Buffer of the ZigZag drawing style                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CBufferZigZag : class="kw">public CBuffer
  {
class="kw">private:
class="kw">public:
class=class="str">"cmt">//--- Constructor
                    CBufferZigZag(class="kw">const class="type">uint index_plot,class="kw">const class="type">uint index_base_array) :
                      CBuffer(BUFFER_STATUS_ZIGZAG,BUFFER_TYPE_DATA,index_plot,index_base_array,class="num">2,class="num">3,class="num">1,"ZigZag class="num">0;ZigZag class="num">1") {}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Buffer of the "Color filling between two levels" drawing style    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CBufferFilling : class="kw">public CBuffer
  {
class="kw">private:
class="kw">public:
class=class="str">"cmt">//--- Constructor
                    CBufferFilling(class="kw">const class="type">uint index_plot,class="kw">const class="type">uint index_base_array) :
                      CBuffer(BUFFER_STATUS_FILLING,BUFFER_TYPE_DATA,index_plot,index_base_array,class="num">2,class="num">2,class="num">1,"Filling class="num">0;Filling class="num">1") {}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Buffer of the Bars drawing style                                  |

◍ 缓冲区的三种派生类怎么写

在 MT5 自定义指标里,把 K 线数据塞进绘图缓冲区,通常从 CBuffer 基类再派生出几个专用子类。下面这段实现给出了 Bars、Candles 与 Calculate 三类构造差异,直接能抄进你的指标工程。 CBufferBars 用 BUFFER_STATUS_BARS 状态、4 个数据列(Open/High/Low/Close),构造参数里第 5、6 位分别传了 5 和 2,对应每根 bar 的数组宽度与偏移约定;CBufferCandles 几乎同构,只是状态换成 BUFFER_STATUS_CANDLES、偏移参数为 1,标签也改成 Candle 前缀。 CBufferCalculate 则是纯计算缓冲,BUFFER_TYPE_CALCULATE 类型、只挂 1 列名为 Calculate 的数组,不负责直接绘图。注意原文里这个类名出现了两次,第二次去掉了高亮部分的 5 传参,实际编译应以无冗余参数的版本为准,否则可能触发重载歧义。 外汇与贵金属指标调用这些缓冲时波动剧烈,参数错配会导致图形错位,建议开 MT5 用样例符号先跑一轮肉眼核对。

MQL5 / C++
class CBufferBars : class="kw">public CBuffer
  {
class="kw">private:
class="kw">public:
class=class="str">"cmt">//--- Constructor
                     CBufferBars(class="kw">const class="type">uint index_plot,class="kw">const class="type">uint index_base_array) :
                       CBuffer(BUFFER_STATUS_BARS,BUFFER_TYPE_DATA,index_plot,index_base_array,class="num">4,class="num">5,class="num">2,"Bar Open;Bar High;Bar Low;Bar Close") {}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|Buffer of the Candles drawing style                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CBufferCandles : class="kw">public CBuffer
  {
class="kw">private:
class="kw">public:
class=class="str">"cmt">//--- Constructor
                     CBufferCandles(class="kw">const class="type">uint index_plot,class="kw">const class="type">uint index_base_array) :
                       CBuffer(BUFFER_STATUS_CANDLES,BUFFER_TYPE_DATA,index_plot,index_base_array,class="num">4,class="num">5,class="num">1,"Candle Open;Candle High;Candle Low;Candle Close") {}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculated buffer                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CBufferCalculate : class="kw">public CBuffer
  {
class="kw">private:
class="kw">public:
class=class="str">"cmt">//--- Constructor
                     CBufferCalculate(class="kw">const class="type">uint index_plot,class="kw">const class="type">uint index_array) :
                       CBuffer(BUFFER_STATUS_NONE,BUFFER_TYPE_CALCULATE,index_plot,index_array,class="num">1,class="num">1,class="num">0,"Calculate") {}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculated buffer                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CBufferCalculate : class="kw">public CBuffer
  {
class="kw">private:
class="kw">public:
class=class="str">"cmt">//--- Constructor
                     CBufferCalculate(class="kw">const class="type">uint index_plot,class="kw">const class="type">uint index_array) :
                       CBuffer(BUFFER_STATUS_NONE,BUFFER_TYPE_CALCULATE,index_plot,index_array,class="num">1,class="num">1,class="num">0,"Calculate") {}
class=class="str">"cmt">//--- Supported integer properties of a buffer
   class="kw">virtual class="type">bool      SupportProperty(ENUM_BUFFER_PROP_INTEGER class="kw">property);
class=class="str">"cmt">//--- Supported real properties of a buffer
   class="kw">virtual class="type">bool      SupportProperty(ENUM_BUFFER_PROP_DOUBLE class="kw">property);
class=class="str">"cmt">//--- Supported class="type">class="kw">string properties of a buffer

缓冲对象怎么把指标数据吃进来

CBufferCalculate 类里留了一组虚函数和 inline 方法,专门负责把外部指标缓冲区接管到自身 DataBuffer[0] 数组。SupportProperty 和 PrintShort 是给调试用的,前者声明支持哪种字符串属性,后者往日志打一句简短描述,方便在 MT5 Experts 标签里肉眼核对缓冲身份。 SetData 和 GetData 是最底层读写接口:写入时调 SetBufferValue(0, series_index, value),读取时从 GetDataBufferValue(0, series_index) 拿 double。注意两个方法都硬编码了缓冲区索引 0,说明这个类默认只管单缓冲序列,多缓冲指标得自己扩。 真正把数据拉满的是三个 FillAsSeries 重载,底层统一走 ::CopyBuffer。第一个按起始位置 start_pos 和条数 count 拷;第二个用 start_time 加 count;第三个给起止时间对 datetime 区间。三者返回的都是实际拷贝的元素个数,拷贝失败会返回 -1,调用方得自己判。 开 MT5 写个 EA 挂个 iMA 句柄,用第二个重载 FillAsSeries(ma_handle, 0, TimeCurrent()-3600, 100),就能把最近一小时、最多 100 根 K 的均线值灌进对象数组,外汇和贵金属品种波动跳空多,这种按时间取数比按位置更稳,但滑点和点差风险仍在。

MQL5 / C++
class="kw">virtual class="type">bool      SupportProperty(ENUM_BUFFER_PROP_STRING class="kw">property);
class=class="str">"cmt">//--- Display a class="type">class="kw">short buffer description in the journal
class="kw">virtual class="type">void      PrintShort(class="type">void);

class=class="str">"cmt">//--- Set the value to the data buffer array
class="type">void              SetData(class="kw">const class="type">uint series_index,class="kw">const class="type">class="kw">double value)          { this.SetBufferValue(class="num">0,series_index,value);      }
class=class="str">"cmt">//--- Return the value from the data buffer array
class="type">class="kw">double            GetData(class="kw">const class="type">uint series_index)                            class="kw">const { class="kw">return this.GetDataBufferValue(class="num">0,series_index);  }

class=class="str">"cmt">//--- Copy data of the specified indicator to the buffer object array
class="type">int               FillAsSeries(class="kw">const class="type">int indicator_handle,class="kw">const class="type">int buffer_num,class="kw">const class="type">int start_pos,class="kw">const class="type">int count);
class="type">int               FillAsSeries(class="kw">const class="type">int indicator_handle,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">datetime start_time,class="kw">const class="type">int count);
class="type">int               FillAsSeries(class="kw">const class="type">int indicator_handle,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">datetime start_time,class="kw">const class="type">class="kw">datetime stop_time);
};

class="type">int CBufferCalculate::FillAsSeries(class="kw">const class="type">int indicator_handle,class="kw">const class="type">int buffer_num,class="kw">const class="type">int start_pos,class="kw">const class="type">int count)
  {
   class="kw">return ::CopyBuffer(indicator_handle,buffer_num,start_pos,count,this.DataBuffer[class="num">0].Array);
  }
class="type">int CBufferCalculate::FillAsSeries(class="kw">const class="type">int indicator_handle,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">datetime start_time,class="kw">const class="type">int count)
  {
   class="kw">return ::CopyBuffer(indicator_handle,buffer_num,start_time,count,this.DataBuffer[class="num">0].Array);
  }
class="type">int CBufferCalculate::FillAsSeries(class="kw">const class="type">int indicator_handle,class="kw">const class="type">int buffer_num,class="kw">const class="type">class="kw">datetime start_time,class="kw">const class="type">class="kw">datetime stop_time)
  {
   class="kw">return ::CopyBuffer(indicator_handle,buffer_num,start_time,stop_time,this.DataBuffer[class="num">0].Array);
  }

「跨周期取数时当前图表符号的坑」

在缓冲区对象跨周期取数时,第一段代码用 buffer.Symbol() 去取当前图表时序,逻辑上没问题;但第二段被标黄的那行直接用了全局 Symbol(),只认主图品种。若把指标挂在 XAUUSD 图上却给 EURUSD 的缓冲区算数据,series_current 会拿到错误(甚至空)时序,后面 bar_current 直接 NULL,函数返回 WRONG_VALUE。 这个函数核心做三件事:拿当前周期和缓冲区周期的时序指针、按索引定位当前 bar 并映射到缓冲周期所属 bar、用 PeriodSeconds 比值算一个周期里塞了几根小周期 bar(算出来是 0 就返回 1)。返回值是整数根数,调用方拿它去循环拷贝缓冲值。 实际排错时,打开 MT5 把这段贴进你的 CBuffersCollection 类,故意挂个与 buffer 不同品种的主图,断点跟到 series_current==NULL 那行,就能复现符号错配。外汇和贵金属跨周期计算滑点大、重绘风险高,这类底层取数错误可能让信号整体偏移,建议先在模拟盘验证。

MQL5 / C++
class="type">int CBuffersCollection::GetBarsData(CBuffer *buffer,class="kw">const class="type">int series_index,class="type">int &index_bar_period)
  {
class=class="str">"cmt">//--- Get timeseries of the current chart and the chart of the buffer timeframe
   CSeriesDE *series_current=this.m_timeseries.GetSeries(Symbol(),PERIOD_CURRENT);
   CSeriesDE *series_period=this.m_timeseries.GetSeries(buffer.Symbol(),buffer.Timeframe());
   if(series_current==NULL || series_period==NULL)
      class="kw">return WRONG_VALUE;
class=class="str">"cmt">//--- Get the bar object of the current timeseries corresponding to the required timeseries index
   CBar *bar_current=series_current.GetBar(series_index);
   if(bar_current==NULL)
      class="kw">return WRONG_VALUE;
class=class="str">"cmt">//--- Get the timeseries bar object of the buffer chart period corresponding to the time the timeseries bar of the current chart falls into
   CBar *bar_period=m_timeseries.GetBarSeriesFirstFromSeriesSecond(NULL,PERIOD_CURRENT,bar_current.Time(),NULL,series_period.Timeframe());
   if(bar_period==NULL)
      class="kw">return WRONG_VALUE;
class=class="str">"cmt">//--- Write down the bar index on the current timeframe which falls into the bar start time of the buffer object chart
   index_bar_period=bar_period.Index(PERIOD_CURRENT);
class=class="str">"cmt">//--- Calculate the amount of bars of the current timeframe included into one bar of the buffer object chart period
class=class="str">"cmt">//--- and class="kw">return this value(class="num">1 if the result is class="num">0)
   class="type">int num_bars=::PeriodSeconds(bar_period.Timeframe())/::PeriodSeconds(bar_current.Timeframe());
   class="kw">return(num_bars>class="num">0 ? num_bars : class="num">1);
  }

◍ 跨周期定位与柱数折算的实现落点

在多周期分析里,经常需要把缓冲对象图表上的某根柱,映射回当前图表对应位置的索引。下面这段逻辑先通过 GetBarSeriesFirstFromSeriesSecond 拿到第一序列里、时间落在第二序列柱开盘时刻的那根柱对象,若返回空指针则直接给出 WRONG_VALUE 的错误值,避免后续计算越界。 index_bar_period=bar_period.Index(PERIOD_CURRENT); 这一行把定位到的柱转成当前周期下的索引,供上层调用。紧接着用 PeriodSeconds 分别除出两个周期的秒数,得到「缓冲周期一根柱涵盖了当前周期多少根柱」的整数倍率——比如 H1 对 M5 会算出 12,M15 对 M1 则是 15。 若折算结果为 0(极端小周期对齐异常),代码返回 1 而不是 0,保证调用方至少按单根柱处理。类里配套的 GetBar 重载和 IndexBarPeriodByBarCurrent 接口,让「按索引取柱 / 按时间取柱 / 跨序列反查」都能在同一套封装下完成,开 MT5 把这段塞进你自己的 CTimeseries 派生类,调一个 timeframe 参数就能验证映射是否如预期。

MQL5 / C++
  CBar *bar_period=m_timeseries.GetBarSeriesFirstFromSeriesSecond(NULL,PERIOD_CURRENT,bar_current.Time(),buffer.Symbol(),series_period.Timeframe());
  if(bar_period==NULL)
      class="kw">return WRONG_VALUE;
class=class="str">"cmt">//--- Write down the bar index on the current timeframe which falls into the bar start time of the buffer object chart
  index_bar_period=bar_period.Index(PERIOD_CURRENT);
class=class="str">"cmt">//--- Calculate the amount of bars of the current timeframe included into one bar of the buffer object chart period
class=class="str">"cmt">//--- and class="kw">return this value(class="num">1 if the result is class="num">0)
  class="type">int num_bars=::PeriodSeconds(bar_period.Timeframe())/::PeriodSeconds(bar_current.Timeframe());
  class="kw">return(num_bars>class="num">0 ? num_bars : class="num">1);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Return the bar object of the specified timeseries of the specified symbol of the specified position(class="num">1) by index, (class="num">2) by time
class=class="str">"cmt">//--- bar object of the first timeseries corresponding to the bar open time on the second timeseries(class="num">3) by index, (class="num">4) by time
  CBar                *GetBar(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index,class="kw">const class="type">bool from_series=true);
  CBar                *GetBar(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">class="kw">datetime bar_time);
  CBar                *GetBarSeriesFirstFromSeriesSecond(class="kw">const class="type">class="kw">string symbol_first,class="kw">const ENUM_TIMEFRAMES timeframe_first,class="kw">const class="type">int index,
                                                        class="kw">const class="type">class="kw">string symbol_second=NULL,class="kw">const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT);
  CBar                *GetBarSeriesFirstFromSeriesSecond(class="kw">const class="type">class="kw">string symbol_first,class="kw">const ENUM_TIMEFRAMES timeframe_first,class="kw">const class="type">class="kw">datetime first_bar_time,
                                                        class="kw">const class="type">class="kw">string symbol_second=NULL,class="kw">const ENUM_TIMEFRAMES timeframe_second=PERIOD_CURRENT);
class=class="str">"cmt">//--- Return the bar index on the specified timeframe chart by the current chart&class="macro">#x27;s bar index              |
  class="type">int                 IndexBarPeriodByBarCurrent(class="kw">const class="type">int series_index,class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe);
class=class="str">"cmt">//--- Return the flag of opening a new bar of the specified timeseries of the specified symbol
交给小布盯盘看跨周期背离
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多周期均线或 MACD 的偏离提示,你不必自己拼缓冲区。

常见问题

它把构造每个缓冲区对象所占用的数组数固化下来,让后续缓冲区索引计算不被绘图样式和颜色缓冲干扰,逻辑更清晰。
因为这种样式不提供独立颜色缓冲区,若按普通绘制缓冲去加数据数组数会算错基础索引,必须按类型分支判断。
可以,本次精练让缓冲区对象依据指标句柄取数组数据,从而支持把任意品种周期的标准指标绘到本图。
能,小布的品种页已用类似机制聚合跨周期信号,你只需切品种不必重跑 EA 做缓冲绑定。
会大幅简化自定义程序里多品种多周期指标的创建流程,后续开发自定义工具时少写重复封装。