DoEasy 函数库中的时间序列(第四十八部分):在单一子窗口里基于一个缓冲区的多周期、多品种指标(基础篇)
单缓冲多周期指标怎么塞进一个子窗口
在 MT5 里做跨品种、跨周期指标时,常见做法是每个周期开一个缓冲区,子窗口很快被占满。DoEasy 库的思路是用一个缓冲区承载多周期多品种数据,全部绘制在同一个独立子窗口中,避免主图和其他指标互相挤占。 原文示例发布于 2020 年 12 月 28 日,截至文档记录获得 1 626 次浏览、2 条评论,说明这类紧凑型指标结构在实际交易者中有明确需求。 打开 MT5 后,你可以在导航器里新建一个自定义指标,刻意只声明一个 plot 缓冲,然后往里按时间偏移写入不同品种的不同周期数值,看子窗口是否能稳定重绘而不串线。外汇与贵金属杠杆高,这类多周期叠加会放大信号噪声,验证时先用模拟盘跑。
◍ 从单缓冲区指标摸清代码复用路径
上一篇文章用加速器振荡器(AC)演示了如何把任意品种、任意时间帧的标准指标数据拉到当前图表。这一步只是验证原理,真正要做的,是把这类读取逻辑抽象成可复用的方法,免得后面每接一个指标都重写一遍。 本文先拿单缓冲区标准指标开刀——这类指标只用一条绘制缓冲区,在主图子窗口输出数值,结构最简单。我会写一个自定义指标示例,在参数面板里选好目标品种和时间帧后,把对应标准指标画在当前图表的子窗口里。 为了撑起这个示例,库类得先做一点小改动。这一步改动看似不大,却是后续接管多缓冲区标准指标、进一步压缩冗余代码的地基。本文的测试目的很明确:找出各指标方法中长得像的代码块,后面统一抽离成独立函数。
「给标准指标缓冲区接上自动 prepar 的管道」
在 DoEasy 函数库的 BuffersCollection 类里,新增了 PreparingDataAllBuffersStdInd() 公开方法,用来一次性把已创建标准指标的计算缓冲区数据备齐。程序端收到命令后不用再去逐个搜对象、取指针,直接调这个方法即可,代码层明显瘦身。 方法内部第一件事是拉取 ID 不等于 -1 的缓冲区对象列表。列表为空就提示“No buffer object for standard indicator”并返回 false。接着按列表长度循环,标准指标至少挂两个缓冲对象(计算+绘制),但这里只喂计算缓冲——因为上一版的 PreparingDataBufferStdInd() 已做过筛选。 每个缓冲对象用自身 symbol/timeframe 去取对应时间序列,拿到可用数据量,再调 PreparingDataBufferStdInd() 复制指标句柄数据。只要有一个对象复制量不达标,res 变量置 false,方法最终返回 res。当前逻辑是每次即时报价全量复制,作者明言这对多指标不实用,但优先保逻辑清晰,后续才改“仅首次或历史变动时复制、平时只搬 1~2 根柱”。 CEngine 主类同步开了访问权限:公开区声明返回 bool 的包装方法,内部拿集合类返回的句柄与 INVALID_HANDLE 比大小,简化终端调用。另外新增按指标类型+ID 返回缓冲描述的方法,实现里取列表首个对象指针就够——绘制与计算缓冲数据同源。 OnDoEasyEvent() 的新柱线处理把复制量下限从“缓冲量 vs 指标量”的最小值,改成再叠一层默认 1000 根柱线的三方最小值。由于库内时间序列默认就是 1000,这一项通常最小,批量复制时反而省事。外汇/贵金属多周期跑这套逻辑需注意:历史重载会触发全量复制,实盘占用可能抖一下。
MSG_LIB_TEXT_BUFFER_TEXT_INVALID_PROPERTY_BUFF, class=class="str">"cmt">// Invalid number of indicator buffers(class="macro">#class="kw">property indicator_buffers) MSG_LIB_TEXT_BUFFER_TEXT_MAX_BUFFERS_REACHED, class=class="str">"cmt">// Reached maximum possible number of indicator buffers MSG_LIB_TEXT_BUFFER_TEXT_NO_BUFFER_OBJ, class=class="str">"cmt">// No buffer object for standard indicator MSG_LIB_TEXT_BUFFER_TEXT_STATUS_NONE, class=class="str">"cmt">// No drawing {"Неправильно указано количество буферов индикатора (class="macro">#class="kw">property indicator_buffers)","Number of indicator buffers incorrect(class="macro">#class="kw">property indicator_buffers)"}, {"Достигнуто максимально возможное количество индикаторных буферов","Maximum number of indicator buffers reached"}, {"Нет ни одного объекта-буфера для стандартного индикатора","There is no buffer object for the standard indicator"}, {"Нет отрисовки","No drawing"}, class=class="str">"cmt">//--- Prepare calculated buffer data of(class="num">1) the specified standard indicator and(class="num">2) all created standard indicators class="type">int PreparingDataBufferStdInd(class="kw">const ENUM_INDICATOR std_ind,class="kw">const class="type">int id,class="kw">const class="type">int total_copy); class="type">bool PreparingDataAllBuffersStdInd(class="type">void); class=class="str">"cmt">//--- Clear buffer data of the specified standard indicator by the timeseries index class=class="str">"cmt">//+------------------------------------------------------------------+
多周期 AC 缓冲区的批量预取逻辑
在 MT5 自定义指标里同时拉取多品种、多周期的标准指标数据,核心在于先把所有缓冲区对象归并到一个列表再逐个刷新。下面这段 CBuffersCollection::PreparingDataAllBuffersStdInd 做的就是这件事:拿到带 ID 的缓冲列表,空列表直接返回 false 并打印错误。 遍历列表时跳过空指针、纯数据缓冲以及未绑定指标类型的项;对剩下的每一项,按它自身的 Symbol() 与 Timeframe() 去取对应时间序列对象。若序列不存在也跳过,避免无效句柄拖慢整体。 关键在 copied 与 used_data 的比较:used_data 来自 series.AvailableUsedData(),即当前序列可用柱数;调用 PreparingDataBufferStdInd 实际拷贝数据,若 copied 小于 used_data,说明该缓冲没拉满,res 会被按位与成 false,但最终仍继续跑完其余缓冲,不会中途断掉。 CreateAC 则是单一指标 AC 的封装:用 iAC(symbol,timeframe) 建句柄,默认 ID 为 IND_AC,并预置三色数组 {clrGreen,clrRed,clrGreen} 用于零轴上下着色。句柄有效后建直方图缓冲并取最后一个缓冲对象写参数。开 MT5 把这段塞进你的指标类,改 symbol/timeframe 参数即可验证多周期 AC 是否按预期刷新。
class="type">bool CBuffersCollection::PreparingDataAllBuffersStdInd(class="type">void) { CArrayObj *list=this.GetListBuffersWithID(); if(list==NULL || list.Total()==class="num">0) { ::Print(DFUN_ERR_LINE,CMessage::Text(MSG_LIB_TEXT_BUFFER_TEXT_NO_BUFFER_OBJ)); class="kw">return class="kw">false; } class="type">bool res=true; class="type">int total=list.Total(); for(class="type">int i=class="num">0;i<total;i++) { CBuffer *buff=list.At(i); if(buff==NULL || buff.TypeBuffer()==BUFFER_TYPE_DATA || buff.IndicatorType()==WRONG_VALUE) class="kw">continue; CSeriesDE *series=this.m_timeseries.GetSeries(buff.Symbol(),buff.Timeframe()); if(series==NULL) class="kw">continue; class="type">int used_data=(class="type">int)series.AvailableUsedData(); class="type">int copied=this.PreparingDataBufferStdInd(buff.IndicatorType(),buff.ID(),used_data); if(copied<used_data) res &=class="kw">false; } class="kw">return res; } class="type">int CBuffersCollection::CreateAC(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int id=WRONG_VALUE) { class="type">int handle=::iAC(symbol,timeframe); class="type">int identifier=(id==WRONG_VALUE ? IND_AC : id); class="type">class="kw">color array_colors[class="num">3]={clrGreen,clrRed,clrGreen}; CBuffer *buff=NULL; if(handle!=INVALID_HANDLE) { this.CreateHistogram(); buff=this.GetLastCreateBuffer();
◍ 给 AC 与 ATR 挂上多周期缓冲对象
在封装多品种指标时,AC(加速震荡器)与 ATR 都走同一套缓冲注册逻辑:先拿指标句柄,再建绘制缓冲并设置品种、周期、ID 与色彩。AC 那段会建两类缓冲——显示缓冲和通过 CreateCalculate() 生成的计算缓冲,两者都要判空,任一返回 NULL 就直接给 INVALID_HANDLE,避免后续访问崩在终端。 ATR 的写法更短,用 iATR(symbol,timeframe,ma_period) 取句柄,默认 ID 用 IND_ATR,若调用方传了 WRONG_VALUE 以外的值就覆盖。注意它显式声明了 color array_colors[1]={clrLightSeaGreen},只给一条线配色,标签里把 ma_period 拼进字符串,比如 EURUSD 的 H1 周期会显示成 ATR(EURUSD,H1: 14)。 外汇与贵金属波动剧烈,ATR 周期参数设多大直接影响止损距离,回测时 14 周期在 XAUUSD 的 M15 上平均真实波幅常落在 8~15 美元区间,实盘可能瞬间翻倍,属典型高风险场景。把下面两段直接贴进你的 CBuffersCollection 类,编译后开 MT5 用不同 symbol+timeframe 调 CreateATR 验证缓冲是否真的画出来。
if(buff==NULL) class="kw">return INVALID_HANDLE; buff.SetSymbol(symbol); buff.SetTimeframe(timeframe); buff.SetID(identifier); buff.SetIndicatorHandle(handle); buff.SetIndicatorType(IND_AC); buff.SetShowData(true); buff.SetLabel("AC("+symbol+","+TimeframeDescription(timeframe)+")"); buff.SetIndicatorName("Accelerator Oscillator"); buff.SetColors(array_colors); class=class="str">"cmt">//--- Create a calculated buffer storing standard indicator data this.CreateCalculate(); class=class="str">"cmt">//--- Get the last created(calculated) buffer object and set all the necessary parameters to it buff=this.GetLastCreateBuffer(); if(buff==NULL) class="kw">return INVALID_HANDLE; buff.SetSymbol(symbol); buff.SetTimeframe(timeframe); buff.SetID(identifier); buff.SetIndicatorHandle(handle); buff.SetIndicatorType(IND_AC); buff.SetEmptyValue(EMPTY_VALUE); buff.SetLabel("AC("+symbol+","+TimeframeDescription(timeframe)+")"); buff.SetIndicatorName("Accelerator Oscillator"); } class="kw">return handle; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create multi-symbol multi-period ATR | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CBuffersCollection::CreateATR(class="kw">const class="type">class="kw">string symbol,class="kw">const ENUM_TIMEFRAMES timeframe,class="kw">const class="type">int ma_period,class="kw">const class="type">int id=WRONG_VALUE) { class=class="str">"cmt">//--- Create the indicator handle and set the class="kw">default ID class="type">int handle=::iATR(symbol,timeframe,ma_period); class="type">int identifier=(id==WRONG_VALUE ? IND_ATR : id); class="type">class="kw">color array_colors[class="num">1]={clrLightSeaGreen}; CBuffer *buff=NULL; if(handle!=INVALID_HANDLE) { class=class="str">"cmt">//--- Create the line buffer this.CreateLine(); class=class="str">"cmt">//--- Get the last created(drawn) buffer object and set all the necessary parameters to it buff=this.GetLastCreateBuffer(); if(buff==NULL) class="kw">return INVALID_HANDLE; buff.SetSymbol(symbol); buff.SetTimeframe(timeframe); buff.SetID(identifier); buff.SetIndicatorHandle(handle); buff.SetIndicatorType(IND_ATR); buff.SetShowData(true); buff.SetLabel("ATR("+symbol+","+TimeframeDescription(timeframe)+": "+(class="type">class="kw">string)ma_period+")");
「把标准指标塞进计算缓冲区的收尾写法」
上面这段逻辑做的是同一件事:给 ATR 这类标准指标在缓冲区集合里建一个计算型缓冲,并把句柄、周期、符号、空值标记全部绑定好。注意 buff.SetLabel("ATR("+symbol+","+TimeframeDescription(timeframe)+": "+(string)ma_period+")") 这行,它直接把图表上显示的标签拼成可读字符串,调试时一眼能分清是哪个品种哪周期哪参数。
PreparingDataBufferStdInd 是真正触发数据填充的入口。它先按指标类型和 ID 取出缓冲链表,再用 CSelect::ByBufferProperty 筛出计算型缓冲;若链表为空直接返回 0,意味着该指标还没初始化完,调用方不该继续拷数据。
目前 switch 里只实装了 IND_AC 分支:取链首缓冲,调 FillAsSeries 从指标句柄按序列把最多 total_copy 根 K 线填进去,返回实际拷贝数。其余如 IND_ATR、IND_ADX 都还是 break 空壳——你想接 ATR 数据就得自己补这个 case,否则运行时只会静默跳过。外汇与贵金属波动剧烈,缓冲拷贝数量设太大既吃内存也拖慢 OnTick,建议先按 500 根试。
buff.SetIndicatorName("Average True Range"); buff.SetColors(array_colors); class=class="str">"cmt">//--- Create a calculated buffer storing standard indicator data this.CreateCalculate(); class=class="str">"cmt">//--- Get the last created(calculated) buffer object and set all the necessary parameters to it buff=this.GetLastCreateBuffer(); if(buff==NULL) class="kw">return INVALID_HANDLE; buff.SetSymbol(symbol); buff.SetTimeframe(timeframe); buff.SetID(identifier); buff.SetIndicatorHandle(handle); buff.SetIndicatorType(IND_ATR); buff.SetEmptyValue(EMPTY_VALUE); buff.SetLabel("ATR("+symbol+","+TimeframeDescription(timeframe)+": "+(class="type">class="kw">string)ma_period+")"); buff.SetIndicatorName("Average True Range"); } class="kw">return handle; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Prepare the calculated buffer data | class=class="str">"cmt">//| of the specified standard indicator | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CBuffersCollection::PreparingDataBufferStdInd(class="kw">const ENUM_INDICATOR std_ind,class="kw">const class="type">int id,class="kw">const class="type">int total_copy) { CArrayObj *list=this.GetListBufferByTypeID(std_ind,id); list=CSelect::ByBufferProperty(list,BUFFER_PROP_TYPE,BUFFER_TYPE_CALCULATE,EQUAL); if(list==NULL || list.Total()==class="num">0) class="kw">return class="num">0; CBufferCalculate *buffer=NULL; class="type">int copies=WRONG_VALUE; class="kw">switch((class="type">int)std_ind) { case IND_AC : buffer=list.At(class="num">0); if(buffer==NULL) class="kw">return class="num">0; copies=buffer.FillAsSeries(buffer.IndicatorHandle(),class="num">0,class="num">0,total_copy); class="kw">return copies; case IND_AD : class="kw">break; case IND_ADX : class="kw">break; case IND_ADXW : class="kw">break; case IND_ALLIGATOR : class="kw">break; case IND_AMA : class="kw">break; case IND_AO : class="kw">break; case IND_ATR : class="kw">break;