DoEasy 函数库中的时间序列(第三十八部分):时间序列集合 - 实时更新以及从程序访问数据·进阶篇
(2/3)· 还在用 OnTimer 毫秒级刷新所有品种数据?这篇讲清如何只在新报价到达时精准更新集合
用类封装新报价事件检测
在 MT5 的 EA 或指标里,判断「是不是来了新 tick」最省事的做法不是每次都裸调 SymbolInfoTick,而是包一层对象。下面这个类从 CBaseObj 继承,内部存了当前 tick 和上一次检查的 tick 两个 MqlTick 结构,外加一个品种名和布尔标记。 私有成员 m_tick 与 m_tick_prev 分别承载本次与上次的价格快照,m_symbol 决定监听哪个品种,m_new_tick 是对外暴露的「新 tick」状态位。调用 Refresh() 会内部跑一次 IsNewTick() 并写回标记,逻辑集中不散落。 IsNewTick() 的实现里有几个细节值得注意:若 SymbolInfoTick 取数失败直接返 false;首次启动(m_first_start 为真)会把当前 tick 拷给 prev 并返 false,避免把初始化当成新事件;之后只要 m_tick.time_msc 不等于 m_tick_prev.time_msc,就更新 prev 并返回 true。用毫秒级时间比对,能抓到同一秒内多笔报价的情况,比用 time 字段更准。 开 MT5 把这段代码塞进你的 Include 目录,配合前一篇的 BaseObj,就能在 OnTick 里用 obj.Refresh() 后再判 IsNewTick() 来驱动逻辑,外汇与贵金属波动快、杠杆高,实盘前务必在策略测试器用真实点差回测。
class="macro">#include "..\..\Objects\BaseObj.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| "New tick" class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CNewTickObj : class="kw">public CBaseObj { class="kw">private: class="type">MqlTick m_tick; class=class="str">"cmt">// Structure of the current prices class="type">MqlTick m_tick_prev; class=class="str">"cmt">// Structure of the current prices during the previous check class="type">class="kw">string m_symbol; class=class="str">"cmt">// Object symbol class="type">bool m_new_tick; class=class="str">"cmt">// New tick flag class="kw">public: class=class="str">"cmt">//--- Set a symbol class="type">void SetSymbol(class="kw">const class="type">class="kw">string symbol) { this.m_symbol=symbol; } class=class="str">"cmt">//--- Return the new tick flag class="type">bool IsNewTick(class="type">void); class=class="str">"cmt">//--- Update price data in the tick structure and set the "New tick" event flag if necessary class="type">void Refresh(class="type">void) { this.m_new_tick=this.IsNewTick(); } class=class="str">"cmt">//--- Constructors CNewTickObj(class="type">void){;} CNewTickObj(class="kw">const class="type">class="kw">string symbol); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the new tick flag | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CNewTickObj::IsNewTick(class="type">void) { class=class="str">"cmt">//--- If failed to get the current prices to the tick structure, class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27; if(!::SymbolInfoTick(this.m_symbol,this.m_tick)) class="kw">return class="kw">false; class=class="str">"cmt">//--- If this is the first launch, copy data of the obtained tick to the previous tick data class=class="str">"cmt">//--- reset the first launch flag and class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27; if(this.m_first_start) { this.m_tick_prev=this.m_tick; this.m_first_start=class="kw">false; class="kw">return class="kw">false; } class=class="str">"cmt">//--- If the time of a new tick is not equal to the time of a tick during the previous check - class=class="str">"cmt">//--- copy data of the obtained tick to the previous tick data and class="kw">return &class="macro">#x27;true&class="macro">#x27; if(this.m_tick.time_msc!=this.m_tick_prev.time_msc)
◍ 新 tick 对象构造与事件枚举的落地写法
在 MT5 里做价格行为监控,第一件事是把「上一笔 tick」和「当前 tick」分开存。CNewTickObj 的带参构造函数就干了这件事:用 SymbolInfoTick 把当前报价塞进 m_tick,再立刻复制给 m_tick_prev,同时把 m_first_start 置为 false,避免首帧误触发信号。 构造函数里先用 ZeroMemory 把两个 MqlTick 结构清零,否则残留字段会让首根 K 线前的差值计算失真。若 SymbolInfoTick 返回成功,说明品种行情已可读,此时 prev 与 curr 同值,下一帧进来才有真实 delta。 事件侧只定义了一个轻量枚举 ENUM_SERIES_EVENT,目前仅含 SERIES_EVENTS_NO_EVENT 与 SERIES_EVENTS_NEW_BAR,基准码从 SYMBOL_EVENTS_NEXT_CODE 顺延,方便以后扩事件而不撞系统码。 定时器宏揭示了实际轮询节奏:交易类定时器暂停 300 毫秒、步进 16、计数器 ID 5;时间序列采集定时器暂停仅 32 毫秒、步进 16、计数器 ID 6。也就是说,K 线集合的刷新扫描比下单逻辑快近 10 倍,做贵金属或外汇高频判新 bar 时这个 32ms 间隔值得你打开 MT5 测一下实际丢 tick 概率。
{
this.m_tick_prev=this.m_tick;
class="kw">return true;
}
class=class="str">"cmt">//--- In all other cases, class="kw">return &class="macro">#x27;class="kw">false&class="macro">#x27;
class="kw">return class="kw">false;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Parametric constructor CNewTickObj |
class=class="str">"cmt">//+------------------------------------------------------------------+
CNewTickObj::CNewTickObj(class="kw">const class="type">class="kw">string symbol) : m_symbol(symbol)
{
class=class="str">"cmt">//--- Reset the structures of the new and previous ticks
::ZeroMemory(this.m_tick);
::ZeroMemory(this.m_tick_prev);
class=class="str">"cmt">//--- If managed to get the current prices to the tick structure,
class=class="str">"cmt">//--- copy data of the obtained tick to the previous tick data and reset the first launch flag
if(::SymbolInfoTick(this.m_symbol,this.m_tick))
{
this.m_tick_prev=this.m_tick;
this.m_first_start=class="kw">false;
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| List of possible timeseries events |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_SERIES_EVENT
{
SERIES_EVENTS_NO_EVENT = SYMBOL_EVENTS_NEXT_CODE, class=class="str">"cmt">// no event
SERIES_EVENTS_NEW_BAR, class=class="str">"cmt">// "New bar" event
};
class="macro">#define SERIES_EVENTS_NEXT_CODE(SERIES_EVENTS_NEW_BAR+class="num">1) class=class="str">"cmt">// Code of the next event after the "New bar" event
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Trading class timer parameters
class="macro">#define COLLECTION_REQ_PAUSE(class="num">300) class=class="str">"cmt">// Trading class timer pause in milliseconds
class="macro">#define COLLECTION_REQ_COUNTER_STEP(class="num">16) class=class="str">"cmt">// Trading class timer counter increment
class="macro">#define COLLECTION_REQ_COUNTER_ID(class="num">5) class=class="str">"cmt">// Trading class timer counter ID
class=class="str">"cmt">//--- Parameters of the timeseries collection timer
class="macro">#define COLLECTION_TS_PAUSE(class="num">32) class=class="str">"cmt">// Timeseries collection timer pause in milliseconds
class="macro">#define COLLECTION_TS_COUNTER_STEP(class="num">16) class=class="str">"cmt">// Account timer counter increment
class="macro">#define COLLECTION_TS_COUNTER_ID(class="num">6) class=class="str">"cmt">// Timeseries timer counter ID
class=class="str">"cmt">//--- Collection list IDs「K线对象怎么挂到时序集合里」
在 MT5 自建指标或 EA 里,若要把单根 Bar 封装成可管理的对象,先得给它一个集合身份。代码里用 0x777A~0x777F 这六个十六进制常量区分历史、行情、事件、账户、品种和时序集合,其中时序集合固定为 0x777F。 CBar 类的两个构造函数都先把 m_type 设为 COLLECTION_SERIES_ID(即 0x777F),这意味着这根 Bar 被归入‘时间序列集合’,后续批量存取或序列化时不会和历史/账户等列表混淆。 第一个构造函数靠 CopyRates(symbol,timeframe,index,1,rates_array) 去抓指定偏移的 1 根 K 线;若返回值小于 1 或时间结构转换失败,就打印错误码并把零值 MqlRates 写回数组,避免野指针。第二个构造函数直接吃外部传入的 MqlRates 引用,少一次内核调用,适合已在循环里拿到 rates 的高频场景。 开 MT5 按 F4 把下面片段塞进你的 Include,调 CopyRates 的 index 参数就能验证不同偏移 Bar 是否都带 0x777F 标记。外汇与贵金属波动剧烈,这类底层封装仅降低出错概率,不消除滑点风险。
class="macro">#define COLLECTION_HISTORY_ID(0x777A) class=class="str">"cmt">// Historical collection list ID class="macro">#define COLLECTION_MARKET_ID(0x777B) class=class="str">"cmt">// Market collection list ID class="macro">#define COLLECTION_EVENTS_ID(0x777C) class=class="str">"cmt">// Event collection list ID class="macro">#define COLLECTION_ACCOUNT_ID(0x777D) class=class="str">"cmt">// Account collection list ID class="macro">#define COLLECTION_SYMBOLS_ID(0x777E) class=class="str">"cmt">// Symbol collection list ID class="macro">#define COLLECTION_SERIES_ID(0x777F) class=class="str">"cmt">// Timeseries collection list ID class=class="str">"cmt">//--- Data parameters for file operations class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor class="num">1 | class=class="str">"cmt">//+------------------------------------------------------------------+ CBar::CBar(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index) { this.m_type=COLLECTION_SERIES_ID; class="type">MqlRates rates_array[class="num">1]; this.SetSymbolPeriod(symbol,timeframe,index); ::ResetLastError(); class=class="str">"cmt">//--- If failed to write bar data to the class="type">MqlRates array by index or set the time to the time structure, class=class="str">"cmt">//--- display an error message, create and fill the structure with zeros, and write it to the rates_array array if(::CopyRates(symbol,timeframe,index,class="num">1,rates_array)<class="num">1 || !::TimeToStruct(rates_array[class="num">0].time,this.m_dt_struct)) { class="type">int err_code=::GetLastError(); ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA),". ",CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ",CMessage::Retcode(err_code)); class="type">MqlRates err={class="num">0}; rates_array[class="num">0]=err; } class=class="str">"cmt">//--- Set the bar properties this.SetProperties(rates_array[class="num">0]); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor class="num">2 | class=class="str">"cmt">//+------------------------------------------------------------------+ CBar::CBar(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index,class="kw">const class="type">MqlRates &rates) { this.m_type=COLLECTION_SERIES_ID; this.SetSymbolPeriod(symbol,timeframe,index); ::ResetLastError();
时间序列对象的时间解析与接口骨架
在自定义时间序列类里,时间字段必须先经过 TimeToStruct 拆解成结构体才能用。若调用失败,代码会取 GetLastError 的错误码,打印包含错误描述的信息,然后用全零的 MqlRates 结构调用 SetProperties 兜底,并直接 return 退出,避免后续逻辑吃到脏数据。 兜底之后,正常分支只是把入参 rates 交给 SetProperties 完成属性赋值。这一段没有返回值,说明它属于对象内部的状态刷新,不是对外查询接口。 类对外暴露的接口值得在 MT5 里照着抄一遍:Create(required=0) 负责建序列,Refresh(time=0, open=0, high=0, low=0, close=0, tick_volume=0, volume=0, spread=0) 用全默认参数把任意一根 bar 推进去更新;SendEvent 会向主控图表发“新 bar”事件。外汇与贵金属行情跳空频繁,Refresh 的默认零值若误用,可能让序列出现虚假零价 bar,属高风险操作。 剩下的 Header、Print、PrintShort 以及两个构造函数(无参 / 带 symbol+timeframe+required)构成最小可用面。打开 MT5 的 MetaEditor,新建一个 CSeries 类把下列声明原样贴入,就能验证编译是否通过。
if(!::TimeToStruct(rates.time,this.m_dt_struct)) { class="type">int err_code=::GetLastError(); ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA),". ",CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ",CMessage::Retcode(err_code)); class="type">MqlRates err={class="num">0}; this.SetProperties(err); class="kw">return; } class=class="str">"cmt">//--- Set the bar properties this.SetProperties(rates); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- (class="num">1) Create and(class="num">2) update the timeseries list class="type">int Create(class="kw">const class="type">uint required=class="num">0); class="type">void Refresh(class="kw">const class="type">class="kw">datetime time=class="num">0, class="kw">const class="type">class="kw">double open=class="num">0, class="kw">const class="type">class="kw">double high=class="num">0, class="kw">const class="type">class="kw">double low=class="num">0, class="kw">const class="type">class="kw">double close=class="num">0, class="kw">const class="type">long tick_volume=class="num">0, class="kw">const class="type">long volume=class="num">0, class="kw">const class="type">int spread=class="num">0); class=class="str">"cmt">//--- Create and send the "New bar" event to the control program chart class="type">void SendEvent(class="type">void); class=class="str">"cmt">//--- Return the timeseries name class="type">class="kw">string Header(class="type">void); class=class="str">"cmt">//--- Display(class="num">1) the timeseries description and(class="num">2) the brief timeseries description in the journal class="type">void Print(class="type">void); class="type">void PrintShort(class="type">void); class=class="str">"cmt">//--- Constructors CSeries(class="type">void); CSeries(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">uint required=class="num">0); };
◍ 跨图表新K线事件与序列同步的底层实现
在 MT5 的自定义时间序列类里,新柱体生成后需要主动通知主控图表,否则依赖该序列的 EA 可能停留在上一根 bar 的逻辑里。下面这段把事件推送到图表,参数顺序很关键:图表 ID、自定义事件号、当前时间、周期、品种。
class="type">void CSeries::SendEvent(class="type">void) { ::EventChartCustom(this.m_chart_id_main,SERIES_EVENTS_NEW_BAR,this.Time(class="num">0),this.Timeframe(),this.Symbol()); }
class="type">void CSeries::SendEvent(class="type">void) { ::EventChartCustom(this.m_chart_id_main,SERIES_EVENTS_NEW_BAR,this.Time(class="num">0),this.Timeframe(),this.Symbol()); } class="type">bool CSeries::SyncData(class="kw">const class="type">uint required,class="kw">const class="type">uint rates_total) { class=class="str">"cmt">//--- If the timeseries is not used, notify of that and exit if(!this.m_available) { ::Print(DFUN,this.m_symbol," ",TimeframeDescription(this.m_timeframe),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_IS_NOT_USE)); class="kw">return class="kw">false; } class=class="str">"cmt">//--- If managed to obtain the available number of bars in the timeseries class="type">int CSeries::Create(class="kw">const class="type">uint required=class="num">0) { class=class="str">"cmt">//--- If the timeseries is not used, notify of that and class="kw">return zero if(!this.m_available) { ::Print(DFUN,this.m_symbol," ",TimeframeDescription(this.m_timeframe),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_IS_NOT_USE)); class="kw">return class="num">0; } class=class="str">"cmt">//--- If the required history depth is not set for the list yet, CBar *CSeries::GetBarBySeriesIndex(class="kw">const class="type">uint index) { CArrayObj *list=this.GetList(BAR_PROP_INDEX,index); class="kw">return(list==NULL || list.Total()==class="num">0 ? NULL : list.At(class="num">0)); } CBar *CSeries::GetBarBySeriesIndex(class="kw">const class="type">uint index) {
「用临时柱对象在序列列表里二分定位」
在自定义时间序列管理里,按索引取 Bar 不能遍历硬找,构造一个临时 CBar 丢进已排序的列表做 Search 才是正路。下面这段代码就是核心取法:先 new 一个同品种同周期、指定 index 的临时柱,空指针直接返回 NULL 防崩。 CBar *tmp=new CBar(this.m_symbol,this.m_timeframe,index); if(tmp==NULL) return NULL; this.m_list_series.Sort(SORT_BY_BAR_INDEX); int idx=this.m_list_series.Search(tmp); delete tmp; CBar *bar=this.m_list_series.At(idx); return(bar!=NULL ? bar : NULL); Sort 调用放在 Search 前是关键——列表若中途被改写过序,不重排 Search 会偏索引。Search 返回的是排序位,不是原始插入位,所以临时对象用完必须 delete,否则每查一次漏一个对象,跑 10 万根 K 线可能吃掉几十 MB。 承接上面 TimeSeries.mqh 的框架,CTimeSeries 类里挂了 m_new_tick(CNewTickObj 类型)和 m_list_series(CArrayObj 类型)。想在 MT5 验证定位效率,把 SORT_BY_BAR_INDEX 换成 SORT_BY_TIME 再跑 Search,同样品种周期下耗时倾向有明显差异,自己打 ticks 计数就看得见。
CBar *tmp=new CBar(this.m_symbol,this.m_timeframe,index); if(tmp==NULL) class="kw">return NULL; this.m_list_series.Sort(SORT_BY_BAR_INDEX); class="type">int idx=this.m_list_series.Search(tmp); class="kw">delete tmp; CBar *bar=this.m_list_series.At(idx); class="kw">return(bar!=NULL ? bar : NULL);