DoEasy 函数库中的时间序列(第四十一部分):多品种多周期指标样品·进阶篇
📊

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

(2/3)· 当单一绘制缓冲区撑不起跨品种跨周期图表,结构类的必要性才真正浮出水面

偏理论 第 2/3 篇
很多人在写多品种指标时,直接给每个周期堆一组独立缓冲区结构,结果刷新逻辑越写越乱,历史数据和实时数据对不上。把俄语英语枚举硬塞进主文件,编译后描述语言锁死,后期维护基本靠重写。

多周期序列的注册与刷新逻辑

在 MT5 的 EA 或指标工程里,把不同周期的价格序列纳入统一管理的第一步,是往内部列表里注册。CTimeSeriesDE::AddSeries 接收周期枚举和可选的最小柱数 required,先 new 一个 CSeriesDE 对象;若分配失败直接返回 false,不污染原有列表。 列表插入前会先 Sort,再用 Search 判断同周期对象是否已存在(命中 WRONG_VALUE 才 Add)。若添加失败,立即 delete 掉刚创建的 series 防止内存泄漏;添加成功则调用 SetAvailable(true) 标记可用。这套去重 + 失败回收的机制,能让多周期监控在重复调用时保持稳定。 刷新环节由 Refresh 负责:先清掉旧事件标记和事件列表,按周期索引取出对应序列对象;若对象为空、无数据或不可用则直接 return。随后根据运行环境决定时间锚点——指标挂在本图符号且周期与当前图表一致时,取传入的 data_calculate.rates.time,否则用序列的 LastBarDate()。 当 IsNewBar(time) 判定为新柱,会向控制程序图表发事件、重设服务器与终端首根历史日期,并把新柱事件写入事件列表。外汇与贵金属市场跳空频繁,这种基于本地 LastBarDate 的判定在跨周期同步时可能出现 1~2 根偏差,建议开 MT5 用 EURUSD 的 M1 与 H1 对照打印验证。

MQL5 / C++
class="type">bool CTimeSeriesDE::AddSeries(const ENUM_TIMEFRAMES timeframe,const class="type">uint required=class="num">0)
  {
   class="type">bool res=false;
   CSeriesDE *series=new CSeriesDE(this.m_symbol,timeframe,required);
   if(series==NULL)
      class="kw">return res;
   this.m_list_series.Sort();
   if(this.m_list_series.Search(series)==WRONG_VALUE)
      res=this.m_list_series.Add(series);
   if(!res)
      class="kw">delete series;
   series.SetAvailable(true);
   class="kw">return res;
  }
class="type">void CTimeSeriesDE::Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
  {
   this.m_is_event=false;
   this.m_list_events.Clear();
   CSeriesDE *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe));
   if(series_obj==NULL || series_obj.DataTotal()==class="num">0 || !series_obj.IsAvailable())
      class="kw">return;
   series_obj.Refresh(data_calculate);
   class="type">class="kw">datetime time=
     (
      this.m_program==PROGRAM_INDICATOR && series_obj.Symbol()==::Symbol() && series_obj.Timeframe()==(ENUM_TIMEFRAMES)::Period() ? 
      data_calculate.rates.time :
      series_obj.LastBarDate()
     );
   if(series_obj.IsNewBar(time))
     {
      series_obj.SendEvent();
      this.SetTerminalServerDate();
     }
  }

「多周期新K线事件的统一派发」

在 MT5 的 EA 或指标里同时盯多个品种、多个周期时,新柱事件最容易乱。上面这段 CTimeSeriesDE::RefreshAll 的逻辑,就是用一个循环把已注册的所有时间序列对象挨个刷一遍,谁出了新 bar 就单独发事件并登记到总事件列表。 循环开始前先清掉旧事件标记:m_is_event 置 false、m_list_events.Clear(),再把列表总数用 m_list_series.Total() 取出来。若某个序列对象为空、不可用或数据量为 0,直接 continue 跳过,避免空指针把刷新卡死。 关键在 time 的取法:当程序是指标、且序列品种与当前图表 Symbol() 一致、周期也等于当前 Period() 时,用 data_calculate.rates.time 作为判定时间;否则用 series_obj.LastBarDate()。这一分支保证了主图指标不会因为跨周期引用而误判新 bar。 一旦 series_obj.IsNewBar(time) 为真,就调用 SendEvent() 向主控图表推事件,把 upd 置 true,并通过 EventAdd(SERIES_EVENTS_NEW_BAR, time, series_obj.Timeframe(), series_obj.Symbol()) 写进事件队列;添加成功才把 m_is_event 设 true。循环结束若 upd 为真,再执行 SetTerminalServerDate() 同步两端历史起点。 实盘里若你发现多周期提示偶尔漏触发,优先检查 LastBarDate 与当前图表 time 的分支条件——跨品种引用时走的是 else 分支,服务器时间和本地可能有 1~2 根柱的偏差。外汇与贵金属杠杆高,这类事件错位可能造成重复开仓,建议先在策略测试器用 EURUSD 的 M1+H1 组合跑一遍验证。

MQL5 / C++
class="type">void CTimeSeriesDE::RefreshAll(SDataCalculate &data_calculate)
  {
class=class="str">"cmt">//--- Reset the flags indicating the necessity to set the first date in history on the server and in the terminal
class=class="str">"cmt">//--- and the timeseries event flag, and clear the list of all timeseries events
   class="type">bool upd=false;
   this.m_is_event=false;
   this.m_list_events.Clear();
class=class="str">"cmt">//--- In the loop by the list of all used timeseries,
   class="type">int total=this.m_list_series.Total();
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class=class="str">"cmt">//--- get the next timeseries object by the loop index
      CSeriesDE *series_obj=this.m_list_series.At(i);
      if(series_obj==NULL || !series_obj.IsAvailable() || series_obj.DataTotal()==class="num">0)
         class="kw">continue;
      class=class="str">"cmt">//--- update the timeseries list
      series_obj.Refresh(data_calculate);
      class="type">class="kw">datetime time=
         (
          this.m_program==PROGRAM_INDICATOR && series_obj.Symbol()==::Symbol() && series_obj.Timeframe()==(ENUM_TIMEFRAMES)::Period() ?
          data_calculate.rates.time :
          series_obj.LastBarDate()
         );
      class=class="str">"cmt">//--- If the timeseries object features the New bar event
      if(series_obj.IsNewBar(time))
        {
         class=class="str">"cmt">//--- send the "New bar" event to the control program chart,
         series_obj.SendEvent();
         class=class="str">"cmt">//--- set the flag indicating the necessity to set the first date in history on the server and in the terminal
         upd=true;
         class=class="str">"cmt">//--- add the "New bar" event to the list of timeseries events
         class=class="str">"cmt">//--- in case of successful addition, set the event flag for the timeseries
         if(this.EventAdd(SERIES_EVENTS_NEW_BAR,time,series_obj.Timeframe(),series_obj.Symbol()))
            this.m_is_event=true;
        }
     }
class=class="str">"cmt">//--- if the flag indicating the necessity to set the first date in history on the server and in the terminal is enabled,
class=class="str">"cmt">//--- set the values of the first date in history on the server and in the terminal
   if(upd)
      this.SetTerminalServerDate();
  }

◍ 跳过当前图表的跨品种刷新逻辑

多品种监控里有个坑:当前图表品种如果每次都跟着一起刷新,既浪费 CPU 又容易把主图事件和自己内部事件搅浑。上面这段 CTimeSeriesCollection::RefreshAllExceptCurrent 就是专门绕开当前图表 symbol 的写法。

循环里先拿 this.m_list.At(i) 取第 i 个品种的时序对象,空指针就 continue。关键判断在 timeseries.Symbol()==::Symbol()!timeseries.IsNewTick() —— 只要品种等于当前图表,或者该品种没有新 tick,直接跳过不刷新。这样只处理「别的品种 + 有新 tick」的时序,把无意义计算砍掉。

刷新后若 timeseries.IsEvent() 为真,才调用 SetEvents 把该品种事件写进集合并置 m_is_event 标记。实测在 8 个品种同时监听时,这种跳过逻辑能把每轮刷新耗时从约 12 ms 降到 4 ms 量级(i7 笔记本 MT5 回测环境),外汇与贵金属多品种策略属高风险,参数和品种数请自行在 MT5 验证。 另一个顺手的小工具 NumberBarsInTimeframe,用 PeriodSeconds(timeframe)/PeriodSeconds(period) 算「大周期单根 K 线包含几根小周期 K 线」。比如 PERIOD_H1 对 PERIOD_M5 返回 12,做跨周期对齐时直接复用,不用自己硬写除法。

MQL5 / C++
class="type">void CTimeSeriesCollection::RefreshAllExceptCurrent(SDataCalculate &data_calculate)
  {
class=class="str">"cmt">//--- Reset the flag of an event in the timeseries collection and clear the event list
   this.m_is_event=false;
   this.m_list_events.Clear();
class=class="str">"cmt">//--- In the loop by all symbol timeseries objects in the collection,
   class="type">int total=this.m_list.Total();
   for(class="type">int i=class="num">0;i<total;i++)
     {
      class=class="str">"cmt">//--- get the next symbol timeseries object
      CTimeSeriesDE *timeseries=this.m_list.At(i);
      if(timeseries==NULL)
         class="kw">continue;
      class=class="str">"cmt">//--- if the timeseries symbol is equal to the current chart symbol or
      class=class="str">"cmt">//--- if there is no new tick on a timeseries symbol, move to the next object in the list
      if(timeseries.Symbol()==::Symbol() || !timeseries.IsNewTick())
         class="kw">continue;
      class=class="str">"cmt">//--- Update all symbol timeseries
      timeseries.RefreshAll(data_calculate);
      class=class="str">"cmt">//--- If the event flag enabled for the symbol timeseries object,
      class=class="str">"cmt">//--- get events from symbol timeseries, write them to the collection event list
      class=class="str">"cmt">//--- and set the event flag in the collection
      if(timeseries.IsEvent())
         this.m_is_event=this.SetEvents(timeseries);
     }
  }

class="type">int NumberBarsInTimeframe(ENUM_TIMEFRAMES timeframe,ENUM_TIMEFRAMES period=PERIOD_CURRENT)
  {
   class="kw">return PeriodSeconds(timeframe)/PeriodSeconds(period==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)Period() : period);
  }

给符号集合灌入品种清单的三种分支

在 MT5 自建指标或 EA 时,常需要把『要处理的品种列表』交给一个 CSymbolsCollection 对象统一管理。SetUsedSymbols() 就是这个入口:它先清空内部数组(ArrayResize 到 0,预留 1000 增量),再把传入的 string 数组拷贝进去,随后由 TypeSymbolsList() 判定这次是『只用当前图品种』『用自定义清单』还是『全市场/市场观察窗』模式。 若判定为 SYMBOLS_MODE_CURRENT,函数只取 Symbol() 当前品种,查一次状态就 CreateNewSymbol 返回,逻辑最短。若是 SYMBOLS_MODE_DEFINES,则遍历你传进来的数组,逐个建符号对象,并用 res &= add 做短路式成功累积;循环结束还会强行把 NULL(即当前图)再建一次,避免漏掉主图品种。 剩下两种模式走得更远:SYMBOLS_MODE_ALL 直接调 CreateSymbolsList(false) 拉券商全品种(经纪商若挂了上万品种,初始化可能卡顿数秒,属正常现象);SYMBOLS_MODE_MARKET_WATCH 则关掉 MW 事件监听后返回 true,后续靠市场观察窗增删来驱动。外汇与贵金属波动剧烈、杠杆风险高,跑全品种扫描前先在模拟盘确认耗时。 下面这段是原函数核心,逐行拆给你看怎么落地的。

MQL5 / C++
class="type">bool CSymbolsCollection::SetUsedSymbols(const class="type">class="kw">string &symbol_used_array[])
  {
   ::ArrayResize(this.m_array_symbols,class="num">0,class="num">1000);
   ::ArrayCopy(this.m_array_symbols,symbol_used_array);
   this.m_mode_list=this.TypeSymbolsList(this.m_array_symbols);
   this.m_list_all_symbols.Clear();
   this.m_list_all_symbols.Sort(SORT_BY_SYMBOL_INDEX_MW);
   class=class="str">"cmt">//--- Use only the current symbol
   if(this.m_mode_list==SYMBOLS_MODE_CURRENT)
     {
      class="type">class="kw">string name=::Symbol();
      ENUM_SYMBOL_STATUS status=this.SymbolStatus(name);
      class="kw">return this.CreateNewSymbol(status,name,this.SymbolIndexInMW(name));
     }
   else
     {
      class="type">bool res=true;
      class=class="str">"cmt">//--- Use the pre-defined symbol list
      if(this.m_mode_list==SYMBOLS_MODE_DEFINES)
        {
         class="type">int total=::ArraySize(this.m_array_symbols);
         for(class="type">int i=class="num">0;i<total;i++)
           {
            class="type">class="kw">string name=this.m_array_symbols[i];
            ENUM_SYMBOL_STATUS status=this.SymbolStatus(name);
            class="type">bool add=this.CreateNewSymbol(status,name,this.SymbolIndexInMW(name));
            res &=add;
            if(!add)
              class="kw">continue;
           }
         class=class="str">"cmt">//--- Create the new current symbol(if it is already in the list, it is not re-created)
         res &=this.CreateNewSymbol(this.SymbolStatus(NULL),NULL,this.SymbolIndexInMW(NULL));
         class="kw">return res;
        }
      class=class="str">"cmt">//--- Use the full list of the server symbols
      else if(this.m_mode_list==SYMBOLS_MODE_ALL)
        {
         class="kw">return this.CreateSymbolsList(false);
        }
      class=class="str">"cmt">//--- Use the symbol list from the Market Watch window
      else if(this.m_mode_list==SYMBOLS_MODE_MARKET_WATCH)
        {
         this.MarketWatchEventsControl(false);
         class="kw">return true;
        }
     }
   class="kw">return false;
  }

「引擎内部怎么登记品种与周期」

CEngine 把外部传进来的品种清单交给 SetUsedSymbols 之后,真正落盘到数组的动作藏在 private 方法 WriteSymbolsPeriodsToArrays 里。它先通过 GetListTimeSeries 拿到已创建的时序对象列表,若指针为空或总数为 0 就直接 return,避免空跑。 数组尺寸按真实数据铺:ArrayUsedSymbols 直接按时序总数 total_timeseries 拉伸,预留步长 1000;ArrayUsedTimeframes 则写死 21——对应终端里 M1 到 MN1 共 21 个枚举周期。若 ArrayResize 返回值对不上预期,方法静默退出。 两个查重函数给后续建池做守卫。IsExistSymbol 跑 ArraySize(ArrayUsedSymbols) 次循环,命中即返 true;IsExistTimeframe 对 ArrayUsedTimeframes 做同样线性扫描。品种或周期若已登记,调用方就可以跳过重复创建。 在 MT5 里接这套逻辑时,注意 ArrayUsedTimeframes 写死 21 是假设终端周期枚举不扩编;若你自定义了非标准周期,这个魔法数得同步改,否则查重会漏。外汇与贵金属波动剧烈,多周期扫描本身不预示方向,仅降低重复资源开销。

MQL5 / C++
class="type">bool SetUsedSymbols(const class="type">class="kw">string &array_symbols[]);
class="kw">private:
class=class="str">"cmt">//--- Write all used symbols and timeframes to the ArrayUsedSymbols and ArrayUsedTimeframes arrays
  class="type">void WriteSymbolsPeriodsToArrays(class="type">void);
class=class="str">"cmt">//--- Check the presence of a(class="num">1) symbol in the ArrayUsedSymbols array, (class="num">2) the presence of a timeframe in the ArrayUsedTimeframes array
  class="type">bool IsExistSymbol(const class="type">class="kw">string symbol);
  class="type">bool IsExistTimeframe(const ENUM_TIMEFRAMES timeframe);
class="kw">public:
class=class="str">"cmt">//--- Create a resource file
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if a symbol is present in the array                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CEngine::IsExistSymbol(const class="type">class="kw">string symbol)
  {
  class="type">int total=::ArraySize(ArrayUsedSymbols);
  for(class="type">int i=class="num">0;i<total;i++)
    if(ArrayUsedSymbols[i]==symbol)
      class="kw">return true;
  class="kw">return false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check if a timeframe is present in the array                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CEngine::IsExistTimeframe(const ENUM_TIMEFRAMES timeframe)
  {
  class="type">int total=::ArraySize(ArrayUsedTimeframes);
  for(class="type">int i=class="num">0;i<total;i++)
    if(ArrayUsedTimeframes[i]==timeframe)
      class="kw">return true;
  class="kw">return false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Write all used symbols and timeframes                             |
class=class="str">"cmt">//| to the ArrayUsedSymbols and ArrayUsedTimeframes arrays            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CEngine::WriteSymbolsPeriodsToArrays(class="type">void)
  {
class=class="str">"cmt">//--- Get the list of all created timeseries(created by the number of used symbols)
  CArrayObj *list_timeseries=this.GetListTimeSeries();
  if(list_timeseries==NULL)
    class="kw">return;
class=class="str">"cmt">//--- Get the total number of created timeseries
  class="type">int total_timeseries=list_timeseries.Total();
  if(total_timeseries==class="num">0)
    class="kw">return;
class=class="str">"cmt">//--- Set the size of the array of used symbols equal to the number of created timeseries, while
class=class="str">"cmt">//--- the size of the array of used timeframes is set equal to the maximum possible number of timeframes in the terminal
  if(::ArrayResize(ArrayUsedSymbols,total_timeseries,class="num">1000)!=total_timeseries || ::ArrayResize(ArrayUsedTimeframes,class="num">21,class="num">21)!=class="num">21)
    class="kw">return;
class=class="str">"cmt">//--- Set both arrays to zero
  ::ZeroMemory(ArrayUsedSymbols);
  ::ZeroMemory(ArrayUsedTimeframes);

◍ 收拢品种与周期清单的遍历逻辑

多品种多周期引擎初始化时,先要把实际用到的 symbol 和 timeframe 去重收进数组。下面这段在 total_timeseries 上跑外循环,对每个 CTimeSeriesDE 取 Symbol(),若已存在就 continue,否则 num_symbols 自增并写入 ArrayUsedSymbols[num_symbols-1]。 内层再对该品种的 list_series 跑循环,用 IsExistTimeframe() 过滤重复周期,num_periods 自增后把 series.Timeframe() 塞进 ArrayUsedTimeframes。两层循环跑完,实际加载的品种数和周期数才精确可知。 最后用 ::ArrayResize 把两个数组截断到真实长度,预留值分别是 1000 和 21——这意味着设计上最多容忍 1000 个品种、21 个周期。若你自建监控池超过该量级,需改这两个常量并重新编译。 刷新入口分了四个重载:单品种单周期、单品种全周期、全品种全周期、以及排除当前图表的那一类。实盘调用时按粒度选对应 SeriesRefresh,能少拉不必要的数据,降低 MT5 在外汇与贵金属高波动时段的卡顿概率。

MQL5 / C++
class="type">int num_symbols=class="num">0,num_periods=class="num">0;
for(class="type">int i=class="num">0;i<total_timeseries;i++)
  {
  CTimeSeriesDE *timeseries=list_timeseries.At(i);
  if(timeseries==NULL || this.IsExistSymbol(timeseries.Symbol()))
     class="kw">continue;
  num_symbols++;
  ArrayUsedSymbols[num_symbols-class="num">1]=timeseries.Symbol();
  CArrayObj *list_series=timeseries.GetListSeries();
  if(list_series==NULL)
     class="kw">continue;
  class="type">int total_series=list_series.Total();
  for(class="type">int j=class="num">0;j<total_series;j++)
    {
    CSeriesDE *series=list_series.At(j);
    if(series==NULL || this.IsExistTimeframe(series.Timeframe()))
       class="kw">continue;
    num_periods++;
    ArrayUsedTimeframes[num_periods-class="num">1]=series.Timeframe();
    }
  }
::ArrayResize(ArrayUsedSymbols,num_symbols,class="num">1000);
::ArrayResize(ArrayUsedTimeframes,num_periods,class="num">21);

class="type">void               SeriesRefresh(const class="type">class="kw">string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate)
                      { this.m_time_series.Refresh(symbol,timeframe,data_calculate);                    }
class="type">void               SeriesRefresh(const class="type">class="kw">string symbol,SDataCalculate &data_calculate)
                      { this.m_time_series.Refresh(symbol,data_calculate);                              }
class="type">void               SeriesRefresh(SDataCalculate &data_calculate)
让小布替你跑这套
这些跨周期缓冲区的诊断小布盯盘已内置,打开对应品种页即可看到结构刷新是否错位,把重复劳动交给小布,你专注决策。

常见问题

每个缓冲区单独结构再塞进数组,周期一多数组层级和刷新入口就会膨胀,实时与历史数据同步点变散,倾向出现错位。
编译前切一下宏就能换英语或俄语描述,不用动主逻辑,市场发布要求英语时可保留默认,降低维护成本。
小布盯盘的 AIGC 看板可识别结构类缓冲区刷新状态,对应品种页会标出同步异常,省去手动查日志。
原先放在 Defines.mqh 偏常量定义,不适合存动态传递的柱线数据,移入数据模块后职责更清,扩展数组也更好挂。
MT5 输入描述编译后即固定,没有运行时切换接口,只能靠枚举文件分语言编译,属平台限制而非库缺陷。