DoEasy 函数库中的时间序列(第四十部分):基于函数库的指标 - 实时刷新数据·进阶篇
⏱️

DoEasy 函数库中的时间序列(第四十部分):基于函数库的指标 - 实时刷新数据·进阶篇

(2/3)· 当新柱线令索引整体偏移,多周期指标如何不重算历史而稳取实时数据

含代码示例偏理论 第 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 的方式在重大数据夜可能取不到,需做好零值结构的兜底。

MQL5 / C++
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 点差(外汇、贵金属杠杆品种点差跳动大,属高风险观测值)。

MQL5 / C++
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 量做信号须配合止损,信号失效概率不低。

MQL5 / C++
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]”的输出,验证你的周期映射对不对。 外汇与贵金属杠杆高,跨周期定位算错可能引发行情误判,实盘前务必用历史数据回测这套映射逻辑。

MQL5 / C++
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 用不同品种跑一遍确认输出。

MQL5 / C++
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 小时图在年初跳空日的编号输出是否符合预期,外汇与贵金属杠杆品种波动剧烈,验证时请控制仓位。

MQL5 / C++
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);
把重复劳动交给小布
这些时间序列的索引偏移诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时柱线时间映射,你专注决策即可。

常见问题

新柱线加入时间序列后零号位移,所有旧柱索引加一,按索引取数会错位,历史计算可能需重做。
每根柱线的开立时间在列表里恒定,以其为引用起点可避开实时刷新中的编号漂移问题。
移除索引属性并将整型属性从14减到13,按时间重构后对象更贴合时序查询,减少冗余状态。
可以,小布内置的 AIGC 看盘口能标出柱线时间映射异常,省去手动逐根核对索引的麻烦。
改为按柱线时间排序,请求时由索引反算时间再从列表提取,兼顾循环习惯与实时正确率。