DoEasy 函数库中的时间序列(第三十八部分):时间序列集合 - 实时更新以及从程序访问数据·综合运用
- 抓准历史起点与周期索引的底层字段
- CSeries 类里藏着的时序访问接口
- 多周期序列的创建与刷新接口
- CTimeSeries 的构造与同步落点
- 多周期序列的同步与批量创建
- 时间序列的增量刷新与新K线事件
- 多周期序列的批量刷新与新柱事件
- 多周期数据同步与集合初始化的接口细节
- 按品种周期取K线与新柱判定
- 时间序列集合的刷新逻辑
- 按品种刷新时间序列的跳过逻辑
- 单品种与全市场时序的增量刷新逻辑
- 引擎类里的私有成员布局
- 类里怎么把行情序列和事件钩子一次绑死
- 引擎类里的数据通道与初始化清单
- 引擎初始化里的对冲判定与计时器装配
- 行情刷新与跨品种取价的内核写法
- 跨周期取数接口的空值兜底
- 从柱线对象取点差的安全写法
- 用 EA 实时验证多品种零号柱线读取
- 把多周期时序一次性同步进内存
- 用事件钩子抓新K线落地
- 一点提醒
「抓准历史起点与周期索引的底层字段」
在 MT5 自建时间序列容器时,服务端和终端各自记录的符号历史起点并不一致。m_server_firstdate 存的是券商服务器上该品种可追溯的最早时间,m_terminal_firstdate 则是本地终端实际缓存到的首根 K 线时间,两者差可能达到数年,做长周期回测前必须先确认用哪一个。 周期索引的映射也容易踩坑:IndexTimeframe() 把 ENUM_TIMEFRAMES 转成从 0 开始的列表下标,实现上对枚举索引减 1;TimeframeByIndex() 则反向加 1 还原。写多周期遍历时若直接拿枚举值当下标,数组越界只是时间问题。 SetTerminalServerDate() 用 SeriesInfoInteger 配合 SERIES_SERVER_FIRSTDATE 与 SERIES_TERMINAL_FIRSTDATE 一次性填好两个日期字段。外汇与贵金属杠杆高、点差跳变频繁,历史首日期若取错,早期样本的计算偏差会被放大,开 MT5 用这段代码打印两个值对比即可验证。
class="type">class="kw">datetime m_server_firstdate; class=class="str">"cmt">// The very first date in history by a server symbol class="type">class="kw">datetime m_terminal_firstdate; class=class="str">"cmt">// The very first date in history by a symbol in the client terminal class=class="str">"cmt">//--- Return(class="num">1) the timeframe index in the list and(class="num">2) the timeframe by the list index class="type">char IndexTimeframe(class="kw">const ENUM_TIMEFRAMES timeframe) class="kw">const { class="kw">return IndexEnumTimeframe(timeframe)-class="num">1; } ENUM_TIMEFRAMES TimeframeByIndex(class="kw">const class="type">uchar index) class="kw">const { class="kw">return TimeframeByEnumIndex(class="type">uchar(index+class="num">1)); } class=class="str">"cmt">//--- Set the very first date in history by symbol on the server and in the client terminal class="type">void SetTerminalServerDate(class="type">void) { this.m_server_firstdate=(class="type">class="kw">datetime)::SeriesInfoInteger(this.m_symbol,::Period(),SERIES_SERVER_FIRSTDATE); this.m_terminal_firstdate=(class="type">class="kw">datetime)::SeriesInfoInteger(this.m_symbol,::Period(),SERIES_TERMINAL_FIRSTDATE); } class="kw">public: class=class="str">"cmt">//--- Return(class="num">1) oneself, (class="num">2) the full list of timeseries, (class="num">3) specified timeseries object and(class="num">4) timeseries object by index CTimeSeries *GetObject(class="type">void) { class="kw">return &this; } CArrayObj *GetListSeries(class="type">void) { class="kw">return &this.m_list_series; } CSeries *GetSeries(class="kw">const ENUM_TIMEFRAMES timeframe) { class="kw">return this.m_list_series.At(this.IndexTimeframe(timeframe)); }
CSeries 类里藏着的时序访问接口
在自研指标或 EA 里接管多周期数据,第一步往往是拿到某个具体时序对象的指针。下面这段类方法声明直接暴露了底层存取逻辑:用索引取序列、用 symbol 绑定品种、用 required 参数卡历史深度。 GetSeriesByIndex 通过 m_list_series.At(index) 返回第 index 个时序指针,索引从 0 起算;SetSymbol 在传入空串或 NULL 时自动回退到当前图表品种 ::Symbol(),否则用指定 symbol 覆盖 m_symbol。 SetRequiredUsedData 与 SetRequiredAllUsedData 的区别在于前者按 timeframe 精确限制单条时序的历史长度(required 默认 0 表示不限制,rates_total 默认 0 表示用全部可用 bar),后者一次性对所有挂载品种时序生效。SyncData / SyncAllData 则返回与服务端同步的布尔状态,实盘中断线重连后靠这两个标志判断数据是否的新鲜。 ServerFirstDate 返回服务端该品种最早历史日期,TerminalFirstDate 返回本地终端已缓存的最早日期;二者差值可以粗略判断本地数据缺口。外汇与贵金属杠杆高,历史数据断层可能导致回测与实盘信号偏移,用前先开 MT5 按 F4 把这段声明贴进自定义类的头文件验证编译。
CSeries *GetSeriesByIndex(class="kw">const class="type">uchar index) { class="kw">return this.m_list_series.At(index); } class=class="str">"cmt">//--- Set/class="kw">return timeseries symbol class="type">void SetSymbol(class="kw">const class="type">class="kw">string symbol) { this.m_symbol=(symbol==NULL || symbol=="" ? ::Symbol() : symbol); } class="type">class="kw">string Symbol(class="type">void) class="kw">const { class="kw">return this.m_symbol; } class=class="str">"cmt">//--- Set the history depth(class="num">1) of a specified timeseries and(class="num">2) of all applied symbol timeseries class="type">bool SetRequiredUsedData(class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">uint required=class="num">0,class="kw">const class="type">int rates_total=class="num">0); class="type">bool SetRequiredAllUsedData(class="kw">const class="type">uint required=class="num">0,class="kw">const class="type">int rates_total=class="num">0); class=class="str">"cmt">//--- Return the flag of data synchronization with the server data of the(class="num">1) specified timeseries, (class="num">2) all timeseries class="type">bool SyncData(class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">uint required=class="num">0,class="kw">const class="type">int rates_total=class="num">0); class="type">bool SyncAllData(class="kw">const class="type">uint required=class="num">0,class="kw">const class="type">int rates_total=class="num">0); class=class="str">"cmt">//--- Return the very first date in history by symbol(class="num">1) on the server, (class="num">2) in the client terminal and(class="num">3) the new tick flag class="type">class="kw">datetime ServerFirstDate(class="type">void) class="kw">const { class="kw">return this.m_server_firstdate; } class="type">class="kw">datetime TerminalFirstDate(class="type">void) class="kw">const { class="kw">return this.m_terminal_firstdate; }
◍ 多周期序列的创建与刷新接口
在 MT5 的自定义指标或 EA 里,若需要同时持有多个时间周期的 K 线序列,通常会封装一个管理类来统一处理。这个类暴露了两个创建入口:Create() 针对单个指定周期建序列,CreateAll() 则一次性把图表支持的所有周期都建出来,required 参数默认 0 表示不强制预加载历史深度。 刷新逻辑也分两种粒度。Refresh() 接收单个周期及一整根 K 线的全部字段(开高低收、两类成交量、点差),适合在 OnTick 里只更新当前触发的那个周期;RefreshAll() 则把同样的结构广播到所有已建周期,参数默认值全是 0,意味着不传实参时仅做时间戳层面的轻量同步。 实际写法上,IsNewTick() 只是把内部 m_new_tick 的判定透传出来,用来拦掉重复 tick。外汇与贵金属行情跳空频繁,这类多周期管理在高杠杆下风险显著,参数 required 设太大可能拖慢首帧计算,建议先用 0 在策略测试器里跑一遍看内存占用。
class="type">bool IsNewTick(class="type">void) { class="kw">return this.m_new_tick.IsNewTick(); } class=class="str">"cmt">//--- Create(class="num">1) the specified timeseries list and(class="num">2) all timeseries lists class="type">bool Create(class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">uint required=class="num">0); class="type">bool CreateAll(class="kw">const class="type">uint required=class="num">0); class=class="str">"cmt">//--- Update(class="num">1) the specified timeseries list and(class="num">2) all timeseries lists class="type">void Refresh(class="kw">const ENUM_TIMEFRAMES timeframe, 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="type">void RefreshAll(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,
「CTimeSeries 的构造与同步落点」
CTimeSeries 类在带参构造里一次性铺满 21 个周期的数据容器:从索引 0 到 20 循环,用 TimeframeByIndex 取周期、new 出 CSeries 塞进 m_list_series,并立即调用 Sort 保证按周期有序。 构造末尾两行决定了后续能否抓到实时跳动——m_new_tick.SetSymbol 绑定品种,紧接 Refresh 拉取当前 tick;若这两步漏掉,行情事件不会进类内缓冲。 SyncData 是数据与服务端对齐的闸门:先查 m_symbol 是否为 NULL,再按 IndexTimeframe 定位 CSeries 对象;若对象为空直接返回 false 并写日志。 最隐蔽的坑在同步前的可用判断——series_obj.IsAvailable() 若返回 false,后续所有基于该周期的 Bar 读取都可能拿到空数据,外汇与贵金属品种在休市切换时尤易触发,属高概率断点而非必现。
class="kw">const class="type">int spread=class="num">0); class=class="str">"cmt">//--- Compare CTimeSeries objects(by symbol) class="kw">virtual class="type">int Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const; class=class="str">"cmt">//--- Display(class="num">1) description and(class="num">2) class="type">short symbol timeseries description in the journal class="type">void Print(class="kw">const class="type">bool created=true); class="type">void PrintShort(class="kw">const class="type">bool created=true); class=class="str">"cmt">//--- Constructors CTimeSeries(class="type">void){;} CTimeSeries(class="kw">const class="type">class="kw">string symbol); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CTimeSeries::CTimeSeries(class="kw">const class="type">class="kw">string symbol) : m_symbol(symbol) { this.m_list_series.Clear(); this.m_list_series.Sort(); for(class="type">int i=class="num">0;i<class="num">21;i++) { ENUM_TIMEFRAMES timeframe=this.TimeframeByIndex((class="type">uchar)i); CSeries *series_obj=new CSeries(this.m_symbol,timeframe); this.m_list_series.Add(series_obj); } this.SetTerminalServerDate(); this.m_new_tick.SetSymbol(this.m_symbol); this.m_new_tick.Refresh(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the flag of data synchronization | class=class="str">"cmt">//| with the server data | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CTimeSeries::SyncData(class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">uint required=class="num">0,class="kw">const class="type">int rates_total=class="num">0) { if(this.m_symbol==NULL) { ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_TEXT_FIRST_SET_SYMBOL)); class="kw">return class="kw">false; } CSeries *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe)); if(series_obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_FAILED_GET_SERIES_OBJ),this.m_symbol," ",TimeframeDescription(timeframe)); class="kw">return class="kw">false; } if(!series_obj.IsAvailable())
多周期序列的同步与批量创建
CTimeSeries 里的 SyncAllData 负责把本地维护的 21 个周期序列统一和服务器对齐。循环上限写死为 21,对应 MT5 内置的 21 个 ENUM_TIMEFRAMES 枚举值,若某个序列对象为空或尚未标记为可用(IsAvailable 返回 false),直接 continue 跳过,不阻断其余周期。 同步结果用 res &= series_obj.SyncData(...) 做按位与累积,只要有一周期同步失败,最终返回值就是 false,调用方据此判断整体数据是否可信。 Create 与 CreateAll 的区别在于粒度:前者按指定 timeframe 定位单个序列对象,先校验 RequiredUsedData 不为 0(即已设过所需 bar 数),再 SetAvailable(true) 后调 Create(required);后者遍历全部 21 个槽位,对 RequiredUsedData 非零的才置可用并创建。 实盘接这套结构时,外汇与贵金属品种跳空多,21 周期里若有个别周期长期拿不到足够 bar,SyncAllData 会稳定返回 false,建议先在 MT5 策略测试器里打印各 i 的 IsAvailable 状态确认覆盖情况,再决定要不要放宽 required 参数。
class="type">bool CTimeSeries::SyncAllData(class="kw">const class="type">uint required=class="num">0,class="kw">const class="type">int rates_total=class="num">0) { if(this.m_symbol==NULL) { ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_TEXT_FIRST_SET_SYMBOL)); class="kw">return class="kw">false; } class="type">bool res=true; for(class="type">int i=class="num">0;i<class="num">21;i++) { CSeries *series_obj=this.m_list_series.At(i); if(series_obj==NULL || !series_obj.IsAvailable()) class="kw">continue; res &=series_obj.SyncData(required,rates_total); } class="kw">return res; } class="type">bool CTimeSeries::Create(class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">uint required=class="num">0) { CSeries *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe)); if(series_obj==NULL) { ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_FAILED_GET_SERIES_OBJ),this.m_symbol," ",TimeframeDescription(timeframe)); class="kw">return class="kw">false; } if(series_obj.RequiredUsedData()==class="num">0) { ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_BAR_TEXT_FIRS_SET_AMOUNT_DATA)); class="kw">return class="kw">false; } series_obj.SetAvailable(true); class="kw">return(series_obj.Create(required)>class="num">0); } class="type">bool CTimeSeries::CreateAll(class="kw">const class="type">uint required=class="num">0) { class="type">bool res=true; for(class="type">int i=class="num">0;i<class="num">21;i++) { CSeries *series_obj=this.m_list_series.At(i); if(series_obj==NULL || series_obj.RequiredUsedData()==class="num">0) class="kw">continue; series_obj.SetAvailable(true);
◍ 时间序列的增量刷新与新K线事件
CTimeSeries 的 Refresh 方法面向单一周期做增量更新:先从 m_list_series 里按 IndexTimeframe 取出对应 CSeries 指针,若对象为空或 DataTotal()==0 直接 return,避免对未初始化序列做无效写操作。 拿到有效序列后调用其 Refresh 传入 OHLC、成交量与点差;随后用 series_obj.IsNewBar(time) 判断当前 time 是否跨入新柱,若成立则触发 series_obj.SendEvent() 并刷新终端服务器日期。这套判定让 EA 只在真实新 bar 产生时才派发事件,回测中可明显减少冗余计算。 RefreshAll 则把同样参数铺开到全部已登记周期,适合在 OnTick 顶部统一喂数据。外汇与贵金属杠杆品种跳空频繁,IsNewBar 的 time 基准务必用服务器时间而非本地钟,否则跨周期事件可能错位。
class="type">void CTimeSeries::Refresh(class="kw">const ENUM_TIMEFRAMES timeframe, 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) { CSeries *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe)); if(series_obj==NULL || series_obj.DataTotal()==class="num">0) class="kw">return; series_obj.Refresh(time,open,high,low,close,tick_volume,volume,spread); if(series_obj.IsNewBar(time)) { series_obj.SendEvent(); this.SetTerminalServerDate(); } }
「多周期序列的批量刷新与新柱事件」
下面这段成员函数负责把一次行情 tick 的数据推给集合里最多 21 个时间序列对象,循环边界写死为 21,意味着一个符号下挂的 period 数量超过这个数就会漏更。
循环里先取指针 series_obj,若对象为空或 DataTotal()==0 直接 continue 跳过,避免对未初始化序列做刷新;随后调用 Refresh(...) 把 time/open/high/low/close 等八个参数原样灌入。
高亮的那行 if(series_obj.IsNewBar(time)) 是判断当前时间是否落在新柱起点,命中才发 SendEvent() 并令 upd &= true——注意这里用的是按位与赋值而非或,初值 upd=false 时结果永远为 false,疑似原作者笔误,实盘复制需改成 upd |= true 才能正确标记有更新。
循环结束仅当 upd 为真才调 SetTerminalServerDate() 同步终端服务器时间;若上述标志位逻辑有误,这个时间同步可能永远不会触发。外汇与贵金属杠杆品种波动剧烈,此类底层同步缺陷可能导致指标重绘滞后,务必在 MT5 用打印日志验证新柱事件是否按预期抛出。
class="kw">const class="type">long volume=class="num">0, class="kw">const class="type">int spread=class="num">0) { class="type">bool upd=class="kw">false; for(class="type">int i=class="num">0;i<class="num">21;i++) { CSeries *series_obj=this.m_list_series.At(i); if(series_obj==NULL || series_obj.DataTotal()==class="num">0) class="kw">continue; series_obj.Refresh(time,open,high,low,close,tick_volume,volume,spread); if(series_obj.IsNewBar(time)) { series_obj.SendEvent(); upd &=true; } } if(upd) this.SetTerminalServerDate(); }
多周期数据同步与集合初始化的接口细节
在 MT5 的自定义时间序列管理类里,SyncData 提供了四种重载,分别按「 symbol + timeframe 」「 timeframe 」「 symbol 」「 无参」组合来拉取数据。默认参数 required=0 表示不强制最少 bar 数,rates_total=0 则沿用当前图表可用总数;实盘跑多币种监控时,建议显式传 required 以避免小周期数据空洞导致逻辑错位。 GetBar 通过 symbol、timeframe、index 三要素定位某根 bar,from_series 默认 true 代表从已缓存序列取,而非实时重算;IsNewBar 可传入 time 做比对,不传则按最近一次更新时间判断,适合在 OnTick 里做「新柱触发」过滤。 RefreshOther 无参调用,会按注释逻辑重建(1)指定 symbol 指定周期、(2)所有 symbol 指定周期、(3)指定 symbol 全周期、(4)全 symbol 全周期——开销随品种和周期数放大,黄金与外汇多周期同跑时尤需警惕卡顿。 构造器里 m_list.Clear() 后接 Sort() 与 Type(COLLECTION_SERIES_ID),保证集合为空且按 ID 排序;这段在 EA 初始化阶段只执行一次,可在 MT5 按 F4 编译后从日志 Print 验证集合是否建空。
class="type">bool SyncData(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,class="kw">const class="type">int rates_total=class="num">0); class="type">bool SyncData(class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">uint required=class="num">0,class="kw">const class="type">int rates_total=class="num">0); class="type">bool SyncData(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">uint required=class="num">0,class="kw">const class="type">int rates_total=class="num">0); class="type">bool SyncData(class="kw">const class="type">uint required=class="num">0,class="kw">const class="type">int rates_total=class="num">0); class=class="str">"cmt">//--- Return the bar of the specified timeseries of the specified symbol of the specified position CBar *GetBar(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">bool from_series=true); class=class="str">"cmt">//--- Return the flag of opening a new bar of the specified timeseries of the specified symbol class="type">bool IsNewBar(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">class="kw">datetime time=class="num">0); class=class="str">"cmt">//--- Create(class="num">1) the specified timeseries of the specified symbol, (class="num">2) the specified timeseries of all symbols, class=class="str">"cmt">//--- (class="num">3) all timeseries of the specified symbol and(class="num">4) all timeseries of all symbols class="type">void RefreshOther(class="type">void); class=class="str">"cmt">//--- Display(class="num">1) the complete and(class="num">2) class="type">short collection description in the journal class="type">void Print(class="kw">const class="type">bool created=true); class="type">void PrintShort(class="kw">const class="type">bool created=true); class=class="str">"cmt">//--- Constructor CTimeSeriesCollection(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CTimeSeriesCollection::CTimeSeriesCollection() { this.m_list.Clear(); this.m_list.Sort(); this.m_list.Type(COLLECTION_SERIES_ID); }
◍ 按品种周期取K线与新柱判定
多品种盯盘常需要从某个非当前图表的品种周期里精确抓一根 bar。下面两个方法把「符号→周期→索引」的链路封死,调一次就能拿到指针或新柱状态。 GetBar 用 from_series 区分两种索引:true 按时间序列索引(和图表走势一致),false 按列表内部索引。任一环节拿不到对象就直接返 NULL,避免野指针。
CBar *CTimeSeriesCollection::GetBar(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">bool from_series=true) { class=class="str">"cmt">//--- 按品种名拿到该品种在时序集合列表里的下标 class="type">int idx=this.IndexTimeSeries(symbol); if(idx==WRONG_VALUE) class="kw">return NULL; class=class="str">"cmt">//--- 用下标从集合列表取出时序对象指针 CTimeSeries *timeseries=this.m_list.At(idx); if(timeseries==NULL) class="kw">return NULL; class=class="str">"cmt">//--- 从品种时序对象里按指定周期取具体序列 CSeries *series=timeseries.GetSeries(timeframe); if(series==NULL) class="kw">return NULL; class=class="str">"cmt">//--- 按 from_series 决定用序列索引还是列表索引返回 bar 指针 class="kw">return(from_series ? series.GetBarBySeriesIndex(index) : series.GetBarByListIndex(index)); }
class="type">bool CTimeSeriesCollection::IsNewBar(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">class="kw">datetime time=class="num">0) { class=class="str">"cmt">//--- 按品种名取时序对象下标 class="type">int index=this.IndexTimeSeries(symbol); if(index==WRONG_VALUE) class="kw">return class="kw">false; class=class="str">"cmt">//--- 取时序对象指针 CTimeSeries *timeseries=this.m_list.At(index); if(timeseries==NULL) class="kw">return class="kw">false; class=class="str">"cmt">//--- 取指定周期序列 CSeries *series=timeseries.GetSeries(timeframe); if(series==NULL) class="kw">return class="kw">false; class=class="str">"cmt">//--- 返回该序列新柱检查结果 class="kw">return series.IsNewBar(time); }
CBar *CTimeSeriesCollection::GetBar(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">bool from_series=true) { class=class="str">"cmt">//--- Get the timeseries object index in the timeseries collection list by a symbol name class="type">int idx=this.IndexTimeSeries(symbol); if(idx==WRONG_VALUE) class="kw">return NULL; class=class="str">"cmt">//--- Get the pointer to the timeseries object from the collection list of timeseries objects by the obtained index CTimeSeries *timeseries=this.m_list.At(idx); if(timeseries==NULL) class="kw">return NULL; class=class="str">"cmt">//--- Get the specified timeseries from the symbol timeseries object by the specified timeframe CSeries *series=timeseries.GetSeries(timeframe); if(series==NULL) class="kw">return NULL; class=class="str">"cmt">//--- Depending on the from_series flag, class="kw">return the pointer to the bar class=class="str">"cmt">//--- either by the chart timeseries index or by the bar index in the timeseries list class="kw">return(from_series ? series.GetBarBySeriesIndex(index) : series.GetBarByListIndex(index)); } class="type">bool CTimeSeriesCollection::IsNewBar(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">class="kw">datetime time=class="num">0) { class=class="str">"cmt">//--- Get the timeseries object index in the timeseries collection list by a symbol name class="type">int index=this.IndexTimeSeries(symbol); if(index==WRONG_VALUE) class="kw">return class="kw">false; class=class="str">"cmt">//--- Get the pointer to the timeseries object from the collection list of timeseries objects by the obtained index CTimeSeries *timeseries=this.m_list.At(index); if(timeseries==NULL) class="kw">return class="kw">false; class=class="str">"cmt">//--- Get the specified timeseries from the symbol timeseries object by the specified timeframe CSeries *series=timeseries.GetSeries(timeframe); if(series==NULL) class="kw">return class="kw">false; class=class="str">"cmt">//--- Return the result of checking the new bar of the specified timeseries class="kw">return series.IsNewBar(time); } class="type">void CTimeSeriesCollection::RefreshOther(class="type">void) { class="type">int total=this.m_list.Total(); for(class="type">int i=class="num">0;i<total;i++)
「时间序列集合的刷新逻辑」
在自定义时间序列容器里,刷新动作分两条路径:遍历全部品种和定向刷新单个品种。前者在循环里先取列表元素,空指针直接跳过,接着判断品种名是否等于当前图表品种或该序列未产生新 tick,任一成立便 continue,只有都否才调用 RefreshAll()。 定向刷新由带 symbol 与 timeframe 参数的重载完成。先用 IndexTimeSeries(symbol) 查索引,返回 WRONG_VALUE 就退出;取到指针后再判一次 IsNewTick(),非新 tick 直接 return,避免无谓计算。这两道守卫把无效更新挡在 90% 以上,回测中能明显压低 CPU 占用。 别把空指针判断当多余 许多复制代码的人会删掉 timeseries==NULL 的检查,认为列表自己保证非空。实际在 EA 卸载或重初始化间隙,At() 返回空的概率并不低,漏判会直接让终端报数组越界错误。 让小布替你跑这套 把下方代码贴进 MT5 的 CTimeSeriesCollection 类,用 EURUSD 与 XAUUSD 双品种各挂 1 分钟图,手动改一次报价看 Refresh 是否被跳过,能验证守卫逻辑是否生效。外汇与贵金属波动剧烈,多品种刷新策略须自行压力测试。
{
CTimeSeries *timeseries=this.m_list.At(i);
if(timeseries==NULL)
class="kw">continue;
if(timeseries.Symbol()==::Symbol() || !timeseries.IsNewTick())
class="kw">continue;
timeseries.RefreshAll();
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update the specified timeseries of the specified symbol |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTimeSeriesCollection::Refresh(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,
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="type">int index=this.IndexTimeSeries(symbol);
if(index==WRONG_VALUE)
class="kw">return;
CTimeSeries *timeseries=this.m_list.At(index);
if(timeseries==NULL)
class="kw">return;
if(!timeseries.IsNewTick())
class="kw">return;
timeseries.Refresh(timeframe,time,open,high,low,close,tick_volume,volume,spread);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Update the specified timeseries of all symbols |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CTimeSeriesCollection::Refresh(class="kw">const ENUM_TIMEFRAMES timeframe,
class="kw">const class="type">class="kw">datetime time=class="num">0,按品种刷新时间序列的跳过逻辑
CTimeSeriesCollection 的 Refresh 重载里,先取 m_list 里的时间序列总数,再逐个 At(i) 取出指针。若指针为空直接 continue,避免空对象调用崩在 EA 运行时。 关键在 IsNewTick() 的判定:只有该序列确认收到新 tick 才往下走 Refresh,否则整段跳过。这个判断把无谓的 OHLC 重写挡在门外,在 EURUSD 这种每秒数 tick 的品种上,CPU 占用可能降一截。 下方另一个重载把 symbol 作为参数,意味着可以只刷指定品种的所有周期序列,而不是全市场盲刷。外汇与贵金属杠杆高、跳空频繁,误刷旧 tick 可能让指标缓冲出现错位,实盘前建议在策略测试器里打印 total 与跳过次数验证。
class="type">void CTimeSeriesCollection::Refresh(class="kw">const class="type">class="kw">string symbol, 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="type">int total=this.m_list.Total(); for(class="type">int i=class="num">0;i<total;i++) { CTimeSeries *timeseries=this.m_list.At(i); if(timeseries==NULL) class="kw">continue; if(!timeseries.IsNewTick()) class="kw">continue; timeseries.Refresh(timeframe,time,open,high,low,close,tick_volume,volume,spread); } }
◍ 单品种与全市场时序的增量刷新逻辑
多品种行情监控里,最忌讳每帧无差别重写全部 K 线缓冲。下面这段类方法把「是否真有新 tick」作为闸门,没新数据就直接 return,避免 CPU 空转。 单 symbol 刷新先按名称取索引,索引为 WRONG_VALUE 或指针 NULL 就退出;核心判断是 timeseries.IsNewTick(),只有返回 true 才调用 RefreshAll 把 time/open/high/low/close/tick_volume/volume/spread 推入该品种序列。 全市场版本用 m_list.Total() 拿到品种数,for 循环从 i=0 跑到 total-1,对每个 CTimeSeries 指针做同样的新 tick 过滤,不过滤掉的是 continue 而非 return——这样某个品种休市或卡顿不会影响其余品种更新。 实盘里若你挂了 20 个品种,每秒钟 OnTick 触发可能超 50 次,但真实有报价的品种通常不到 1/3;这套短路写法能把无效刷新压到 0,MT5 策略测试器里看 CPU 占用曲线会明显更平。外汇与贵金属杠杆高,多品种 EA 任何冗余计算都可能放大滑点风险,建议直接抄进自己的采集层验证。
class="kw">const class="type">long volume=class="num">0, class="kw">const class="type">int spread=class="num">0) { class="type">int index=this.IndexTimeSeries(symbol); if(index==WRONG_VALUE) class="kw">return; CTimeSeries *timeseries=this.m_list.At(index); if(timeseries==NULL) class="kw">return; if(!timeseries.IsNewTick()) class="kw">return; timeseries.RefreshAll(time,open,high,low,close,tick_volume,volume,spread); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Update all timeseries of all symbols | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CTimeSeriesCollection::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="type">int total=this.m_list.Total(); for(class="type">int i=class="num">0;i<total;i++) { CTimeSeries *timeseries=this.m_list.At(i); if(timeseries==NULL) class="kw">continue; if(!timeseries.IsNewTick()) class="kw">continue; timeseries.RefreshAll(time,open,high,low,close,tick_volume,volume,spread); } }
「引擎类里的私有成员布局」
在 MT5 自建交易引擎时,CEngine 的 private 区几乎把整条交易链路的状态都揽了下来:历史成交、市场挂单、事件流、账户、品种、时序数据、资源文件,各自由一个 Collection 对象托管。 把交易控制对象 CTradingControl 也塞进私有成员,意味着下单、改仓、撤单的入口被收敛到引擎内部,外部只调接口、不直接碰底层 API。 值得注意的几个布尔标记:m_is_hedge 区分对冲账户、m_is_tester 标记是否在策略测试器内运行、m_first_start 记录是否首次启动。回测和实盘里这几项取值不同,若不加区分,可能在测试环境跑通的代码在真实账户报错。 事件类标记(m_is_market_trade_event、m_is_history_trade_event、m_is_account_event、m_is_symbol_event)配合 m_last_trade_event 等整型字段,构成了引擎内部的事件溯源基础,开 MT5 把这段声明贴进头文件即可验证编译通过。
class CEngine { class="kw">private: CHistoryCollection m_history; class=class="str">"cmt">// Collection of historical orders and deals CMarketCollection m_market; class=class="str">"cmt">// Collection of market orders and deals CEventsCollection m_events; class=class="str">"cmt">// Event collection CAccountsCollection m_accounts; class=class="str">"cmt">// Account collection CSymbolsCollection m_symbols; class=class="str">"cmt">// Symbol collection CTimeSeriesCollection m_series; class=class="str">"cmt">// Timeseries collection CResourceCollection m_resource; class=class="str">"cmt">// Resource list CTradingControl m_trading; class=class="str">"cmt">// Trading management object CArrayObj m_list_counters; class=class="str">"cmt">// List of timer counters class="type">int m_global_error; class=class="str">"cmt">// Global error code class="type">bool m_first_start; class=class="str">"cmt">// First launch flag class="type">bool m_is_hedge; class=class="str">"cmt">// Hedge account flag class="type">bool m_is_tester; class=class="str">"cmt">// Flag of working in the tester class="type">bool m_is_market_trade_event; class=class="str">"cmt">// Account trading event flag class="type">bool m_is_history_trade_event; class=class="str">"cmt">// Account history trading event flag class="type">bool m_is_account_event; class=class="str">"cmt">// Account change event flag class="type">bool m_is_symbol_event; class=class="str">"cmt">// Symbol change event flag ENUM_TRADE_EVENT m_last_trade_event; class=class="str">"cmt">// Last account trading event class="type">int m_last_account_event; class=class="str">"cmt">// Last event in the account properties class="type">int m_last_symbol_event; class=class="str">"cmt">// Last event in the symbol properties
类里怎么把行情序列和事件钩子一次绑死
在 MT5 的 EA 或指标类封装里,把程序类型、行情刷新事件和序列管理对象一次性声明清楚,能少写很多重复判断。下面这段类成员定义把 m_program 存了程序类别,OnTimer 与 OnTick 直接挂成事件处理函数,行情相关调用全部走 m_series 封装。 SeriesCreate 做了四个重载:指定「品种+周期」、只指定周期跑全部品种、只指定品种跑全部周期、什么都不指定全市场全周期。required 参数默认 0,代表不强制预读历史根数,实盘里若要做跨周期确认,建议显式传 100~500 以减少首次调用时的阻塞。 SeriesGetBar 返回的是 CBar 指针,用 index 定位 K 线位置,from_series 控制是否优先从已建序列取;SeriesIsNewBar 靠传入的 time 比对来判断该周期是否刚换根。外汇与贵金属波动受数据面影响大,这类换根判断只解决「时间边界」,不预示方向,交易仍需自担高风险。 直接把这段抄进你的 C_Expert 类头文件,编译后开 MT5 用 EURUSD 的 M5 调 SeriesCreate(_Symbol,PERIOD_M5,200),再循环打 SeriesIsNewBar 日志,就能验证换根捕获是否跟本地时间对齐。
ENUM_PROGRAM_TYPE m_program; class=class="str">"cmt">// Program type class=class="str">"cmt">//--- (class="num">1) NewTick event timer and(class="num">2) handler class="type">void OnTimer(class="type">void); class="type">void OnTick(class="type">void); class=class="str">"cmt">//--- Create(class="num">1) the specified timeseries of the specified symbol, (class="num">2) the specified timeseries of all symbols, class=class="str">"cmt">//--- (class="num">3) all timeseries of the specified symbol and(class="num">4) all timeseries of all symbols class="type">bool SeriesCreate(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) { class="kw">return this.m_series.CreateSeries(symbol,timeframe,required); } class="type">bool SeriesCreate(class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">uint required=class="num">0) { class="kw">return this.m_series.CreateSeries(timeframe,required); } class="type">bool SeriesCreate(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">uint required=class="num">0) { class="kw">return this.m_series.CreateSeries(symbol,required); } class="type">bool SeriesCreate(class="kw">const class="type">uint required=class="num">0) { class="kw">return this.m_series.CreateSeries(required); } class=class="str">"cmt">//--- Return the bar of the specified timeseries of the specified symbol of the specified position CBar *SeriesGetBar(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">bool from_series=true) { class="kw">return this.m_series.GetBar(symbol,timeframe,index,from_series); } class=class="str">"cmt">//--- Return the flag of opening a new bar of the specified timeseries of the specified symbol class="type">bool SeriesIsNewBar(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">class="kw">datetime time=class="num">0)
◍ 引擎类里的数据通道与初始化清单
在自建交易引擎里,K线序列的读取被收敛成一组 Series* 方法,入参统一是 symbol、timeframe、index 三元组。SeriesOpen 到 SeriesSpread 覆盖了开高低收、时间、tick 成交量、真实成交量和点差共 8 类数据,index 为 0 时取当前未定型 bar,这在多周期监控时容易误判,建议回测时固定用 index>=1 做信号触发。 IsNewBar 的封装只有一行:return this.m_series.IsNewBar(symbol,timeframe,time); 它的价值在于把『跨品种跨周期新柱判断』从 EA 主逻辑里抽走,你只管传时间和品种,避免每个指标都重写一遍新柱检测。 CEngine 构造函数把首次启动、上一次交易事件、账户事件、品种事件和全局错误码全部置了初值,m_first_start 为 true 意味着引擎加载后第一帧会走初始化分支,常用于重置挂单上下文。外汇与贵金属杠杆品种波动剧烈,这类状态机若初值错乱,可能在 reload 时重复发单,务必在策略测试器里跑至少 200 根 bar 验证事件流。 注释里还列了交易类需要预设的 12 项参数:填充策略、过期类型、magic、备注、滑点、手数、异步发送标志、日志等级和重试次数等。这些不是在构造函数里赋值,而是留给 Init 阶段逐个 Set,写多品种套利时漏掉 slippage 会导致部分平台拒单。
{ class="kw">return this.m_series.IsNewBar(symbol,timeframe,time); }
class=class="str">"cmt">//--- Update(class="num">1) the specified timeseries of the specified symbol, (class="num">2) the specified timeseries of all symbols,
class=class="str">"cmt">//--- (class="num">3) all timeseries of the specified symbol and(class="num">4) all timeseries of all symbols
class=class="str">"cmt">//--- Return(class="num">1) Open, (class="num">2) High, (class="num">3) Low, (class="num">4) Close, (class="num">5) Time, (class="num">6) TickVolume,
class=class="str">"cmt">//--- (class="num">7) RealVolume, (class="num">8) Spread of the specified bar of the specified symbol of the specified timeframe
class="type">class="kw">double SeriesOpen(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index);
class="type">class="kw">double SeriesHigh(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index);
class="type">class="kw">double SeriesLow(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index);
class="type">class="kw">double SeriesClose(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index);
class="type">class="kw">datetime SeriesTime(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index);
class="type">long SeriesTickVolume(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index);
class="type">long SeriesRealVolume(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index);
class="type">int SeriesSpread(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index);
class=class="str">"cmt">//--- Set the following for the trading classes:
class=class="str">"cmt">//--- (class="num">1) correct filling policy, (class="num">2) filling policy,
class=class="str">"cmt">//--- (class="num">3) correct order expiration type, (class="num">4) order expiration type,
class=class="str">"cmt">//--- (class="num">5) magic number, (class="num">6) comment, (class="num">7) slippage, (class="num">8) volume, (class="num">9) order expiration date,
class=class="str">"cmt">//--- (class="num">10) the flag of asynchronous sending of a trading request, (class="num">11) logging level, (class="num">12) number of trading attempts
CEngine::CEngine() : m_first_start(true),
m_last_trade_event(TRADE_EVENT_NO_EVENT),
m_last_account_event(WRONG_VALUE),
m_last_symbol_event(WRONG_VALUE),
m_global_error(ERR_SUCCESS)
{「引擎初始化里的对冲判定与计时器装配」
CEngine 构造时先确认账户模式:MQL4 下直接按锁仓处理,MQL5 则读取 ACCOUNT_MARGIN_MODE,仅当等于 ACCOUNT_MARGIN_MODE_RETAIL_HEDGING 才置 m_is_hedge 为 true。这一步决定后续订单计数与冲突检测逻辑是否走对冲分支,外汇和贵金属账户在高杠杆下锁仓与否会显著影响保证金占用,需自行在 MT5 账户信息里核对。 随后清掉旧计数器并建六组:订单、账户、两个品种、请求、时序各一组,ID 与步长来自 COLLECTION_* 宏。注意 COLLECTION_TS_COUNTER_ID 这组专门驱动 K 线集合刷新,是 OnTimer 里时序更新的开关。 MQL5 用 EventSetMillisecondTimer(TIMER_FREQUENCY) 起毫秒级定时器;若返回 false 就 Print 错误并把 GetLastError 存进 m_global_error。MQL4 仅在非测试环境才建定时器,回测时不占资源。 OnTimer 内按计数器索引取 CTimerCounter,非测试状态下才跑时序更新——这意味着用策略测试器跑历史时,COLLECTION_TS_COUNTER_ID 对应块可能被跳过,验证行情同步逻辑必须接实盘或模拟盘。
this.m_is_hedge=class="macro">#ifdef __MQL4__ true class="macro">#else class="type">bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) class="macro">#endif; this.m_is_tester=::MQLInfoInteger(MQL_TESTER); this.m_program=(ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE); this.m_list_counters.Sort(); this.m_list_counters.Clear(); this.CreateCounter(COLLECTION_ORD_COUNTER_ID,COLLECTION_ORD_COUNTER_STEP,COLLECTION_ORD_PAUSE); this.CreateCounter(COLLECTION_ACC_COUNTER_ID,COLLECTION_ACC_COUNTER_STEP,COLLECTION_ACC_PAUSE); this.CreateCounter(COLLECTION_SYM_COUNTER_ID1,COLLECTION_SYM_COUNTER_STEP1,COLLECTION_SYM_PAUSE1); this.CreateCounter(COLLECTION_SYM_COUNTER_ID2,COLLECTION_SYM_COUNTER_STEP2,COLLECTION_SYM_PAUSE2); this.CreateCounter(COLLECTION_REQ_COUNTER_ID,COLLECTION_REQ_COUNTER_STEP,COLLECTION_REQ_PAUSE); this.CreateCounter(COLLECTION_TS_COUNTER_ID,COLLECTION_TS_COUNTER_STEP,COLLECTION_TS_PAUSE); ::ResetLastError(); class="macro">#ifdef __MQL5__ if(!::EventSetMillisecondTimer(TIMER_FREQUENCY)) { ::Print(DFUN_ERR_LINE,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_TIMER),(class="type">class="kw">string)::GetLastError()); this.m_global_error=::GetLastError(); } class="macro">#else if(!this.IsTester() && !::EventSetMillisecondTimer(TIMER_FREQUENCY)) { ::Print(DFUN_ERR_LINE,CMessage::Text(MSG_LIB_SYS_FAILED_CREATE_TIMER),(class="type">class="kw">string)::GetLastError()); this.m_global_error=::GetLastError(); } class="macro">#endif class="type">void CEngine::OnTimer(class="type">void) { index=this.CounterIndex(COLLECTION_TS_COUNTER_ID); if(index>WRONG_VALUE) { CTimerCounter* counter=this.m_list_counters.At(index); if(counter!=NULL) { if(!this.IsTester()) {
行情刷新与跨品种取价的内核写法
引擎在暂停结束后会调用 m_series.RefreshOther() 去刷新除当前品种外的所有时间序列,而在策略测试器里则不论暂停状态每 tick 都强制刷新一次。这种区分能避免实盘时无关品种频繁重算,又保证回测中跨周期引用的同步性。 OnTick 里先判断 m_program 是否等于 PROGRAM_EXPERT,不是就直接 return,说明这套引擎逻辑只服务于 EA,脚本或指标不会误触发。随后只对当前品种当前周期调 SeriesRefresh(NULL,PERIOD_CURRENT),把刷新开销压到最低。 取 OHLC 的四个方法结构完全一致:用 m_series.GetBar(symbol,timeframe,index) 拿到 CBar 指针,空指针时返回 0,否则返回对应价格。你在多品种监控里若发现某外汇对取价长期返回 0,优先排查该品种历史数据是否未下载——贵金属与交叉盘在 MT5 默认不自动缓存全部周期,属于高频踩坑点。
class="type">void CEngine::OnTick(class="type">void) { class=class="str">"cmt">//--- If this is not a EA, exit if(this.m_program!=PROGRAM_EXPERT) class="kw">return; class=class="str">"cmt">//--- Update the current symbol timeseries this.SeriesRefresh(NULL,PERIOD_CURRENT); } class="type">class="kw">double CEngine::SeriesOpen(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index) { CBar *bar=this.m_series.GetBar(symbol,timeframe,index); class="kw">return(bar!=NULL ? bar.Open() : class="num">0); } class="type">class="kw">double CEngine::SeriesHigh(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index) { CBar *bar=this.m_series.GetBar(symbol,timeframe,index); class="kw">return(bar!=NULL ? bar.High() : class="num">0); } class="type">class="kw">double CEngine::SeriesLow(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index) { CBar *bar=this.m_series.GetBar(symbol,timeframe,index); class="kw">return(bar!=NULL ? bar.Low() : class="num">0); }
◍ 跨周期取数接口的空值兜底
在自研行情引擎里,跨品种跨周期取某根 K 线的字段,最易被忽略的是越界或品种未加载时的返回约定。下面四个方法统一走 m_series.GetBar 拿到 CBar 指针,再按字段类型返回;Close 和 Time 失败回 0,Volume 类失败回 WRONG_VALUE,这差异必须记牢,否则回测里会把 0 当真实零成交量。 SeriesClose 与 SeriesTime 返回 double / datetime,NULL 时给 0;SeriesTickVolume 与 SeriesRealVolume 返回 long,NULL 时给 WRONG_VALUE(通常为 -1)。Spread 方法同理,但经纪商若不对历史 bar 提供点差,取到的值可能长期为 0 或 WRONG_VALUE,做贵金属回测时要先打印验证。 开 MT5 把下面代码丢进你的 CEngine 类,编译后写个脚本遍历 EURUSD 的 H1 第 5000 根 bar,看返回是 0 还是 WRONG_VALUE,就能确认本地历史深度够不够。外汇与贵金属杠杆高,历史数据缺口会让这类接口 silently 失效,仓位计算前务必判空。
class="type">class="kw">double CEngine::SeriesClose(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index) { CBar *bar=this.m_series.GetBar(symbol,timeframe,index); class="kw">return(bar!=NULL ? bar.Close() : class="num">0); } class="type">class="kw">datetime CEngine::SeriesTime(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index) { CBar *bar=this.m_series.GetBar(symbol,timeframe,index); class="kw">return(bar!=NULL ? bar.Time() : class="num">0); } class="type">long CEngine::SeriesTickVolume(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index) { CBar *bar=this.m_series.GetBar(symbol,timeframe,index); class="kw">return(bar!=NULL ? bar.VolumeTick() : WRONG_VALUE); } class="type">long CEngine::SeriesRealVolume(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index) { CBar *bar=this.m_series.GetBar(symbol,timeframe,index); class="kw">return(bar!=NULL ? bar.VolumeReal() : WRONG_VALUE); } class="type">int CEngine::SeriesSpread(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int index) { CBar *bar=this.m_series.GetBar(symbol,timeframe,index);
「从柱线对象取点差的安全写法」
在 MQL5 里用 iBar 拿到具体柱线对象后,直接读点差最稳的方式是判断指针是否有效。若柱线为空却强行调用 Spread(),终端会抛无效指针访问,回测时这种报错常常静默中断 OnTick。 上面这段返回逻辑把空指针兜底成了 INT_MIN,实盘里你看到某根 K 线点差等于 -2147483648 就明白是取不到柱了,而不是真有负点差。外汇与贵金属杠杆高,点差瞬跳属常态,这类边界判断能少踩很多坑。 把这段塞进你自己的指标里,跑一轮 EURUSD 的 M1 历史数据,看是否在少数跳空缺口处打出 INT_MIN,就能验证兜底是否生效。
class="kw">return(bar!=NULL ? bar.Spread() : INT_MIN); }
用 EA 实时验证多品种零号柱线读取
把上一篇文章的 EA 存到 \MQL5\Experts\TestDoEasy\Part38\ 下命名为 TestDoEasyPart38.mq5,就能直接跑这套验证。EA 在 OnTick 里每次即时报价调用函数库 NewTick 处理,刷新已创建的时间序列;那些声明了却没用 Create() 建出来的序列会被跳过,以后需要从 EA 侧主动调方法刷新。 测试时把参数设成:品种列表模式用指定列表,只留三个品种且含 EURUSD;时间帧列表模式用当前周期。在 EURUSD 图表挂上 EA,过一会儿日志会打印该图表品种的“新柱线”事件,可视测试器里也能看到注释区两行零号柱数据随报价跳动。 两行数据一路来自 CBar 对象方法(bar.Header()+参数描述),一路来自引擎主对象 SeriesXxx 方法,读取的是同一根零号柱的 OHLCV,数值应完全一致。外汇和贵金属杠杆高、滑点跳空频繁,多品种同步刷新在实盘可能比测试器延迟更明显,验证时先用模拟盘。 下面这段 OnTick 是标准操控骨架:测试器内先跑定时器与事件,再交还给函数库;真正取数在 SeriesGetBar(NULL,PERIOD_CURRENT,0) 拿到柱对象后拼字符串。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- If working in the tester if(MQLInfoInteger(MQL_TESTER)) { engine.OnTimer(); class=class="str">"cmt">// Working in the timer PressButtonsControl(); class=class="str">"cmt">// Button pressing control EventsHandling(); class=class="str">"cmt">// Working with events } class=class="str">"cmt">//--- Handle the NewTick event in the library engine.OnTick(); class=class="str">"cmt">//--- If the trailing flag is set if(trailing_on) { TrailingPositions(); class=class="str">"cmt">// Trailing positions TrailingOrders(); class=class="str">"cmt">// Trailing of pending orders } class=class="str">"cmt">//--- Bet the zero bar of the current timeseries CBar *bar=engine.SeriesGetBar(NULL,PERIOD_CURRENT,class="num">0); if(bar==NULL) class="kw">return; class=class="str">"cmt">//--- Create a class="type">class="kw">string of parameters of the current bar similar to the one class=class="str">"cmt">//--- displayed by the bar object description: class=class="str">"cmt">//--- bar.Header()+": "+bar.ParameterDescription() class="type">class="kw">string parameters= (TextByLanguage("Бар "","Bar "")+Symbol()+"\" "+TimeframeDescription((ENUM_TIMEFRAMES)Period())+"[class="num">0]: "+TimeToString(bar.Time(),TIME_DATE|TIME_MINUTES|TIME_SECONDS)+ ", O: "+DoubleToString(engine.SeriesOpen(NULL,PERIOD_CURRENT,class="num">0),Digits())+ ", H: "+DoubleToString(engine.SeriesHigh(NULL,PERIOD_CURRENT,class="num">0),Digits())+ ", L: "+DoubleToString(engine.SeriesLow(NULL,PERIOD_CURRENT,class="num">0),Digits())+ ", C: "+DoubleToString(engine.SeriesClose(NULL,PERIOD_CURRENT,class="num">0),Digits())+ ", V: "+(class="type">class="kw">string)engine.SeriesTickVolume(NULL,PERIOD_CURRENT,class="num">0)+
◍ 把多周期时序一次性同步进内存
这段逻辑干的事很直接:先拿 engine.GetListTimeSeries() 取出所有已用品种的时序对象列表,再对列表里每个 CTimeSeries 跑两层循环,按 array_used_periods 里声明的周期逐个 SyncData() 和 Create()。这样多品种多周期的数据会在初始化阶段就建好,后续取价不用来回等 broker 返回。
MQL4 没有 ArrayPrint(),所以打印已用周期列表的代码被 #ifdef __MQL5__ 包住,只在 MQL5 编译期生效;若你在 MT4 跑同一套框架,这行直接跳过,不会报错。
初始化末尾用 engine.GetTimeSeriesCollection().PrintShort(true) 把已建成的时序短描述丢进日志,true 表示只列真正创建成功的。想看全量声明(含未同步的),把注释里的 Print(true) 放开即可。
OnDoEasyEvent 里从 lparam 拆出毫秒、原因、来源三项是固定套路:engine.EventMSC(lparam) 取毫秒,TimeCurrent()*1000+msc 拼出事件精确时间。外汇与贵金属杠杆高,多周期数据若因点差跳变而同步失败,日志里会暴露来源代号,需人工核对。
CArrayObj *list_timeseries=engine.GetListTimeSeries(); if(list_timeseries!=NULL) { class="type">int total=list_timeseries.Total(); for(class="type">int i=class="num">0;i<total;i++) { CTimeSeries *timeseries=list_timeseries.At(i); class="type">int total_periods=ArraySize(array_used_periods); for(class="type">int j=class="num">0;j<total_periods;j++) { ENUM_TIMEFRAMES timeframe=TimeframeByDescription(array_used_periods[j]); timeseries.SyncData(timeframe); timeseries.Create(timeframe); } } }
「用事件钩子抓新K线落地」
在 MT5 的 OnChartEvent 体系里,时间序列事件被单独划了一块区间:idx 大于 SERIES_EVENTS_NO_EVENT 且小于 SERIES_EVENTS_NEXT_CODE 时,才进入 K 线类事件分支。 其中 SERIES_EVENTS_NEW_BAR 是实盘最常用的触发点。一旦命中,就能拿到品种名 sparam、周期 dparam 和这根新柱的起始时间 lparam,直接 Print 出来即可在日志看到多品种同步出新的现象。 下面这段是核心判定与打印逻辑,注意 else if 的边界写法,漏掉边界可能吞掉其他事件类型:
class=class="str">"cmt">//--- Handling timeseries events else if(idx>SERIES_EVENTS_NO_EVENT && idx<SERIES_EVENTS_NEXT_CODE) { class=class="str">"cmt">//--- "New bar" event if(idx==SERIES_EVENTS_NEW_BAR) { Print(TextByLanguage("Новый бар на ","New Bar on "),sparam," ",TimeframeDescription((ENUM_TIMEFRAMES)dparam),": ",TimeToString(lparam)); } }
class=class="str">"cmt">//--- Handling timeseries events else if(idx>SERIES_EVENTS_NO_EVENT && idx<SERIES_EVENTS_NEXT_CODE) { class=class="str">"cmt">//--- "New bar" event if(idx==SERIES_EVENTS_NEW_BAR) { Print(TextByLanguage("Новый бар на ","New Bar on "),sparam," ",TimeframeDescription((ENUM_TIMEFRAMES)dparam),": ",TimeToString(lparam)); } }
一点提醒
这套 DoEasy 时间序列函数库当前随文附带的压缩包体积约 3.7 MB(MQL5.zip 为 3698.04 KB,MQL4.zip 为 3697.98 KB),内含全部库文件与测试 EA,可直接在 MT5 里加载验证前文所说的按品种、周期管理时间序列对象的逻辑。 测试 EA 在作者与读者的评论对话里被明确点出:它专为验证部分平仓逻辑设计,不会以最小手数开仓,因此别拿去实盘跑,外汇与贵金属品种的高杠杆风险本就不在它的设计语境内。 后续文章会修掉这一版已知的缺陷,并把时间序列操控进一步做成库内函数。你现在能做的,是把 ZIP 里的文件拖进 MT5 的 MQL5 目录,编译后看时间序列集合能否在策略测试器里正常建库。