DoEasy 函数库中的时间序列(第六十一部分):品种即时报价序列集合·进阶篇
📊

DoEasy 函数库中的时间序列(第六十一部分):品种即时报价序列集合·进阶篇

(2/3)· 单品种列表散落难维护?把各品种 Tick 序列收进一个集合,索引与更新一次搞定

含代码示例偏理论 第 2/3 篇
很多自行扩展函数库的人会为每个品种单独建列表,运行时散落各处,取数要先判断哪个列表存在。把各品种即时报价列表收进一个集合类,检索和更新都有统一入口,后续统计才不会乱。

「Tick序列的取用与开关控制」

在自建的 Tick 数据管理类里,按品种名拿到对应的序列对象是第一步。GetTickseries 先用 IndexTickSeries 定位下标,再从链表 m_list 里 At 取出指针,没命中就返回空。 单个品种的可用开关靠 SetAvailableTickSeries(symbol, flag) 控制,默认 flag=true 即启用;若对应序列不存在则直接 return,不会抛错。批量版本则遍历 m_list.Total() 个节点,逐个置位,循环里遇到空指针用 continue 跳过。 查询侧有对称设计:IsAvailableTickSeries(symbol) 查单品种,无对象返回 false;无参版本用 res &= tickseries.IsAvailable() 做按位与归约,只有全部品种都开启才返回 true。外汇与贵金属 tick 高频刷新,这类开关能降低不必要计算负载,但行情跳变仍属高风险,启停逻辑建议先在策略测试器验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the object of tick series of a specified symbol              |
class=class="str">"cmt">//+------------------------------------------------------------------+
CTickSeries *CTickSeriesCollection::GetTickseries(class="kw">const class="type">class="kw">string symbol)
  {
   class="type">int index=this.IndexTickSeries(symbol);
   class="kw">return this.m_list.At(index);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the flag of using the tick series of a specified symbol      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTickSeriesCollection::SetAvailableTickSeries(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">bool flag=true)
  {
   CTickSeries *tickseries=this.GetTickseries(symbol);
   if(tickseries==NULL)
      class="kw">return;
   tickseries.SetAvailable(flag);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the flag of using the tick series of all symbols              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTickSeriesCollection::SetAvailableTickSeries(class="kw">const class="type">bool flag=true)
  {
   for(class="type">int i=class="num">0;i<this.m_list.Total();i++)
     {
      CTickSeries *tickseries=this.m_list.At(i);
      if(tickseries==NULL)
         class="kw">continue;
      tickseries.SetAvailable(flag);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the flag of using the tick series of a specified symbol    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTickSeriesCollection::IsAvailableTickSeries(class="kw">const class="type">class="kw">string symbol)
  {
   CTickSeries *tickseries=this.GetTickseries(symbol);
   if(tickseries==NULL)
      class="kw">return class="kw">false;
   class="kw">return tickseries.IsAvailable();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the flag of using tick series of all symbols               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTickSeriesCollection::IsAvailableTickSeries(class="type">void)
  {
   class="type">bool res=true;
   class="type">int total=this.m_list.Total();
   for(class="type">int i=class="num">0;i<total;i++)
     {
      CTickSeries *tickseries=this.m_list.At(i);
      if(tickseries==NULL)
         class="kw">continue;
      res &=tickseries.IsAvailable();
     }
   class="kw">return res;
  }

按品种或全市场约束 tick 历史天数

做高频重放或 tick 级回测时,内存和加载耗时直接由历史天数决定。下面这组 CTickSeriesCollection 的成员函数,就是用来动态设定某个品种、或一次性设定所有品种所需 tick 天数的入口。 单品种版本 SetRequiredUsedDays(const string symbol, const uint required=0) 先通过 GetTickseries 拿到该品种的 tick 序列指针,拿不到就返回 false;拿到后调用序列自身的 SetRequiredUsedDays(required) 并回 true。required 默认 0,表示不额外限制、沿用既有的加载逻辑。 全市场版本只传一个 required 参数,用 for 循环遍历 m_list 里全部 CTickSeries 指针,逐个下发同样的天数约束;只要有一个指针为 NULL,就用 res &= false 记下失败但继续跑完,最后返回 res。这种“局部失败不中断”的写法,适合在 EA 初始化阶段批量预设几十个品种的天数。 取数接口有两个重载:按 index 取用 GetTick(symbol, index) 走列表下标,适合顺序扫描;按 tick_time 取用 GetTick(symbol, tick_time) 走时间定位,适合事件对齐。两者都先校验品种序列是否存在,不存在返回 NULL,调用方必须判空。外汇与贵金属 tick 数据量大、跳空频繁,实际设天数时建议先在策略测试器里用 1~3 天小样本验证指针与耗时,再放大到实盘可能需要的窗口。

MQL5 / C++
class="type">bool CTickSeriesCollection::SetRequiredUsedDays(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">uint required=class="num">0)
  {
   CTickSeries *tickseries=this.GetTickseries(symbol);
   if(tickseries==NULL)
      class="kw">return class="kw">false;
   tickseries.SetRequiredUsedDays(required);
   class="kw">return true;
  }
class="type">bool CTickSeriesCollection::SetRequiredUsedDays(class="kw">const class="type">uint required=class="num">0)
  {
   class="type">bool res=true;
   for(class="type">int i=class="num">0;i<this.m_list.Total();i++)
     {
      CTickSeries *tickseries=this.m_list.At(i);
      if(tickseries==NULL)
        {
         res &=class="kw">false;
         class="kw">continue;
        }
      tickseries.SetRequiredUsedDays(required);
     }
   class="kw">return res;
  }
CDataTick *CTickSeriesCollection::GetTick(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">int index)
  {
   CTickSeries *tickseries=this.GetTickseries(symbol);
   if(tickseries==NULL)
      class="kw">return NULL;
   class="kw">return tickseries.GetTickByListIndex(index);
  }
CDataTick *CTickSeriesCollection::GetTick(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">class="kw">datetime tick_time)
  {
   CTickSeries *tickseries=this.GetTickseries(symbol);
   if(tickseries==NULL)
      class="kw">return NULL;
   class="kw">return tickseries.GetTick(tick_time);
  }

◍ 按时间戳抓tick与批量建序列的实现细节

在 CTickSeriesCollection 类里,取某个品种最后一笔 tick 靠 GetTick() 完成:先通过 GetTickseries(symbol) 拿到该品种的序列指针,空指针直接返回 NULL,否则把毫秒时间戳交给序列对象去定位。 判断新 tick 用 IsNewTick(),逻辑同构——拿不到序列就返回 false,拿到就透传序列内部的新 tick 标志。这两个方法都不自己存数据,只是薄封装。 建序列有单品种和全市场两版。CreateTickSeries(symbol, required) 中 required 默认 0,代表不强制预取数量,返回 tickseries.Create(required)>0 的布尔结果;CreateTickSeriesAll(required) 则遍历 m_list 里全部序列,用 res &= 做逐条与运算,任一条创建失败都会让总结果变 false。 把下面代码贴进 MT5 的 MQL5 类实现里,能直接验证:若 required 传 1000,则每个序列倾向预载约 1000 个 tick 结构,外汇与贵金属 tick 流高风险,实盘前请用策略测试器跑一遍。

MQL5 / C++
class=class="str">"cmt">//| Return the last tick object of the specified symbol          |
class=class="str">"cmt">//| by time in milliseconds                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
CDataTick *CTickSeriesCollection::GetTick(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">long tick_time_msc)
  {
   CTickSeries *tickseries=this.GetTickseries(symbol);  class=class="str">"cmt">// 按品种名取序列指针
   if(tickseries==NULL)
      class="kw">return NULL;  class=class="str">"cmt">// 无序列直接空
   class="kw">return tickseries.GetTick(tick_time_msc);  class=class="str">"cmt">// 按毫秒时间取tick
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the new tick flag of a specified symbol                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTickSeriesCollection::IsNewTick(class="kw">const class="type">class="kw">string symbol)
  {
   CTickSeries *tickseries=this.GetTickseries(symbol);  class=class="str">"cmt">// 取序列指针
   if(tickseries==NULL)
      class="kw">return class="kw">false;  class=class="str">"cmt">// 无序列返回否
   class="kw">return tickseries.IsNewTick();  class=class="str">"cmt">// 透传新tick标志
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a tick series of a specified symbol                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTickSeriesCollection::CreateTickSeries(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">uint required=class="num">0)
  {
   CTickSeries *tickseries=this.GetTickseries(symbol);  class=class="str">"cmt">// 取序列指针
   if(tickseries==NULL)
      class="kw">return class="kw">false;  class=class="str">"cmt">// 无序列返回否
   class="kw">return(tickseries.Create(required)>class="num">0);  class=class="str">"cmt">// 建序列并判断数量>class="num">0
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create tick series of all symbols                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTickSeriesCollection::CreateTickSeriesAll(class="kw">const class="type">uint required=class="num">0)
  {
   class="type">bool res=true;  class=class="str">"cmt">// 总结果初值真
   class="type">int total=this.m_list.Total();  class=class="str">"cmt">// 序列总数
   for(class="type">int i=class="num">0;i<total;i++)  class=class="str">"cmt">// 遍历所有序列
     {
      CTickSeries *tickseries=this.m_list.At(i);  class=class="str">"cmt">// 取第i个序列
      if(tickseries==NULL)
         class="kw">continue;  class=class="str">"cmt">// 空则跳过
      res &=(tickseries.Create(required)>class="num">0);  class=class="str">"cmt">// 与运算累结果
     }
   class="kw">return res;  class=class="str">"cmt">// 返回总结果
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「逐行拆一个 tick 序列集合类的刷新与打印」

做多品种 tick 级监控时,把各品种 tick 序列塞进一个集合类统一管理,比在 EA 里散着调 CopyTicks 更稳。下面这段是集合类的几个核心成员,直接决定你刷新和排查时的行为边界。 先看单品种刷新入口:传入 symbol 后从集合里取对应 CTickSeries 指针,取不到就直接 return,不抛错。这意味着你传了个没注册过的品种名,代码会静默跳过,日志里什么都看不到,排查时容易误判成「行情没动」。

MQL5 / C++
class="type">void CTickSeriesCollection::Refresh(class="kw">const class="type">class="kw">string symbol)
  {
   CTickSeries *tickseries=this.GetTickseries(symbol);
   if(tickseries==NULL)
      class="kw">return;
   tickseries.Refresh();
  }
逐行拆解:第 1 行定义按品种名刷新的重载;第 3 行用 GetTickseries 拿该品种的 tick 序列对象指针;第 4–5 行空指针判断,没取到直接退出;第 6 行调用该序列自己的 Refresh 拉最新 tick。 全量刷新则是遍历 m_list,从索引 0 到 Total()-1,每个元素取到指针后判空,非空就调 Refresh。这种写法在挂了 20 个品种时,一次 Refresh() 就是 20 次底层 tick 同步,CPU 占用会随品种数线性走,实盘挂多品种前最好在 MT5 策略测试器里跑一遍看负载。
MQL5 / C++
class="type">void CTickSeriesCollection::Refresh(class="type">void)
  {
   for(class="type">int i=class="num">0;i<this.m_list.Total();i++)
     {
      CTickSeries *tickseries=this.m_list.At(i);
      if(tickseries==NULL)
         class="kw">continue;
      tickseries.Refresh();
     }
  }
Print 和 PrintShort 结构完全一致,区别只在后者调 tickseries.PrintShort() 打简版描述。注意循环里对 NULL 用的是 continue 而非 return,所以某个品种对象损坏不会导致整个集合打印中断,但日志里那个损坏项就是一片空白,得自己对照 m_list 顺序数对位置。 CreateCollection 的入口有个隐蔽点:list_symbols 为 NULL 直接返回 false,而 required 参数默认 0 表示不强制要求最少品种数。如果你期望「至少挂 5 个品种才启动逻辑」,不传 required=5 的话,传一个空壳列表也能建出空集合,后面跑策略可能直接因无数据而静默失效。外汇与贵金属 tick 数据受点差跳空影响,这类底层集合只解决「拿得到」,不解决「拿得准」,实盘前务必用真实点差环境验证。

MQL5 / C++
class="type">void CTickSeriesCollection::Refresh(class="kw">const class="type">class="kw">string symbol)
  {
   CTickSeries *tickseries=this.GetTickseries(symbol);
   if(tickseries==NULL)
      class="kw">return;
   tickseries.Refresh();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update tick series of all symbols                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTickSeriesCollection::Refresh(class="type">void)
  {
   for(class="type">int i=class="num">0;i<this.m_list.Total();i++)
     {
      CTickSeries *tickseries=this.m_list.At(i);
      if(tickseries==NULL)
         class="kw">continue;
      tickseries.Refresh();
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display complete collection description to the journal           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTickSeriesCollection::Print(class="type">void)
  {
   for(class="type">int i=class="num">0;i<this.m_list.Total();i++)
     {
      CTickSeries *tickseries=this.m_list.At(i);
      if(tickseries==NULL)
         class="kw">continue;
      tickseries.Print();
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Display the class="type">short collection description in the journal          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTickSeriesCollection::PrintShort(class="type">void)
  {
   for(class="type">int i=class="num">0;i<this.m_list.Total();i++)
     {
      CTickSeries *tickseries=this.m_list.At(i);
      if(tickseries==NULL)
         class="kw">continue;
      tickseries.PrintShort();
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Create a collection list of symbol tick series                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CTickSeriesCollection::CreateCollection(class="kw">const CArrayObj *list_symbols,class="kw">const class="type">uint required=class="num">0)
  {
class=class="str">"cmt">//--- If an empty list of symbol objects is passed, exit
   if(list_symbols==NULL)
      class="kw">return class="kw">false;
class=class="str">"cmt">//--- Get the number of symbol objects in the passed list
   class="type">int total=list_symbols.Total();

多品种 tick 序列的批量装载与去重

在初始化行情缓存时,先调用 this.m_list.Clear() 把旧 tick 序列集合清空,避免跨周期重载时残留上一轮品种对象。随后用 for(int i=0;i<total;i++) 遍历外部传入的 symbol 对象列表,逐个建立 CTickSeries 实例。 循环里对每个 symbol_obj 先做空指针判断,取不到对象就 continue 跳过;new CTickSeries() 失败同样跳过,保证单品种内存分配异常不会中断整批加载。接着 tickseries.SetSymbol(symbol_obj.Name()) 绑定品种名,并每次 this.m_list.Sort() 维持集合有序。 去重逻辑靠 this.m_list.Search(tickseries),若返回值大于 WRONG_VALUE 说明同品种已存在,直接 delete tickseries;否则进入 else 分支,用 tickseries.SetRequiredUsedDays(required) 设定回看天数(required 由调用方传入,常见取值 1~30 天),再尝试 Add 进集合,添加失败则释放对象。 函数末尾 return this.m_list.Total()>0 给出布尔标志:只要成功挂进至少一个品种,就认为集合可用。实际在 MT5 里跑这段代码时,若 total 为 50 而 required 设 7,则正常情况下 m_list.Total() 应等于 50,外汇与贵金属品种切换频繁,需警惕点差跳变导致个别 symbol_obj 取空。

MQL5 / C++
  class=class="str">"cmt">//--- Clear the tick series collection list
   this.m_list.Clear();
  class=class="str">"cmt">//--- In a loop by all symbol objects
   for(class="type">int i=class="num">0;i<total;i++)
     {
     class=class="str">"cmt">//--- get the next symbol object
      CSymbol *symbol_obj=list_symbols.At(i);
     class=class="str">"cmt">//--- if failed to get a symbol object, move on to the next one in the list
      if(symbol_obj==NULL)
         class="kw">continue;
     class=class="str">"cmt">//--- Create a new empty tick series object
      CTickSeries *tickseries=new CTickSeries();
     class=class="str">"cmt">//--- If failed to create the tick series object, move on to the next symbol in the list
      if(tickseries==NULL)
         class="kw">continue;
     class=class="str">"cmt">//--- Set a symbol name for a tick series object
      tickseries.SetSymbol(symbol_obj.Name());
     class=class="str">"cmt">//--- Set the sorted list flag for the tick series collection list
      this.m_list.Sort();
     class=class="str">"cmt">//--- If the object with the same symbol name is already present in the tick series collection list, remove the tick series object
      if(this.m_list.Search(tickseries)>WRONG_VALUE)
         class="kw">delete tickseries;
     class=class="str">"cmt">//--- otherwise, there is no object with such a symbol name in the collection yet
      else
        {
        class=class="str">"cmt">//--- Set the number of tick data days for a tick series object
         tickseries.SetRequiredUsedDays(required);
        class=class="str">"cmt">//--- if failed to add the tick series object to the collection list, remove the tick series object
         if(!this.m_list.Add(tickseries))
            class="kw">delete tickseries;
        }
     }
  class=class="str">"cmt">//--- Return the flag indicating that the created collection list has a size greater than zero
   class="kw">return this.m_list.Total()>class="num">0;

◍ 引擎基类如何挂载tick序列

CEngine 作为交易引擎的基类,把历史、市场、事件、账户、品种、时序、缓冲、指标、资源、交易控制等集合全部收进私有成员,统一在实例化时分配内存。 高亮新增的 TickSeriesCollection 头文件与 CTickSeriesCollection m_tick_series 成员,说明该架构在原生 K 线之外补上了逐笔 tick 序列容器,给高频重绘与微观价差分析留了接口。 SetUsedSymbols 的第三个参数 required 默认 0,用来在创建品种时序集合时声明最少需要的 tick 数量;若实盘品种 tick 供给不足,返回 false 的概率会显著上升,外汇与贵金属品种在高波动时段才容易喂满。 把下面这段直接贴进 MT5 的 include 链,就能在自有 EA 里复用该 tick 容器,省去自己写 tick 缓存的麻烦。

MQL5 / C++
class="macro">#include "Collections\EventsCollection.mqh"
class="macro">#include "Collections\AccountsCollection.mqh"
class="macro">#include "Collections\SymbolsCollection.mqh"
class="macro">#include "Collections\ResourceCollection.mqh"
class="macro">#include "Collections\TimeSeriesCollection.mqh"
class="macro">#include "Collections\BuffersCollection.mqh"
class="macro">#include "Collections\IndicatorsCollection.mqh"
class="macro">#include "Collections\TickSeriesCollection.mqh"
class="macro">#include "TradingControl.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Library basis class                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CEngine
  {
class="kw">private:
   CHistoryCollection    m_history;             class=class="str">"cmt">// Collection of historical orders and deals
   CMarketCollection     m_market;             class=class="str">"cmt">// Collection of market orders and deals
   CEventsCollection     m_events;             class=class="str">"cmt">// Event collection
   CAccountsCollection   m_accounts;           class=class="str">"cmt">// Account collection
   CSymbolsCollection    m_symbols;            class=class="str">"cmt">// Symbol collection
   CTimeSeriesCollection m_time_series;        class=class="str">"cmt">// Timeseries collection
   CBuffersCollection    m_buffers;            class=class="str">"cmt">// Collection of indicator buffers
   CIndicatorsCollection m_indicators;         class=class="str">"cmt">// Indicator collection
   CTickSeriesCollection m_tick_series;        class=class="str">"cmt">// Collection of tick series
   CResourceCollection   m_resource;           class=class="str">"cmt">// Resource list
   CTradingControl       m_trading;            class=class="str">"cmt">// Trading management object
   CPause                m_pause;              class=class="str">"cmt">// Pause object
   CArrayObj             m_list_counters;      class=class="str">"cmt">// List of timer counters
class=class="str">"cmt">//--- Set the list of used symbols in the symbol collection and create the collection of symbol timeseries
   class="type">bool                  SetUsedSymbols(class="kw">const class="type">class="kw">string &array_symbols[],class="kw">const class="type">uint required=class="num">0);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the list of used symbols in the symbol collection            |
class=class="str">"cmt">//| and create the symbol timeseries collection                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
交给小布盯盘看盘口
这些跨品种 Tick 归集与索引逻辑,小布盯盘的 AIGC 已内置到对应品种页,打开即可看到实时序列与异常提示,你只需专注策略判断。

常见问题

合并后可用品种名直接索引序列,避免散落判断,统一更新与释放,也方便做跨品种统计分析。
可以,小布盯盘对应品种页已内置多品种 Tick 归集视图,打开就能看到各序列的存储与刷新情况。
其底层 CListObj 重写了 Type() 虚方法,默认返回 0x7778 这类自定义数组类型 ID,便于在函数库对象体系中识别。
会创建一个临时对象设同名并搜排序列表后删除,开销很小,但保证了按品种名定位索引的简洁与一致。
上篇准备单品种即时报价列表类,本篇将其指针收进集合,形成一对多的管理结构,概念延续函数库已有集合模式。