DoEasy 函数库中的时间序列(第四十一部分):多品种多周期指标样品·综合运用
📘

DoEasy 函数库中的时间序列(第四十一部分):多品种多周期指标样品·综合运用

第 3/3 篇

◍ 定时刷新与计算事件的时序隔离

引擎用 OnTimer 做后台时序维护,避免在每根 K 线计算时重复拉取全部历史。计时器 ID 为 COLLECTION_TS_COUNTER_ID 的计数器到期后才调用 SeriesRefreshAllExceptCurrent,只更新非当前柱的时间序列,当前柱留给 OnCalculate 单独处理。 看 OnTimer 里的关键分支:先按 CounterIndex 取 cnt6 指针,空指针直接跳过;cnt6.IsTimeDone() 为真才执行刷新。这样在实时行情中,历史序列按节奏批量更新,当前未闭合柱不被打扰,回测模式下则走 tick 级集合事件分支。 OnCalculate 的退出条件很硬:非指标程序直接 return 0;任意序列未同步也 return 0。同步通过后,非 tester 环境调 SeriesRefresh(NULL,...) 刷新当前符号,最后用三元判断返回——空序列计数为 NULL 时给 rates_total,否则给 0。这个 0/rates_total 的返回值直接决定指标是否输出,调参时若发现指标不画,先查 SeriesGetSeriesEmpty 是不是非空。 外汇与贵金属品种跳空多,序列同步失败概率偏高,实盘加载前建议在策略测试器用 2023 年 XAUUSD 的 M5 数据跑一遍,确认 OnCalculate 返回值稳定为 rates_total 而非频繁 0。

MQL5 / C++
class="kw">protected:
  class="type">void                SeriesRefreshAllExceptCurrent(SDataCalculate &data_calculate)
    { this.m_time_series.GetObject().RefreshAllExceptCurrent(data_calculate);    }
class="kw">public:
class=class="str">"cmt">//--- Return(class="num">1) the timeseries object of the specified symbol and(class="num">2) the timeseries object of the specified symbol/period
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CEngine timer                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CEngine::OnTimer(SDataCalculate &data_calculate)
  {
class=class="str">"cmt">//
class=class="str">"cmt">// here I have removed some code not needed for the current example
class=class="str">"cmt">//
  class=class="str">"cmt">//--- Timeseries collection timer
    index=this.CounterIndex(COLLECTION_TS_COUNTER_ID);
    CTimerCounter* cnt6=this.m_list_counters.At(index);
    if(cnt6!=NULL)
      {
       class=class="str">"cmt">//--- If the pause is over, work with the timeseries list(update all except the current one)
       if(cnt6.IsTimeDone())
         this.SeriesRefreshAllExceptCurrent(data_calculate);
      }
   }
class=class="str">"cmt">//--- If this is a tester, work with collection events by tick
   else
     {
class=class="str">"cmt">//
class=class="str">"cmt">// here I have removed some code not needed for the current example
class=class="str">"cmt">//
     }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculate event handler                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CEngine::OnCalculate(SDataCalculate &data_calculate,const class="type">uint required=class="num">0)
  {
class=class="str">"cmt">//--- If this is not an indicator, exit
   if(this.m_program!=PROGRAM_INDICATOR)
      class="kw">return class="num">0;
class=class="str">"cmt">//--- Re-create empty timeseries
class=class="str">"cmt">//--- If at least one of the timeseries is not synchronized, class="kw">return zero
   if(!this.SeriesSync(data_calculate,required))
      class="kw">return class="num">0;
class=class="str">"cmt">//--- Update the timeseries of the current symbol(not in the tester) and
class=class="str">"cmt">//--- class="kw">return either class="num">0 (in case there are empty timeseries), or rates_total
   if(!this.IsTester())
      this.SeriesRefresh(NULL,data_calculate);
   class="kw">return(this.SeriesGetSeriesEmpty()==NULL ? data_calculate.rates_total : class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

批量建时间序列的引擎入口

多品种多周期策略在 MT5 里最怕启动阶段时序没就绪,这段代码就是 CEngine 里一次性把所有用到的 symbol 和 period 的时间序列都建起来的核心函数。 函数 SeriesCreateAll 接收周期描述数组 array_periods、可选 bars 数 rates_total 和 required 标志。先取已用品种链表,空了直接 Print 报错并返回 false;随后双层循环:外层遍历品种,内层遍历周期,用 TimeframeByDescription 把字符串周期转成 ENUM_TIMEFRAMES,再调 this.SeriesCreate 建序列,结果用 &= 累乘到 res。 注意 res &= 的写法——任一品种任一周期建失败,整体返回值就是 false,但循环不会中断,会继续把剩余品种跑完。最后那行 WriteSymbolsPeriodsToArrays() 把实际用到的 symbol 和 timeframe 落进两个数组,供后续指标订阅使用。 外汇与贵金属杠杆高、点差跳变频繁,多品种时序初始化若漏掉某个周期,后续 CopyBuffer 可能返回 0 根数据,回测和实盘都可能 silently 出错,建议把 res 返回值和 Print 日志都接出来自查。

MQL5 / C++
class="type">bool CEngine::SeriesCreateAll(const class="type">class="kw">string &array_periods[],const class="type">int rates_total=class="num">0,const class="type">uint required=class="num">0)
  {
class=class="str">"cmt">//--- Set the flag of successful creation of all timeseries of all symbols
   class="type">bool res=true;
class=class="str">"cmt">//--- Get the list of all used symbols
   CArrayObj* list_symbols=this.GetListAllUsedSymbols();
   if(list_symbols==NULL)
     {
      ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_GET_SYMBOLS_ARRAY));
      class="kw">return false;
     }
   class=class="str">"cmt">//--- In the loop by the total number of symbols
   for(class="type">int i=class="num">0;i<list_symbols.Total();i++)
     {
      class=class="str">"cmt">//--- get the next symbol object
      CSymbol *symbol=list_symbols.At(i);
      if(symbol==NULL)
        {
         ::Print(DFUN,"index ",i,": ",CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_SYM_OBJ));
         class="kw">continue;
        }
      class=class="str">"cmt">//--- In the loop by the total number of used timeframes,
      class="type">int total_periods=::ArraySize(array_periods);
      for(class="type">int j=class="num">0;j<total_periods;j++)
        {
         class=class="str">"cmt">//--- create the timeseries object of the next symbol.
         class=class="str">"cmt">//--- Add the timeseries creation result to the res variable
         ENUM_TIMEFRAMES timeframe=TimeframeByDescription(array_periods[j]);
         res &=this.SeriesCreate(symbol.Name(),timeframe,rates_total,required);
        }
     }
class=class="str">"cmt">//--- Write all used symbols and timeframes to the ArrayUsedSymbols and ArrayUsedTimeframes arrays
   this.WriteSymbolsPeriodsToArrays();
class=class="str">"cmt">//--- Return the result of creating all timeseries for all symbols
   class="kw">return res;
  }

「用结构数组管住 21 个指标缓冲区」

在 MT5 里做一个多品种多周期测试指标时,缓冲区数量很容易算错。以 DRAW_COLOR_CANDLES 绘图样式为例,每套烛条绘制要占用 5 个关联数组:开盘、最高、最低、收盘以及颜色。若最大品种数设为 4,则 4 套绘制缓冲区吃掉 20 个指标缓冲区,再加 1 个存柱线时间的计算用缓冲区,总数正好是 21。 把这套关系写进一个烛条缓冲区结构,比手工定义四组雷同缓冲区省事得多。结构里存下 OHLC 与 Color 各自的缓冲区索引,随时按数组下标取用;下一个可绑定的空闲索引由 m_buff_next_index 配合 IndexNextBuffer() 给出,返回位置恒在上一结构的颜色缓冲区之后。日志打印显示:每根后续烛条缓冲区的“开盘价”索引,与上一项的“下一个缓冲区索引”严丝合缝,末位空闲索引落到 20,正好拿去挂时间缓冲区。 缓冲区绑定有个坑得记牢:SetIndexBuffer() 按“所有数组的序列编号”绑,而 PlotIndexGetInteger() 这类函数按“绘制缓冲区编号”取。三个绘制缓冲区各带两个数组时,数组编号是 0–5,但绘制缓冲区编号只是 0、1、2。想读第二个绘制缓冲区的属性,得传索引 1,不是数组编号 2。 实操上,把指标丢进 \MQL5\Indicators\TestDoEasy\Part41\ 编译,挂到 EURUSD M15,能看到前四个品种按钮;点开品种才弹出周期按钮,选定后烛条画上图,按钮状态写进终端全局变量,重开指标仍记得上次周期。外汇与贵金属杠杆高,多品种指标仅作结构验证,实盘前务必在策略测试器跑风险。

MQL5 / C++
class="macro">#class="kw">property indicator_separate_window
class="macro">#class="kw">property indicator_buffers class="num">21   class=class="str">"cmt">// class="num">5 arrays(Open[] High[] Low[] Close[] Color[]) * class="num">4 drawn buffers + class="num">1 BufferTime[] calculated buffer

◍ 多品种蜡烛同屏的缓冲区排布

想在 MT5 副图里同时摆 4 个品种的彩色蜡烛,先得在预编译指令里把绘图槽位占满。indicator_plots 设成 4,代表指标要画 4 条 plot;每条 plot 都用 DRAW_COLOR_CANDLES 类型,各自绑定一组开高低收与颜色数组,所以 4 个品种实际吃掉 4×5=20 个底层 buffer。 代码里给每个 plot 配了独立配色:Pair1 涨绿跌红、Pair2 深蓝配砖红、Pair3 紫调对暗鲑、Pair4 碧绿对紫红,第三色都是灰色系用于非交易时段。这样肉眼扫四个品种时不会因颜色撞车而误读。 SYMBOLS_TOTAL 宏写死为 4,PERIODS_TOTAL 给 21,意味着这套结构最多接 4 个符号、底层按 21 个周期常量预留。若你只盯欧美黄金两个品种,把 SYMBOLS_TOTAL 改 2 并删掉后两组 #property 即可省资源。 SDataBuffer 结构体把每个品种的 timeframe、symbol 名和 5 个数组对应的 buffer 索引封在一起,m_buff_next_index 记录下一个空闲 buffer 位。开 MT5 新建指标时,照这个私有成员布局写,能在 OnInit 里动态分配 buffer 而不乱序。

MQL5 / C++
class="macro">#class="kw">property indicator_plots   class="num">4   class=class="str">"cmt">// class="num">1 candlesticks buffer consisting of class="num">5 arrays(Open[] High[] Low[] Close[] Color[]) * class="num">4 symbols
class=class="str">"cmt">//--- plot Pair1
class="macro">#class="kw">property indicator_label1  "Pair class="num">1"
class="macro">#class="kw">property indicator_type1   DRAW_COLOR_CANDLES
class="macro">#class="kw">property indicator_color1  clrLimeGreen,clrRed,clrDarkGray
class=class="str">"cmt">//--- plot Pair2
class="macro">#class="kw">property indicator_label2  "Pair class="num">2"
class="macro">#class="kw">property indicator_type2   DRAW_COLOR_CANDLES
class="macro">#class="kw">property indicator_color2  clrDeepSkyBlue,clrFireBrick,clrDarkGray
class=class="str">"cmt">//--- plot Pair3
class="macro">#class="kw">property indicator_label3  "Pair class="num">3"
class="macro">#class="kw">property indicator_type3   DRAW_COLOR_CANDLES
class="macro">#class="kw">property indicator_color3  clrMediumPurple,clrDarkSalmon,clrGainsboro
class=class="str">"cmt">//--- plot Pair4
class="macro">#class="kw">property indicator_label4  "Pair class="num">4"
class="macro">#class="kw">property indicator_type4   DRAW_COLOR_CANDLES
class="macro">#class="kw">property indicator_color4  clrMediumAquamarine,clrMediumVioletRed,clrGainsboro
class=class="str">"cmt">//--- classes
class=class="str">"cmt">//--- enums
class=class="str">"cmt">//--- defines
class="macro">#define PERIODS_TOTAL(class="num">21)          class=class="str">"cmt">// Total amount of available chart periods
class="macro">#define SYMBOLS_TOTAL(class="num">4)           class=class="str">"cmt">// Maximum number of drawn symbol buffers
class=class="str">"cmt">//--- structures
class=class="str">"cmt">//--- structures
class="kw">struct SDataBuffer               class=class="str">"cmt">// Candlesticks buffer structure
  {
class="kw">private:
   ENUM_TIMEFRAMES   m_buff_timeframe;   class=class="str">"cmt">// Buffer timeframe
   class="type">class="kw">string            m_buff_symbol;      class=class="str">"cmt">// Buffer symbol
   class="type">int               m_buff_open_index;  class=class="str">"cmt">// The index of the indicator buffer related to the Open[] array
   class="type">int               m_buff_high_index;  class=class="str">"cmt">// The index of the indicator buffer related to the High[] array
   class="type">int               m_buff_low_index;   class=class="str">"cmt">// The index of the indicator buffer related to the Low[] array
   class="type">int               m_buff_close_index; class=class="str">"cmt">// The index of the indicator buffer related to the Close[] array
   class="type">int               m_buff_color_index; class=class="str">"cmt">// The index of the class="type">color buffer related to the Color[] array
   class="type">int               m_buff_next_index;  class=class="str">"cmt">// The index of the next free indicator buffer
   class="type">bool              m_used;             class=class="str">"cmt">// The flag of using the buffer in the indicator

给自定义缓冲结构绑定 OHLC 与色彩索引

在 MQL5 里做多品种或多周期指标时,常把 OHLC 和颜色缓冲封装进一个结构里统一管理。下面这段声明把 Open/High/Low/Close 四个数组标为 INDICATOR_DATA,Color 数组标为 INDICATOR_COLOR_INDEX,相当于给绘图缓冲排好了队。 SetIndexes 方法接收一个起始索引 index_first,按顺序把 m_buff_open_index 到 m_buff_next_index 依次 +0 到 +5 排开。也就是说,一组完整缓冲至少占用 5 个连续索引位(0~4 为 OHLC+Color,+5 留给下一个结构),写多实例指标时数错一位就会绘错图。 SetTimeframe 里有个细节:传入 PERIOD_CURRENT 时,会用 ::Period() 取当前图表周期兜底,避免空周期。SetSymbol / SetUsed / SetShowDataFlag 只是把私有成员写进去,控制该缓冲绑定的品种、是否启用、以及图表上是否显示数据。 开 MT5 新建一个指标,把这段结构粘进类定义,故意把 SetIndexes(0) 改成 SetIndexes(1) 看看缓冲错位后的报错,比看文档来得快。外汇与贵金属杠杆高,这类底层封装出错可能让信号整体偏移,验证务必在模拟盘跑。

MQL5 / C++
class="type">bool m_show_data; class=class="str">"cmt">// The flag of displaying the buffer on the chart before enabling/disabling its display
class="kw">public:
  class="type">class="kw">double Open[]; class=class="str">"cmt">// The array assigned as INDICATOR_DATA by the Open indicator buffer
  class="type">class="kw">double High[]; class=class="str">"cmt">// The array assigned as INDICATOR_DATA by the High indicator buffer
  class="type">class="kw">double Low[];  class=class="str">"cmt">// The array assigned as INDICATOR_DATA by the Low indicator buffer
  class="type">class="kw">double Close[]; class=class="str">"cmt">// The array assigned as INDICATOR_DATA by the Close indicator buffer
  class="type">class="kw">double Color[]; class=class="str">"cmt">// The array assigned as INDICATOR_COLOR_INDEX by the Color indicator buffer
class=class="str">"cmt">//--- Set indices for the drawn OHLC and Color buffers
  class="type">void SetIndexes(const class="type">int index_first)
    {
      this.m_buff_open_index=index_first;
      this.m_buff_high_index=index_first+class="num">1;
      this.m_buff_low_index=index_first+class="num">2;
      this.m_buff_close_index=index_first+class="num">3;
      this.m_buff_color_index=index_first+class="num">4;
      this.m_buff_next_index=index_first+class="num">5;
    }
class=class="str">"cmt">//--- Methods of setting and returning values of the class="kw">private structure members
  class="type">void SetTimeframe(const ENUM_TIMEFRAMES timeframe) { this.m_buff_timeframe=(timeframe==PERIOD_CURRENT ? ::Period() : timeframe); }
  class="type">void SetSymbol(const class="type">class="kw">string symbol)                { this.m_buff_symbol=symbol;        }
  class="type">void SetUsed(const class="type">bool flag)                      { this.m_used=flag;                }
  class="type">void SetShowDataFlag(const class="type">bool flag)              { this.m_show_data=flag;           }

「缓冲区句柄的读取接口与日志打印」

在自定义指标的数据结构里,开高低收四个价格缓冲以及颜色、后继缓冲的索引都不是直接暴露的成员变量,而是通过一组 const 成员函数返回。这样做能在编译期锁死外部改写,只在类内部维护 m_buff_open_index 等私有字段。 下面这组接口是实际调指标时要频繁碰到的:IndexOpenBuffer 回传开盘缓冲序号,IndexHighBuffer 回传最高价缓冲序号,IndexLowBuffer 与 IndexCloseBuffer 同理对应最低和收盘;IndexColorBuffer 给着色用,IndexNextBuffer 指向下一段数据。Timeframe 与 Symbol 返回该缓冲绑定的周期和品种,IsUsed 与 GetShowDataFlag 控制是否参与绘制。 Print 方法把上述信息拼成 8 行字符串数组打到终端,首行格式为「Buffer 品种 周期:」。在 MT5 里若发现某指标缓冲没画出来,先调一次 Print 看索引和 Used 标记,比盲目改 OnCalculate 更快定位。外汇与贵金属波动剧烈,这类底层接口误用可能让图形完全失真,验证时请用模拟环境。

MQL5 / C++
class="type">int IndexOpenBuffer(class="type">void) const { class="kw">return this.m_buff_open_index; }
class="type">int IndexHighBuffer(class="type">void) const { class="kw">return this.m_buff_high_index; }
class="type">int IndexLowBuffer(class="type">void) const { class="kw">return this.m_buff_low_index; }
class="type">int IndexCloseBuffer(class="type">void) const { class="kw">return this.m_buff_close_index; }
class="type">int IndexColorBuffer(class="type">void) const { class="kw">return this.m_buff_color_index; }
class="type">int IndexNextBuffer(class="type">void) const { class="kw">return this.m_buff_next_index; }
ENUM_TIMEFRAMES Timeframe(class="type">void) const { class="kw">return this.m_buff_timeframe; }
class="type">class="kw">string Symbol(class="type">void) const { class="kw">return this.m_buff_symbol; }
class="type">bool IsUsed(class="type">void) const { class="kw">return this.m_used; }
class="type">bool GetShowDataFlag(class="type">void) const { class="kw">return this.m_show_data; }
class="type">void Print(class="type">void);
};
class=class="str">"cmt">//--- Display structure data to the journal
class="type">void SDataBuffer::Print(class="type">void)
  {
   class="type">class="kw">string array[class="num">8];
   array[class="num">0]="Buffer "+this.Symbol()+" "+TimeframeDescription(this.Timeframe())+":";
   array[class="num">1]=" Open buffer index: "+(class="type">class="kw">string)this.IndexOpenBuffer();
   array[class="num">2]=" High buffer index: "+(class="type">class="kw">string)this.IndexHighBuffer();

◍ 缓冲区索引在跨品种实例里的实际排布

自定义指标若要同时挂多个品种,每个品种会占用一组独立的绘图缓冲区。下面这段打印逻辑把每个实例的 Open/High/Low/Close/Color/Next 缓冲索引和启用状态吐到日志,方便核对有没有越界或错位。 array[3]=" Low buffer index: "+(string)this.IndexLowBuffer(); array[4]=" Close buffer index: "+(string)this.IndexCloseBuffer(); array[5]=" Color buffer index: "+(string)this.IndexColorBuffer(); array[6]=" Next buffer index: "+(string)this.IndexNextBuffer(); array[7]=" Used: "+(string)(bool)this.IsUsed(); for(int i=0;i<ArraySize(array);i++) ::Print(array[i]); 上面代码逐行拆解:第1~3行把 Low、Close、Color 缓冲区的索引号转成字符串塞进数组;第4行取 Next 缓冲索引;第5行把 IsUsed() 的布尔值强转后记录该实例是否启用;for 循环从 0 跑到数组长度,用全局域 ::Print 把每行打到专家日志。 实跑日志(2020.04.08 21:55 于 MT5)显示:EURUSD H1 占用索引 0~5 且 Used=false;AUDUSD H1 占 5~10 且 Used=true;EURAUD H1 从 10 起排到 13。可见第二个品种并非从 0 重排,而是紧接上一个的尾号,多品种指标写缓冲区时得按这个偏移量来算。 外汇与贵金属杠杆高、滑点随机,这类底层索引错一个就可能画错线或崩脚本;开 MT5 把这段贴进你的指标类 Print 一下,能直接看出当前实例的缓冲分配是否符合预期。

MQL5 / C++
  array[class="num">3]=" Low buffer index: "+(class="type">class="kw">string)this.IndexLowBuffer();
  array[class="num">4]=" Close buffer index: "+(class="type">class="kw">string)this.IndexCloseBuffer();
  array[class="num">5]=" Color buffer index: "+(class="type">class="kw">string)this.IndexColorBuffer();
  array[class="num">6]=" Next buffer index: "+(class="type">class="kw">string)this.IndexNextBuffer();
  array[class="num">7]=" Used: "+(class="type">class="kw">string)(class="type">bool)this.IsUsed();
  for(class="type">int i=class="num">0;i<ArraySize(array);i++)
     ::Print(array[i]);

日志里的缓冲区索引与多品种参数声明

在 MT5 指标初始化日志里,EURGBP H1 这条记录暴露了缓冲区排布的连续规律:Open 占 15、High 16、Low 17、Close 18,Color 跳到 19,Next 指向 20,且 Used 标记 false。这说明该品种虽被分配了独立缓冲槽,但当前并未激活绘制,可能只是预留给后续切换。 对照同一时刻 EURUSD 类记录,Color buffer 在 14、Next 在 15,可见每个品种按 5 个缓冲(O/H/L/C/Color)步进分配,索引全局连续不重叠。若你写多品种面板指标,直接照这个步长算偏移能少踩坑。 下面的输入变量定义了品种与周期的来源方式:InpModeUsedSymbols 设为 SYMBOLS_MODE_DEFINES 表示走预定义宏,而 InpUsedSymbols 字符串里列了 EURUSD,AUDUSD,EURAUD,EURGBP 等 11 个品种(注意 EURUSD 重复出现一次,可能系笔误)。周期则通过 TIMEFRAMES_MODE_LIST 配合 M1 到 MN1 的九段字符串传入。 按钮位移 InpButtShiftX=0、InpButtShiftY=10,声音开关 InpUseSounds=true。开 MT5 把这段 sinput 原样贴进 EA 头部,改 InpUsedSymbols 删掉重复项,编译后看日志缓冲索引是否仍连续,就能验证你的多品种容器有没有写错。

MQL5 / C++
class=class="str">"cmt">/*sinput*/ ENUM_SYMBOLS_MODE   InpModeUsedSymbols=   SYMBOLS_MODE_DEFINES;        class=class="str">"cmt">// Mode of used symbols list
sinput   class="type">class="kw">string                InpUsedSymbols   = "EURUSD,AUDUSD,EURAUD,EURGBP,EURCAD,EURJPY,EURUSD,GBPUSD,NZDUSD,USDCAD,USDJPY";   class=class="str">"cmt">// List of used symbols(comma - separator)
sinput   ENUM_TIMEFRAMES_MODE InpModeUsedTFs     =  TIMEFRAMES_MODE_LIST;          class=class="str">"cmt">// Mode of used timeframes list
sinput   class="type">class="kw">string                InpUsedTFs        =  "M1,M5,M15,M30,H1,H4,D1,W1,MN1"; class=class="str">"cmt">// List of used timeframes(comma - separator)
sinput   class="type">uint                  InpButtShiftX     =  class="num">0;    class=class="str">"cmt">// Buttons X shift 
sinput   class="type">uint                  InpButtShiftY     =  class="num">10;   class=class="str">"cmt">// Buttons Y shift 
sinput   class="type">bool                  InpUseSounds      =  true; class=class="str">"cmt">// Use sounds

「多品种模式与缓冲区声明的落地写法」

在写跨品种指标时,先得决定引擎扫哪些标的。代码里给了三种枚举值:SYMBOLS_MODE_DEFINES 只处理你手写的那串指定品种;SYMBOLS_MODE_MARKET_WATCH 只认终端『市场报价』窗口里挂着的;SYMBOLS_MODE_ALL 则把平台全部品种都拉进来算。实盘跑外汇或贵金属前,建议先用 MARKET_WATCH 缩小范围,全量模式在品种多的券商端可能拖慢 MT5 的刷新。 下方全局变量块定义了核心载体:Buffers[] 是绑定到时间序列的指标缓冲结构数组,BufferTime[] 专门承接 time[] 数组的传递数据;engine 是 CEngine 主对象,prefix 管图形对象命名前缀,min_bars 设了计算所需最少 BAR 数,used_symbols_mode 记录当前扫标模式,array_used_symbols[] 与 array_used_periods[] 分别把要用的品种、周期塞给库。 SetPlotBufferState() 这个函数控制某个缓冲画不画。入参 buffer_index 定位缓冲,state 为 true 时 PlotIndexSetInteger 打开 PLOT_SHOW_DATA,并在数据窗口显示由『品种+周期 Open/High/Low/Close』拼的描述;为 false 时标签置 NULL 不显示。激活状态下还会用 IndicatorSetString 把当前品种周期写进指标短名,方便在副图上一眼区分。 开 MT5 把这段全局声明原样贴进 ea 或指标头,把 used_symbols_mode 改成 1(MARKET_WATCH),挂一个 XAUUSD 到报价窗,编译后看副图短名是否随缓冲切换而变,就能验证管道通没通。外汇与贵金属杠杆高,多品种并发计算可能引发点差扩大时的滑点风险,参数需先在模拟盘试。

MQL5 / C++
  SYMBOLS_MODE_DEFINES,                                                          class=class="str">"cmt">// Work with the specified symbol list 
  SYMBOLS_MODE_MARKET_WATCH,                                                      class=class="str">"cmt">// Work with the Market Watch window symbols 
  SYMBOLS_MODE_ALL                                                                class=class="str">"cmt">// Work with the full symbol list 
 };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- indicator buffers
SDataBuffer   Buffers[];                                                          class=class="str">"cmt">// Array of the indicator buffer data structures assigned to the timeseries
class="type">class="kw">double        BufferTime[];                                                       class=class="str">"cmt">// The calculated buffer for storing and passing data from the time[] array
class=class="str">"cmt">//--- global variables
CEngine       engine;                                                             class=class="str">"cmt">// CEngine library main object
class="type">class="kw">string        prefix;                                                             class=class="str">"cmt">// Prefix of graphical object names
class="type">int           min_bars;                                                           class=class="str">"cmt">// The minimum number of bars for the indicator calculation
class="type">int           used_symbols_mode;                                                  class=class="str">"cmt">// Mode of working with symbols
class="type">class="kw">string        array_used_symbols[];                                               class=class="str">"cmt">// The array for passing used symbols to the library
class="type">class="kw">string        array_used_periods[];                                               class=class="str">"cmt">// The array for passing used timeframes to the library
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the state for drawn buffers                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void SetPlotBufferState(const class="type">int buffer_index,const class="type">bool state)
  {
class=class="str">"cmt">//--- Depending on a passed status, define whether data should be displayed in the data window(state==true) or not(state==false)
   PlotIndexSetInteger(buffer_index,PLOT_SHOW_DATA,state);
class=class="str">"cmt">//--- Create the buffer description consisting of a symbol and timeframe and set the buffer description by its buffer_index index
   class="type">class="kw">string params=Buffers[buffer_index].Symbol()+" "+TimeframeDescription(Buffers[buffer_index].Timeframe());
   class="type">class="kw">string label=params+" Open;"+params+" High;"+params+" Low;"+params+" Close";
   PlotIndexSetString(buffer_index,PLOT_LABEL,(state ? label : NULL));
class=class="str">"cmt">//--- If the buffer is active(drawn), set a class="type">short name for the indicator with the displayed symbol and timeframe
   if(state)
      IndicatorSetString(INDICATOR_SHORTNAME,engine.Name()+" "+Buffers[buffer_index].Symbol()+" "+TimeframeDescription(Buffers[buffer_index].Timeframe()));
  }

◍ 多品种面板的底层查表与按钮状态函数

做多品种盯盘面板时,最容易被忽略的是「符号→缓冲区」的映射逻辑。下面这组函数解决的就是:给定品种名,快速判断它是否在监控列表、对应哪条绘制缓冲、以及哪里还能塞下新缓冲。 IsUsedSymbolByInput 遍历 array_used_symbols 数组,命中即返回 true,否则 false,时间复杂度 O(n),品种数在两位数内时开销可忽略。IndexBuffer 则在 Buffers 结构数组里按 Symbol() 匹配,找不到返回 WRONG_VALUE,调用方必须判错,否则后续画对象会越界。 FirstFreePlotBufferIndex 扫一遍所有缓冲的 IndexNextBuffer(),取最大值作为新缓冲下标。这里返回的是「下一个可用序号」而非「第一个空槽」,所以删除中间品种后不会回填,属于有意为之。 ButtonState / SetButtonState 直接读写 OBJPROP_STATE。按下时背景染成 C'220,255,240'(浅绿),弹起回到 C'240,240,240'(灰白);非回测环境下还会写终端全局变量,方便跨 EA 同步开关状态。外汇与贵金属波动剧烈,这类界面状态若依赖全局变量,需警惕多图表实例互相覆盖的高风险。 开 MT5 把这几段塞进你的指标源码,改掉 array_used_symbols 的初始化长度,就能验证面板是否按预期增删品种。

MQL5 / C++
class="type">bool IsUsedSymbolByInput(const class="type">class="kw">string symbol)
  {
   class="type">int total=ArraySize(array_used_symbols);
   for(class="type">int i=class="num">0;i<total;i++)
      if(array_used_symbols[i]==symbol)
         class="kw">return true;
   class="kw">return false;
  }
class="type">int IndexBuffer(const class="type">class="kw">string symbol)
  {
   class="type">int total=ArraySize(Buffers);
   for(class="type">int i=class="num">0;i<total;i++)
      if(Buffers[i].Symbol()==symbol)
         class="kw">return i;
   class="kw">return WRONG_VALUE;
  }
class="type">int FirstFreePlotBufferIndex(class="type">void)
  {
   class="type">int num=WRONG_VALUE,total=ArraySize(Buffers);
   for(class="type">int i=class="num">0;i<total;i++)
      if(Buffers[i].IndexNextBuffer()>num)
         num=Buffers[i].IndexNextBuffer();
   class="kw">return num;
  }
class="type">bool ButtonState(const class="type">class="kw">string name)
  {
   class="kw">return (class="type">bool)ObjectGetInteger(class="num">0,name,OBJPROP_STATE);
  }
class="type">void SetButtonState(const class="type">class="kw">string button_name,const class="type">bool state)
  {
class=class="str">"cmt">//--- Set the button status and its class="type">color depending on the status
   ObjectSetInteger(class="num">0,button_name,OBJPROP_STATE,state);
   if(state)
      ObjectSetInteger(class="num">0,button_name,OBJPROP_BGCOLOR,C&class="macro">#x27;class="num">220,class="num">255,class="num">240&class="macro">#x27;);
   else
      ObjectSetInteger(class="num">0,button_name,OBJPROP_BGCOLOR,C&class="macro">#x27;class="num">240,class="num">240,class="num">240&class="macro">#x27;);
class=class="str">"cmt">//--- If not in the tester, 
class=class="str">"cmt">//--- set the status to the terminal global variable
   if(!engine.IsTester())

品种按钮与时间轴按钮的全局状态联动

面板里点一个品种按钮,不只是改它自己的高亮,还得把对应的一排周期按钮一起显隐。代码用 GlobalVariableSet 把状态写进终端全局变量,键名拼法是 ChartID()+“_”+按钮名,这样多图表互不串味。 SetButtonSymbolState 里有个坑:名字含 PERIOD_CURRENT 的周期按钮不写全局变量,只走 SetButtonPeriodVisible 控制可见性。意思是当前周期按钮的状态由品种按钮托管,避免重复记录。 SetButtonPeriodVisible 按 array_used_periods 数组长度循环,给每个周期按钮拼名后设 OBJPROP_TIMEFRAMES。回测环境(engine.IsTester())一律 OBJ_ALL_PERIODS,实盘则看品种按钮 state_symbol:true 全显,false 用 OBJ_NO_PERIODS 全藏。 ResetButtonSymbolState 从 ObjectsTotal(0,0)-1 倒序扫图表对象,准备把其他品种按钮复位。倒序是为了删除或改名时索引不漂移,这是 MT5 对象遍历的常识坑。

MQL5 / C++
class="type">void SetButtonSymbolState(const class="type">class="kw">string button_symbol_name,const class="type">bool state)
  {
class=class="str">"cmt">//--- Set the symbol button status
   SetButtonState(button_symbol_name,state);
class=class="str">"cmt">//--- Detect wrong names if the timeframe button status is not specified
class=class="str">"cmt">//--- Write the button status to the global variable only if its name contains no "PERIOD_CURRENT" substring
   if(StringFind(button_symbol_name,"PERIOD_CURRENT")==WRONG_VALUE)
      GlobalVariableSet((class="type">class="kw">string)ChartID()+"_"+button_symbol_name,state);
class=class="str">"cmt">//--- Set the visibility for all period buttons corresponding to the symbol button
   SetButtonPeriodVisible(button_symbol_name,state);
  }
class="type">void SetButtonPeriodState(const class="type">class="kw">string button_period_name,const class="type">bool state)
  {
class=class="str">"cmt">//--- Set the button status and write it to the terminal global variable
   SetButtonState(button_period_name,state);
   GlobalVariableSet((class="type">class="kw">string)ChartID()+"_"+button_period_name,state);
  }
class="type">void SetButtonPeriodVisible(const class="type">class="kw">string button_symbol_name,const class="type">bool state_symbol)
  {
class=class="str">"cmt">//--- In the loop by the amount of used timeframes
   class="type">int total=ArraySize(array_used_periods);
   for(class="type">int j=class="num">0;j<total;j++)
     {
      class=class="str">"cmt">//--- create the name of the next period button
      class="type">class="kw">string butt_name_period=button_symbol_name+"_"+EnumToString(ArrayUsedTimeframes[j]);
      class=class="str">"cmt">//--- Set the status and visibility for the period button depending on the symbol button status
      ObjectSetInteger(class="num">0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : (state_symbol ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS)));
     }  
  }
class="type">void ResetButtonSymbolState(const class="type">class="kw">string button_symbol_name)
  {
class=class="str">"cmt">//--- In the loop by all chart objects,
   for(class="type">int i=ObjectsTotal(class="num">0,class="num">0)-class="num">1;i>WRONG_VALUE;i--)
     {
      class=class="str">"cmt">//--- get the name of the next object
      class="type">class="kw">string name=ObjectName(class="num">0,i,class="num">0);

「按钮状态复位与周期识别的底层循环」

在自定义指标面板里,符号按钮和周期按钮往往是成组出现的图形对象。为了避免切换时旧状态残留,需要用两个独立的遍历函数分别清空符号按钮和周期按钮的按下态。 ResetButtonSymbolState 的循环里,凡是遇到当前已按下的符号按钮、不属于本指标前缀的对象、或者名字里带 '_PERIOD_' 的周期按钮,都直接 continue 跳过;剩下的才调用 SetButtonSymbolState(name,false) 复位。注意 StringFind(name,prefix)==WRONG_VALUE 意味着对象名中找不到指标前缀,这类垃圾对象不能碰。 周期按钮的清理由 ResetButtonPeriodState 负责,它多了一层过滤:必须同时不包含 '_PERIOD_' 或不属于传入 symbol 的按钮才会跳过。也就是说,只有『本指标 + 周期键 + 指定符号』三者都满足的对象才会被 ResetButtonPeriodState(name,false) 处理。 GetNamePressedTimeframe 则是反向操作:在同样的对象遍历中,找到符合条件且 ButtonState(name) 返回 true 的周期按钮,立即 return 其对象名;若全程没找到,返回 NULL。这三个函数都基于 ObjectsTotal(0,0) 倒序遍历,i 从总数减 1 走到大于 WRONG_VALUE(即 -1)为止,能覆盖图表上全部第一层对象。 别把对象遍历当免费午餐 图表对象超过几百个时,每次事件都跑全套 ObjectsTotal 倒序扫描,CPU 占用会随对象数线性上涨。实盘前在 MT5 用 ObjectsTotal 打个点,看看你的面板在加载 50 个符号 × 9 个周期 = 450 个按钮时的耗时倾向。

MQL5 / C++
class=class="str">"cmt">//--- If this is a pressed button, or the object does not belong to the indicator, or this is a period button, move on to the next one
   if(name==button_symbol_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")>class="num">0)
      class="kw">continue;
   class=class="str">"cmt">//--- Reset the symbol button status by object name
   SetButtonSymbolState(name,false);
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Reset the states of the remaining symbol period buttons          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ResetButtonPeriodState(const class="type">class="kw">string button_period_name,const class="type">class="kw">string symbol)
  {
class=class="str">"cmt">//--- In the loop by all chart objects,
   for(class="type">int i=ObjectsTotal(class="num">0,class="num">0)-class="num">1;i>WRONG_VALUE;i--)
     {
      class=class="str">"cmt">//--- get the name of the next object
      class="type">class="kw">string name=ObjectName(class="num">0,i,class="num">0);
      class=class="str">"cmt">//--- If this is a pressed button, or the object does not belong to the indicator, or this is not a period button, or the button does not belong to the symbol, move on to the next one
      if(name==button_period_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")==WRONG_VALUE || StringFind(name,symbol)==WRONG_VALUE)
         class="kw">continue;
      class=class="str">"cmt">//--- Reset the period button status by object name
      SetButtonPeriodState(name,false);
     }
  }
class=class="str">"cmt">//+---------------------------------------------------------------------------+
class=class="str">"cmt">//| Return the name of the pressed period button corresponding to the symbol   |
class=class="str">"cmt">//+---------------------------------------------------------------------------+
class="type">class="kw">string GetNamePressedTimeframe(const class="type">class="kw">string button_symbol_name,const class="type">class="kw">string symbol)
  {
class=class="str">"cmt">//--- In the loop by all chart objects,
   for(class="type">int i=ObjectsTotal(class="num">0,class="num">0)-class="num">1;i>WRONG_VALUE;i--)
     {
      class=class="str">"cmt">//--- get the name of the next object
      class="type">class="kw">string name=ObjectName(class="num">0,i,class="num">0);
      class=class="str">"cmt">//--- If this is a pressed button, or the object does not belong to the indicator, or this is not a period button, or the button does not belong to the symbol, move on to the next one
      if(name==button_symbol_name || StringFind(name,prefix)==WRONG_VALUE || StringFind(name,"_PERIOD_")==WRONG_VALUE || StringFind(name,symbol)==WRONG_VALUE)
         class="kw">continue;
      class=class="str">"cmt">//--- If the button is pressed, class="kw">return the name of the pressed button graphic object
      if(ButtonState(name))
         class="kw">return name;
     }
class=class="str">"cmt">//--- Return NULL if no symbol period buttons are pressed
   class="kw">return NULL;
  }

◍ 按钮事件怎么驱动多周期缓冲切换

在多符号多周期面板里,用户点一下按钮就要决定哪根缓冲线被绘制、其余隐藏。核心不是重画图表,而是改写每个 buffer 结构里的 used 标志,再顺手把按钮配色和全局变量同步掉。 下面这段函数负责批量复位:传入当前生效的 symbol,循环 ArraySize(Buffers) 把所有缓冲的 used 置 false,仅命中 IndexBuffer(symbol) 的那一个置 true,同时把 show_data 标志强清。这样同屏永远只有一条数据线在跑,避免 MT5 客户端把无关周期一起刷出来拖慢帧率。 [CODE]void SetAllBuffersState(const string symbol) { int total=ArraySize(Buffers); //--- 取指定品种的缓冲索引 int index=IndexBuffer(symbol); //--- 按已绘制缓冲数量循环 for(int i=0;i<total;i++) { //--- 循环索引等于指定缓冲索引时,used 置 true,否则 false //--- 同时强制把“按钮已处理”标志写掉 Buffers[i].SetUsed(i!=index ? false : true); Buffers[i].SetShowDataFlag(false); } }[/CODE] PressButtonEvents 则是按钮消息入口:从 button_name 里用 StringSubstr 剥掉前缀拿到 ID,再用 StringFind("_PERIOD_") 切出 symbol 与周期串。非回测环境下用 GlobalVariableSet 把 1/0 状态写进终端全局变量,方便跨 EA 实例读取。 点的是品种按钮还是周期按钮靠 WRONG_VALUE 分支区分,各自调 SetButtonSymbolState / SetButtonPeriodState 改颜色与结构。随后用 TimeframeByDescription 把按钮文本还原成 ENUM_TIMEFRAMES,调 SetAllBuffersState(symbol) 清场,再给命中缓冲 SetTimeframe(timeframe)——此时图表才真正切到用户选的那根线。外汇与贵金属波动剧烈,这类面板只解决显示逻辑,信号本身不代表方向,实盘前务必在 MT5 策略测试器跑一遍确认无句柄泄漏。

MQL5 / C++
class="type">void SetAllBuffersState(const class="type">class="kw">string symbol)
  {
   class="type">int total=ArraySize(Buffers);
class=class="str">"cmt">//--- Get the specified buffer index
   class="type">int index=IndexBuffer(symbol);
class=class="str">"cmt">//--- In a loop by the number of drawn buffers
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class=class="str">"cmt">//--- if the loop index is equal to the specified buffer index,
      class=class="str">"cmt">//--- set the flag of its usage to &class="macro">#x27;true&class="macro">#x27;, otherwise - to &class="macro">#x27;false&class="macro">#x27;
      class=class="str">"cmt">//--- forcibly set the flag indicating that pressing the button for the buffer has already been handled
      Buffers[i].SetUsed(i!=index ? false : true);
      Buffers[i].SetShowDataFlag(false);
     }
  }

按钮回调里怎么灌数据和重绘

在指标面板的按钮事件回调中,先调用 InitBuffersAll() 初始化所有缓冲区,再根据 state 判断按钮是否被按下。若 state 为真,就对当前 buffer_index 执行 BufferFill() 回填历史数据,并用 SetShowDataFlag(state) 标记该缓冲区已处理,避免重复触发。 回调后半段留了扩展位:按下的分支里用 button=="BUTT_M1" / "BUTT_M2" 这类字符串判断具体是哪个周期按钮,未按下时同理走 else 分支。这里原逻辑留空,你可以塞入切换 timeframe 或联动小布 AIGC 标注的指令。 无论分支如何,结尾都调 ChartRedraw() 强制重绘图表,否则 MT5 上按钮状态变了但画面不刷新。外汇与贵金属波动剧烈、杠杆高,面板逻辑出错可能误显信号,实盘前务必在策略测试器跑一遍。 CreateButtons() 给出了面板布局的实参数:按钮单元宽 ws=48、高 hs=18,内文区 w=26、h=16,垂直间距 shift_h=2;循环里 ys=y+(hs+shift_h)*i 决定每个符号按钮的 y 坐标。测试环境下 i==0 的品种默认 state_symbol=true,主图会直接显首符号数据。

MQL5 / C++
   class=class="str">"cmt">//--- Initialize all indicator buffers
   InitBuffersAll();
   class=class="str">"cmt">//--- If the buffer is active, fill it with historical data
   if(state)
      BufferFill(buffer_index);
   class=class="str">"cmt">//--- Set the flag indicating that pressing the button has already been handled
   Buffers[buffer_index].SetShowDataFlag(state);
   }
class=class="str">"cmt">//--- Here you can add additional handling of button pressing:
class=class="str">"cmt">//--- If the button is pressed
   if(state)
   {
   class=class="str">"cmt">//--- If M1 button is pressed
   if(button=="BUTT_M1")
     {
     
     }
   class=class="str">"cmt">//--- If button M2 is pressed
   else if(button=="BUTT_M2")
     {
     
     }
   class=class="str">"cmt">//--- Remaining buttons ...
   }
class=class="str">"cmt">//--- Not pressed
else
   {
   class=class="str">"cmt">//--- M1 button
   if(button=="BUTT_M1")
     {
     
     }
   class=class="str">"cmt">//--- M2 button
   if(button=="BUTT_M2")
     {
     
     }
   }
class=class="str">"cmt">//--- re-draw the chart
   ChartRedraw();
   }

class="type">bool CreateButtons(const class="type">int shift_x=class="num">20,const class="type">int shift_y=class="num">0)
  {
  class="type">int total_symbols=ArraySize(array_used_symbols);
  class="type">int total_periods=ArraySize(ArrayUsedTimeframes);
  class="type">uint ws=class="num">48,hs=class="num">18,w=class="num">26,h=class="num">16,shift_h=class="num">2,x=InpButtShiftX+class="num">1, y=InpButtShiftY+h+class="num">1;
  for(class="type">int i=class="num">0;i<SYMBOLS_TOTAL;i++)
   {
   class="type">class="kw">string butt_name_symbol=prefix+"BUTT_"+array_used_symbols[i];
   class="type">uint ys=y+(hs+shift_h)*i;
   if(ButtonCreate(butt_name_symbol,x,ys,ws,hs,array_used_symbols[i],clrGray))
     {
     class="type">bool state_symbol=(engine.IsTester() && i==class="num">0 ? true : false);
     class=class="str">"cmt">//--- If not in the tester,

「用全局变量记住按钮开关状态」

在实盘图表里,按钮的选中状态不能随脚本重跑就丢失。这段逻辑只在非回测环境(!engine.IsTester())下,把每个符号按钮的状态写进终端全局变量,键名由 ChartID() 拼接按钮名构成,例如 '123456789_EURUSD_sym'。 若全局变量不存在就先 GlobalVariableSet 成 false,之后每次用 GlobalVariableGet 读回 state_symbol,再交给 SetButtonState 还原界面。这样关掉重开 MT5,按钮灰亮状态大概率还在。 周期按钮在 for 循环里按 j 索引排开,横向偏移算成 x+ws+2+(w+1)*j;创建失败会弹 Alert 并 return false,实盘里若某周期按钮没出来,先查这个报错而非盲目调坐标。 回测时(engine.IsTester())周期按钮状态被强制设为当前 Period() 才 true,其余 false,且 OBJPROP_TIMEFRAMES 设成 OBJ_ALL_PERIODS,和实盘用 OBJ_NO_PERIODS 不同——外汇贵金属波动剧烈,这种环境差异可能导致你肉眼验证和回测表现不一致。

MQL5 / C++
if(!engine.IsTester())
  {
  class=class="str">"cmt">//--- set the name of the terminal global variable for storing the symbol button status
  class="type">class="kw">string name_gv_symbol=(class="type">class="kw">string)ChartID()+"_"+butt_name_symbol;
  class=class="str">"cmt">//--- if there is no global variable with the symbol name, create it set to &class="macro">#x27;false&class="macro">#x27;,
  if(!GlobalVariableCheck(name_gv_symbol))
     GlobalVariableSet(name_gv_symbol,false);
  class=class="str">"cmt">//--- get the symbol button status from the terminal global variable
  state_symbol=GlobalVariableGet(name_gv_symbol);
  }
class=class="str">"cmt">//--- Set the status for the symbol button
SetButtonState(butt_name_symbol,state_symbol);

class=class="str">"cmt">//--- In the loop by the amount of used timeframes
for(class="type">int j=class="num">0;j<total_periods;j++)
  {
  class=class="str">"cmt">//--- create the name of the next period button
  class="type">class="kw">string butt_name_period=butt_name_symbol+"_"+EnumToString(ArrayUsedTimeframes[j]);
  class="type">uint yp=ys-(hs-h)/class="num">2;
  class=class="str">"cmt">//--- create the next period button with a shift calculated as
  class=class="str">"cmt">//--- (symbol button width + (period button width + class="num">1) * loop index)
  if(ButtonCreate(butt_name_period,x+ws+class="num">2+(w+class="num">1)*j,yp,w,h,TimeframeDescription(ArrayUsedTimeframes[j]),clrGray))
     ObjectSetInteger(class="num">0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS));
  else
    {
    Alert(TextByLanguage("Не удалось создать кнопку "","Could not create button "),butt_name_period,"");
    class="kw">return false;
    }
  class="type">bool state_period=(engine.IsTester() && ArrayUsedTimeframes[j]==Period() ? true : false);
  class=class="str">"cmt">//--- If not in the tester,
  if(!engine.IsTester())
    {
    class=class="str">"cmt">//--- set the name of the terminal global variable for storing the period button status
    class="type">class="kw">string name_gv_period=(class="type">class="kw">string)ChartID()+"_"+butt_name_period;
    class=class="str">"cmt">//--- if there is no global variable with the period name, create it set to &class="macro">#x27;false&class="macro">#x27;,
    if(!GlobalVariableCheck(name_gv_period))

◍ 用全局变量接管周期按钮的显隐

面板里的周期按钮不能写死,得跟着品种按钮的状态走。这段逻辑先把终端全局变量 name_gv_period 置为 false,再从全局变量读回 state_period,相当于用终端级变量在图表对象之间传状态,避免局部变量跨函数丢失。 读回状态后调用 SetButtonState 同步按钮外观,再用 ObjectSetInteger 改 OBJPROP_TIMEFRAMES:回测环境(engine.IsTester())或品种按钮开启时给 OBJ_ALL_PERIODS,否则 OBJ_NO_PERIODS。这样在 MT5 测试器里按钮永远全周期可见,实盘则受品种开关约束。 缓冲区初始化另有一套:InitBuffer 对传入索引做 WRONG_VALUE 拦截,随后把 Open/High/Low/Close 四个数组全用 EMPTY_VALUE 填一遍,Color 数组填 0,再靠 SetPlotBufferState 按 IsUsed() 决定数据窗口是否显示该缓冲。InitBuffersAll 则循环 ArraySize(Buffers) 次把全部缓冲初始化掉。 开 MT5 把这段粘进 EA,切到回测模式看周期按钮是否强制全显;再关掉品种按钮,确认 OBJ_NO_PERIODS 生效、按钮从图表消失。外汇与贵金属杠杆高,面板逻辑仅影响交互,不改变订单风险。

MQL5 / C++
GlobalVariableSet(name_gv_period,false);
class=class="str">"cmt">//--- get the period button status from the terminal global variable
state_period=GlobalVariableGet(name_gv_period);
}
class=class="str">"cmt">//--- Set the status and visibility for the period button depending on the symbol button status
SetButtonState(butt_name_period,state_period);
ObjectSetInteger(class="num">0,butt_name_period,OBJPROP_TIMEFRAMES,(engine.IsTester() ? OBJ_ALL_PERIODS : (state_symbol ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS)));
 }  
   }
else
   {
   Alert(TextByLanguage("Не удалось создать кнопку "","Could not create button "),butt_name_symbol,"");
   class="kw">return false;
   }
  }
 ChartRedraw(class="num">0);
 class="kw">return true;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Initialize the timeseries and the appropriate buffers by index    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool InitBuffer(const class="type">int buffer_index)
  {
class=class="str">"cmt">//--- Leave if the wrong index is passed
  if(buffer_index==WRONG_VALUE)
    class="kw">return false;
class=class="str">"cmt">//--- Initialize drawn OHLC buffers using the "empty" value, while Color is initialized using zero
  ArrayInitialize(Buffers[buffer_index].Open,EMPTY_VALUE);
  ArrayInitialize(Buffers[buffer_index].High,EMPTY_VALUE);
  ArrayInitialize(Buffers[buffer_index].Low,EMPTY_VALUE);
  ArrayInitialize(Buffers[buffer_index].Close,EMPTY_VALUE);
  ArrayInitialize(Buffers[buffer_index].Color,class="num">0);
class=class="str">"cmt">//--- Set the flag of the buffer display in the data window by index
  SetPlotBufferState(buffer_index,Buffers[buffer_index].IsUsed());
  class="kw">return true;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Initialize all timeseries and the appropriate buffers            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void InitBuffersAll(class="type">void)
  {
class=class="str">"cmt">//--- Initialize the next buffer in the loop by the total number of chart periods
  class="type">int total=ArraySize(Buffers);
  for(class="type">int i=class="num">0;i<total;i++)
    InitBuffer(i);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculating a single bar of all active buffers                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CalculateSeries(const class="type">int index,const class="type">class="kw">datetime time)
  {
class=class="str">"cmt">//--- In the loop by the total number of buffers, get the next buffer
  class="type">int total=ArraySize(Buffers);
  for(class="type">int i=class="num">0;i<total;i++)
   {

跨周期 K 线如何回填到当前图表缓冲区

多周期面板里,每个符号按钮对应一条缓冲区。若某缓冲区未被占用(按钮弹起),直接写 NULL 并跳到下一个,不浪费计算资源。 取缓冲区对应的时间序列对象要用按钮上的品种与周期:CSeriesDE *series=engine.SeriesGetSeries(Buffers[i].Symbol(),(ENUM_TIMEFRAMES)Buffers[i].Timeframe()); 若 series 为 NULL,或传入的 index 超过该序列总 bar 数减 1(Total()-1),同样 continue 跳过。 拿到序列后,用 engine.SeriesGetBarSeriesFirstFromSeriesSecond(NULL,PERIOD_CURRENT,time,Symbol,Timeframe) 找出当前图表时间对齐的那根 bar。若返回 NULL 也跳过,避免越界写脏数据。 SetBufferData 内部先算 n=iBarShift(NULL,PERIOD_CURRENT,bar.Time()),即目标 bar 在当前图表上的索引。当传入 index<n 时,从 n 循环到 0 把开高低收与实体颜色(0涨/1跌/2十字)逐一写入;index>=n 则只写传入位置。外汇与贵金属跨周期渲染波动大,MT5 上验证时建议先开 EURUSD 的 M1 与 H1 对照看回填是否对齐。

MQL5 / C++
  class=class="str">"cmt">//--- if the buffer is not used(the symbol button is released), move on to the next one
  if(!Buffers[i].IsUsed())
    {
     SetBufferData(i,index,NULL);
     class="kw">continue;
    }
  class=class="str">"cmt">//--- get the timeseries object by the buffer timeframe
  CSeriesDE *series=engine.SeriesGetSeries(Buffers[i].Symbol(),(ENUM_TIMEFRAMES)Buffers[i].Timeframe());  class=class="str">"cmt">// Here we should use the timeframe from the pressed button next to the pressed symbol button
  class=class="str">"cmt">//--- if the timeseries is not received
  class=class="str">"cmt">//--- or the bar index passed to the function is beyond the total number of bars in the timeseries, move on to the next buffer
  if(series==NULL || index>series.GetList().Total()-class="num">1)
     class="kw">continue;
  class=class="str">"cmt">//--- get the bar object from the timeseries corresponding to the one passed to the bar time function on the current chart
  CBar *bar=engine.SeriesGetBarSeriesFirstFromSeriesSecond(NULL,PERIOD_CURRENT,time,Buffers[i].Symbol(),Buffers[i].Timeframe());
  if(bar==NULL)
     class="kw">continue;
  class=class="str">"cmt">//--- get the specified class="kw">property from the obtained bar and
  class=class="str">"cmt">//--- call the function of writing the value to the buffer by i index
  SetBufferData(i,index,bar);
   }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Write data on a single bar to the specified buffer                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void SetBufferData(const class="type">int buffer_index,const class="type">int index,const CBar *bar)
  {
class=class="str">"cmt">//--- Get the bar index by its time falling within the time limits on the current chart
   class="type">int n=(bar!=NULL ? iBarShift(NULL,PERIOD_CURRENT,bar.Time()) : index);
class=class="str">"cmt">//--- If the passed index on the current chart(index) is less than the calculated time of bar start on another timeframe
   if(index<n)
    class=class="str">"cmt">//--- in the loop from the n bar on the current chart to zero
    while(n>WRONG_VALUE && !IsStopped())
      {
       class=class="str">"cmt">//--- fill in the buffer by the n index with the passed values of the bar passed to the function(class="num">0 - EMPTY_VALUE)
       class=class="str">"cmt">//--- and decrease the n value
       Buffers[buffer_index].Open[n]=(bar.Open()>class="num">0 ? bar.Open() : EMPTY_VALUE);
       Buffers[buffer_index].High[n]=(bar.High()>class="num">0 ? bar.High() : EMPTY_VALUE);
       Buffers[buffer_index].Low[n]=(bar.Low()>class="num">0 ? bar.Low() : EMPTY_VALUE);
       Buffers[buffer_index].Close[n]=(bar.Close()>class="num">0 ? bar.Close() : EMPTY_VALUE);
       Buffers[buffer_index].Color[n]=(bar.TypeBody()==BAR_BODY_TYPE_BULLISH ? class="num">0 : bar.TypeBody()==BAR_BODY_TYPE_BEARISH ? class="num">1 : class="num">2);
       n--;
      }
class=class="str">"cmt">//--- If the passed index on the current chart(index) is not less than the calculated time of bar start on another timeframe
class=class="str">"cmt">//--- Set &class="macro">#x27;value&class="macro">#x27; for the buffer by the &class="macro">#x27;index&class="macro">#x27; passed to the function(class="num">0 - EMPTY_VALUE)
   else
    {

「把 K 线对象灌进指标缓冲区的两种分支」

在自定义指标里处理单根 K 线时,常见写法是先判断传入的 bar 对象是否为空。若不为空,就把这根 bar 的 OHLC 与实体类型写进对应缓冲位;若为空,则整组缓冲位统一填 EMPTY_VALUE,避免图表上画出残缺图形。 下面这段逻辑里,Open/High/Low/Close 都用了大于 0 才取值的三元判断——因为某些非常规品种或合成数据可能返回 0,直接写 0 会让 MT5 误以为有有效报价。Color 字段按实体方向给 0/1/2:涨柱 0、跌柱 1、其他(十字星等)2。 [CODE] //--- 若传入了 bar 对象,用它的数据填充指标缓冲区 if(bar!=NULL) { Buffers[buffer_index].Open[index]=(bar.Open()>0 ? bar.Open() : EMPTY_VALUE); Buffers[buffer_index].High[index]=(bar.High()>0 ? bar.High() : EMPTY_VALUE); Buffers[buffer_index].Low[index]=(bar.Low()>0 ? bar.Low() : EMPTY_VALUE); Buffers[buffer_index].Close[index]=(bar.Close()>0 ? bar.Close() : EMPTY_VALUE); Buffers[buffer_index].Color[index]=(bar.TypeBody()==BAR_BODY_TYPE_BULLISH ? 0 : bar.TypeBody()==BAR_BODY_TYPE_BEARISH ? 1 : 2); } //--- 若传入的是 NULL 而不是 bar 对象,用空值填充指标缓冲区 else { Buffers[buffer_index].Open[index]=Buffers[buffer_index].High[index]=Buffers[buffer_index].Low[index]=Buffers[buffer_index].Close[index]=EMPTY_VALUE; Buffers[buffer_index].Color[index]=2; } [/CODE] 初始化函数里还有一处容易忽略:用 ArrayMaximum 找出最大可用周期索引后,算出的 num_bars 若大于 2 才采用,否则强制 min_bars=2。这意味着即便你切到 M1 去看日线级汇总,指标也至少保留 2 根聚合柱,防止数组越界。 OnInit 中调用 engine.Pause(600) 后播 SND_NEWS,只是库自带的初始化反馈音,实盘里若不需要可注释掉以省 0.6 秒加载。外汇与贵金属波动剧烈,这类多品种缓冲指标在跳空时可能短暂显示 EMPTY_VALUE,属正常现象,不代表逻辑错误。

MQL5 / C++
if(bar!=NULL)
  {
   Buffers[buffer_index].Open[index]=(bar.Open()>class="num">0 ? bar.Open() : EMPTY_VALUE);
   Buffers[buffer_index].High[index]=(bar.High()>class="num">0 ? bar.High() : EMPTY_VALUE);
   Buffers[buffer_index].Low[index]=(bar.Low()>class="num">0 ? bar.Low() : EMPTY_VALUE);
   Buffers[buffer_index].Close[index]=(bar.Close()>class="num">0 ? bar.Close() : EMPTY_VALUE);
   Buffers[buffer_index].Color[index]=(bar.TypeBody()==BAR_BODY_TYPE_BULLISH ? class="num">0 : bar.TypeBody()==BAR_BODY_TYPE_BEARISH ? class="num">1 : class="num">2);
  }
else
  {
   Buffers[buffer_index].Open[index]=Buffers[buffer_index].High[index]=Buffers[buffer_index].Low[index]=Buffers[buffer_index].Close[index]=EMPTY_VALUE;
   Buffers[buffer_index].Color[index]=class="num">2;
  }

◍ 多品种缓冲区的按钮状态与时段绑定

在自定义多品种指标里,每个循环索引 i 对应的缓冲区都要先挂上品种标识,再算清它在整体缓冲序列里的起始下标。首段下标用三元式处理:i 为 0 时从自身开始,否则承接上一个缓冲区的 IndexNextBuffer(),这样多品种 OHLC 数组才不会互相踩内存。 实盘与回测的按钮逻辑要分开写。回测环境下强制让第一个品种按钮为激活态(engine.IsTester() && i==0),避免历史测试时图表对象缺失导致取不到状态;实盘则通过 ObjectFind 确认图表上存在该 symbol 按钮后,从终端全局变量里读 state_symbol,变量名拼法为 ChartID()+"_"+按钮名。 时段不是写死的。代码用 GetNamePressedTimeframe 捞被按下的周期按钮名,再靠 StringSubstr 掐头去尾(偏移量 +8 跳过 "_PERIOD_" 这串)交给 TimeframeByDescription 还原成 ENUM_TIMEFRAMES。若按钮名里压根没找到 "_PERIOD_"(返回 WRONG_VALUE),就退而求其次解析 pressed_period 里的周期片段。 最后把解析出的 timeframe 与 state_symbol 写回结构体,并调四次 SetIndexBuffer 把 Open/High/Low/Close 分别绑到对应的 Index*Buffer() 下标上,标 INDICAOR_DATA 类型,指标才能正常画多品种蜡烛。开 MT5 把这段塞进多品种指标初始化循环,改 prefix 变量即可验证按钮名拼接是否撞车。

MQL5 / C++
Buffers[i].SetSymbol(symbol);
class=class="str">"cmt">//--- set the values of all indices for binding the indicator buffers with the structure arrays and
class=class="str">"cmt">//--- specify the next buffer index
class="type">int index_first=(i==class="num">0 ? i : Buffers[i-class="num">1].IndexNextBuffer());
Buffers[i].SetIndexes(index_first);

class=class="str">"cmt">//--- Setting the drawn buffer according to the button status
class=class="str">"cmt">//--- Set the symbol button status. The first button is active in the tester
class="type">bool state_symbol=(engine.IsTester() && i==class="num">0 ? true : false);
class=class="str">"cmt">//--- Set the name of the symbol button corresponding to the buffer with the loop index and its timeframe
class="type">class="kw">string name_butt_symbol=prefix+"BUTT_"+Buffers[i].Symbol();
class="type">class="kw">string name_butt_period=name_butt_symbol+"_PERIOD_"+TimeframeDescription(Buffers[i].Timeframe());
class=class="str">"cmt">//--- If not in the tester, while the chart features the button with the specified name,
if(!engine.IsTester() && ObjectFind(ChartID(),name_butt_symbol)==class="num">0)
  {
   class=class="str">"cmt">//--- set the name of the terminal global variable for storing the button status
   class="type">class="kw">string name_gv_symbol=(class="type">class="kw">string)ChartID()+"_"+name_butt_symbol;
   class="type">class="kw">string name_gv_period=(class="type">class="kw">string)ChartID()+"_"+name_butt_period;
   class=class="str">"cmt">//--- get the symbol button status from the terminal global variable
   state_symbol=GlobalVariableGet(name_gv_symbol);
   }

class=class="str">"cmt">//--- Get the timeframe from pressed symbol timeframe buttons
class="type">class="kw">string pressed_period=GetNamePressedTimeframe(name_butt_symbol,symbol);
class=class="str">"cmt">//--- Convert button name into its class="type">class="kw">string ID
class="type">class="kw">string button=StringSubstr(name_butt_symbol,StringLen(prefix));
ENUM_TIMEFRAMES timeframe=
  (
   StringFind(button,"_PERIOD_")==WRONG_VALUE ?
   TimeframeByDescription(StringSubstr(pressed_period,StringFind(pressed_period,"_PERIOD_")+class="num">8)) :
   TimeframeByDescription(StringSubstr(button,StringFind(button,"_PERIOD_")+class="num">8))
   );

class=class="str">"cmt">//--- Set the values for all structure fields
Buffers[i].SetTimeframe(timeframe);
Buffers[i].SetUsed(state_symbol);
Buffers[i].SetShowDataFlag(state_symbol);

class=class="str">"cmt">//--- Bind drawn indicator buffers by the buffer index equal to the loop index with the structure bar price arrays(OHLC)
SetIndexBuffer(Buffers[i].IndexOpenBuffer(),Buffers[i].Open,INDICATOR_DATA);
SetIndexBuffer(Buffers[i].IndexHighBuffer(),Buffers[i].High,INDICATOR_DATA);
SetIndexBuffer(Buffers[i].IndexLowBuffer(),Buffers[i].Low,INDICATOR_DATA);
SetIndexBuffer(Buffers[i].IndexCloseBuffer(),Buffers[i].Close,INDICATOR_DATA);
class=class="str">"cmt">//--- Bind the class="type">color buffer by the buffer index equal to the loop index with the corresponding structure arrays

多符号蜡烛缓冲的初始化细节

在自定义指标里同时绘制多个交易品种的彩色蜡烛,核心是把每个品种的 OHLC 与颜色缓冲正确绑定到绘图索引。下面这段初始化循环展示了如何给第 i 个品种结构设置颜色索引缓冲,并把开高低收与颜色缓冲的空值统一处理。 SetIndexBuffer(Buffers[i].IndexColorBuffer(),Buffers[i].Color,INDICATOR_COLOR_INDEX); 把颜色数组挂到对应绘图位;随后 5 行 PlotIndexSetDouble 分别给开、高、低、收缓冲设 EMPTY_VALUE,给颜色缓冲设 0 作为空值——这意味着没数据的柱会被引擎跳过不画。 PlotIndexSetInteger(i,PLOT_DRAW_TYPE,DRAW_COLOR_CANDLES) 决定该 Plot 画成彩色蜡烛;SetPlotBufferState(i,state_symbol) 则按按钮状态控制数据窗口是否显示此品种序列。接着 5 行 ArraySetAsSeries(...,true) 把各缓冲索引方向设为与时间序列一致,保证新 K 线在 0 号位置。 循环外还有一处容易漏:用 FirstFreePlotBufferIndex() 拿到下一个空闲索引,把 BufferTime[] 绑成 INDICATOR_CALCULATIONS 型缓冲并同样设时间序列方向,最后 return(INIT_SUCCEEDED)。外汇与贵金属市场波动剧烈、杠杆风险高,这类多品种指标仅作辅助观察,信号失效概率不低。

MQL5 / C++
SetIndexBuffer(Buffers[i].IndexColorBuffer(),Buffers[i].Color,INDICATOR_COLOR_INDEX);
class=class="str">"cmt">//--- set the "empty value" for all structure buffers, 
PlotIndexSetDouble(Buffers[i].IndexOpenBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
PlotIndexSetDouble(Buffers[i].IndexHighBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
PlotIndexSetDouble(Buffers[i].IndexLowBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
PlotIndexSetDouble(Buffers[i].IndexCloseBuffer(),PLOT_EMPTY_VALUE,EMPTY_VALUE);
PlotIndexSetDouble(Buffers[i].IndexColorBuffer(),PLOT_EMPTY_VALUE,class="num">0);
class=class="str">"cmt">//--- set the drawing type
PlotIndexSetInteger(i,PLOT_DRAW_TYPE,DRAW_COLOR_CANDLES);
class=class="str">"cmt">//--- Depending on the button status, set the graphical series name
class=class="str">"cmt">//--- and specify whether the buffer data in the data window is displayed or not
SetPlotBufferState(i,state_symbol);
class=class="str">"cmt">//--- set the direction of indexing of all structure buffers as in the timeseries
ArraySetAsSeries(Buffers[i].Open,true);
ArraySetAsSeries(Buffers[i].High,true);
ArraySetAsSeries(Buffers[i].Low,true);
ArraySetAsSeries(Buffers[i].Close,true);
ArraySetAsSeries(Buffers[i].Color,true);

class=class="str">"cmt">//--- Print data on the next buffer in the journal
class=class="str">"cmt">//Buffers[i].Print();
}
class=class="str">"cmt">//--- Bind the calculated indicator buffer by the FirstFreePlotBufferIndex() buffer index with the BufferTime[] array of the indicator
class=class="str">"cmt">//--- set the direction of indexing the BufferTime[] calculated buffer as in the timeseries
class="type">int buffer_temp_index=FirstFreePlotBufferIndex();
SetIndexBuffer(buffer_temp_index,BufferTime,INDICATOR_CALCULATIONS);
ArraySetAsSeries(BufferTime,true);
class=class="str">"cmt">//---
class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator iteration function                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],
                const class="type">class="kw">double &high[],
                const class="type">class="kw">double &low[],
                const class="type">class="kw">double &close[],
                const class="type">long &tick_volume[],
                const class="type">long &volume[],

「把 OnCalculate 接入价格结构引擎」

在 MT5 自定义指标里,OnCalculate 不只是算数值,还要把当前品种的全部时序喂给底层价格结构库。先调用 CopyData 把 time/open/high/low/close/tick_volume/volume/spread 这 11 个数组整体传进去,再判断 rates_total<min_bars 或 Point()==0 时直接 return 0,避免小数位为 0 的异常品种触发计算。 引擎自己有一套 OnCalculate 处理,如果它的返回值等于 0,说明某些时间序列还没就绪,这时要留在下一 tick 再跑,不能强行往下走。回测环境下用 MQLInfoInteger(MQL_TESTER) 识别,然后手动调 engine.OnTimer、PressButtonsControl 和 EventsHandling,把定时器与事件逻辑在测试器里补齐。 指标侧先把 9 个传入数组全部 ArraySetAsSeries(...,true) 设成时间序列,再从 limit=rates_total-prev_calculated 推算重算范围:limit=0 只算当前柱,limit=1 算前一根加当前,limit>1 则 InitBuffersAll 全量重算。循环里写 BufferTime[i]=(double)time[i] 并调 CalculateSeries(i,time[i]),直到 i 降到 WRONG_VALUE 或 IsStopped 为真,最后 return rates_total 交还 prev_calculated。外汇与贵金属杠杆高,这类全量重算在Tick密集时可能拖慢执行,建议先用 min_bars≥100 在策略测试器验证耗时。

MQL5 / C++
const class="type">int &spread[])
{
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| OnCalculate code block for working with the library:              |
class=class="str">"cmt">//+------------------------------------------------------------------+

class=class="str">"cmt">//--- Pass the current symbol data from OnCalculate() to the price structure
  CopyData(rates_data,rates_total,prev_calculated,time,open,high,low,close,tick_volume,volume,spread);
class=class="str">"cmt">//--- Check for the minimum number of bars for calculation
  if(rates_total<min_bars || Point()==class="num">0) class="kw">return class="num">0;

class=class="str">"cmt">//--- Handle the Calculate event in the library
class=class="str">"cmt">//--- If the OnCalculate() method of the library returns zero, not all timeseries are ready - leave till the next tick
  if(engine.OnCalculate(rates_data)==class="num">0)
     class="kw">return class="num">0;

class=class="str">"cmt">//--- If working in the tester
  if(MQLInfoInteger(MQL_TESTER))
     {
      engine.OnTimer(rates_data);   class=class="str">"cmt">// Working in the timer
      PressButtonsControl();        class=class="str">"cmt">// Button pressing control
      EventsHandling();             class=class="str">"cmt">// Working with events
     }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| OnCalculate code block for working with the indicator:            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Set OnCalculate arrays as timeseries
  ArraySetAsSeries(open,true);
  ArraySetAsSeries(high,true);
  ArraySetAsSeries(low,true);
  ArraySetAsSeries(close,true);
  ArraySetAsSeries(time,true);
  ArraySetAsSeries(tick_volume,true);
  ArraySetAsSeries(volume,true);
  ArraySetAsSeries(spread,true);
class=class="str">"cmt">//--- Check and calculate the number of calculated bars
class=class="str">"cmt">//--- If limit = class="num">0, there are no new bars - calculate the current one
class=class="str">"cmt">//--- If limit = class="num">1, a new bar has appeared - calculate the first and the current ones
class=class="str">"cmt">//--- If limit > class="num">1, there are changes in history - the full recalculation of all data
  class="type">int limit=rates_total-prev_calculated;

class=class="str">"cmt">//--- Recalculate the entire history
  if(limit>class="num">1)
     {
      limit=rates_total-class="num">1;
      InitBuffersAll();
     }
class=class="str">"cmt">//--- Prepare data
class=class="str">"cmt">//--- Calculate the indicator
  for(class="type">int i=limit; i>WRONG_VALUE && !IsStopped(); i--)
     {
      BufferTime[i]=(class="type">class="kw">double)time[i];
      CalculateSeries(i,time[i]);
     }
class=class="str">"cmt">//--- class="kw">return value of prev_calculated for next call
  class="kw">return(rates_total);
}
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 下一步要做的缓冲区类

下一篇起我会动手写指标缓冲区类,把现在散着的绘图逻辑收进一个统一的对象体系里。当前附件里是函数库全部文件和测试 EA,整包 3709.42 KB,只在 MT5 环境能直接跑。 MT4 这边先别指望,DRAW_COLOR_CANDLES 这种彩色蜡烛缓冲类型在四系根本不支持,等缓冲区类成型后我才会试着把部分 MQL5 能力往 MT4 搬。 你现在就能做的:把 ZIP 解进 MT5 的 MQL5 目录,挂上测试 EA 看实时缓冲刷新,顺便在评论里把想支持的缓冲类型列出来,后面类设计会照着需求排优先级。外汇和贵金属品种波动大、杠杆高,跑任何未审计的库文件前先在模拟盘验一遍。

常见问题

用定时刷新把计算事件和价格跳动事件隔离,定时触发统一重算,避免每根 tick 都重跑全部品种周期。
用一个批量建序列的引擎入口函数,传入品种周期数组循环初始化,别在 OnInit 里手写十几次重复代码。
可以,小布能按你给的结构数组自动比对 21 个缓冲区的 OHLC 绑定和色彩索引,标出越界或重复句柄。
在结构里分别存 OHLC 索引与色彩索引字段,初始化时一对一赋值,读取接口按索引取数,日志打印句柄备查。
用结构数组管住每个品种的缓冲区段,按品种顺序偏移排布,日志打印各句柄确认无交叉占用。