DoEasy 函数库中的时间序列(第三十八部分):时间序列集合 - 实时更新以及从程序访问数据·进阶篇
⏱️

DoEasy 函数库中的时间序列(第三十八部分):时间序列集合 - 实时更新以及从程序访问数据·进阶篇

(2/3)· 还在用 OnTimer 毫秒级刷新所有品种数据?这篇讲清如何只在新报价到达时精准更新集合

案例拆解新手友好 第 2/3 篇
很多 EA 在 OnTimer 里无差别刷新全部品种时间序列,新报价没来也空转,CPU 白白吃满。更隐蔽的是,跨品种图表若不挂 EA 就完全追踪不到报价变化,数据滞后到决策失真。把计时器当万能刷子,是这类库最常见的性能坑。

用类封装新报价事件检测

在 MT5 的 EA 或指标里,判断「是不是来了新 tick」最省事的做法不是每次都裸调 SymbolInfoTick,而是包一层对象。下面这个类从 CBaseObj 继承,内部存了当前 tick 和上一次检查的 tick 两个 MqlTick 结构,外加一个品种名和布尔标记。 私有成员 m_tick 与 m_tick_prev 分别承载本次与上次的价格快照,m_symbol 决定监听哪个品种,m_new_tick 是对外暴露的「新 tick」状态位。调用 Refresh() 会内部跑一次 IsNewTick() 并写回标记,逻辑集中不散落。 IsNewTick() 的实现里有几个细节值得注意:若 SymbolInfoTick 取数失败直接返 false;首次启动(m_first_start 为真)会把当前 tick 拷给 prev 并返 false,避免把初始化当成新事件;之后只要 m_tick.time_msc 不等于 m_tick_prev.time_msc,就更新 prev 并返回 true。用毫秒级时间比对,能抓到同一秒内多笔报价的情况,比用 time 字段更准。 开 MT5 把这段代码塞进你的 Include 目录,配合前一篇的 BaseObj,就能在 OnTick 里用 obj.Refresh() 后再判 IsNewTick() 来驱动逻辑,外汇与贵金属波动快、杠杆高,实盘前务必在策略测试器用真实点差回测。

MQL5 / C++
class="macro">#include "..\..\Objects\BaseObj.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "New tick" class                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CNewTickObj : class="kw">public CBaseObj
  {
class="kw">private:
   class="type">MqlTick            m_tick;            class=class="str">"cmt">// Structure of the current prices
   class="type">MqlTick            m_tick_prev;       class=class="str">"cmt">// Structure of the current prices during the previous check
   class="type">class="kw">string             m_symbol;          class=class="str">"cmt">// Object symbol
   class="type">bool               m_new_tick;        class=class="str">"cmt">// New tick flag
class="kw">public:
class=class="str">"cmt">//--- Set a symbol
   class="type">void               SetSymbol(class="kw">const class="type">class="kw">string symbol)  { this.m_symbol=symbol;                    }
class=class="str">"cmt">//--- Return the new tick flag
   class="type">bool               IsNewTick(class="type">void);
class=class="str">"cmt">//--- Update price data in the tick structure and set the "New tick" event flag if necessary
   class="type">void               Refresh(class="type">void)                       { this.m_new_tick=this.IsNewTick(); }
class=class="str">"cmt">//--- Constructors
                     CNewTickObj(class="type">void){;}
                     CNewTickObj(class="kw">const class="type">class="kw">string symbol);
  };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the new tick flag                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CNewTickObj::IsNewTick(class="type">void)
  {
class=class="str">"cmt">//--- If failed to get the current prices to the tick structure, class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
   if(!::SymbolInfoTick(this.m_symbol,this.m_tick))
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- If this is the first launch, copy data of the obtained tick to the previous tick data
class=class="str">"cmt">//--- reset the first launch flag and class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
   if(this.m_first_start)
     {
      this.m_tick_prev=this.m_tick;
      this.m_first_start=class="kw">false;
      class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- If the time of a new tick is not equal to the time of a tick during the previous check -
class=class="str">"cmt">//--- copy data of the obtained tick to the previous tick data and class="kw">return &class="macro">#x27;true&class="macro">#x27;
   if(this.m_tick.time_msc!=this.m_tick_prev.time_msc)

◍ 新 tick 对象构造与事件枚举的落地写法

在 MT5 里做价格行为监控,第一件事是把「上一笔 tick」和「当前 tick」分开存。CNewTickObj 的带参构造函数就干了这件事:用 SymbolInfoTick 把当前报价塞进 m_tick,再立刻复制给 m_tick_prev,同时把 m_first_start 置为 false,避免首帧误触发信号。 构造函数里先用 ZeroMemory 把两个 MqlTick 结构清零,否则残留字段会让首根 K 线前的差值计算失真。若 SymbolInfoTick 返回成功,说明品种行情已可读,此时 prev 与 curr 同值,下一帧进来才有真实 delta。 事件侧只定义了一个轻量枚举 ENUM_SERIES_EVENT,目前仅含 SERIES_EVENTS_NO_EVENT 与 SERIES_EVENTS_NEW_BAR,基准码从 SYMBOL_EVENTS_NEXT_CODE 顺延,方便以后扩事件而不撞系统码。 定时器宏揭示了实际轮询节奏:交易类定时器暂停 300 毫秒、步进 16、计数器 ID 5;时间序列采集定时器暂停仅 32 毫秒、步进 16、计数器 ID 6。也就是说,K 线集合的刷新扫描比下单逻辑快近 10 倍,做贵金属或外汇高频判新 bar 时这个 32ms 间隔值得你打开 MT5 测一下实际丢 tick 概率。

MQL5 / C++
  {
      this.m_tick_prev=this.m_tick;
      class="kw">return true;
   }
class=class="str">"cmt">//--- In all other cases, class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
   class="kw">return class="kw">false;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Parametric constructor CNewTickObj                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CNewTickObj::CNewTickObj(class="kw">const class="type">class="kw">string symbol) : m_symbol(symbol)
  {
class=class="str">"cmt">//--- Reset the structures of the new and previous ticks
  ::ZeroMemory(this.m_tick);
  ::ZeroMemory(this.m_tick_prev);
class=class="str">"cmt">//--- If managed to get the current prices to the tick structure,
class=class="str">"cmt">//--- copy data of the obtained tick to the previous tick data and reset the first launch flag
  if(::SymbolInfoTick(this.m_symbol,this.m_tick))
    {
      this.m_tick_prev=this.m_tick;
      this.m_first_start=class="kw">false;
    }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| List of possible timeseries events                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_SERIES_EVENT
  {
   SERIES_EVENTS_NO_EVENT = SYMBOL_EVENTS_NEXT_CODE,      class=class="str">"cmt">// no event
   SERIES_EVENTS_NEW_BAR,                                 class=class="str">"cmt">// "New bar" event
  };
class="macro">#define SERIES_EVENTS_NEXT_CODE(SERIES_EVENTS_NEW_BAR+class="num">1)   class=class="str">"cmt">// Code of the next event after the "New bar" event
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Trading class timer parameters
class="macro">#define COLLECTION_REQ_PAUSE(class="num">300)                class=class="str">"cmt">// Trading class timer pause in milliseconds
class="macro">#define COLLECTION_REQ_COUNTER_STEP(class="num">16)                 class=class="str">"cmt">// Trading class timer counter increment
class="macro">#define COLLECTION_REQ_COUNTER_ID(class="num">5)                  class=class="str">"cmt">// Trading class timer counter ID
class=class="str">"cmt">//--- Parameters of the timeseries collection timer
class="macro">#define COLLECTION_TS_PAUSE(class="num">32)                 class=class="str">"cmt">// Timeseries collection timer pause in milliseconds
class="macro">#define COLLECTION_TS_COUNTER_STEP(class="num">16)                 class=class="str">"cmt">// Account timer counter increment
class="macro">#define COLLECTION_TS_COUNTER_ID(class="num">6)                  class=class="str">"cmt">// Timeseries timer counter ID
class=class="str">"cmt">//--- Collection list IDs

「K线对象怎么挂到时序集合里」

在 MT5 自建指标或 EA 里,若要把单根 Bar 封装成可管理的对象,先得给它一个集合身份。代码里用 0x777A~0x777F 这六个十六进制常量区分历史、行情、事件、账户、品种和时序集合,其中时序集合固定为 0x777F。 CBar 类的两个构造函数都先把 m_type 设为 COLLECTION_SERIES_ID(即 0x777F),这意味着这根 Bar 被归入‘时间序列集合’,后续批量存取或序列化时不会和历史/账户等列表混淆。 第一个构造函数靠 CopyRates(symbol,timeframe,index,1,rates_array) 去抓指定偏移的 1 根 K 线;若返回值小于 1 或时间结构转换失败,就打印错误码并把零值 MqlRates 写回数组,避免野指针。第二个构造函数直接吃外部传入的 MqlRates 引用,少一次内核调用,适合已在循环里拿到 rates 的高频场景。 开 MT5 按 F4 把下面片段塞进你的 Include,调 CopyRates 的 index 参数就能验证不同偏移 Bar 是否都带 0x777F 标记。外汇与贵金属波动剧烈,这类底层封装仅降低出错概率,不消除滑点风险。

MQL5 / C++
class="macro">#define COLLECTION_HISTORY_ID(0x777A)             class=class="str">"cmt">// Historical collection list ID
class="macro">#define COLLECTION_MARKET_ID(0x777B)             class=class="str">"cmt">// Market collection list ID
class="macro">#define COLLECTION_EVENTS_ID(0x777C)             class=class="str">"cmt">// Event collection list ID
class="macro">#define COLLECTION_ACCOUNT_ID(0x777D)             class=class="str">"cmt">// Account collection list ID
class="macro">#define COLLECTION_SYMBOLS_ID(0x777E)             class=class="str">"cmt">// Symbol collection list ID
class="macro">#define COLLECTION_SERIES_ID(0x777F)             class=class="str">"cmt">// Timeseries collection list ID
class=class="str">"cmt">//--- Data parameters for file operations
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor class="num">1                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CBar::CBar(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index)
  {
   this.m_type=COLLECTION_SERIES_ID;
   class="type">MqlRates rates_array[class="num">1];
   this.SetSymbolPeriod(symbol,timeframe,index);
   ::ResetLastError();
class=class="str">"cmt">//--- If failed to write bar data to the class="type">MqlRates array by index or set the time to the time structure,
class=class="str">"cmt">//--- display an error message, create and fill the structure with zeros, and write it to the rates_array array
   if(::CopyRates(symbol,timeframe,index,class="num">1,rates_array)<class="num">1 || !::TimeToStruct(rates_array[class="num">0].time,this.m_dt_struct))
     {
      class="type">int err_code=::GetLastError();
      ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA),". ",CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ",CMessage::Retcode(err_code));
      class="type">MqlRates err={class="num">0};
      rates_array[class="num">0]=err;
     }
class=class="str">"cmt">//--- Set the bar properties
   this.SetProperties(rates_array[class="num">0]);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Constructor class="num">2                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CBar::CBar(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">MqlRates &rates)
  {
   this.m_type=COLLECTION_SERIES_ID;
   this.SetSymbolPeriod(symbol,timeframe,index);
   ::ResetLastError();

时间序列对象的时间解析与接口骨架

在自定义时间序列类里,时间字段必须先经过 TimeToStruct 拆解成结构体才能用。若调用失败,代码会取 GetLastError 的错误码,打印包含错误描述的信息,然后用全零的 MqlRates 结构调用 SetProperties 兜底,并直接 return 退出,避免后续逻辑吃到脏数据。 兜底之后,正常分支只是把入参 rates 交给 SetProperties 完成属性赋值。这一段没有返回值,说明它属于对象内部的状态刷新,不是对外查询接口。 类对外暴露的接口值得在 MT5 里照着抄一遍:Create(required=0) 负责建序列,Refresh(time=0, open=0, high=0, low=0, close=0, tick_volume=0, volume=0, spread=0) 用全默认参数把任意一根 bar 推进去更新;SendEvent 会向主控图表发“新 bar”事件。外汇与贵金属行情跳空频繁,Refresh 的默认零值若误用,可能让序列出现虚假零价 bar,属高风险操作。 剩下的 Header、Print、PrintShort 以及两个构造函数(无参 / 带 symbol+timeframe+required)构成最小可用面。打开 MT5 的 MetaEditor,新建一个 CSeries 类把下列声明原样贴入,就能验证编译是否通过。

MQL5 / C++
if(!::TimeToStruct(rates.time,this.m_dt_struct))
  {
    class="type">int err_code=::GetLastError();
    ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA),". ",CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ",CMessage::Retcode(err_code));
    class="type">MqlRates err={class="num">0};
    this.SetProperties(err);
    class="kw">return;
  }
class=class="str">"cmt">//--- Set the bar properties
  this.SetProperties(rates);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- (class="num">1) Create and(class="num">2) update the timeseries list
  class="type">int                Create(class="kw">const class="type">uint required=class="num">0);
  class="type">void               Refresh(class="kw">const class="type">class="kw">datetime time=class="num">0,
                             class="kw">const class="type">class="kw">double open=class="num">0,
                             class="kw">const class="type">class="kw">double high=class="num">0,
                             class="kw">const class="type">class="kw">double low=class="num">0,
                             class="kw">const class="type">class="kw">double close=class="num">0,
                             class="kw">const class="type">long tick_volume=class="num">0,
                             class="kw">const class="type">long volume=class="num">0,
                             class="kw">const class="type">int spread=class="num">0);
class=class="str">"cmt">//--- Create and send the "New bar" event to the control program chart
  class="type">void               SendEvent(class="type">void);
class=class="str">"cmt">//--- Return the timeseries name
  class="type">class="kw">string             Header(class="type">void);
class=class="str">"cmt">//--- Display(class="num">1) the timeseries description and(class="num">2) the brief timeseries description in the journal
  class="type">void               Print(class="type">void);
  class="type">void               PrintShort(class="type">void);
class=class="str">"cmt">//--- Constructors
                     CSeries(class="type">void);
                     CSeries(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">uint required=class="num">0);
};

◍ 跨图表新K线事件与序列同步的底层实现

在 MT5 的自定义时间序列类里,新柱体生成后需要主动通知主控图表,否则依赖该序列的 EA 可能停留在上一根 bar 的逻辑里。下面这段把事件推送到图表,参数顺序很关键:图表 ID、自定义事件号、当前时间、周期、品种。

MQL5 / C++
class="type">void CSeries::SendEvent(class="type">void)
  {
   ::EventChartCustom(this.m_chart_id_main,SERIES_EVENTS_NEW_BAR,this.Time(class="num">0),this.Timeframe(),this.Symbol());
  }
同步数据前先判可用性是常见防御写法。若 m_available 为 false,直接 Print 提示该品种对应周期序列未启用并返回 false,避免后续拉空数据。Create 方法里同样有这段判断,返回 0 而非错误码,调用方需自行区分“未启用”与“建空列表”。 按序列索引取 bar 对象时,GetBarBySeriesIndex 先走 GetList(BAR_PROP_INDEX, index) 拿到对象数组,再取 At(0)。若列表为空或总数为 0 返回 NULL——这意味着调用处必须做空指针保护,否则在刚切换品种、历史深度不足的瞬间容易崩 EA。外汇与贵金属行情跳空频繁,这类边界在实盘里更容易被触发,属高风险环节。 让小布替你跑这套 把 SendEvent 里的 SERIES_EVENTS_NEW_BAR 改成你自己的事件常量,在多周期共振 EA 里用 EventChartCustom 跨图表传新 bar 信号,比轮询 CopyRates 更省 CPU,回测时也能少掉几帧卡顿。

MQL5 / C++
class="type">void CSeries::SendEvent(class="type">void)
  {
   ::EventChartCustom(this.m_chart_id_main,SERIES_EVENTS_NEW_BAR,this.Time(class="num">0),this.Timeframe(),this.Symbol());
  }

class="type">bool CSeries::SyncData(class="kw">const class="type">uint required,class="kw">const class="type">uint rates_total)
  {
class=class="str">"cmt">//--- If the timeseries is not used, notify of that and exit
   if(!this.m_available)
     {
      ::Print(DFUN,this.m_symbol," ",TimeframeDescription(this.m_timeframe),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_IS_NOT_USE));
      class="kw">return class="kw">false;
     }
class=class="str">"cmt">//--- If managed to obtain the available number of bars in the timeseries

class="type">int CSeries::Create(class="kw">const class="type">uint required=class="num">0)
  {
class=class="str">"cmt">//--- If the timeseries is not used, notify of that and class="kw">return zero
   if(!this.m_available)
     {
      ::Print(DFUN,this.m_symbol," ",TimeframeDescription(this.m_timeframe),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_IS_NOT_USE));
      class="kw">return class="num">0;
     }
class=class="str">"cmt">//--- If the required history depth is not set for the list yet,

CBar *CSeries::GetBarBySeriesIndex(class="kw">const class="type">uint index)
  {
   CArrayObj *list=this.GetList(BAR_PROP_INDEX,index);
   class="kw">return(list==NULL || list.Total()==class="num">0 ? NULL : list.At(class="num">0));
  }

CBar *CSeries::GetBarBySeriesIndex(class="kw">const class="type">uint index)
  {

「用临时柱对象在序列列表里二分定位」

在自定义时间序列管理里,按索引取 Bar 不能遍历硬找,构造一个临时 CBar 丢进已排序的列表做 Search 才是正路。下面这段代码就是核心取法:先 new 一个同品种同周期、指定 index 的临时柱,空指针直接返回 NULL 防崩。 CBar *tmp=new CBar(this.m_symbol,this.m_timeframe,index); if(tmp==NULL) return NULL; this.m_list_series.Sort(SORT_BY_BAR_INDEX); int idx=this.m_list_series.Search(tmp); delete tmp; CBar *bar=this.m_list_series.At(idx); return(bar!=NULL ? bar : NULL); Sort 调用放在 Search 前是关键——列表若中途被改写过序,不重排 Search 会偏索引。Search 返回的是排序位,不是原始插入位,所以临时对象用完必须 delete,否则每查一次漏一个对象,跑 10 万根 K 线可能吃掉几十 MB。 承接上面 TimeSeries.mqh 的框架,CTimeSeries 类里挂了 m_new_tick(CNewTickObj 类型)和 m_list_series(CArrayObj 类型)。想在 MT5 验证定位效率,把 SORT_BY_BAR_INDEX 换成 SORT_BY_TIME 再跑 Search,同样品种周期下耗时倾向有明显差异,自己打 ticks 计数就看得见。

MQL5 / C++
CBar *tmp=new CBar(this.m_symbol,this.m_timeframe,index);
if(tmp==NULL)
   class="kw">return NULL;
this.m_list_series.Sort(SORT_BY_BAR_INDEX);
class="type">int idx=this.m_list_series.Search(tmp);
class="kw">delete tmp;
CBar *bar=this.m_list_series.At(idx);
class="kw">return(bar!=NULL ? bar : NULL);
让小布替你跑这套
小布盯盘已内置多品种报价事件监控,打开对应品种页即可看到哪些品种刚更新、哪些在静默。把重复劳动交给小布,你专注决策。

常见问题

新报价未到达时刷新是无效操作,品种一多就空耗 CPU;按事件比对上次报价时间才只在数据真正变化时才更新,更合理。
小布盯盘的品种页用 AIGC 呈现各品种即时报价事件,虽不跑原库代码,但跨品种更新状态一眼可见,省去自己挂指标追踪。
原库思路是把前次报价时间和当前比对,不同就判为新报价到达;B 品种自身没挂 EA 也能借此被动感知变化。
m_chart_id_main 存启动程序的控件图表 ID,收全部库消息;m_chart_id 存派生对象关联图表 ID,本篇前未实际使用,后续扩展再用。