DoEasy 函数库中的时间序列(第三十八部分):时间序列集合 - 实时更新以及从程序访问数据(基础篇)
◍ 让集合自己跟着报价跑
DoEasy 函数库把不同周期、不同品种的时间序列装进一个集合对象,关键不在存了多少根 K 线,而在它能不能跟着实时报价自己往前推。原先每来一个 tick 都要外部显式调用刷新,现在库内部挂了“新即时报价”事件,报价一进来集合就重算对应序列的末尾柱,省掉手动同步的麻烦。 实际跑起来,在 MT5 里加载基于该集合的 EA,肉眼能看到 M1 和 M5 的末根蜡烛随报价跳动同步延长,不会出现“图表动了、EA 里的序列还停在上一个 tick”的错位。这类实时性对剥头皮和跨周期过滤信号直接有用,但外汇和贵金属杠杆高、滑点突兀,实时更新快不代表胜率,只代表你拿到的是当前价而非上一帧。 从程序访问数据走的是集合下标 + 周期标识的方式,不用再各自维护几个数组。下面这段是库里处理新报价时触发序列更新的核心片段,看它怎么把 tick 路由到具体周期序列。
报价刷新不能靠傻等定时器
早先构建的时间序列集合类能按品种和周期缓存历史数据,并支持快速检索排序,但这只解决了“存量”问题。程序跑起来之后,每个所用品种的当前数据必须在新报价到来时刷新,否则分析用的就是过期快照。 最简单的做法是在 EA 的 OnTimer() 里按毫秒周期统一刷新全部序列。但这样会带来无谓开销:计时器到点就更新,并不管报价到底有没有动,性能上不划算。 当前图表品种的报价事件我们是清楚的——EA 的 OnTick() 或指标的 OnCalculate() 被调用即代表有新 tick。可程序里引用的其他品种若没挂 EA/指标,就无法被动感知其价格变动。 库内实用的轻量解法是比对“前次报价时间”和“当前报价时间”:两者不同,就判定该品种来了新报价(即便程序是挂在别的图表上启动的)。在写“新报价”类和实时刷新逻辑前,先把手上的类做点小改进。
「把消息路由钉死在主图表上」
DoEasy 函数库里所有对象的基类 CBaseObj 原先带两个图表 ID 变量:m_chart_id_main 存的是启动 EA 的控制图表 ID,m_chart_id 存派生对象关联的图表 ID,后者此前基本没被用到。问题出在新增 m_chart_id_main 之后,消息仍被发往 m_chart_id,导致事件没回到控制程序的那张图。 修复方式很直接——把事件类、集合类里所有发消息的 "m_chart_id" 全替换成 "m_chart_id_main"。涉及 \MQL5\Include\DoEasy\Objects\Events\ 下的全部事件类,以及 AccountsCollection.mqh、EventsCollection.mqh、SymbolsCollection.mqh 这几个集合类,改动后所有库内消息都会归集到主图表。 柱线对象的日志输出也顺手理了一遍。CBar 类在 Bar.mqh 中新增 ParameterDescription() 方法,原先嵌在日志打印里的参数描述代码被抽出来,显示时由对象简称加该方法生成的文本拼成复合消息,便于在日志里一眼看清某根非当前品种柱线的开高低收和时间。 对于程序启动后才挂上的「非本名」时间序列,作者倾向用新建的「新即时报价」类,只在 New tick 事件触发时按需刷新对应品种数据,避免无谓的周期性重算。外汇与贵金属市场杠杆高、滑点大,这类底层刷新逻辑改完请在 MT5 用真实品种切图验证事件是否只落在主图表。
class=class="str">"cmt">//--- CTimeSeries MSG_LIB_TEXT_TS_TEXT_FIRST_SET_SYMBOL, class=class="str">"cmt">// First, set a symbol using SetSymbol() MSG_LIB_TEXT_TS_TEXT_IS_NOT_USE, class=class="str">"cmt">// Timeseries is not used. Set the flag using SetAvailable() MSG_LIB_TEXT_TS_TEXT_UNKNOWN_TIMEFRAME, class=class="str">"cmt">// Unknown timeframe {"Сначала нужно установить символ при помощи SetSymbol()","First you need to set Symbol using SetSymbol()"}, {"Таймсерия не используется. Нужно установить флаг использования при помощи SetAvailable()","Timeseries not used. Need to set usage flag using SetAvailable()"}, {"Неизвестный таймфрейм","Unknown timeframe"}, class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Base object class for all library objects | class=class="str">"cmt">//+------------------------------------------------------------------+ class CBaseObj : class="kw">public CObject { class="kw">protected: ENUM_LOG_LEVEL m_log_level; class=class="str">"cmt">// Logging level ENUM_PROGRAM_TYPE m_program; class=class="str">"cmt">// Program type class="type">bool m_first_start; class=class="str">"cmt">// First launch flag class="type">bool m_use_sound; class=class="str">"cmt">// Flag of playing the sound set for an object class="type">bool m_available; class=class="str">"cmt">// Flag of using a descendant object in the program class="type">int m_global_error; class=class="str">"cmt">// Global error code class="type">long m_chart_id_main; class=class="str">"cmt">// Control program chart ID class="type">long m_chart_id; class=class="str">"cmt">// Chart ID
◍ 给K线对象写一套可读的属性描述
在 MT5 自定义类里给 CBar 这类 K 线对象做封装时,光存 OHLC 不够,还得让对象能自己报出「我是谁、我长什么样」。下面这段私有成员就承担了命名与归类:m_name 是对象名,m_folder_name 指向存放派生对象的文件夹,m_sound_name 存触发音效文件名,m_type 对应集合 ID 标识类型。 公开接口则把「描述」拆成三类:GetPropertyDescription 分别重载整型、双精度、字符串三种 ENUM_BAR_PROP,把 Bar 的属性常量翻成中文可读串;BodyTypeDescription 返回实体类型说明;Print 可按 full_prop 开关打印全部或仅支持的属性到日志,PrintShort 只吐简短描述。 真正落地输出的是 ParameterDescription,它拼出「时间, O/H/L」一行。注意 dg 取 m_digits,若品种无小数位则兜底为 1,避免 DoubleToString 报错。 别让日志变成乱码堆 调用前确认 m_digits 已被正确赋值,否则 EURUSD 这类 5 位小数品种可能被截成 1 位,回测时你很难一眼核对点位。
class="type">class="kw">string m_name; class=class="str">"cmt">// Object name class="type">class="kw">string m_folder_name; class=class="str">"cmt">// Name of the folder storing CBaseObj descendant objects class="type">class="kw">string m_sound_name; class=class="str">"cmt">// Object sound file name class="type">int m_type; class=class="str">"cmt">// Object type(corresponds to the collection IDs) class="kw">public: class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Descriptions of bar object properties | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Get description of a bar&class="macro">#x27;s(class="num">1) integer, (class="num">2) real and(class="num">3) class="type">class="kw">string properties class="type">class="kw">string GetPropertyDescription(ENUM_BAR_PROP_INTEGER class="kw">property); class="type">class="kw">string GetPropertyDescription(ENUM_BAR_PROP_DOUBLE class="kw">property); class="type">class="kw">string GetPropertyDescription(ENUM_BAR_PROP_STRING class="kw">property); class=class="str">"cmt">//--- Return the bar type description class="type">class="kw">string BodyTypeDescription(class="type">void) class="kw">const; class=class="str">"cmt">//--- Send description of bar properties to the journal(full_prop=true - all properties, class="kw">false - only supported ones) class="type">void Print(class="kw">const class="type">bool full_prop=class="kw">false); class=class="str">"cmt">//--- Display a class="type">short bar description in the journal class="kw">virtual class="type">void PrintShort(class="type">void); class=class="str">"cmt">//--- Return the(class="num">1) class="type">short name and(class="num">2) description of bar object parameters class="kw">virtual class="type">class="kw">string Header(class="type">void); class="type">class="kw">string ParameterDescription(class="type">void); class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the bar object parameters | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CBar::ParameterDescription(class="type">void) { class="type">int dg=(this.m_digits>class="num">0 ? this.m_digits : class="num">1); class="kw">return ( ::TimeToString(this.Time(),TIME_DATE|TIME_MINUTES|TIME_SECONDS)+", "+ "O: "+::DoubleToString(this.Open(),dg)+", "+ "H: "+::DoubleToString(this.High(),dg)+", "+ "L: "+::DoubleToString(this.Low(),dg)+", "+
把单根 K 线压成一行日志
在 MT5 里做价格行为复盘,最怕日志刷出来一堆看不懂的结构。上面这段 CBar 类的打印逻辑,把一根 bar 的核心字段拼成短串:开盘价、最高价、最低价、收盘价用 DoubleToString 控精度,Tick 成交量与真实成交量(VolumeReal)分开列,最后补一根 BodyTypeDescription 说明实体类型。 PrintShort 方法更狠,只调 Header 加 ParameterDescription 两截,直接在 Journal 里输出「时间头: C=x.xx, H=x.xx … V=xx, R=xx, 实体类型」这种单行。你开 MT5 新建个 EA 把这段塞进 OnTick,每根收盘就能肉眼扫一遍,不用点开数据窗口。 外汇与贵金属波动受杠杆放大,这类日志仅用于结构观察,不代表任何方向概率判定,实盘前请在策略测试器跑历史样本核对字段是否缺漏。
"C: "+::DoubleToString(this.Close(),dg)+", "+ "V: "+(class="type">class="kw">string)this.VolumeTick()+", "+ (this.VolumeReal()>class="num">0 ? "R: "+(class="type">class="kw">string)this.VolumeReal()+", " : "")+ this.BodyTypeDescription() ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display a class="type">short bar description in the journal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CBar::PrintShort(class="type">void) { ::Print(this.Header(),": ",this.ParameterDescription()); } class=class="str">"cmt">//+------------------------------------------------------------------+
「"新即时报价"类和数据更新」
在 \MQL5\Include\DoEasy\ Objects\ 里,创建 Ticks\ 文件夹,其内文件 NewTickObj.mqh 包含 派生自所有 CBaseObj 函数库基准对象的 CNewTickObj 类( 其文件包含在类文件中 ),并填充必要的数据: m_tick 变量存储最后到达的即时报价数据。 m_tick_prev 变量存储前一次即时报价的数据。 m_symbol 变量存储需要跟踪其新即时报价的品种。 新即时报价标志保存在 m_new_tick 变量,稍后会用到。 对于当前的函数库需求,品种产生的 “New tick” 事件由 IsNewTick() 方法来定义: 该类含有两个已定义的构造函数: 默认没有参数的构造函数可在另一个类中定义“新即时报价”对象。 在这种情况下,利用 SetSymbol() 类方法为 CNewTickObj 类对象设置品种,并为其定义“新即时报价”事件。 参数型构造函数通过 new 运算符创建类对象。 在这种情况下,在创建对象时可立即为所创建对象指定品种。 这是新即时报价对象的整个类。 该思路很简单:在即时报价结构里设置价格,比较新到达即时报价与前一个的时间。 如果这些时间不相等,则新的即时报价已到来。 在 EA 中可能会忽略即时报价,但在此这并不重要。 我们能够在计时器中的“非本名”品种上跟踪新的即时报价,从而仅在新即时报价到达时才刷新数据,而不是由计时器不断地进行更新。 如果一个指标可以跟踪所有批量到达的即时报价,则应在 OnCalculate() 应对程序中完成指标所在品种当前时间序列数据的刷新。 在计时器中跟踪“非本名”品种的新即时报价(在 OnOnCalculate() 中无法接收“非本名”品种的新即时报价事件),因此,仅跟踪“非本名”品种的新旧即时报价之间的时间差就足够了,从而能够及时刷新时间序列数据。 令 CSeries 时间序列对象能将其 “New bar” 事件发送到控制程序。 这会令我们能够从程序中的任何时间序列里获取此类事件,并对其进行响应。 在 Defines.mqh 清单文件的末尾,添加 新的枚举,以及可能的时间序列对象事件的列表 : 在此,我们只有两个时间序列事件状态:“无事件”和“新柱线”事件。 我们需要这些枚举常量,从而能够按 指定属性 搜索柱线集合列表(CSeries 时间序列)中的柱线对象。 鉴于时间序列对象是在函数库计时器里刷新的,因此 将刷新时间序列对象集合的定时器参数,与 时间序列集合列表 ID 一起添加到 Defines.mqh 清单文件中 : 在创建 CEngine 函数库基准对象 时,我们已研究过集合计时器参数,而在 重新编排函数库结构 时,已讲述过集合 ID 的作用。 因为时间序列对象是一个列表,其内包含指向属于该列表的柱线对象指针,故应立即将时间序列集合 ID 分配给柱线对象。 再次打开 \MQL5\Include\DoEasy\Objects\Series\ Bar.mqh ,然后将 对象类型 添加到两个构造函数当中: 现在,我们来改进 \MQL5\Include\DoEasy\Objects\Series\Series.mqh 里的 CSeries 时间序列对象类。 在类的公开部分中,声明 将事件发送到控制程序图表的新方法 : 在类清单的末尾,实现声明的方法: 在此,我们 创建并将事件发送到控制程序图表 。 该事件包括: 事件接收者的图表 ID , 事件 ID (新柱线), 发送新柱线的创立时间,作为一个 long 型的事件参数 , 将发生事件