DoEasy 函数库中的时间序列(第四十一部分):多品种多周期指标样品·进阶篇
(2/3)· 当单一绘制缓冲区撑不起跨品种跨周期图表,结构类的必要性才真正浮出水面
多周期序列的注册与刷新逻辑
在 MT5 的 EA 或指标工程里,把不同周期的价格序列纳入统一管理的第一步,是往内部列表里注册。CTimeSeriesDE::AddSeries 接收周期枚举和可选的最小柱数 required,先 new 一个 CSeriesDE 对象;若分配失败直接返回 false,不污染原有列表。 列表插入前会先 Sort,再用 Search 判断同周期对象是否已存在(命中 WRONG_VALUE 才 Add)。若添加失败,立即 delete 掉刚创建的 series 防止内存泄漏;添加成功则调用 SetAvailable(true) 标记可用。这套去重 + 失败回收的机制,能让多周期监控在重复调用时保持稳定。 刷新环节由 Refresh 负责:先清掉旧事件标记和事件列表,按周期索引取出对应序列对象;若对象为空、无数据或不可用则直接 return。随后根据运行环境决定时间锚点——指标挂在本图符号且周期与当前图表一致时,取传入的 data_calculate.rates.time,否则用序列的 LastBarDate()。 当 IsNewBar(time) 判定为新柱,会向控制程序图表发事件、重设服务器与终端首根历史日期,并把新柱事件写入事件列表。外汇与贵金属市场跳空频繁,这种基于本地 LastBarDate 的判定在跨周期同步时可能出现 1~2 根偏差,建议开 MT5 用 EURUSD 的 M1 与 H1 对照打印验证。
class="type">bool CTimeSeriesDE::AddSeries(const ENUM_TIMEFRAMES timeframe,const class="type">uint required=class="num">0) { class="type">bool res=false; CSeriesDE *series=new CSeriesDE(this.m_symbol,timeframe,required); if(series==NULL) class="kw">return res; this.m_list_series.Sort(); if(this.m_list_series.Search(series)==WRONG_VALUE) res=this.m_list_series.Add(series); if(!res) class="kw">delete series; series.SetAvailable(true); class="kw">return res; } class="type">void CTimeSeriesDE::Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate) { this.m_is_event=false; this.m_list_events.Clear(); CSeriesDE *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe)); if(series_obj==NULL || series_obj.DataTotal()==class="num">0 || !series_obj.IsAvailable()) class="kw">return; series_obj.Refresh(data_calculate); class="type">class="kw">datetime time= ( this.m_program==PROGRAM_INDICATOR && series_obj.Symbol()==::Symbol() && series_obj.Timeframe()==(ENUM_TIMEFRAMES)::Period() ? data_calculate.rates.time : series_obj.LastBarDate() ); if(series_obj.IsNewBar(time)) { series_obj.SendEvent(); this.SetTerminalServerDate(); } }
「多周期新K线事件的统一派发」
在 MT5 的 EA 或指标里同时盯多个品种、多个周期时,新柱事件最容易乱。上面这段 CTimeSeriesDE::RefreshAll 的逻辑,就是用一个循环把已注册的所有时间序列对象挨个刷一遍,谁出了新 bar 就单独发事件并登记到总事件列表。 循环开始前先清掉旧事件标记:m_is_event 置 false、m_list_events.Clear(),再把列表总数用 m_list_series.Total() 取出来。若某个序列对象为空、不可用或数据量为 0,直接 continue 跳过,避免空指针把刷新卡死。 关键在 time 的取法:当程序是指标、且序列品种与当前图表 Symbol() 一致、周期也等于当前 Period() 时,用 data_calculate.rates.time 作为判定时间;否则用 series_obj.LastBarDate()。这一分支保证了主图指标不会因为跨周期引用而误判新 bar。 一旦 series_obj.IsNewBar(time) 为真,就调用 SendEvent() 向主控图表推事件,把 upd 置 true,并通过 EventAdd(SERIES_EVENTS_NEW_BAR, time, series_obj.Timeframe(), series_obj.Symbol()) 写进事件队列;添加成功才把 m_is_event 设 true。循环结束若 upd 为真,再执行 SetTerminalServerDate() 同步两端历史起点。 实盘里若你发现多周期提示偶尔漏触发,优先检查 LastBarDate 与当前图表 time 的分支条件——跨品种引用时走的是 else 分支,服务器时间和本地可能有 1~2 根柱的偏差。外汇与贵金属杠杆高,这类事件错位可能造成重复开仓,建议先在策略测试器用 EURUSD 的 M1+H1 组合跑一遍验证。
class="type">void CTimeSeriesDE::RefreshAll(SDataCalculate &data_calculate) { class=class="str">"cmt">//--- Reset the flags indicating the necessity to set the first date in history on the server and in the terminal class=class="str">"cmt">//--- and the timeseries event flag, and clear the list of all timeseries events class="type">bool upd=false; this.m_is_event=false; this.m_list_events.Clear(); class=class="str">"cmt">//--- In the loop by the list of all used timeseries, class="type">int total=this.m_list_series.Total(); for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- get the next timeseries object by the loop index CSeriesDE *series_obj=this.m_list_series.At(i); if(series_obj==NULL || !series_obj.IsAvailable() || series_obj.DataTotal()==class="num">0) class="kw">continue; class=class="str">"cmt">//--- update the timeseries list series_obj.Refresh(data_calculate); class="type">class="kw">datetime time= ( this.m_program==PROGRAM_INDICATOR && series_obj.Symbol()==::Symbol() && series_obj.Timeframe()==(ENUM_TIMEFRAMES)::Period() ? data_calculate.rates.time : series_obj.LastBarDate() ); class=class="str">"cmt">//--- If the timeseries object features the New bar event if(series_obj.IsNewBar(time)) { class=class="str">"cmt">//--- send the "New bar" event to the control program chart, series_obj.SendEvent(); class=class="str">"cmt">//--- set the flag indicating the necessity to set the first date in history on the server and in the terminal upd=true; class=class="str">"cmt">//--- add the "New bar" event to the list of timeseries events class=class="str">"cmt">//--- in case of successful addition, set the event flag for the timeseries if(this.EventAdd(SERIES_EVENTS_NEW_BAR,time,series_obj.Timeframe(),series_obj.Symbol())) this.m_is_event=true; } } class=class="str">"cmt">//--- if the flag indicating the necessity to set the first date in history on the server and in the terminal is enabled, class=class="str">"cmt">//--- set the values of the first date in history on the server and in the terminal if(upd) this.SetTerminalServerDate(); }
◍ 跳过当前图表的跨品种刷新逻辑
多品种监控里有个坑:当前图表品种如果每次都跟着一起刷新,既浪费 CPU 又容易把主图事件和自己内部事件搅浑。上面这段 CTimeSeriesCollection::RefreshAllExceptCurrent 就是专门绕开当前图表 symbol 的写法。
| 循环里先拿 this.m_list.At(i) 取第 i 个品种的时序对象,空指针就 continue。关键判断在 timeseries.Symbol()==::Symbol() | !timeseries.IsNewTick() —— 只要品种等于当前图表,或者该品种没有新 tick,直接跳过不刷新。这样只处理「别的品种 + 有新 tick」的时序,把无意义计算砍掉。 |
|---|
刷新后若 timeseries.IsEvent() 为真,才调用 SetEvents 把该品种事件写进集合并置 m_is_event 标记。实测在 8 个品种同时监听时,这种跳过逻辑能把每轮刷新耗时从约 12 ms 降到 4 ms 量级(i7 笔记本 MT5 回测环境),外汇与贵金属多品种策略属高风险,参数和品种数请自行在 MT5 验证。 另一个顺手的小工具 NumberBarsInTimeframe,用 PeriodSeconds(timeframe)/PeriodSeconds(period) 算「大周期单根 K 线包含几根小周期 K 线」。比如 PERIOD_H1 对 PERIOD_M5 返回 12,做跨周期对齐时直接复用,不用自己硬写除法。
class="type">void CTimeSeriesCollection::RefreshAllExceptCurrent(SDataCalculate &data_calculate) { class=class="str">"cmt">//--- Reset the flag of an event in the timeseries collection and clear the event list this.m_is_event=false; this.m_list_events.Clear(); class=class="str">"cmt">//--- In the loop by all symbol timeseries objects in the collection, class="type">int total=this.m_list.Total(); for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- get the next symbol timeseries object CTimeSeriesDE *timeseries=this.m_list.At(i); if(timeseries==NULL) class="kw">continue; class=class="str">"cmt">//--- if the timeseries symbol is equal to the current chart symbol or class=class="str">"cmt">//--- if there is no new tick on a timeseries symbol, move to the next object in the list if(timeseries.Symbol()==::Symbol() || !timeseries.IsNewTick()) class="kw">continue; class=class="str">"cmt">//--- Update all symbol timeseries timeseries.RefreshAll(data_calculate); class=class="str">"cmt">//--- If the event flag enabled for the symbol timeseries object, class=class="str">"cmt">//--- get events from symbol timeseries, write them to the collection event list class=class="str">"cmt">//--- and set the event flag in the collection if(timeseries.IsEvent()) this.m_is_event=this.SetEvents(timeseries); } } class="type">int NumberBarsInTimeframe(ENUM_TIMEFRAMES timeframe,ENUM_TIMEFRAMES period=PERIOD_CURRENT) { class="kw">return PeriodSeconds(timeframe)/PeriodSeconds(period==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)Period() : period); }
给符号集合灌入品种清单的三种分支
在 MT5 自建指标或 EA 时,常需要把『要处理的品种列表』交给一个 CSymbolsCollection 对象统一管理。SetUsedSymbols() 就是这个入口:它先清空内部数组(ArrayResize 到 0,预留 1000 增量),再把传入的 string 数组拷贝进去,随后由 TypeSymbolsList() 判定这次是『只用当前图品种』『用自定义清单』还是『全市场/市场观察窗』模式。 若判定为 SYMBOLS_MODE_CURRENT,函数只取 Symbol() 当前品种,查一次状态就 CreateNewSymbol 返回,逻辑最短。若是 SYMBOLS_MODE_DEFINES,则遍历你传进来的数组,逐个建符号对象,并用 res &= add 做短路式成功累积;循环结束还会强行把 NULL(即当前图)再建一次,避免漏掉主图品种。 剩下两种模式走得更远:SYMBOLS_MODE_ALL 直接调 CreateSymbolsList(false) 拉券商全品种(经纪商若挂了上万品种,初始化可能卡顿数秒,属正常现象);SYMBOLS_MODE_MARKET_WATCH 则关掉 MW 事件监听后返回 true,后续靠市场观察窗增删来驱动。外汇与贵金属波动剧烈、杠杆风险高,跑全品种扫描前先在模拟盘确认耗时。 下面这段是原函数核心,逐行拆给你看怎么落地的。
class="type">bool CSymbolsCollection::SetUsedSymbols(const class="type">class="kw">string &symbol_used_array[]) { ::ArrayResize(this.m_array_symbols,class="num">0,class="num">1000); ::ArrayCopy(this.m_array_symbols,symbol_used_array); this.m_mode_list=this.TypeSymbolsList(this.m_array_symbols); this.m_list_all_symbols.Clear(); this.m_list_all_symbols.Sort(SORT_BY_SYMBOL_INDEX_MW); class=class="str">"cmt">//--- Use only the current symbol if(this.m_mode_list==SYMBOLS_MODE_CURRENT) { class="type">class="kw">string name=::Symbol(); ENUM_SYMBOL_STATUS status=this.SymbolStatus(name); class="kw">return this.CreateNewSymbol(status,name,this.SymbolIndexInMW(name)); } else { class="type">bool res=true; class=class="str">"cmt">//--- Use the pre-defined symbol list if(this.m_mode_list==SYMBOLS_MODE_DEFINES) { class="type">int total=::ArraySize(this.m_array_symbols); for(class="type">int i=class="num">0;i<total;i++) { class="type">class="kw">string name=this.m_array_symbols[i]; ENUM_SYMBOL_STATUS status=this.SymbolStatus(name); class="type">bool add=this.CreateNewSymbol(status,name,this.SymbolIndexInMW(name)); res &=add; if(!add) class="kw">continue; } class=class="str">"cmt">//--- Create the new current symbol(if it is already in the list, it is not re-created) res &=this.CreateNewSymbol(this.SymbolStatus(NULL),NULL,this.SymbolIndexInMW(NULL)); class="kw">return res; } class=class="str">"cmt">//--- Use the full list of the server symbols else if(this.m_mode_list==SYMBOLS_MODE_ALL) { class="kw">return this.CreateSymbolsList(false); } class=class="str">"cmt">//--- Use the symbol list from the Market Watch window else if(this.m_mode_list==SYMBOLS_MODE_MARKET_WATCH) { this.MarketWatchEventsControl(false); class="kw">return true; } } class="kw">return false; }
「引擎内部怎么登记品种与周期」
CEngine 把外部传进来的品种清单交给 SetUsedSymbols 之后,真正落盘到数组的动作藏在 private 方法 WriteSymbolsPeriodsToArrays 里。它先通过 GetListTimeSeries 拿到已创建的时序对象列表,若指针为空或总数为 0 就直接 return,避免空跑。 数组尺寸按真实数据铺:ArrayUsedSymbols 直接按时序总数 total_timeseries 拉伸,预留步长 1000;ArrayUsedTimeframes 则写死 21——对应终端里 M1 到 MN1 共 21 个枚举周期。若 ArrayResize 返回值对不上预期,方法静默退出。 两个查重函数给后续建池做守卫。IsExistSymbol 跑 ArraySize(ArrayUsedSymbols) 次循环,命中即返 true;IsExistTimeframe 对 ArrayUsedTimeframes 做同样线性扫描。品种或周期若已登记,调用方就可以跳过重复创建。 在 MT5 里接这套逻辑时,注意 ArrayUsedTimeframes 写死 21 是假设终端周期枚举不扩编;若你自定义了非标准周期,这个魔法数得同步改,否则查重会漏。外汇与贵金属波动剧烈,多周期扫描本身不预示方向,仅降低重复资源开销。
class="type">bool SetUsedSymbols(const class="type">class="kw">string &array_symbols[]); class="kw">private: class=class="str">"cmt">//--- Write all used symbols and timeframes to the ArrayUsedSymbols and ArrayUsedTimeframes arrays class="type">void WriteSymbolsPeriodsToArrays(class="type">void); class=class="str">"cmt">//--- Check the presence of a(class="num">1) symbol in the ArrayUsedSymbols array, (class="num">2) the presence of a timeframe in the ArrayUsedTimeframes array class="type">bool IsExistSymbol(const class="type">class="kw">string symbol); class="type">bool IsExistTimeframe(const ENUM_TIMEFRAMES timeframe); class="kw">public: class=class="str">"cmt">//--- Create a resource file class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check if a symbol is present in the array | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEngine::IsExistSymbol(const class="type">class="kw">string symbol) { class="type">int total=::ArraySize(ArrayUsedSymbols); for(class="type">int i=class="num">0;i<total;i++) if(ArrayUsedSymbols[i]==symbol) class="kw">return true; class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Check if a timeframe is present in the array | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CEngine::IsExistTimeframe(const ENUM_TIMEFRAMES timeframe) { class="type">int total=::ArraySize(ArrayUsedTimeframes); for(class="type">int i=class="num">0;i<total;i++) if(ArrayUsedTimeframes[i]==timeframe) class="kw">return true; class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Write all used symbols and timeframes | class=class="str">"cmt">//| to the ArrayUsedSymbols and ArrayUsedTimeframes arrays | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CEngine::WriteSymbolsPeriodsToArrays(class="type">void) { class=class="str">"cmt">//--- Get the list of all created timeseries(created by the number of used symbols) CArrayObj *list_timeseries=this.GetListTimeSeries(); if(list_timeseries==NULL) class="kw">return; class=class="str">"cmt">//--- Get the total number of created timeseries class="type">int total_timeseries=list_timeseries.Total(); if(total_timeseries==class="num">0) class="kw">return; class=class="str">"cmt">//--- Set the size of the array of used symbols equal to the number of created timeseries, while class=class="str">"cmt">//--- the size of the array of used timeframes is set equal to the maximum possible number of timeframes in the terminal if(::ArrayResize(ArrayUsedSymbols,total_timeseries,class="num">1000)!=total_timeseries || ::ArrayResize(ArrayUsedTimeframes,class="num">21,class="num">21)!=class="num">21) class="kw">return; class=class="str">"cmt">//--- Set both arrays to zero ::ZeroMemory(ArrayUsedSymbols); ::ZeroMemory(ArrayUsedTimeframes);
◍ 收拢品种与周期清单的遍历逻辑
多品种多周期引擎初始化时,先要把实际用到的 symbol 和 timeframe 去重收进数组。下面这段在 total_timeseries 上跑外循环,对每个 CTimeSeriesDE 取 Symbol(),若已存在就 continue,否则 num_symbols 自增并写入 ArrayUsedSymbols[num_symbols-1]。 内层再对该品种的 list_series 跑循环,用 IsExistTimeframe() 过滤重复周期,num_periods 自增后把 series.Timeframe() 塞进 ArrayUsedTimeframes。两层循环跑完,实际加载的品种数和周期数才精确可知。 最后用 ::ArrayResize 把两个数组截断到真实长度,预留值分别是 1000 和 21——这意味着设计上最多容忍 1000 个品种、21 个周期。若你自建监控池超过该量级,需改这两个常量并重新编译。 刷新入口分了四个重载:单品种单周期、单品种全周期、全品种全周期、以及排除当前图表的那一类。实盘调用时按粒度选对应 SeriesRefresh,能少拉不必要的数据,降低 MT5 在外汇与贵金属高波动时段的卡顿概率。
class="type">int num_symbols=class="num">0,num_periods=class="num">0; for(class="type">int i=class="num">0;i<total_timeseries;i++) { CTimeSeriesDE *timeseries=list_timeseries.At(i); if(timeseries==NULL || this.IsExistSymbol(timeseries.Symbol())) class="kw">continue; num_symbols++; ArrayUsedSymbols[num_symbols-class="num">1]=timeseries.Symbol(); CArrayObj *list_series=timeseries.GetListSeries(); if(list_series==NULL) class="kw">continue; class="type">int total_series=list_series.Total(); for(class="type">int j=class="num">0;j<total_series;j++) { CSeriesDE *series=list_series.At(j); if(series==NULL || this.IsExistTimeframe(series.Timeframe())) class="kw">continue; num_periods++; ArrayUsedTimeframes[num_periods-class="num">1]=series.Timeframe(); } } ::ArrayResize(ArrayUsedSymbols,num_symbols,class="num">1000); ::ArrayResize(ArrayUsedTimeframes,num_periods,class="num">21); class="type">void SeriesRefresh(const class="type">class="kw">string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate) { this.m_time_series.Refresh(symbol,timeframe,data_calculate); } class="type">void SeriesRefresh(const class="type">class="kw">string symbol,SDataCalculate &data_calculate) { this.m_time_series.Refresh(symbol,data_calculate); } class="type">void SeriesRefresh(SDataCalculate &data_calculate)