多周期指标缓冲区基础:用索引与时间帧拆解 DoEasy 跨周期绘图(基础篇)
(1/3)· 面对 M15 图表套 H1 数据的错位显示,多数自行拼凑的跨周期指标都在重复造轮子
很多交易者在写多周期指标时,习惯手动逐根对齐不同周期柱线,结果图表上数值错位、重绘频发。其实只要给缓冲区设定好时间帧,跨周期填充本可以由底层函数库自动接管。第四十五部分开始系统梳理这套机制,本篇先铺垫最基础的类结构与索引逻辑。
多周期指标缓冲区的类改造起点
在 DoEasy 函数库里,时间序列相关逻辑已经推进到第四十五部分,这一节聚焦的是多周期模式下指标缓冲区的操控类改造。原有结构在跨周期取缓冲数据时存在冗余调用,重写的类目标就是把周期切换和缓冲索引的映射收拢到同一层。 目前这套改动的示例发布于 2020 年 10 月 24 日 14:17,原文页面显示阅读量 1812、互动 6 条,说明当时社区对多周期指标封装的刚需并不小。 这一节没有给出完整回测,只明确了类的接口方向和测试入口;真正落地要看后续把多周期缓冲读取接进具体指标后的表现,外汇与贵金属品种在多点差时段跨周期刷新延迟可能更明显,需自行在 MT5 用真实点差验证。
「多周期缓冲区怎么按样式各自编号」
做多周期指标时,最麻烦的是把不同时间帧的数据正确铺到当前图表上。假设当前图表是 M15,而某个缓冲区对象设的是 H1,那么 H1 每根柱线的数值会映射到 M15 里时间落在该 H1 柱范围内的四根小周期柱线上——也就是连续四根 M15 柱显示同一个 H1 值。函数库把这套对齐逻辑封装掉,用户只给缓冲区设 period 就行。 缓冲区索引不是全局排号,而是按绘图样式各算各的序列。先建箭头缓冲区,索引 0;再建线条缓冲区,它作为第一个线条样式索引也是 0;接着第二个箭头缓冲区,箭头序列里排到 1。这样每种图形类型内部从 0 自增,不受其他样式插入影响。 实际创建顺序对应的索引如下:箭头0、线条0、箭头1、之字折线0、之字折线1、箭头2、箭头3、线条1、蜡烛0、箭头4。除了按样式内索引取数,还能用图形系列名称、时间、数据窗口索引和集合列表索引访问,也能直接抓最后加进集合的那个对象,省去额外记句柄的麻烦。 这套机制让新建缓冲区变成单行代码串的事:建完对象顺手改属性,MT5 里打开任意小周期图挂上测试指标就能看到大周期数值被摊平显示。外汇与贵金属波动剧烈,多周期错位本身不代表方向,验证时请先用模拟盘。
◍ 多周期品种扩容后的缓冲区索引接管方案
MT5 构建 2430 之后,市场观察窗口可列品种从 1000 提升到 5000。DoEasy 库在 Defines.mqh 引入宏替换,把 SymbolsCollection.mqh、Engine.mqh 里所有写死的 1000 改成宏,这样后续要再扩容量只改一处即可。 缓冲区对象新增了一个整数型属性,用来记录“下一个指标缓冲区基准数组索引”,枚举数量从 19 增到 20,同时把原 BUFFER_PROP_INDEX_NEXT 重命名为 BUFFER_PROP_INDEX_NEXT_BASE。创建缓冲区时直接把基准数组索引和图形序列索引写进当前对象属性,新缓冲区只需读取上一个对象的属性就能拿到应分配的索引,不必按 1~5 个数组重算一遍。 日志简述里用 P(Plot 图形序列索引)、B(Base 基准数组索引)、C(Color 颜色数组索引)标记。实测连续创建所有可能缓冲区对象后,计算缓冲区显示 P8、C27,表示它的图形序列和颜色数组应排在其后创建的真实缓冲区;计算缓冲区本身无图形序列和颜色数组,仅留 B。 新增 CBufferCalculate 类承载“只算不画”的缓冲区。类里加 m_act_state_trigger 标志避免按钮触发类操作被重复执行;IndexNextBaseBuffer() 替代旧 IndexNextBuffer(),另加返回下一个绘制缓冲区索引的方法。构造函数里若对象是计算缓冲区,设置图形序列值前直接 return,索引计算也跳过颜色数组。 CBuffersCollection 现在持有指向时间序列集合的指针,才能跨周期取数映射回当前图表缓冲区。GetBarsData() 私法返回某非本地周期一根柱对应的当前 TF 柱数,供跨周期写缓冲使用;GetBufferByTimeframe() 按周期取最后一个同周期缓冲区指针,列表空则返回 NULL。
缓冲区集合与引擎类的多周期初始化衔接
做多周期指标时,第一步是把绘制缓冲区和计算缓冲区分开处理。绘制缓冲区用 InitializeAll() 带输入参数初始化,计算缓冲区则走 InitializeCalculates() 重载,两者都依赖先从列表里按类型筛出对应对象再逐个遍历。 箭头缓冲区的编号容易踩坑:它是按创建顺序在「同类型」里排的,第一个箭头缓冲区是 0、第二个是 1,和集合列表里的升序索引不是一回事。调 BufferSetDataArrow() 时传的编号是类型内序号,不是列表索引。 CEngine 主类里我加了 28 个方法,逻辑彼此雷同,差异只在数组数量。关键约束是:计算缓冲区必须在所有绘制缓冲区建完之后再建,中途插一个计算缓冲区会打乱数组绑定顺序,其后创建的绘制线直接不显示——这个局限目前没查到根因,后续再修。 OnInit() 阶段要把时间序列集合类的指针塞给缓冲区集合类,调用 CollectionOnInit() 里新增的传递动作,底层走缓冲区集合类的 OnInit() 方法。基于函数库的程序只需在 OnInitDoEasy() 里被自动调起即可,不用手写挂载。 所有 BufferSetDataXxx() 和对应的 Color / ColorIndex 方法都改成多周期感知:as_current 标志为 false(默认)时,会自动把其他周期数据映射到现在图表柱线上;置 true 就只认当前周期。一次调用就能跨周期出图,不用自己算柱线偏移。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Macro substitutions | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Describe the function with the error line number .... class=class="str">"cmt">//--- Symbol parameters class="macro">#define CLR_DEFAULT(0xFF000000) class=class="str">"cmt">// Default symbol background class="type">class="kw">color in the navigator
「符号容量与同步参数的编译期分叉」
在 MT5 构建版本低于 2430 时,可工作符号总量被限制为 1000;达到 2430 及以上则放宽到 5000。这个阈值用预编译宏直接写死,意味着老终端跑多品种扫描 EA 时,循环上限会悄悄砍掉八成容量,升级客户端后才可能拿到完整 5000 符号池。 同步逻辑里还有几个硬参数值得记:默认时间序列取 1000 根,每次同步尝试之间暂停 16 毫秒,最多重试 5 次。若你的 EA 在登录后频繁报「符号未同步」,优先怀疑这 5×16ms 的窗口不够,而不是网络本身。 下面这段宏定义和符号写入代码,建议直接贴进 MT5 的 MetaEditor 里验证分叉行为。
class="macro">#ifdef __MQL5__ class="macro">#define SYMBOLS_COMMON_TOTAL(TerminalInfoInteger(TERMINAL_BUILD)<class="num">2430 ? class="num">1000 : class="num">5000) class=class="str">"cmt">// Total number of MQL5 working symbols class="macro">#else class="macro">#define SYMBOLS_COMMON_TOTAL(class="num">1000) class=class="str">"cmt">// Total number of MQL4 working symbols class="macro">#endif class=class="str">"cmt">//--- Pending request type IDs class="macro">#define PENDING_REQUEST_ID_TYPE_ERR(class="num">1) class=class="str">"cmt">// Type of a pending request created based on the server class="kw">return code class="macro">#define PENDING_REQUEST_ID_TYPE_REQ(class="num">2) class=class="str">"cmt">// Type of a pending request created by request class=class="str">"cmt">//--- Timeseries parameters class="macro">#define SERIES_DEFAULT_BARS_COUNT(class="num">1000) class=class="str">"cmt">// Required class="kw">default amount of timeseries data class="macro">#define PAUSE_FOR_SYNC_ATTEMPTS(class="num">16) class=class="str">"cmt">// Amount of pause milliseconds between synchronization attempts class="macro">#define ATTEMPTS_FOR_SYNC(class="num">5) class=class="str">"cmt">// Number of attempts to receive synchronization with the server class="type">bool CSymbolsCollection::SetUsedSymbols(class="kw">const class="type">class="kw">string &symbol_used_array[]) { ::ArrayResize(this.m_array_symbols,class="num">0,SYMBOLS_COMMON_TOTAL); ::ArrayCopy(this.m_array_symbols,symbol_used_array); } class="type">bool CSymbolsCollection::CreateSymbolsList(class="kw">const class="type">bool flag) { class="type">bool res=true; class="type">int total=::SymbolsTotal(flag); for(class="type">int i=class="num">0;i<total && i<SYMBOLS_COMMON_TOTAL;i++) { } }
◍ 把多品种多周期塞进两个数组
引擎里真正干活的是 WriteSymbolsPeriodsToArrays,它把已创建的时序对象摊平,分别写进品种数组和时间框架数组。调用前先拿 GetListTimeSeries 的总量,若总量为 0 直接 return,避免空跑。 数组尺寸先按极端情况开:品种数组用 total_timeseries 做初始长度、预留 SYMBOLS_COMMON_TOTAL 作为储备;时间框架数组直接写死 21——这是 MT5 终端里最大可能的周期数。随后 ZeroMemory 清两遍,保证旧数据不串味。 双重循环是核心。外层按时序对象走,跳过已记录品种;内层从该品种的时序列表里抓周期,跳过已记录周期。每落一个新品种 num_symbols 加一、新周期 num_periods 加一,下标都用减一填。 循环结束再 ArrayResize 一次,把两个数组收缩到 num_symbols 和 num_periods 的实值。你在 MT5 里改 SYMBOLS_COMMON_TOTAL 或把 21 调小,能直接看到内存占用和后续遍历速度的变化,外汇贵金属多品种监控本就是高资源消耗场景,参数别盲设。
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, class="kw">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,SYMBOLS_COMMON_TOTAL)!=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); class=class="str">"cmt">//--- Reset the number of added symbols and timeframes to zero and, class=class="str">"cmt">//--- in a loop by the total number of timeseries, 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++) { class=class="str">"cmt">//--- get the next object of all timeseries of a single symbol CTimeSeriesDE *timeseries=list_timeseries.At(i); if(timeseries==NULL || this.IsExistSymbol(timeseries.Symbol())) class="kw">continue; class=class="str">"cmt">//--- increase the number of used symbols and(num_symbols variable), and class=class="str">"cmt">//--- write the timeseries symbol name to the array of used symbols by the num_symbols-class="num">1 index num_symbols++; ArrayUsedSymbols[num_symbols-class="num">1]=timeseries.Symbol(); class=class="str">"cmt">//--- Get the list of all its timeseries from the object of all symbol timeseries CArrayObj *list_series=timeseries.GetListSeries(); if(list_series==NULL) class="kw">continue; class=class="str">"cmt">//--- In the loop by the total number of symbol timeseries, class="type">int total_series=list_series.Total(); for(class="type">int j=class="num">0;j<total_series;j++) { class=class="str">"cmt">//--- get the next timeseries object CSeriesDE *series=list_series.At(j); if(series==NULL || this.IsExistTimeframe(series.Timeframe())) class="kw">continue; class=class="str">"cmt">//--- increase the number of used timeframes and(num_periods variable), and class=class="str">"cmt">//--- write the timeseries timeframe value to the array of used timeframes by num_periods-class="num">1 index num_periods++; ArrayUsedTimeframes[num_periods-class="num">1]=series.Timeframe(); } } class=class="str">"cmt">//--- Upon the loop completion, change the size of both arrays to match the exact number of added symbols and timeframes ::ArrayResize(ArrayUsedSymbols,num_symbols,SYMBOLS_COMMON_TOTAL); ::ArrayResize(ArrayUsedTimeframes,num_periods,class="num">21); }