DoEasy 函数库中的时间序列(第四十部分):基于函数库的指标 - 实时刷新数据·进阶篇
(2/3)· 当新柱线令索引整体偏移,多周期指标如何不重算历史而稳取实时数据
不少开发者在写多周期指标时,习惯按柱线索引循环取数,却在实时刷新时撞上索引整体加一的坑:新柱一来,旧逻辑全乱。本文承接上篇,继续深挖 DoEasy 库里时间序列类的改造思路。
用指定时间抓取单根 K 线的构造逻辑
在 MT5 的自定义库里,CBar 的第一个构造函数允许你按『品种 + 周期 + 具体时间』定位某一根 K 线,而不是用索引倒推。这在回测或复盘某次 CPI 行情时很实用:直接传时间就能拿到那一根的 MqlRates。 构造函数内部先声明一个长度为 1 的 MqlRates 数组,调用 CopyRates(symbol, timeframe, time, 1, rates_array) 试图按时间取一根。若返回值小于 1,说明该时间点上对应周期没有形成完整 Bar(比如周末或流动性断层),此时会打印错误码并把一个 time 字段设为原请求时间、其余字段置 0 的结构写回数组,避免后续空引用崩掉。 拿到 rates_array[0] 后,还会用 TimeToStruct 把 Bar 时间拆成 tm 结构存到 m_dt_struct。若这一步失败(返回 false),同样打印系统错误码。两条错误分支都走 GetLastError + Print,方便在『小布盯盘』日志里直接看到是哪一根、哪个周期、什么错误。外汇与贵金属市场高杠杆、跳空频繁,这种按时间取 Bar 的方式在重大数据夜可能取不到,需做好零值结构的兜底。
CBar::CBar(const class="type">class="kw">string symbol,const ENUM_TIMEFRAMES timeframe,const class="type">class="kw">datetime time,const class="type">class="kw">string source) { this.m_type=COLLECTION_SERIES_ID; class="type">MqlRates rates_array[class="num">1]; this.SetSymbolPeriod(symbol,timeframe,time); ::ResetLastError(); class=class="str">"cmt">//--- If failed to get the requested data by time and write bar data to the class="type">MqlRates array, 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,time,class="num">1,rates_array)<class="num">1) { class="type">int err_code=::GetLastError(); ::Print( DFUN,"(class="num">1)-> ",source,symbol," ",TimeframeDescription(timeframe)," ",::TimeToString(time),": ", 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=class="str">"cmt">//--- Set the requested bar time to the structure with zero fields class="type">MqlRates err={class="num">0}; err.time=time; rates_array[class="num">0]=err; } ::ResetLastError(); class=class="str">"cmt">//--- If failed to set time to the time structure, display the error message if(!::TimeToStruct(rates_array[class="num">0].time,this.m_dt_struct)) { class="type">int err_code=::GetLastError(); ::Print( DFUN,"(class="num">1) ",symbol," ",TimeframeDescription(timeframe)," ",::TimeToString(time),": ", CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE),". ", CMessage::Text(MSG_LIB_SYS_ERROR),"> ",CMessage::Text(err_code)," ", CMessage::Retcode(err_code) ); } class=class="str">"cmt">//--- Set the bar properties
「从 MqlRates 构造 K 线对象的容错细节」
CBar 的第二个构造函数直接吃进 symbol、timeframe 和一份 MqlRates 引用,先把 m_type 标成 COLLECTION_SERIES_ID,再调用 SetSymbolPeriod 把品种、周期和 rates.time 绑进去。
绑时间这步不是必然成功。代码里先用 ::ResetLastError() 清掉错误码,再跑 ::TimeToStruct(rates.time, this.m_dt_struct);若返回 false,就通过 ::GetLastError() 拿到错误码并打印失败信息,随后建一个全零的 MqlRates err={0},只把 err.time=rates.time 填回原值,用这份残缺结构调 SetProperties 后直接 return。这样即使时间结构写不进,对象也不会崩,只是属性里只有时间是对的。
时间结构写成功后,才走正常的 this.SetProperties(rates) 把开高低收、成交量等全量属性灌进对象。
构造完之后有一组轻量访问器,直接转发 GetProperty:TypeBody() 取 BAR_PROP_TYPE,Timeframe() 取 BAR_PROP_PERIOD 并转回 ENUM_TIMEFRAMES,Spread() 取 BAR_PROP_SPREAD 转 int。在 MT5 里你只要 #include 这个头文件,用 CBar bar(_Symbol,_Period,rates[0]) 就能拿到一根带容错能力的 K 线对象,调 bar.Spread() 可立刻看到当前 bar 点差(外汇、贵金属杠杆品种点差跳动大,属高风险观测值)。
CBar::CBar(const class="type">class="kw">string symbol,const ENUM_TIMEFRAMES timeframe,const class="type">MqlRates &rates) { this.m_type=COLLECTION_SERIES_ID; this.SetSymbolPeriod(symbol,timeframe,rates.time); ::ResetLastError(); class=class="str">"cmt">//--- If failed to set time to the time structure, display the error message, class=class="str">"cmt">//--- create and fill the structure with zeros, set the bar properties from this structure and exit if(!::TimeToStruct(rates.time,this.m_dt_struct)) { class="type">int err_code=::GetLastError(); ::Print( DFUN,"(class="num">2) ",symbol," ",TimeframeDescription(timeframe)," ",::TimeToString(rates.time),": ", CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE),". ", CMessage::Text(MSG_LIB_SYS_ERROR),"> ",CMessage::Text(err_code)," ", CMessage::Retcode(err_code) ); class=class="str">"cmt">//--- Set the requested bar time to the structure with zero fields class="type">MqlRates err={class="num">0}; err.time=rates.time; this.SetProperties(err); class="kw">return; } class=class="str">"cmt">//--- Set the bar properties this.SetProperties(rates); } ENUM_BAR_BODY_TYPE TypeBody(class="type">void) const { class="kw">return (ENUM_BAR_BODY_TYPE)this.GetProperty(BAR_PROP_TYPE); } ENUM_TIMEFRAMES Timeframe(class="type">void) const { class="kw">return (ENUM_TIMEFRAMES)this.GetProperty(BAR_PROP_PERIOD); } class="type">int Spread(class="type">void) const { class="kw">return (class="type">int)this.GetProperty(BAR_PROP_SPREAD); }
◍ K线时间维度与成交量的取值接口
在 MT5 的自定义 K 线封装类里,时间相关属性和成交量属性都通过 GetProperty 统一抽取,只是传入的枚举不同。下面这组方法覆盖了从tick成交量、真实成交量到年/月/日/时等时间切片的全套读取。 VolumeTick 返回 BAR_PROP_VOLUME_TICK,即该根 K 线内的报价 tick 总数;VolumeReal 对应 BAR_PROP_VOLUME_REAL,是券商提供的真实成交手数(部分品种可能为 0)。两者都是 long 型,对比它们能粗略判断流动性是否被经纪商补全。 Time 方法把 BAR_PROP_TIME 强转为 datetime 返回开盘时间;Year、Month、Day 分别取 BAR_PROP_TIME_YEAR / MONTH / DAY,得到绝对日期分量。DayOfWeek 返回 0~6 的星期序号,DayOfYear 是年内第几天,Hour 取 0~23 的小时数——这些在写会话过滤(比如只做伦敦时段 8~17 点)时直接调用即可,不用自己算结构体。 开 MT5 新建 EA 把这段贴进你的 Bar 类,打印 Hour() 和 VolumeReal() 对照 EURUSD 的 M15,能立刻看到真实成交量在大部分零售券商处长期为 0,此时只能拿 VolumeTick 当代理变量。外汇与贵金属杠杆高,用 tick 量做信号须配合止损,信号失效概率不低。
class="type">long VolumeTick(class="type">void) const { class="kw">return this.GetProperty(BAR_PROP_VOLUME_TICK); } class="type">long VolumeReal(class="type">void) const { class="kw">return this.GetProperty(BAR_PROP_VOLUME_REAL); } class="type">class="kw">datetime Time(class="type">void) const { class="kw">return (class="type">class="kw">datetime)this.GetProperty(BAR_PROP_TIME); } class="type">long Year(class="type">void) const { class="kw">return this.GetProperty(BAR_PROP_TIME_YEAR); } class="type">long Month(class="type">void) const { class="kw">return this.GetProperty(BAR_PROP_TIME_MONTH); } class="type">long DayOfWeek(class="type">void) const { class="kw">return this.GetProperty(BAR_PROP_TIME_DAY_OF_WEEK); } class="type">long DayOfYear(class="type">void) const { class="kw">return this.GetProperty(BAR_PROP_TIME_DAY_OF_YEAR); } class="type">long Day(class="type">void) const { class="kw">return this.GetProperty(BAR_PROP_TIME_DAY); } class="type">long Hour(class="type">void) const { class="kw">return this.GetProperty(BAR_PROP_TIME_HOUR); }
CBar 里取分钟、序号与跨周期定位的实现
在 MQL5 的 CBar 封装里,单根 K 线的元数据被拆成多个只读属性。Minute() 直接回传 BAR_PROP_TIME_MINUTE,拿到该根 bar 所处的分钟数;Symbol() 回传 BAR_PROP_SYMBOL,确认这根 bar 属于哪个交易品种——这两个调用在写多品种监控时基本每 tick 都会用到。 Index() 有两个重载:无参版本返回 BAR_PROP_INDEX,即当前图表周期下的 bar 序号;带 ENUM_TIMEFRAMES 参数的版本则调用 iBarShift,把当前 bar 的时间映射到指定周期上去找对应序号。比如当前在 M1,传 PERIOD_H1 就能知道这根分钟 bar 落在哪根小时 bar 里,对做多周期共振过滤很有用。 Header() 把品种、周期描述、序号拼成短名,方便日志或对象树显示;GetPropertyDescription() 则对整型属性做可读化,BAR_PROP_INDEX 若不被底层支持会回显“不支持”。在 MT5 里建个 CBar 实例打印 Header(),能直接看到形如“Bar "EURUSD" H1[123]”的输出,验证你的周期映射对不对。 外汇与贵金属杠杆高,跨周期定位算错可能引发行情误判,实盘前务必用历史数据回测这套映射逻辑。
class="type">long Minute(class="type">void) const { class="kw">return this.GetProperty(BAR_PROP_TIME_MINUTE); } class="type">long Index(class="type">void) const { class="kw">return this.GetProperty(BAR_PROP_INDEX); } class=class="str">"cmt">//--- Return bar symbol class="type">class="kw">string Symbol(class="type">void) const { class="kw">return this.GetProperty(BAR_PROP_SYMBOL); } class=class="str">"cmt">//--- Return bar index on the specified timeframe the bar time falls into class="type">int Index(const ENUM_TIMEFRAMES timeframe=PERIOD_CURRENT) const { class="kw">return ::iBarShift(this.Symbol(),(timeframe>PERIOD_CURRENT ? timeframe : this.Timeframe()),this.Time()); } class="type">class="kw">string CBar::Header(class="type">void) { class="kw">return ( CMessage::Text(MSG_LIB_TEXT_BAR)+" \""+this.GetProperty(BAR_PROP_SYMBOL)+"\" "+ TimeframeDescription((ENUM_TIMEFRAMES)this.GetProperty(BAR_PROP_PERIOD))+"["+(class="type">class="kw">string)this.Index()+"]" ); } class="type">class="kw">string CBar::GetPropertyDescription(ENUM_BAR_PROP_INTEGER class="kw">property) { class="kw">return ( class="kw">property==BAR_PROP_INDEX ? CMessage::Text(MSG_LIB_TEXT_BAR_INDEX)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==BAR_PROP_TYPE ? CMessage::Text(MSG_ORD_TYPE)+
「K线整型属性的可读化输出」
在自定义 CBar 类里,GetPropertyDescription 负责把枚举类型的整型属性翻成交易者能直接读的文字,而不是冷冰冰的数字代号。它用嵌套三元运算符按 property 分支处理,覆盖时间、类型、周期、点差、tick量、真实量、年、月等 8 类属性。 每个分支都先调 SupportProperty 判断当前品种或历史模式是否支持该属性;不支持就拼上“不支持”提示,支持则取具体值并格式化。比如 BAR_PROP_TIME 用 TimeToString 以“日期+时分秒”显示,BAR_PROP_TIME_MONTH 再走 MonthDescription 转成月份名。 这种写法让你在日志或面板里打印 K 线信息时能一眼看懂,而不是去翻 ENUM_BAR_PROP_INTEGER 定义。外汇与贵金属行情存在跳空与不同 broker 点差差异,不支持属性在高杠杆品种上更常见,实盘前应在 MT5 用不同品种跑一遍确认输出。
class="type">class="kw">string CBar::GetPropertyDescription(ENUM_BAR_PROP_INTEGER class="kw">property) { class="kw">return ( class="kw">property==BAR_PROP_TIME ? CMessage::Text(MSG_LIB_TEXT_BAR_TIME)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+::TimeToString(this.GetProperty(class="kw">property),TIME_DATE|TIME_MINUTES|TIME_SECONDS) ) : class="kw">property==BAR_PROP_TYPE ? CMessage::Text(MSG_ORD_TYPE)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+this.BodyTypeDescription() ) : class="kw">property==BAR_PROP_PERIOD ? CMessage::Text(MSG_LIB_TEXT_BAR_PERIOD)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+this.m_period_description ) : class="kw">property==BAR_PROP_SPREAD ? CMessage::Text(MSG_LIB_TEXT_BAR_SPREAD)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==BAR_PROP_VOLUME_TICK ? CMessage::Text(MSG_LIB_TEXT_BAR_VOLUME_TICK)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==BAR_PROP_VOLUME_REAL ? CMessage::Text(MSG_LIB_TEXT_BAR_VOLUME_REAL)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(class="type">class="kw">string)this.GetProperty(class="kw">property) ) : class="kw">property==BAR_PROP_TIME_YEAR ? CMessage::Text(MSG_LIB_TEXT_BAR_TIME_YEAR)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+(class="type">class="kw">string)this.Year() ) : class="kw">property==BAR_PROP_TIME_MONTH ? CMessage::Text(MSG_LIB_TEXT_BAR_TIME_MONTH)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : ": "+MonthDescription((class="type">int)this.Month()) ) :
◍ 时间属性枚举的拼接实现与序列类入口
上面这段分支判断处理的是 K 线时间类属性的字符串化输出。以 BAR_PROP_TIME_DAY_OF_YEAR 为例,若当前品种不支持该属性,就返回「: 不支持」的提示;若支持,则用 IntegerToString(this.DayOfYear(),3,'0') 把一年中的第几天格式化为 3 位定宽字符串,比如第 9 天显示成「009」。 其余的 DAY_OF_WEEK、DAY、HOUR、MINUTE 走同一套三元表达式逻辑,宽度分别为星期描述符、2 位日、2 位时、2 位分,最后兜底返回空串。 CSeriesDE 作为时序容器类公开了 GetObject 直接回传自身指针,以及 GetBarByListIndex / GetBar 两类取棒函数:前者按链表真实下标,后者按时序倒序下标。开 MT5 把这段粘进自定义指标,调一下 DayOfYear 的位数参数,能直接验证黄金 1 小时图在年初跳空日的编号输出是否符合预期,外汇与贵金属杠杆品种波动剧烈,验证时请控制仓位。
class="kw">property==BAR_PROP_TIME_DAY_OF_YEAR ? CMessage::Text(MSG_LIB_TEXT_BAR_TIME_DAY_OF_YEAR)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)::IntegerToString(this.DayOfYear(),class="num">3,&class="macro">#x27;class="num">0&class="macro">#x27;) ) : class="kw">property==BAR_PROP_TIME_DAY_OF_WEEK ? CMessage::Text(MSG_LIB_TEXT_BAR_TIME_DAY_OF_WEEK)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+DayOfWeekDescription((ENUM_DAY_OF_WEEK)this.DayOfWeek()) ) : class="kw">property==BAR_PROP_TIME_DAY ? CMessage::Text(MSG_LIB_TEXT_BAR_TIME_DAY)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)::IntegerToString(this.Day(),class="num">2,&class="macro">#x27;class="num">0&class="macro">#x27;) ) : class="kw">property==BAR_PROP_TIME_HOUR ? CMessage::Text(MSG_LIB_TEXT_BAR_TIME_HOUR)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)::IntegerToString(this.Hour(),class="num">2,&class="macro">#x27;class="num">0&class="macro">#x27;) ) : class="kw">property==BAR_PROP_TIME_MINUTE ? CMessage::Text(MSG_LIB_TEXT_BAR_TIME_MINUTE)+ (!this.SupportProperty(class="kw">property) ? ": "+CMessage::Text(MSG_LIB_PROP_NOT_SUPPORTED) : " : "+(class="type">class="kw">string)::IntegerToString(this.Minute(),class="num">2,&class="macro">#x27;class="num">0&class="macro">#x27;) ) : "" ); } class CSeriesDE : class="kw">public CBaseObj { class="kw">private: class="kw">public: CSeriesDE *GetObject(class="type">void) { class="kw">return &this; } CBar *GetBarByListIndex(const class="type">uint index); CBar *GetBar(const class="type">uint index);