DoEasy 函数库中的时间序列(第六十部分):品种即时报价数据的序列列表·进阶篇
(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) 把结果列表也托管起来,防止内存泄漏。
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),外层务必判。
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。
{
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 流在高波动时段可能瞬间翻倍,此类查找务必放在独立线程或降频执行,相关交易存在高风险。
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 对象压入列表,返回实际入库数量。下面这段头文件展示了类的依赖关系与私有成员声明。
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 天数的数据已缓存完整,否则筛选结果可能缺段。
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 {