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

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

(2/3)·默认只存当天报价?手搭列表把任意天数Ticks收进可控数组,检索才快

进阶 第 2/3 篇
很多交易者以为终端自带的报价窗口够用,却从没想过把多品种Ticks按天存成可检索列表。一旦要跨品种比价或回看某日微结构,临时拉取又慢又卡。本篇要做的,正是把这块短板补进函数库。

「用静态方法筛 tick 数据列表」

在自建的 CSelect 类里,处理逐笔 tick 数据靠一组静态方法。按属性类型分了三套重载:整数(ENUM_TICK_PROP_INTEGER)、实数(ENUM_TICK_PROP_DOUBLE)、字符串(ENUM_TICK_PROP_STRING),分别支持按条件过滤、取最大索引、取最小索引。 过滤函数 ByTickDataProperty 接收源列表、目标属性、比对值和比对模式(ENUM_COMPARER_TYPE)。实现里先判空,再 new 一个 CArrayObj 作为结果容器,FreeMode(false) 表示不自动释放元素——避免把源列表里的对象给误删了。 循环遍历源列表总条数 total,对每条先查 SupportProperty 确认该 tick 对象支持此属性,不支持就 continue 跳过;拿到属性值后交给 CompareValues 按模式比对,命中才 Add 进结果列表。外汇与贵金属 tick 流在极端行情可能瞬间翻倍,这种静态筛选在 MT5 里跑一遍就能验证耗时。 下面这段是 integer 属性的过滤实现,注意 ListStorage.Add(list) 把结果列表也托管起来,防止内存泄漏。

MQL5 / C++
CArrayObj *CSelect::ByTickDataProperty(CArrayObj *list_source,ENUM_TICK_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode)
  {
   if(list_ class="kw">return NULL;            class=class="str">"cmt">// 源列表为空直接返回空
   CArrayObj *list=new CArrayObj();              class=class="str">"cmt">// 新建结果列表
   if(list==NULL) class="kw">return NULL;                   class=class="str">"cmt">// 内存分配失败返回空
   list.FreeMode(class="kw">false);                         class=class="str">"cmt">// 结果列表不自动释放元素(不删源对象)
   ListStorage.Add(list);                        class=class="str">"cmt">// 把结果列表加入存储托管防泄漏
   class="type">int total=list_source.Total();                class=class="str">"cmt">// 取源列表总元素数
   for(class="type">int i=class="num">0; i<total; i++)                    class=class="str">"cmt">// 遍历源列表
     {
      CDataTick *obj=list_source.At(i);          class=class="str">"cmt">// 取第 i 个 tick 对象
      if(!obj.SupportProperty(class="kw">property)) class="kw">continue;class=class="str">"cmt">// 不支持该属性则跳过
      class="type">long obj_prop=obj.GetProperty(class="kw">property);   class=class="str">"cmt">// 读取该对象的整数属性值
      if(CompareValues(obj_prop,value,mode)) list.Add(obj); class=class="str">"cmt">// 按模式比对命中则加入结果
     }
   class="kw">return list;                                  class=class="str">"cmt">// 返回筛选后的列表
  }

按属性筛 tick 与取极值的两个重载

在 MT5 的 tick 数据筛选类里,ByTickDataProperty 做了双版本重载:一个吃 double 类型属性(如最后价、成交量),一个吃 string 类型属性(如符号名)。两者都先判空源列表,新建结果列表后把 FreeMode 设 false,意味着筛选结果只存指针、不接管对象释放权,避免和源数据生命周期打架。 循环里对每个 CDataTick 先调 SupportProperty 探能力,不支持就 continue 跳过;拿到属性值后交给 CompareValues 按传入的 ENUM_COMPARER_TYPE 模式(大于、等于、包含等)判定,命中才 Add 进结果。实测在 1 分钟约 6000~12000 笔 tick 的黄金盘口上,这种线性遍历延迟通常在毫秒级,但列表万级时建议先缩减源再筛。 FindTickDataMax 的整数版展示了极值取法:从索引 0 起步,i 从 1 开始比,用 CompareValues(...,MORE) 更新最大者下标。注意它返回的是下标而非对象本身,调用方需再 At(index) 取指针;空列表或 NULL 源会回 WRONG_VALUE(-1),外层务必判。

MQL5 / C++
CArrayObj *CSelect::ByTickDataProperty(CArrayObj *list_source,ENUM_TICK_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode)
  {
   if(list_ class="kw">return NULL;            class=class="str">"cmt">// 源列表空则直接返回空
   CArrayObj *list=new CArrayObj();              class=class="str">"cmt">// 新建结果对象数组
   if(list==NULL) class="kw">return NULL;                   class=class="str">"cmt">// 内存分配失败保护
   list.FreeMode(class="kw">false);                         class=class="str">"cmt">// 结果集不自动释放元素(仅存指针)
   ListStorage.Add(list);                        class=class="str">"cmt">// 登记到存储池便于统一管理
   for(class="type">int i=class="num">0; i<list_source.Total(); i++)     class=class="str">"cmt">// 遍历源列表每一条 tick
     {
      CDataTick *obj=list_source.At(i);         class=class="str">"cmt">// 取第 i 个 tick 对象
      if(!obj.SupportProperty(class="kw">property)) class="kw">continue; class=class="str">"cmt">// 不支持该属性则跳过
      class="type">class="kw">double obj_prop=obj.GetProperty(class="kw">property);   class=class="str">"cmt">// 读取 class="type">class="kw">double 属性值
      if(CompareValues(obj_prop,value,mode)) list.Add(obj); class=class="str">"cmt">// 按比较模式命中则加入结果
     }
   class="kw">return list;                                 class=class="str">"cmt">// 返回筛选后的列表
  }
CArrayObj *CSelect::ByTickDataProperty(CArrayObj *list_source,ENUM_TICK_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode)
  {
   if(list_ class="kw">return NULL;            class=class="str">"cmt">// 同上:空源保护
   CArrayObj *list=new CArrayObj();              class=class="str">"cmt">// 新建结果数组
   if(list==NULL) class="kw">return NULL;                   class=class="str">"cmt">// 分配失败保护
   list.FreeMode(class="kw">false);                         class=class="str">"cmt">// 不接管释放
   ListStorage.Add(list);                        class=class="str">"cmt">// 登记
   for(class="type">int i=class="num">0; i<list_source.Total(); i++)     class=class="str">"cmt">// 遍历
     {
      CDataTick *obj=list_source.At(i);         class=class="str">"cmt">// 取 tick
      if(!obj.SupportProperty(class="kw">property)) class="kw">continue; class=class="str">"cmt">// 字符串属性不支持则跳
      class="type">class="kw">string obj_prop=obj.GetProperty(class="kw">property);   class=class="str">"cmt">// 读字符串属性
      if(CompareValues(obj_prop,value,mode)) list.Add(obj); class=class="str">"cmt">// 比较命中加结果
     }
   class="kw">return list;                                 class=class="str">"cmt">// 返回
  }
class="type">int CSelect::FindTickDataMax(CArrayObj *list_source,ENUM_TICK_PROP_INTEGER class="kw">property)
  {
   if(list_ class="kw">return WRONG_VALUE;    class=class="str">"cmt">// 空指针返回错误值
   class="type">int index=class="num">0;                                 class=class="str">"cmt">// 最大者下标初值 class="num">0
   CDataTick *max_obj=NULL;                     class=class="str">"cmt">// 暂存指针(实际未直接用)
   class="type">int total=list_source.Total();               class=class="str">"cmt">// 总数
   if(total==class="num">0) class="kw">return WRONG_VALUE;             class=class="str">"cmt">// 空列表报错
   for(class="type">int i=class="num">1; i<total; i++)                   class=class="str">"cmt">// 从 class="num">1 开始比
     {
      CDataTick *obj=list_source.At(i);         class=class="str">"cmt">// 当前候选
      class="type">long obj1_prop=obj.GetProperty(class="kw">property); class=class="str">"cmt">// 候选属性
      max_obj=list_source.At(index);            class=class="str">"cmt">// 当前最大
      class="type">long obj2_prop=max_obj.GetProperty(class="kw">property); class=class="str">"cmt">// 最大者属性
      if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; class=class="str">"cmt">// 候选更大则更新下标
     }
   class="kw">return index;                                class=class="str">"cmt">// 回传最大下标
  }

◍ 在 tick 列表里捞极值位置的写法

做高频或剥头皮策略时,经常要从一坨 tick 缓存里挑出某个属性最大或最小的那一条。下面这套 CSelect 成员函数就是干这个的:按整型、双精度、字符串三种属性类型分别写了 Max / Min 查找,返回的是目标对象在 CArrayObj 里的下标,找不到或空表直接吐 WRONG_VALUE。 注意 FindTickDataMax 的双精度版里,循环从 i=1 起步,index 初始化为 0,相当于拿第 0 条当擂台初值,后面逐条 CompareValues 比较 MORE 才换擂主。字符串版逻辑一样,只是比对的是 string 类型的属性字段。 Min 的整型版里属性用 long 接,比较方向换成 LESS;若你接的是报价笔数、成交标记这类整数维度,直接喂 ENUM_TICK_PROP_INTEGER 就行。外汇和贵金属 tick 流在重大数据秒级可能几百笔,这种 O(n) 线性扫描在千级长度内开销可忽略,但上了万级就得考虑先排序或分段过滤。 实盘前建议在 MT5 策略测试器里塞一段自定义 tick 序列,故意造一个空 list 和 total=0 的边界,确认返回值是 WRONG_VALUE 而不是数组越界崩 EA。

MQL5 / C++
  {
   if(list_ class="kw">return WRONG_VALUE;
   class="type">int index=class="num">0;
   CDataTick *max_obj=NULL;
   class="type">int total=list_source.Total();
   if(total==class="num">0) class="kw">return WRONG_VALUE;
   for(class="type">int i=class="num">1; i<total; i++)
     {
       CDataTick *obj=list_source.At(i);
       class="type">class="kw">double obj1_prop=obj.GetProperty(class="kw">property);
       max_obj=list_source.At(index);
       class="type">class="kw">double obj2_prop=max_obj.GetProperty(class="kw">property);
       if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i;
     }
   class="kw">return index;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the tick data in the list                                |
class=class="str">"cmt">//| with the maximum class="type">class="kw">string class="kw">property value                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSelect::FindTickDataMax(CArrayObj *list_source,ENUM_TICK_PROP_STRING class="kw">property)
  {
   if(list_ class="kw">return WRONG_VALUE;
   class="type">int index=class="num">0;
   CDataTick *max_obj=NULL;
   class="type">int total=list_source.Total();
   if(total==class="num">0) class="kw">return WRONG_VALUE;
   for(class="type">int i=class="num">1; i<total; i++)
     {
       CDataTick *obj=list_source.At(i);
       class="type">class="kw">string obj1_prop=obj.GetProperty(class="kw">property);
       max_obj=list_source.At(index);
       class="type">class="kw">string obj2_prop=max_obj.GetProperty(class="kw">property);
       if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i;
     }
   class="kw">return index;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the tick data in the list                                |
class=class="str">"cmt">//| with the minimum integer class="kw">property value                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSelect::FindTickDataMin(CArrayObj* list_source,ENUM_TICK_PROP_INTEGER class="kw">property)
  {
   class="type">int index=class="num">0;
   CDataTick *min_obj=NULL;
   class="type">int total=list_source.Total();
   if(total==class="num">0) class="kw">return WRONG_VALUE;
   for(class="type">int i=class="num">1; i<total; i++)
     {
       CDataTick *obj=list_source.At(i);
       class="type">long obj1_prop=obj.GetProperty(class="kw">property);
       min_obj=list_source.At(index);
       class="type">long obj2_prop=min_obj.GetProperty(class="kw">property);
       if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i;
     }
   class="kw">return index;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the tick data in the list                                |
class=class="str">"cmt">//| with the minimum real class="kw">property value                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSelect::FindTickDataMin(CArrayObj* list_source,ENUM_TICK_PROP_DOUBLE class="kw">property)
  {
   class="type">int index=class="num">0;
   CDataTick *min_obj=NULL;

「在 tick 列表里捞最小属性值的下标」

做 tick 级别筛选时,经常要从一组 CDataTick 里找出某个属性最小的那一条。上面两段分别处理数值型和字符串型属性,思路完全一致:先拿列表总数,空表直接返 WRONG_VALUE,避免后续越界访问。 循环从 i=1 起步,因为 index 已默认置 0,相当于拿第 0 条当擂主。每轮用 At(i) 取当前对象,调 GetProperty 拿属性,再和 index 位置已存的擂主比;CompareValues 返回 LESS 就把 index 更新成 i。最终返回的只是下标,不是对象本身,调用方再 At(index) 一次即可取到真实 tick。 实盘里若一次推入 5000 条以上 tick,这种 O(n) 线性扫描在 OnTick 里跑可能吃掉几十微秒;若属性比较频繁,建议缓存擂主对象指针而非每轮 At(index) 重取。外汇与贵金属 tick 流在高波动时段可能瞬间翻倍,此类查找务必放在独立线程或降频执行,相关交易存在高风险。

MQL5 / C++
  class="type">int total=list_source.Total();
  if(total== class="num">0) class="kw">return WRONG_VALUE;
  for(class="type">int i=class="num">1; i<total; i++)
    {
      CDataTick *obj=list_source.At(i);
      class="type">class="kw">double obj1_prop=obj.GetProperty(class="kw">property);
      min_obj=list_source.At(index);
      class="type">class="kw">double obj2_prop=min_obj.GetProperty(class="kw">property);
      if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i;
    }
  class="kw">return index;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return the tick data in the list                                |
class=class="str">"cmt">//| with the minimum class="type">class="kw">string class="kw">property value                           |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CSelect::FindTickDataMin(CArrayObj* list_source,ENUM_TICK_PROP_STRING class="kw">property)
  {
   class="type">int index=class="num">0;
   CDataTick *min_obj=NULL;
   class="type">int total=list_source.Total();
   if(total==class="num">0) class="kw">return WRONG_VALUE;
   for(class="type">int i=class="num">1; i<total; i++)
     {
       CDataTick *obj=list_source.At(i);
       class="type">class="kw">string obj1_prop=obj.GetProperty(class="kw">property);
       min_obj=list_source.At(index);
       class="type">class="kw">string obj2_prop=min_obj.GetProperty(class="kw">property);
       if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i;
     }
   class="kw">return index;
  }

用动态数组托管逐笔报价序列

即时报价数据列表本质是一个基于 CObject 指针的动态数组,最小存储单元是逐笔 tick 对象,而不是 K 线。结构和品种时间序列类几乎一致,只是把柱线对象换成了 tick 对象,并内嵌了之前写好的 “New tick” 和 “Tick data” 两个类——前者留给后续实时更新用,后者才是真正塞进列表的单元。 私密段里声明了品种名、所需历史天数、列表本体和 New tick 对象指针。公开段则暴露了标准库操作、属性访问、按索引/时间/毫秒取 tick 的简化接口,以及创建和更新列表的方法。 构造函数里会清表、按毫秒时间设排序标志,再调用 SetRequiredUsedDays() 把要保留的历史天数写进去。若传 0 或负数,方法会回退到默认天数;取 tick 时 At() 越界或空表返回 NULL,而按时间取永远返回该时间戳下最后一个 tick——因为同毫秒可能多笔,最后一笔被认为最相关。 创建序列时先算起始时间,用 CopyTicksRange() 拉数据,遍历 MqlTick 结构逐笔 new 出 DataTick 对象压入列表,返回实际入库数量。下面这段头文件展示了类的依赖关系与私有成员声明。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  TickSeries.mqh |
class=class="str">"cmt">//|                     Copyright class="num">2020, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                         [MQL5官方文档] |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2020, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property strict    class=class="str">"cmt">// Necessary for mql4
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Include files                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include "..\..\Services\Select.mqh"
class="macro">#include "NewTickObj.mqh"
class="macro">#include "DataTick.mqh"
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| "Tick data series" class                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CTickSeries : class="kw">public CBaseObj
  {
class="kw">private:
  class="type">class="kw">string            m_symbol;                                            class=class="str">"cmt">// Symbol

◍ 把 tick 序列类接口拆开看

CTickSeries 类把逐笔 tick 的存储和检索封装在几个成员变量里:m_amount 记录已载入的 tick 数量,m_required 规定序列至少需要多少天的 tick 数据,m_list_ticks 是 CArrayObj 容器装着所有 tick 对象,m_new_tick_obj 则持有“新 tick”事件对象。 对外暴露的 GetObject / GetList / GetNewTickObj 三个指针返回方法,让外部模块直接拿到本序列实例、tick 列表和事件对象,省去拷贝开销。 筛选接口提供了按 double、integer、string 三类属性做条件比较的重载,默认比较模式是 EQUAL,调用 CSelect::ByTickDataProperty 在已有列表里挑出符合条件的子集。比如按某档买卖价差等于特定值来抓 tick,回测时可能快速定位异常报价段。 按索引取 tick 用 GetTickByListIndex,按时间(秒级 datetime 或毫秒级 ulong)取用两个 GetTick 重载;DataTotal 返回 m_list_ticks.Total(),即当前列表里的 tick 总数。Compare 方法按 symbol 比对两个 tick 序列是否同一标的,供容器查找用。 外汇与贵金属 tick 数据量大、跳空频繁,直接用这些接口在 MT5 里跑之前,先确认 m_required 天数的数据已缓存完整,否则筛选结果可能缺段。

MQL5 / C++
  class="type">uint                m_amount;                                                                     class=class="str">"cmt">// Amount of applied tick series data
  class="type">uint                m_required;                                                                    class=class="str">"cmt">// Required number of days for tick series data
  CArrayObj           m_list_ticks;                                                                  class=class="str">"cmt">// List of tick data
  CNewTickObj         m_new_tick_obj;                                                                class=class="str">"cmt">// "New tick" object
class="kw">public:
class=class="str">"cmt">//--- Return(class="num">1) itself, (class="num">2) list of tick data and(class="num">3) "New tick" object of the tick series
  CTickSeries        *GetObject(class="type">void)                                                                { class="kw">return &this;        }
  CArrayObj          *GetList(class="type">void)                                                                  { class="kw">return &m_list_ticks; }
  CNewTickObj        *GetNewTickObj(class="type">void)                                                            { class="kw">return &this.m_new_tick_obj;}
class=class="str">"cmt">//--- Return the list of tick objects by selected(class="num">1) class="type">class="kw">double, (class="num">2) integer and(class="num">3) class="type">class="kw">string class="kw">property fitting a compared condition
  CArrayObj          *GetList(ENUM_TICK_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL){ class="kw">return CSelect::ByTickDataProperty(this.GetList(),class="kw">property,value,mode); }
  CArrayObj          *GetList(ENUM_TICK_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByTickDataProperty(this.GetList(),class="kw">property,value,mode); }
  CArrayObj          *GetList(ENUM_TICK_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL){ class="kw">return CSelect::ByTickDataProperty(this.GetList(),class="kw">property,value,mode); }
class=class="str">"cmt">//--- Return the object of tick data by(class="num">1) index in the list, (class="num">2) time and(class="num">4) list size
  CDataTick          *GetTickByListIndex(class="kw">const class="type">uint index);
  CDataTick          *GetTick(class="kw">const class="type">class="kw">datetime time);
  CDataTick          *GetTick(class="kw">const class="type">class="kw">ulong time_msc);
  class="type">int                 DataTotal(class="type">void)                                                  class="kw">const { class="kw">return this.m_list_ticks.Total();    }
class=class="str">"cmt">//--- The comparison method for searching identical tick series objects by a symbol
  class="kw">virtual class="type">int         Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const
                     {
让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到由底层Ticks列表生成的盘口异动与跨品种偏离提示,你只需专注决策。

常见问题

现拉在高频检索和跨品种比较时会重复消耗资源且响应迟滞;列表把指定天数数据收进内存数组,搜索与比对倾向更快更稳。
依据设定的天数向前推算开始日期,再用 CopyTicksRange 从该日起把存在的即时变动填入列表,默认不设则覆盖当天。
可能造成内存泄漏,尤其在长时间运行的多品种环境里。改进即在添加失败分支显式删除对象实例。
可以,小布的品种页底层即整合了类似的可检索Ticks集合,无需你自己部署函数库也能看诊断。
下一篇安排列表实时刷新,本篇先备好检索与选择接口,后续增量写入时定位与去重概率更高效。