DoEasy 函数库中的时间序列(第四十八部分):在单一子窗口里基于一个缓冲区的多周期、多品种指标(基础篇)
📘

DoEasy 函数库中的时间序列(第四十八部分):在单一子窗口里基于一个缓冲区的多周期、多品种指标(基础篇)

第 1/3 篇

单缓冲多周期指标怎么塞进一个子窗口

在 MT5 里做跨品种、跨周期指标时,常见做法是每个周期开一个缓冲区,子窗口很快被占满。DoEasy 库的思路是用一个缓冲区承载多周期多品种数据,全部绘制在同一个独立子窗口中,避免主图和其他指标互相挤占。 原文示例发布于 2020 年 12 月 28 日,截至文档记录获得 1 626 次浏览、2 条评论,说明这类紧凑型指标结构在实际交易者中有明确需求。 打开 MT5 后,你可以在导航器里新建一个自定义指标,刻意只声明一个 plot 缓冲,然后往里按时间偏移写入不同品种的不同周期数值,看子窗口是否能稳定重绘而不串线。外汇与贵金属杠杆高,这类多周期叠加会放大信号噪声,验证时先用模拟盘跑。

◍ 从单缓冲区指标摸清代码复用路径

上一篇文章用加速器振荡器(AC)演示了如何把任意品种、任意时间帧的标准指标数据拉到当前图表。这一步只是验证原理,真正要做的,是把这类读取逻辑抽象成可复用的方法,免得后面每接一个指标都重写一遍。 本文先拿单缓冲区标准指标开刀——这类指标只用一条绘制缓冲区,在主图子窗口输出数值,结构最简单。我会写一个自定义指标示例,在参数面板里选好目标品种和时间帧后,把对应标准指标画在当前图表的子窗口里。 为了撑起这个示例,库类得先做一点小改动。这一步改动看似不大,却是后续接管多缓冲区标准指标、进一步压缩冗余代码的地基。本文的测试目的很明确:找出各指标方法中长得像的代码块,后面统一抽离成独立函数。

「给标准指标缓冲区接上自动 prepar 的管道」

在 DoEasy 函数库的 BuffersCollection 类里,新增了 PreparingDataAllBuffersStdInd() 公开方法,用来一次性把已创建标准指标的计算缓冲区数据备齐。程序端收到命令后不用再去逐个搜对象、取指针,直接调这个方法即可,代码层明显瘦身。 方法内部第一件事是拉取 ID 不等于 -1 的缓冲区对象列表。列表为空就提示“No buffer object for standard indicator”并返回 false。接着按列表长度循环,标准指标至少挂两个缓冲对象(计算+绘制),但这里只喂计算缓冲——因为上一版的 PreparingDataBufferStdInd() 已做过筛选。 每个缓冲对象用自身 symbol/timeframe 去取对应时间序列,拿到可用数据量,再调 PreparingDataBufferStdInd() 复制指标句柄数据。只要有一个对象复制量不达标,res 变量置 false,方法最终返回 res。当前逻辑是每次即时报价全量复制,作者明言这对多指标不实用,但优先保逻辑清晰,后续才改“仅首次或历史变动时复制、平时只搬 1~2 根柱”。 CEngine 主类同步开了访问权限:公开区声明返回 bool 的包装方法,内部拿集合类返回的句柄与 INVALID_HANDLE 比大小,简化终端调用。另外新增按指标类型+ID 返回缓冲描述的方法,实现里取列表首个对象指针就够——绘制与计算缓冲数据同源。 OnDoEasyEvent() 的新柱线处理把复制量下限从“缓冲量 vs 指标量”的最小值,改成再叠一层默认 1000 根柱线的三方最小值。由于库内时间序列默认就是 1000,这一项通常最小,批量复制时反而省事。外汇/贵金属多周期跑这套逻辑需注意:历史重载会触发全量复制,实盘占用可能抖一下。

MQL5 / C++
  MSG_LIB_TEXT_BUFFER_TEXT_INVALID_PROPERTY_BUFF,      class=class="str">"cmt">// Invalid number of indicator buffers(class="macro">#class="kw">property indicator_buffers)
  MSG_LIB_TEXT_BUFFER_TEXT_MAX_BUFFERS_REACHED,        class=class="str">"cmt">// Reached maximum possible number of indicator buffers
  MSG_LIB_TEXT_BUFFER_TEXT_NO_BUFFER_OBJ,              class=class="str">"cmt">// No buffer object for standard indicator
  MSG_LIB_TEXT_BUFFER_TEXT_STATUS_NONE,                class=class="str">"cmt">// No drawing
  {"Неправильно указано количество буферов индикатора (class="macro">#class="kw">property indicator_buffers)","Number of indicator buffers incorrect(class="macro">#class="kw">property indicator_buffers)"},
  {"Достигнуто максимально возможное количество индикаторных буферов","Maximum number of indicator buffers reached"},
  {"Нет ни одного объекта-буфера для стандартного индикатора","There is no buffer object for the standard indicator"},
  {"Нет отрисовки","No drawing"},
class=class="str">"cmt">//--- Prepare calculated buffer data of(class="num">1) the specified standard indicator and(class="num">2) all created standard indicators
  class="type">int                PreparingDataBufferStdInd(class="kw">const ENUM_INDICATOR std_ind,class="kw">const class="type">int id,class="kw">const class="type">int total_copy);
  class="type">bool               PreparingDataAllBuffersStdInd(class="type">void);
class=class="str">"cmt">//--- Clear buffer data of the specified standard indicator by the timeseries index
class=class="str">"cmt">//+------------------------------------------------------------------+

多周期 AC 缓冲区的批量预取逻辑

在 MT5 自定义指标里同时拉取多品种、多周期的标准指标数据,核心在于先把所有缓冲区对象归并到一个列表再逐个刷新。下面这段 CBuffersCollection::PreparingDataAllBuffersStdInd 做的就是这件事:拿到带 ID 的缓冲列表,空列表直接返回 false 并打印错误。 遍历列表时跳过空指针、纯数据缓冲以及未绑定指标类型的项;对剩下的每一项,按它自身的 Symbol() 与 Timeframe() 去取对应时间序列对象。若序列不存在也跳过,避免无效句柄拖慢整体。 关键在 copied 与 used_data 的比较:used_data 来自 series.AvailableUsedData(),即当前序列可用柱数;调用 PreparingDataBufferStdInd 实际拷贝数据,若 copied 小于 used_data,说明该缓冲没拉满,res 会被按位与成 false,但最终仍继续跑完其余缓冲,不会中途断掉。 CreateAC 则是单一指标 AC 的封装:用 iAC(symbol,timeframe) 建句柄,默认 ID 为 IND_AC,并预置三色数组 {clrGreen,clrRed,clrGreen} 用于零轴上下着色。句柄有效后建直方图缓冲并取最后一个缓冲对象写参数。开 MT5 把这段塞进你的指标类,改 symbol/timeframe 参数即可验证多周期 AC 是否按预期刷新。

MQL5 / C++
class="type">bool CBuffersCollection::PreparingDataAllBuffersStdInd(class="type">void)
  {
   CArrayObj *list=this.GetListBuffersWithID();
   if(list==NULL || list.Total()==class="num">0)
     {
      ::Print(DFUN_ERR_LINE,CMessage::Text(MSG_LIB_TEXT_BUFFER_TEXT_NO_BUFFER_OBJ));
      class="kw">return class="kw">false;
     }
   class="type">bool res=true;
   class="type">int total=list.Total();
   for(class="type">int i=class="num">0;i<total;i++)
     {
      CBuffer *buff=list.At(i);
      if(buff==NULL || buff.TypeBuffer()==BUFFER_TYPE_DATA || buff.IndicatorType()==WRONG_VALUE)
        class="kw">continue;
      CSeriesDE *series=this.m_timeseries.GetSeries(buff.Symbol(),buff.Timeframe());
      if(series==NULL)
        class="kw">continue;
      class="type">int used_data=(class="type">int)series.AvailableUsedData();
      class="type">int copied=this.PreparingDataBufferStdInd(buff.IndicatorType(),buff.ID(),used_data);
      if(copied<used_data)
        res &=class="kw">false;
     }
   class="kw">return res;
  }
class="type">int CBuffersCollection::CreateAC(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int id=WRONG_VALUE)
  {
   class="type">int handle=::iAC(symbol,timeframe);
   class="type">int identifier=(id==WRONG_VALUE ? IND_AC : id);
   class="type">class="kw">color array_colors[class="num">3]={clrGreen,clrRed,clrGreen};
   CBuffer *buff=NULL;
   if(handle!=INVALID_HANDLE)
     {
      this.CreateHistogram();
      buff=this.GetLastCreateBuffer();

◍ 给 AC 与 ATR 挂上多周期缓冲对象

在封装多品种指标时,AC(加速震荡器)与 ATR 都走同一套缓冲注册逻辑:先拿指标句柄,再建绘制缓冲并设置品种、周期、ID 与色彩。AC 那段会建两类缓冲——显示缓冲和通过 CreateCalculate() 生成的计算缓冲,两者都要判空,任一返回 NULL 就直接给 INVALID_HANDLE,避免后续访问崩在终端。 ATR 的写法更短,用 iATR(symbol,timeframe,ma_period) 取句柄,默认 ID 用 IND_ATR,若调用方传了 WRONG_VALUE 以外的值就覆盖。注意它显式声明了 color array_colors[1]={clrLightSeaGreen},只给一条线配色,标签里把 ma_period 拼进字符串,比如 EURUSD 的 H1 周期会显示成 ATR(EURUSD,H1: 14)。 外汇与贵金属波动剧烈,ATR 周期参数设多大直接影响止损距离,回测时 14 周期在 XAUUSD 的 M15 上平均真实波幅常落在 8~15 美元区间,实盘可能瞬间翻倍,属典型高风险场景。把下面两段直接贴进你的 CBuffersCollection 类,编译后开 MT5 用不同 symbol+timeframe 调 CreateATR 验证缓冲是否真的画出来。

MQL5 / C++
if(buff==NULL)
  class="kw">return INVALID_HANDLE;
buff.SetSymbol(symbol);
buff.SetTimeframe(timeframe);
buff.SetID(identifier);
buff.SetIndicatorHandle(handle);
buff.SetIndicatorType(IND_AC);
buff.SetShowData(true);
buff.SetLabel("AC("+symbol+","+TimeframeDescription(timeframe)+")");
buff.SetIndicatorName("Accelerator Oscillator");
buff.SetColors(array_colors);

class=class="str">"cmt">//--- Create a calculated buffer storing standard indicator data
this.CreateCalculate();
class=class="str">"cmt">//--- Get the last created(calculated) buffer object and set all the necessary parameters to it
buff=this.GetLastCreateBuffer();
if(buff==NULL)
  class="kw">return INVALID_HANDLE;
buff.SetSymbol(symbol);
buff.SetTimeframe(timeframe);
buff.SetID(identifier);
buff.SetIndicatorHandle(handle);
buff.SetIndicatorType(IND_AC);
buff.SetEmptyValue(EMPTY_VALUE);
buff.SetLabel("AC("+symbol+","+TimeframeDescription(timeframe)+")");
buff.SetIndicatorName("Accelerator Oscillator");
 }
 class="kw">return handle;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create multi-symbol multi-period ATR                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CBuffersCollection::CreateATR(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int ma_period,class="kw">const class="type">int id=WRONG_VALUE)
  {
class=class="str">"cmt">//--- Create the indicator handle and set the class="kw">default ID
  class="type">int handle=::iATR(symbol,timeframe,ma_period);
  class="type">int identifier=(id==WRONG_VALUE ? IND_ATR : id);
  class="type">class="kw">color array_colors[class="num">1]={clrLightSeaGreen};
  CBuffer *buff=NULL;
  if(handle!=INVALID_HANDLE)
   {
   class=class="str">"cmt">//--- Create the line buffer
   this.CreateLine();
   class=class="str">"cmt">//--- Get the last created(drawn) buffer object and set all the necessary parameters to it
   buff=this.GetLastCreateBuffer();
   if(buff==NULL)
     class="kw">return INVALID_HANDLE;
   buff.SetSymbol(symbol);
   buff.SetTimeframe(timeframe);
   buff.SetID(identifier);
   buff.SetIndicatorHandle(handle);
   buff.SetIndicatorType(IND_ATR);
   buff.SetShowData(true);
   buff.SetLabel("ATR("+symbol+","+TimeframeDescription(timeframe)+": "+(class="type">class="kw">string)ma_period+")");

「把标准指标塞进计算缓冲区的收尾写法」

上面这段逻辑做的是同一件事:给 ATR 这类标准指标在缓冲区集合里建一个计算型缓冲,并把句柄、周期、符号、空值标记全部绑定好。注意 buff.SetLabel("ATR("+symbol+","+TimeframeDescription(timeframe)+": "+(string)ma_period+")") 这行,它直接把图表上显示的标签拼成可读字符串,调试时一眼能分清是哪个品种哪周期哪参数。 PreparingDataBufferStdInd 是真正触发数据填充的入口。它先按指标类型和 ID 取出缓冲链表,再用 CSelect::ByBufferProperty 筛出计算型缓冲;若链表为空直接返回 0,意味着该指标还没初始化完,调用方不该继续拷数据。 目前 switch 里只实装了 IND_AC 分支:取链首缓冲,调 FillAsSeries 从指标句柄按序列把最多 total_copy 根 K 线填进去,返回实际拷贝数。其余如 IND_ATRIND_ADX 都还是 break 空壳——你想接 ATR 数据就得自己补这个 case,否则运行时只会静默跳过。外汇与贵金属波动剧烈,缓冲拷贝数量设太大既吃内存也拖慢 OnTick,建议先按 500 根试。

MQL5 / C++
buff.SetIndicatorName("Average True Range");
buff.SetColors(array_colors);

class=class="str">"cmt">//--- Create a calculated buffer storing standard indicator data
this.CreateCalculate();
class=class="str">"cmt">//--- Get the last created(calculated) buffer object and set all the necessary parameters to it
buff=this.GetLastCreateBuffer();
if(buff==NULL)
   class="kw">return INVALID_HANDLE;
buff.SetSymbol(symbol);
buff.SetTimeframe(timeframe);
buff.SetID(identifier);
buff.SetIndicatorHandle(handle);
buff.SetIndicatorType(IND_ATR);
buff.SetEmptyValue(EMPTY_VALUE);
buff.SetLabel("ATR("+symbol+","+TimeframeDescription(timeframe)+": "+(class="type">class="kw">string)ma_period+")");
buff.SetIndicatorName("Average True Range");
 }
 class="kw">return handle;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Prepare the calculated buffer data                              |
class=class="str">"cmt">//| of the specified standard indicator                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CBuffersCollection::PreparingDataBufferStdInd(class="kw">const ENUM_INDICATOR std_ind,class="kw">const class="type">int id,class="kw">const class="type">int total_copy)
 {
  CArrayObj *list=this.GetListBufferByTypeID(std_ind,id);
  list=CSelect::ByBufferProperty(list,BUFFER_PROP_TYPE,BUFFER_TYPE_CALCULATE,EQUAL);
  if(list==NULL || list.Total()==class="num">0)
    class="kw">return class="num">0;
  CBufferCalculate *buffer=NULL;
  class="type">int copies=WRONG_VALUE;
  class="kw">switch((class="type">int)std_ind)
   {
    case IND_AC :
      buffer=list.At(class="num">0);
      if(buffer==NULL) class="kw">return class="num">0;
      copies=buffer.FillAsSeries(buffer.IndicatorHandle(),class="num">0,class="num">0,total_copy);
      class="kw">return copies;

    case IND_AD :
      class="kw">break;
    case IND_ADX :
      class="kw">break;
    case IND_ADXW :
      class="kw">break;
    case IND_ALLIGATOR :
      class="kw">break;
    case IND_AMA :
      class="kw">break;
    case IND_AO :
      class="kw">break;
    case IND_ATR :
      class="kw">break;

常见问题

用单一缓冲区对象承接不同周期的计算结果,在子窗口初始化时只创建一个绘图缓冲,按周期切换写入对应数据即可。
可以,给标准指标封装缓冲对象并在初始化阶段调用自动 prepar,就能跳过手动申请缓冲的重复代码。
小布可以扫描你的指标代码,标出未挂多周期缓冲对象的品种或周期,并提示补上批量预取逻辑。
常见坑是周期枚举写死导致新增周期漏接;用循环批量挂载缓冲对象比逐个声明更稳。
确保子窗口索引与缓冲编号一致,且在 OnInit 里完成所有缓冲绑定,避免首根 K 线报数组越界。