DoEasy 函数库中的时间序列(第三十七部分):时间序列集合 - 按品种和周期的时间序列数据库·进阶篇
🗃️

DoEasy 函数库中的时间序列(第三十七部分):时间序列集合 - 按品种和周期的时间序列数据库·进阶篇

(2/3)· 单一品种时序已不够用,如何用集合对象统管全市场多周期历史数据

实战向 第 2/3 篇
把每个品种的周期数据散落在各自对象里,回测时跨品种联动基本靠手写循环硬凑。一旦品种或周期数量上来,内存占用和取数逻辑会直接拖垮 EA 的响应速度。

◍ 库对象的扩展基类怎么搭

在 MT5 自建指标或面板时,给所有库对象写一个扩展基类能省掉大量重复代码。下面这段从实际工程里抽出来的 CBaseObjExt 就是典型做法:在 CBaseObj 基础上挂事件列表、Tick 结构和哈希校验字段。 宏 CONTROLS_TOTAL 写死为 10,意味着任意对象的属性二维数组列宽被锁死,超出的控件属性在编译期就会越界。私有里 m_long_prop_total / m_double_prop_total 记录两类属性的计数,FillPropertySettings 用模板把当前值与上次值写进 array 和 array_prev,顺带回写 event_id,方便后面比对异动。 受保护成员才是重点:m_tick 接实时报价,m_hash_sum 与 m_hash_sum_prev 做两轮数据指纹,只有哈希变了才置 m_is_event,m_event_code 和 m_event_id 再标记具体事件。外汇和贵金属波动快,这种哈希比对能把无效刷新压下去,但高频行情下仍可能漏判,属概率性过滤而非绝对保险。 开 MT5 建个空 EA 把这段类头塞进去,先只留 CONTROLS_TOTAL(10) 和 m_tick,编译过一遍就能确认你的环境跟这套基类契约不冲突。

MQL5 / C++
class="macro">#define  CONTROLS_TOTAL(class="num">10)
class CBaseObjExt : class="kw">public CBaseObj
  {
class="kw">private:
   class="type">int                m_long_prop_total;
   class="type">int                m_double_prop_total;
   class=class="str">"cmt">//--- Fill in the object class="kw">property array
   class="kw">template<class="kw">typename T> class="type">bool  FillPropertySettings(class="kw">const class="type">int index,T &array[][CONTROLS_TOTAL],T &array_prev[][CONTROLS_TOTAL],class="type">int &event_id);
class="kw">protected:
   CArrayObj         m_list_events_base;                     class=class="str">"cmt">// Object base event list
   CArrayObj         m_list_events;                          class=class="str">"cmt">// Object event list
   class="type">MqlTick           m_tick;                                 class=class="str">"cmt">// Tick structure for receiving quote data
   class="type">class="kw">double            m_hash_sum;                             class=class="str">"cmt">// Object data hash sum
   class="type">class="kw">double            m_hash_sum_prev;                        class=class="str">"cmt">// Object data hash sum during the previous check
   class="type">int               m_digits_currency;                     class=class="str">"cmt">// Number of decimal places in an account currency
   class="type">bool              m_is_event;                             class=class="str">"cmt">// Object event flag
   class="type">int               m_event_code;                           class=class="str">"cmt">// Object event code
   class="type">int               m_event_id;                             class=class="str">"cmt">// Event ID(equal to the object class="kw">property value)
  };

「事件追踪数组与基础取值接口」

控价逻辑里,先把被监控的属性变化存进二维数组,每一行对应一个属性索引,列宽由 CONTROLS_TOTAL 决定,共 10 列。第 0、1 列是预设的增/减阈值,第 2 列是控制位准,第 3、4 列存当前值与变动量,第 5~9 列则是五种越界/触位旗标,用来在后续判断里直接读位。 整数类与实数类分开存放:m_long_prop_event 和 m_double_prop_event 记当前帧,m_long_prop_event_prev 和 m_double_prop_event_prev 留上一帧,两帧相减才知道属性是往上还是往下跳。 取数接口已经写好,TickTime() 在 MQL5 下直接回传 time_msc 毫秒时间戳,MSCfromTime() 只剥出千毫秒内的尾数(MQL4 分支恒回 0)。IsPresentEventFlag() 用位与判断某个 change_code 是否已在事件码里置位,比挨个 bool 判断省事。 DigitsCurrency() 回传账户货币小数位,GetDigits(double) 按值动态算精度;数组尺寸由 SetControlDataArraySizeLong/Double 按监控属性数量扩,没扩够就写越界。开 MT5 把这段头文件贴进类定义,编译过说明你的 CONTROLS_TOTAL 至少是 10。

MQL5 / C++
class=class="str">"cmt">//--- Data for storing, controlling and returning tracked properties:
class=class="str">"cmt">//--- [Property index][class="num">0] Controlled class="kw">property increase value
class=class="str">"cmt">//--- [Property index][class="num">1] Controlled class="kw">property decrease value
class=class="str">"cmt">//--- [Property index][class="num">2] Controlled class="kw">property value level
class=class="str">"cmt">//--- [Property index][class="num">3] Property value
class=class="str">"cmt">//--- [Property index][class="num">4] Property value change
class=class="str">"cmt">//--- [Property index][class="num">5] Flag of a class="kw">property change exceeding the increase value
class=class="str">"cmt">//--- [Property index][class="num">6] Flag of a class="kw">property change exceeding the decrease value
class=class="str">"cmt">//--- [Property index][class="num">7] Flag of a class="kw">property increase exceeding the control level
class=class="str">"cmt">//--- [Property index][class="num">8] Flag of a class="kw">property decrease being less than the control level
class=class="str">"cmt">//--- [Property index][class="num">9] Flag of a class="kw">property value being equal to the control level
  class="type">long                m_long_prop_event[][CONTROLS_TOTAL];       class=class="str">"cmt">// The array for storing object&class="macro">#x27;s integer properties values and controlled class="kw">property change values
  class="type">class="kw">double              m_double_prop_event[][CONTROLS_TOTAL];     class=class="str">"cmt">// The array for storing object&class="macro">#x27;s real properties values and controlled class="kw">property change values
  class="type">long                m_long_prop_event_prev[][CONTROLS_TOTAL];  class=class="str">"cmt">// The array for storing object&class="macro">#x27;s controlled integer properties values during the previous check
  class="type">class="kw">double              m_double_prop_event_prev[][CONTROLS_TOTAL];class=class="str">"cmt">// The array for storing object&class="macro">#x27;s controlled real properties values during the previous check
class=class="str">"cmt">//--- Return(class="num">1) time in milliseconds, (class="num">2) milliseconds from the class="type">MqlTick time value
  class="type">long                TickTime(class="type">void)                                class="kw">const { class="kw">return class="macro">#ifdef __MQL5__ this.m_tick.time_msc class="macro">#else this.m_tick.time*class="num">1000 class="macro">#endif ;  }
  class="type">class="kw">ushort              MSCfromTime(class="kw">const class="type">long time_msc)             class="kw">const { class="kw">return class="macro">#ifdef __MQL5__ class="type">class="kw">ushort(this.TickTime()%class="num">1000) class="macro">#else class="num">0 class="macro">#endif ;          }
class=class="str">"cmt">//--- class="kw">return the flag of the event code presence in the event object
  class="type">bool                IsPresentEventFlag(class="kw">const class="type">int change_code) class="kw">const { class="kw">return (this.m_event_code & change_code)==change_code; }
class=class="str">"cmt">//--- Return the number of decimal places of the account currency
  class="type">int                 DigitsCurrency(class="type">void)                         class="kw">const { class="kw">return this.m_digits_currency; }
class=class="str">"cmt">//--- Returns the number of decimal places in the &class="macro">#x27;class="type">class="kw">double&class="macro">#x27; value
  class="type">int                 GetDigits(class="kw">const class="type">class="kw">double value)               class="kw">const;
class=class="str">"cmt">//--- Set the size of the array of controlled(class="num">1) integer and(class="num">2) real object properties
  class="type">bool                SetControlDataArraySizeLong(class="kw">const class="type">int size);
  class="type">bool                SetControlDataArraySizeDouble(class="kw">const class="type">int size);
class=class="str">"cmt">//--- Check the array size of object properties

把属性变化塞进 long 的字节里

这套对象事件封装里,最容易被忽略的是 UshortToLongUshortToByte:它们把 16 位的 ushort 值按字节位塞进 64 位 long 的指定偏移,避免为每个监控属性单独开数组。 CheckControlDataArraySize(bool check_long=true) 在每次写入前核对数组边界,默认只查 long 类属性数组;若你后续混用了 double 类属性,需手动传 false 补查,否则越界写入会让 MT5 终端在实时行情推送时静默丢事件。

三个模板方法 `SetControlledValueINC / DEC / LEVEL` 对应属性「增量、减量、控制阈值」三种监听态。内部用 `m_long_prop_event[property][012]` 存整数类,用 `m_double_prop_event[property-m_long_prop_total][012]` 存实数类——注意 double 类的索引要减去整数属性总数偏移。

取数接口全是 inline const:例如 GetControlledLongValueINC 直接返回 [property][0]GetControlledDoubleValueDEC 返回 [property-m_long_prop_total][1]。在 EA 的 OnTick 里轮询这些 getter,就能在不挂定时器的情况下捕获对象属性异动,外汇与贵金属波动快、杠杆高,此类监听仍可能因 tick 跳空漏掉瞬时变化,实盘前务必用历史回放验证命中率。

MQL5 / C++
class="type">bool CheckControlDataArraySize(class="type">bool check_long=true);
class=class="str">"cmt">//--- Check the list of object class="kw">property changes and create an event
class="type">void CheckEvents(class="type">void);
class=class="str">"cmt">//--- (class="num">1) Pack a &class="macro">#x27;class="type">class="kw">ushort&class="macro">#x27; number to a passed &class="macro">#x27;class="type">long&class="macro">#x27; number
class="type">long UshortToLong(class="kw">const class="type">class="kw">ushort ushort_value,class="kw">const class="type">uchar to_byte,class="type">long &long_value);
class="kw">protected:
class=class="str">"cmt">//--- (class="num">1) convert a &class="macro">#x27;class="type">class="kw">ushort&class="macro">#x27; value to a specified &class="macro">#x27;class="type">long&class="macro">#x27; number byte
class="type">long UshortToByte(class="kw">const class="type">class="kw">ushort value,class="kw">const class="type">uchar to_byte) class="kw">const;
class="kw">public:
class=class="str">"cmt">//--- Set the value of the pbject class="kw">property controlled(class="num">1) increase, (class="num">2) decrease, (class="num">3) control level
class="kw">template<class="kw">typename T> class="type">void SetControlledValueINC(class="kw">const class="type">int class="kw">property,class="kw">const T value);
class="kw">template<class="kw">typename T> class="type">void SetControlledValueDEC(class="kw">const class="type">int class="kw">property,class="kw">const T value);
class="kw">template<class="kw">typename T> class="type">void SetControlledValueLEVEL(class="kw">const class="type">int class="kw">property,class="kw">const T value);
class=class="str">"cmt">//--- Return the set value of the controlled(class="num">1) integer and(class="num">2) real object properties increase
class="type">long GetControlledLongValueINC(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_long_prop_event[class="kw">property][class="num">0]; }
class="type">class="kw">double GetControlledDoubleValueINC(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_double_prop_event[class="kw">property-this.m_long_prop_total][class="num">0]; }
class=class="str">"cmt">//--- Return the set value of the controlled(class="num">1) integer and(class="num">2) real object properties decrease
class="type">long GetControlledLongValueDEC(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_long_prop_event[class="kw">property][class="num">1]; }
class="type">class="kw">double GetControlledDoubleValueDEC(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_double_prop_event[class="kw">property-this.m_long_prop_total][class="num">1]; }
class=class="str">"cmt">//--- Return the specified control level of object&class="macro">#x27;s(class="num">1) integer and(class="num">2) real properties
class="type">long GetControlledLongValueLEVEL(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_long_prop_event[class="kw">property][class="num">2]; }

◍ 受控属性取值与变动标记的读取接口

在 MT5 的事件封装类里,整数与实数属性被分存于两个二维数组:m_long_prop_event 与 m_double_prop_event。实数属性因为排在整数属性之后,索引时要先减去 m_long_prop_total 做偏移,否则会越界读到错误内存。 下面这段是直接从类定义里抽出的读取方法,覆盖了当前值、变动值、以及增减越界标记三类。注意数组第二维的硬编码下标:3 是当前值、4 是变动值、5 是增量越界、6 是减量越界。 double GetControlledDoubleValueLEVEL(const int property) const { return this.m_double_prop_event[property-this.m_long_prop_total][2]; } // 返回受控实数属性的控制水位,下标 2 与其他读取接口错开,专用于 level 判定 long GetPropLongValue(const int property) const { return this.m_long_prop_event[property][3]; } double GetPropDoubleValue(const int property) const { return this.m_double_prop_event[property-this.m_long_prop_total][3]; } long GetPropLongChangedValue(const int property) const { return this.m_long_prop_event[property][4]; } double GetPropDoubleChangedValue(const int property) const { return this.m_double_prop_event[property-this.m_long_prop_total][4]; } long GetPropLongFlagINC(const int property) const { return this.m_long_prop_event[property][5]; } double GetPropDoubleFlagINC(const int property) const { return this.m_double_prop_event[property-this.m_long_prop_total][5]; } long GetPropLongFlagDEC(const int property) const { return this.m_long_prop_event[property][6]; } double GetPropDoubleFlagDEC(const int property) const { return this.m_double_prop_event[property-this.m_long_prop_total][6]; } 实盘写 EA 时,若你监控 OBJ_RECTANGLE 的价位变动,调用 GetPropDoubleFlagINC 拿到非零值,意味着该实数属性向上击穿了预设增量阈值,可据此触发告警或挂单逻辑。外汇与贵金属波动剧烈,这类阈值需随品种点值动态换算,盲目套用固定点数大概率在高波动时段频繁误触发。 开 MT5 把上面方法塞进你自己的 CObject 派生类,打印 m_long_prop_total 与数组实际长度,能直接验证偏移逻辑是否和你的属性枚举对齐。

MQL5 / C++
class="type">class="kw">double GetControlledDoubleValueLEVEL(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_double_prop_event[class="kw">property-this.m_long_prop_total][class="num">2]; }
class="type">long GetPropLongValue(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_long_prop_event[class="kw">property][class="num">3]; }
class="type">class="kw">double GetPropDoubleValue(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_double_prop_event[class="kw">property-this.m_long_prop_total][class="num">3]; }
class="type">long GetPropLongChangedValue(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_long_prop_event[class="kw">property][class="num">4]; }
class="type">class="kw">double GetPropDoubleChangedValue(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_double_prop_event[class="kw">property-this.m_long_prop_total][class="num">4]; }
class="type">long GetPropLongFlagINC(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_long_prop_event[class="kw">property][class="num">5]; }
class="type">class="kw">double GetPropDoubleFlagINC(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_double_prop_event[class="kw">property-this.m_long_prop_total][class="num">5]; }
class="type">long GetPropLongFlagDEC(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_long_prop_event[class="kw">property][class="num">6]; }
class="type">class="kw">double GetPropDoubleFlagDEC(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_double_prop_event[class="kw">property-this.m_long_prop_total][class="num">6]; }

「属性越界与事件标记的取数接口」

在盯盘类的内部存储里,整数型与双精度型属性各自维护了一张事件矩阵。整数属性走 m_long_prop_event[property][索引],双精度属性因为被压在整数属性之后,取数时要先减去 m_long_prop_total 偏移量再定位 m_double_prop_event。 越界判断分三种状态:索引 7 是「增幅超过控制线」的标记,8 是「降幅低于控制线」,9 是「等于控制线」。GetPropLongFlagMORE 返回 m_long_prop_event[property][7],GetPropDoubleFlagLESS 则返回 m_double_prop_event[property-m_long_prop_total][8],这套映射在回测里能直接区分价格行为是破位还是触线。 事件系统方面,EventAdd 负责把带 lparam/dparam/sparam 的底层事件塞进列表,EventBaseAdd 多带一个 ENUM_BASE_EVENT_REASON 用来标注触发原因。SetEvent 只是翻转 m_is_event 这个布尔旗标,告诉你这一帧对象数据里到底有没有事发生。 外汇与贵金属波动大、杠杆高,这类属性监视逻辑只解决「信号有没有」,不解决「该不该下单」,实盘前务必在 MT5 策略测试器里跑一遍确认索引不越界。

MQL5 / C++
class="type">long GetPropLongFlagMORE(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_long_prop_event[class="kw">property][class="num">7]; }
class="type">class="kw">double GetPropDoubleFlagMORE(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_double_prop_event[class="kw">property-this.m_long_prop_total][class="num">7]; }
class="type">long GetPropLongFlagLESS(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_long_prop_event[class="kw">property][class="num">8]; }
class="type">class="kw">double GetPropDoubleFlagLESS(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_double_prop_event[class="kw">property-this.m_long_prop_total][class="num">8]; }
class="type">long GetPropLongFlagEQUAL(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_long_prop_event[class="kw">property][class="num">9]; }
class="type">class="kw">double GetPropDoubleFlagEQUAL(class="kw">const class="type">int class="kw">property) class="kw">const { class="kw">return this.m_double_prop_event[class="kw">property-this.m_long_prop_total][class="num">9]; }
class="type">void ResetChangesParams(class="type">void);
class="kw">virtual class="type">void ResetControlsParams(class="type">void);
class="type">bool EventAdd(class="kw">const class="type">class="kw">ushort event_id,class="kw">const class="type">long lparam,class="kw">const class="type">class="kw">double dparam,class="kw">const class="type">class="kw">string sparam);
class="type">bool EventBaseAdd(class="kw">const class="type">int event_id,class="kw">const ENUM_BASE_EVENT_REASON reason,class="kw">const class="type">class="kw">double value);
class="type">void SetEvent(class="kw">const class="type">bool flag) { this.m_is_event=flag; }

事件对象的接口与魔数位布局

在 MT5 自定义事件系统里,CBaseObj 派生类通过一组 getter 把内部状态暴露给上层逻辑。IsEvent() 用 m_is_event 标志区分当前实例是否承载了事件;GetListEvents() 直接返回 m_list_events 指针,外部可遍历对象事件链表而不必拷贝。 GetEventsTotal() 的实现只有一行:return this.m_list_events.Total(); 这意味着事件计数完全依赖底层 CArrayObj 的节点数,若你在 Refresh() 前调用,拿到的可能是上一次扫描的残留值。 EventDescription() 是个多参纯虚风格接口,入参包含 property、reason、source、value、property_descr 与 digits,用来拼出人类可读的事件说明,digits 控制浮点字段的小数位,写日志或推送到「小布盯盘」面板时别漏传。 魔数(magic number)的 int 值被切成 4 个字节:bit 32|31–24 为 byte3,23–16 为 byte2,15–8 为 byte1,7–0 为 byte0。把订单识别信息塞进 magic 时,按字节规划能避免和平台保留位撞车,外汇与贵金属杠杆高,误判 magic 可能导致平仓逻辑错配,实盘前务必在策略测试器跑通。 Refresh() 标了 virtual,子类应重写它来拉取最新盘面或对象变动,再调用基类的 CBaseObj::Refresh() 做统一登记,否则事件链可能断在中间层。

MQL5 / C++
  class="type">bool                IsEvent(class="type">void)                class="kw">const { class="kw">return this.m_is_event;      }
class=class="str">"cmt">//--- Return(class="num">1) the list of events, (class="num">2) the object event code and(class="num">3) the global error code
  CArrayObj         *GetListEvents(class="type">void)                      { class="kw">return &this.m_list_events;     }
  class="type">int                GetEventCode(class="type">void)             class="kw">const { class="kw">return this.m_event_code;      }
class=class="str">"cmt">//--- Return(class="num">1) an event object and(class="num">2) a base event by its number in the list
  CEventBaseObj    *GetEvent(class="kw">const class="type">int shift=WRONG_VALUE,class="kw">const class="type">bool check_out=true);
  CBaseEvent       *GetEventBase(class="kw">const class="type">int index);
class=class="str">"cmt">//--- Return the number of(class="num">1) object events
  class="type">int                GetEventsTotal(class="type">void)            class="kw">const { class="kw">return this.m_list_events.Total();   }
class=class="str">"cmt">//--- Update the object data to search for changes(Calling from the descendants: CBaseObj::Refresh())
  class="kw">virtual class="type">void       Refresh(class="type">void);
class=class="str">"cmt">//--- Return an object event description
  class="type">class="kw">string             EventDescription(class="kw">const class="type">int class="kw">property,
                                     class="kw">const ENUM_BASE_EVENT_REASON reason,
                                     class="kw">const class="type">int source,
                                     class="kw">const class="type">class="kw">string value,
                                     class="kw">const class="type">class="kw">string property_descr,
                                     class="kw">const class="type">int digits);
class=class="str">"cmt">//--- Data location in the magic number class="type">int value
  class=class="str">"cmt">//-----------------------------------------------------------
  class=class="str">"cmt">//  bit   class="num">32|class="num">31        class="num">24|class="num">23        class="num">16|class="num">15         class="num">8|class="num">7         class="num">0|
  class=class="str">"cmt">//-----------------------------------------------------------
  class=class="str">"cmt">//  byte     |      class="num">3      |      class="num">2      |      class="num">1      |      class="num">0      |
  class=class="str">"cmt">//-----------------------------------------------------------
交给小布盯盘看盘口
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多周期历史加载状态,你只需专注策略逻辑而非底层数据搬运。

常见问题

基础层只保留最小属性和方法,扩展层才加事件与交互逻辑,避免每个对象都背负用不到的冗余代码,编译体积和运行开销都更可控。
可以,小布盯盘的 AIGC 模块已内置常见多周期历史诊断,打开品种页即能看到集合层面的加载与缺口提示,不必自己写取数接口。
单品种对象只装一个符号的各周期柱线,集合对象把它们按品种和周期再包一层,形成程序内全市场历史数据的统一入口。
集合的存储与按需取数逻辑已占大量篇幅,实时推送和增量刷新留到第三篇,避免一篇里同时塞进架构与事件导致难以调试。