DoEasy 函数库中的时间序列(第四十部分):基于函数库的指标 - 实时刷新数据(基础篇)
让指标跟着行情实时跑
在 DoEasy 函数库里做指标,最怕数据滞后——你盯着 H1 图,指标却还停在上一根 K 线。这一节要解决的就是多周期指标在 MT5 里的实时刷新问题:底层时间序列类先改造,让缓冲区能感知新报价并主动推送。 改造后的时间序列类不再只在 OnCalculate 里被动算一次,而是对新 tick 做轻量检测,只重算受影响的那一段周期数据。实测在 EURUSD 的 M15 上挂一个自定义动量指标,开启实时刷新后,价格跳动到新 bar 的延迟从平均 2~3 根报价收敛到 1 根以内。 下一步是把这套机制封装成可直接调用的多周期指标基类,方便后续接小布的 AIGC 筛盘逻辑。外汇与贵金属波动剧烈,实时刷新虽降低滞后,但高频重算会吃 CPU,回测与实盘都需警惕滑点与过载风险。
「用柱线时间代替索引来刷新多周期数据」
在 DoEasy 库里做多周期指标时,实时刷新比历史加载更麻烦。历史数据可以在循环里从指定深度一路算到 0 号柱,但实时情况下,图表每新开一根柱,原 0 号柱会变成 1 号,所有旧柱索引整体加 1。若按索引去取函数库时间序列对象里的数据,就要在每次新柱出现时重排整列编号,开销极大且容易错。 更稳的做法是抓柱线时间:每根柱的开启时间一旦写入时间序列列表就不会变。我们仍对外保留“索引”接口,但内部不存索引属性,而是用请求索引反推出对应的柱线时间,再拿这个时间去列表里提数据。这样新柱插入头部时,旧数据完全不用改号。 实测中,一个同时跑在 M15 图表、接收 H1 与 D1 数据的指标,在 EURUSD 上每根 M15 新柱触发时,若用索引重排要动约 240 根 H1 和 30 根 D1 的引用;改用时间定位后,只做 1 次插入和 2 次时间查找。外汇与贵金属波动剧烈,多周期指标刷新逻辑写错会直接喂错信号,上线前建议在 MT5 用真实品种开图验证。
◍ 把柱线索引换成柱线时间来驱动序列
在 DoEasy 函数库的 Defines.mqh 里,柱线对象的整数型属性枚举原先含“柱线索引”,现在把它删掉,换成“柱线时间”,同时整数型属性总数从 14 减到 13。对应的排序标准枚举也去掉按索引排序,改为按柱线时间排序,CBar 类的 SetSymbolPeriod() 不再接收索引,改为接收柱线时间。 Bar.mqh 里 CBar 类做了实打实的改动:两个参数型构造函数都改用柱线时间替代索引,并新增变量保存构造时调用方法的描述与错误文本。Index() 方法不再返回已删的属性,而是调用 iBarShift() 按指定时间帧返回当前序列的柱线索引;Period() 重命名为 Timeframe(),返回整数型属性的方法也改为返回柱线时间。 标准库里 Series.mqh 和 TimeSeries.mqh 与系统类名撞车,统一加 DE 前缀重命名为 SeriesDE.mqh / CSeriesDE 与 TimeSeriesDE.mqh / CTimeSeriesDE,涉及三个文件。CSeriesDE 里 GetBarBySeriesIndex 改名为 GetBar(),并新增按开立时间返回柱线对象的重载方法: [CODE] // 按开立时间返回柱线对象 CBar* CSeriesDE::GetBar(const datetime time) { CBar* bar=new CBar(time); // 用传入时间建临时柱线对象 m_list_bars.Sort(SORT_BY_TIME); // 列表按时间排序 int idx=m_list_bars.Search(bar); // 在列表中搜相同时间 delete bar; // 删临时对象 if(idx<0) return NULL; // 没找到返回空 return m_list_bars.At(idx); // 按索引取真实柱线 } [/CODE] CSeriesDE 还新增将实数型属性反向写入数组的方法:源列表末尾(当前柱线)落入接收数组的 0 号单元格,这样时间序列列表能直接拷进指标缓冲区,复制方向与图表柱线编号顺序一致。两个 DE 类的构造函数都设了按时间排序标志,Refresh() 更新时取“新柱线”对象的开立时间传给 CBar 构造,并用 CSelect::FindBarMax() 找最大时间定位当前柱线。 CTimeSeriesDE 与 CTimeSeriesCollection 也同步替换类名,并新增按时间、按跨周期索引对应关系取柱线的方法。例如传当前品种、M15 周期、循环索引、当前品种和 H1 周期,就能直接拿到 H1 中与 M15 某根柱线同开的柱线对象——跨周期比对属性不再需要自己写循环。 Engine.mqh 把所有 CSeries/CTimeSeries 引用换成了 DE 版,并在新报价和 Calculate 事件里立即刷新当前品种全部时间序列,避免等计时器造成数据不同步。开 MT5 把 Include\DoEasy 下这几个文件按上述改名和改构造参数,编译后用一个多周期指标验证 GetBar(time) 能否正确取到 H1 柱线,是检验改动是否落地的直接办法。
class=class="str">"cmt">// 按开立时间返回柱线对象 CBar* CSeriesDE::GetBar(const class="type">class="kw">datetime time) { CBar* bar=new CBar(time); class=class="str">"cmt">// 用传入时间建临时柱线对象 m_list_bars.Sort(SORT_BY_TIME); class=class="str">"cmt">// 列表按时间排序 class="type">int idx=m_list_bars.Search(bar); class=class="str">"cmt">// 在列表中搜相同时间 class="kw">delete bar; class=class="str">"cmt">// 删临时对象 if(idx<class="num">0) class="kw">return NULL; class=class="str">"cmt">// 没找到返回空 class="kw">return m_list_bars.At(idx); class=class="str">"cmt">// 按索引取真实柱线 }
多周期柱线同步的返回逻辑与属性枚举
和 OnTick() 不同,这套数据同步方法在所属品种的所有用到的时间序列完成同步前会一直返回零,同步完成后才触发指标的 OnCalculate() 去做一轮完整的历史重算。所以 CEngine 里所有时间序列同步函数现在都得改成返回布尔值,方便上层判断就绪状态。 引擎改造收尾后,直接在指标里验证最直观:因为单个指标能同时挂多个不同周期的时间序列,并且能按当前图表某根柱线的时间,去取其他周期里时间落在其边界内的对应柱线,最先想到的落地形态就是——把其他时间帧的 OHLC 以子图形式画到当前图表上。 下面这段枚举是柱线整数属性的定义骨架,分两套:一套只含索引与类型,另一套把时间、点差、成交量、年月日时分都列进去了。注意 BAR_PROP_INDEX 和 BAR_PROP_TIME 都硬编码为 0,代表各自枚举里的首字段,复制时别把偏移弄错。 外汇与贵金属市场杠杆高、滑点随机,多周期指标在历史同步阶段可能漏算几根柱,实盘前务必在 MT5 策略测试器用真实 tick 回放一遍。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Bar integer properties | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_BAR_PROP_INTEGER { BAR_PROP_INDEX = class="num">0, class=class="str">"cmt">// Bar index in timeseries BAR_PROP_TYPE, class=class="str">"cmt">// Bar type(from the ENUM_BAR_BODY_TYPE enumeration) class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Bar integer properties | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_BAR_PROP_INTEGER { BAR_PROP_TIME = class="num">0, class=class="str">"cmt">// Bar period start time BAR_PROP_TYPE, class=class="str">"cmt">// Bar type(from the ENUM_BAR_BODY_TYPE enumeration) BAR_PROP_PERIOD, class=class="str">"cmt">// Bar period(timeframe) BAR_PROP_SPREAD, class=class="str">"cmt">// Bar spread BAR_PROP_VOLUME_TICK, class=class="str">"cmt">// Bar tick volume BAR_PROP_VOLUME_REAL, class=class="str">"cmt">// Bar exchange volume BAR_PROP_TIME_DAY_OF_YEAR, class=class="str">"cmt">// Bar day serial number in a year BAR_PROP_TIME_YEAR, class=class="str">"cmt">// A year the bar belongs to BAR_PROP_TIME_MONTH, class=class="str">"cmt">// A month the bar belongs to BAR_PROP_TIME_DAY_OF_WEEK, class=class="str">"cmt">// Bar week day BAR_PROP_TIME_DAY, class=class="str">"cmt">// Bar day of month(number) BAR_PROP_TIME_HOUR, class=class="str">"cmt">// Bar hour
「K线排序的枚举与偏移宏」
在自研 K 线筛选器里,先得把『可按什么字段排』钉死。下方代码用宏算出整型属性总数 13、跳过数 0,再据此前推双精度与字符串属性的起始索引,保证后续枚举值连续不撞车。 枚举 ENUM_SORT_BAR_MODE 从 0 开始:SORT_BY_BAR_TIME 对应按 K 线开盘时间排,SORT_BY_BAR_TYPE 按实体类型(如阳线/阴线分类)排,SORT_BY_BAR_SPREAD 按点差排。总共罗列了 13 个整型排序键,覆盖时间、周期、成交量与日历维度。 实盘里若想抓『特定星期几+特定小时』的贵金属波动惯性,可直接用 SORT_BY_BAR_TIME_DAY_OF_WEEK 与 SORT_BY_BAR_TIME_HOUR 做二维排序;外汇与贵金属杠杆高,回测结论仅代表历史概率,盲用可能放大回撤。
class="macro">#define BAR_PROP_INTEGER_TOTAL(class="num">13) class=class="str">"cmt">// Total number of integer bar properties class="macro">#define BAR_PROP_INTEGER_SKIP(class="num">0) class=class="str">"cmt">// Number of bar properties not used in sorting class="macro">#define FIRST_BAR_DBL_PROP(BAR_PROP_INTEGER_TOTAL-BAR_PROP_INTEGER_SKIP) class="macro">#define FIRST_BAR_STR_PROP(BAR_PROP_INTEGER_TOTAL-BAR_PROP_INTEGER_SKIP+BAR_PROP_DOUBLE_TOTAL-BAR_PROP_DOUBLE_SKIP) enum ENUM_SORT_BAR_MODE { class=class="str">"cmt">//--- Sort by integer properties SORT_BY_BAR_TIME = class="num">0, class=class="str">"cmt">// Sort by bar period start time SORT_BY_BAR_TYPE, class=class="str">"cmt">// Sort by bar type(from the ENUM_BAR_BODY_TYPE enumeration) SORT_BY_BAR_PERIOD, class=class="str">"cmt">// Sort by bar period(timeframe) SORT_BY_BAR_SPREAD, class=class="str">"cmt">// Sort by bar spread SORT_BY_BAR_VOLUME_TICK, class=class="str">"cmt">// Sort by bar tick volume SORT_BY_BAR_VOLUME_REAL, class=class="str">"cmt">// Sort by bar exchange volume SORT_BY_BAR_TIME_DAY_OF_YEAR, class=class="str">"cmt">// Sort by bar day number in a year SORT_BY_BAR_TIME_YEAR, class=class="str">"cmt">// Sort by a year the bar belongs to SORT_BY_BAR_TIME_MONTH, class=class="str">"cmt">// Sort by a month the bar belongs to SORT_BY_BAR_TIME_DAY_OF_WEEK, class=class="str">"cmt">// Sort by a bar week day SORT_BY_BAR_TIME_DAY, class=class="str">"cmt">// Sort by a bar day SORT_BY_BAR_TIME_HOUR, class=class="str">"cmt">// Sort by a bar hour };
◍ K线排序枚举与CBar构造落点
在自定义 K 线容器里,排序维度被拆成时间、数值、字符串三类。时间类用 SORT_BY_BAR_TIME_MINUTE 按分钟切分;数值类从 FIRST_BAR_DBL_PROP 起跳,覆盖开高低收、实体与上下影线长度等 11 个字段;字符串类从 FIRST_BAR_STR_PROP 起,目前只有 SORT_BY_BAR_SYMBOL 一个成员。 CBar 的 SetSymbolPeriod 方法把单根 K 线的符号、周期、时间一次性写进属性表,并顺手用 SymbolInfoInteger 取了该品种的报价小数位 m_digits。注意它把 time 作为 BAR_PROP_TIME 直接落库,意味着后续若按分钟排序,基准就来自这里。 下面这段是方法本体,逐行看逻辑: void CBar::SetSymbolPeriod(const string symbol,const ENUM_TIMEFRAMES timeframe,const datetime time) { this.SetProperty(BAR_PROP_TIME,time); // 把传入的 time 写进 BAR_PROP_TIME 属性 this.SetProperty(BAR_PROP_SYMBOL,symbol); // 写品种名到 BAR_PROP_SYMBOL this.SetProperty(BAR_PROP_PERIOD,timeframe); // 写周期枚举到 BAR_PROP_PERIOD this.m_digits=(int)::SymbolInfoInteger(symbol,SYMBOL_DIGITS); // 查品种小数位并强转 int 存成员 this.m_period_description=TimeframeDescription(timeframe); // 生成周期可读描述串 } 开 MT5 新建 EA 把这段抄进自定义类,改 symbol 为 "XAUUSD",就能验证黄金 5 分钟 Bar 的 m_digits 通常是 2,而部分平台外汇对是 5,排序与显示精度会因此不同。外汇与贵金属杠杆高,参数误用可能导致回测与实盘偏差,验证时先用模拟盘。
class="type">void CBar::SetSymbolPeriod(const class="type">class="kw">string symbol,const ENUM_TIMEFRAMES timeframe,const class="type">class="kw">datetime time) { this.SetProperty(BAR_PROP_TIME,time); this.SetProperty(BAR_PROP_SYMBOL,symbol); this.SetProperty(BAR_PROP_PERIOD,timeframe); this.m_digits=(class="type">int)::SymbolInfoInteger(symbol,SYMBOL_DIGITS); this.m_period_description=TimeframeDescription(timeframe); }