MQL5 酷宝典 - 创建的环形缓存用于快速计算滑动窗口中的指标·进阶篇
「双精度环形缓存的索引与读写逻辑」
在 MT5 里做价格序列滚动统计,用普通数组容易越界或频繁拷贝。CRiBuffDbl 这套双精度环形缓存把写入头指针 m_head_index 和满标记 m_full_buff 分开管,能在固定内存里存最近 N 个 double 值。 SetMaxTotal 先比对 ArraySize(m_buffer) 与入参,相等就直接 return,不等才 ArrayResize 并把返回值赋给 m_max_total。注意 ArrayResize 返回的是新数组大小,所以 m_max_total 始终和实际分配长度一致。 AddValue 每次先 ++m_head_index,等于 m_max_total 时归零并置 m_full_buff=true,再把 value 写进 m_buffer[m_head_index]。这意味着缓存写满后,最老的数据会被自然覆盖,无需手动移位。 GetTotal 在未满时返回 m_head_index+1,满了返回 m_max_total——这是判断「有效样本数」的唯一可靠来源,不能直接用数组长度当样本量。 ToRealInd 是虚实时索引转换的核心:未满时虚拟索引就是真实索引;满后根据 head 位置和 delta 算偏移。若 index 越界(≥GetTotal() 或 <0)返回 m_max_total,调用方需自行拦截这个哨兵值。 GetValue 与 ChangeValue 都依赖 ToRealInd,后者会先存 prev_value 再覆盖,方便做差值校验。开 MT5 把这几段塞进 EA 的 include,调 SetMaxTotal(200) 就能滚动存 200 根收盘价做均值偏离。外汇与贵金属波动剧烈,环形缓存只解决存储效率,信号真伪仍须多周期验证。
class="type">void CRiBuffDbl::SetMaxTotal(class="type">int max_total) { if(ArraySize(m_buffer) == max_total) class="kw">return; m_max_total = ArrayResize(m_buffer, max_total); } class="type">int CRiBuffDbl::GetMaxTotal(class="type">void) { class="kw">return m_max_total; } class="type">class="kw">double CRiBuffDbl::GetValue(class="type">int index) { class="kw">return m_buffer[ToRealInd(index)]; } class="type">int CRiBuffDbl::GetTotal(class="type">void) { if(m_full_buff) class="kw">return m_max_total; class="kw">return m_head_index+class="num">1; } class="type">void CRiBuffDbl::AddValue(class="type">class="kw">double value) { if(++m_head_index == m_max_total) { m_head_index = class="num">0; m_full_buff = true; } class=class="str">"cmt">//... m_buffer[m_head_index] = value; } class="type">void CRiBuffDbl::ChangeValue(class="type">int index, class="type">class="kw">double value) { class="type">int r_index = ToRealInd(index); class="type">class="kw">double prev_value = m_buffer[r_index]; m_buffer[r_index] = value; } class="type">int CRiBuffDbl::ToRealInd(class="type">int index) { if(index >= GetTotal() || index < class="num">0) class="kw">return m_max_total; if(!m_full_buff) class="kw">return index; class="type">int delta = (m_max_total-class="num">1) - m_head_index; if(index < delta) class="kw">return m_max_total + (index - delta); class="kw">return index - delta; }
◍ 双精度环形缓存的虚函数钩子
在 MT5 里做指标或 EA 的滚动统计,常会自己封一个双精度环形缓存类,避免每次新 tick 都整体平移数组。上面这段是类声明的受保护段,只列了五个虚函数,没给实现,但已经把缓存生命周期里的关键事件点透了。 OnAddValue 在压入新值后触发,OnRemoveValue 在挤出最旧值时触发,这两个钩子适合用来维护滚动和、滚动方差之类增量量。OnChangeValue 带 index 和前后值,能在某位被覆写时做精确修正,比全量重算省 CPU。 OnChangeArray 与 OnSetMaxTotal 分别对应底层数组整体变动和缓存容量调整。容量若从 256 改成 1024,旧数据是否保留、统计量如何重置,就靠这两个虚函数拿捏。外汇与贵金属行情跳变频繁,这类缓存若容量设太小,滚动窗口会截断近期波动,高风险下误判概率会上升。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 双精度环形缓存 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CRiBuffDbl { class="kw">private: ... class="kw">protected: class="kw">virtual class="type">void OnAddValue(class="type">class="kw">double value); class="kw">virtual class="type">void OnRemoveValue(class="type">class="kw">double value); class="kw">virtual class="type">void OnChangeValue(class="type">int index, class="type">class="kw">double prev_value, class="type">class="kw">double new_value); class="kw">virtual class="type">void OnChangeArray(class="type">void); class="kw">virtual class="type">void OnSetMaxTotal(class="type">int max_total); };
用环形缓存重写 SMA 的计算内核
把经典简单移动平均搬进环形缓存,核心是把『总和』当状态量维护,而不是每根 K 线重算整段序列。公式仍是 SMA = (窗口总和 - 滑出值 + 新值) / 周期,但减法与加法被拆到元素进出缓存的钩子里。 派生自双精度环形缓存基类后,只需重写三个受保护方法:OnAddValue 在压入新值时常量累加 m_sum;OnRemoveValue 在旧值滑出时做等量递减;OnChangeValue 负责最后一根柱线价格跳变时,用新值替换旧值并修正总和。 指标主体只需在每根 tick 调 AddValue,过时元素清理由缓存自动完成。图形输出与 MT5 标准 SMA 等效,验证时可在 EURUSD 的 M15 上叠加两者,肉眼比对应当重合——外汇与贵金属杠杆高,重合不代表任何方向确定性,仅说明算法等价。 下面这段类实现里,m_sum 初始化为 0.0,三个虚函数各自只做一行算术,因此即便 200 周期均线,单 tick 开销也是 O(1) 而非 O(n)。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| RingBuffer.mqh | class=class="str">"cmt">//| 版权所有 class="num">2016, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2016, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include "RiBuffDbl.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算环形缓存中的移动平均值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CRiSMA : class="kw">public CRiBuffDbl { class="kw">private: class="type">class="kw">double m_sum; class="kw">protected: class="kw">virtual class="type">void OnAddValue(class="type">class="kw">double value); class="kw">virtual class="type">void OnRemoveValue(class="type">class="kw">double value); class="kw">virtual class="type">void OnChangeValue(class="type">int index, class="type">class="kw">double del_value, class="type">class="kw">double new_value); class="kw">public: CRiSMA(class="type">void); class="type">class="kw">double SMA(class="type">void); }; CRiSMA::CRiSMA(class="type">void) : m_sum(class="num">0.0) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 递增总和 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiSMA::OnAddValue(class="type">class="kw">double value) { m_sum += value; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 递减总和 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiSMA::OnRemoveValue(class="type">class="kw">double value) { m_sum -= value; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 修改总和 | class=class="str">"cmt">//+------------------------------------------------------------------+
「用环形缓冲改写 SMA 指标的计算内核」
把简单移动均值的求和逻辑塞进类里,是为了避开每根 K 线都从头累加的低效写法。CRiSMA 在数值变更时只做加减:剔除滑出窗口的旧值 del_value,纳入新值 new_value,再除以当前样本数 GetTotal() 即得均值。
class="type">void CRiSMA::OnChangeValue(class="type">int index,class="type">class="kw">double del_value,class="type">class="kw">double new_value) { m_sum -= del_value; m_sum += new_value; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回简单移动均值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CRiSMA::SMA(class="type">void) { class="kw">return m_sum/GetTotal(); }
class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_buffers class="num">1 class="macro">#class="kw">property indicator_plots class="num">1 class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_color1 clrRed class="macro">#include <RingBuffer\RiSMA.mqh> class="kw">input class="type">int MaPeriod = class="num">13; class="type">class="kw">double buff[]; CRiSMA Sma; class="type">int OnInit() { SetIndexBuffer(class="num">0, buff, INDICATOR_DATA); Sma.SetMaxTotal(MaPeriod); class="kw">return(INIT_SUCCEEDED); }
class="type">void CRiSMA::OnChangeValue(class="type">int index,class="type">class="kw">double del_value,class="type">class="kw">double new_value) { m_sum -= del_value; m_sum += new_value; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 返回简单移动均值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CRiSMA::SMA(class="type">void) { class="kw">return m_sum/GetTotal(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| RiEma.mq5 | class=class="str">"cmt">//| 版权所有 class="num">2016, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2016, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_buffers class="num">1 class="macro">#class="kw">property indicator_plots class="num">1 class="macro">#class="kw">property indicator_type1 DRAW_LINE class="macro">#class="kw">property indicator_color1 clrRed class="macro">#include <RingBuffer\RiSMA.mqh> class="kw">input class="type">int MaPeriod = class="num">13; class="type">class="kw">double buff[]; CRiSMA Sma; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 指标缓存映射 SetIndexBuffer(class="num">0, buff, INDICATOR_DATA); Sma.SetMaxTotal(MaPeriod); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[]) { class=class="str">"cmt">//--- class="type">bool calc = class="kw">false; for(class="type">int i = prev_calculated; i < rates_total; i++) { Sma.AddValue(price[i]); buff[i] = Sma.SMA();
◍ 只更新尾值的轻量模式
当 calc 标志为 false 时,说明并非每根 K 线都需要重算整段均线,此时只改写最新一根的平滑值即可。 Sma.ChangeValue(MaPeriod-1, price[rates_total-1]) 把第 MaPeriod-1 个位置替换成当前收盘价,再取 Sma.SMA() 写入 buff 尾元素,计算量从 O(n) 降到 O(1)。 实际在 EURUSD M5 上,若 90% 的 tick 仅触发尾值刷新,CPU 占用可能比全量重算低一个数量级;但贵金属 XAUUSD 跳空多,calc 误判会漏算,属高风险品种需谨慎。 return(rates_total-1) 告知终端只更新到末位索引,避免无谓的重绘。
calc = true; } if(!calc) { Sma.ChangeValue(MaPeriod-class="num">1, price[rates_total-class="num">1]); buff[rates_total-class="num">1] = Sma.SMA(); } class="kw">return(rates_total-class="num">1); } class=class="str">"cmt">//+------------------------------------------------------------------+
环形缓存里重算指数均值的类实现
指数移动平均和简单平均不同,它不受环形缓存清掉旧元素的影响,所以得在派生类里重写 OnAddValue 和 OnChangeValue 才能持续算对。我们建一个继承 CRiBuffDbl 的 CRiEMA 类,把平滑因子、前值和末值都存成私有成员。 平滑因子按 MetaQuotes 的 EMA 公式来:2.0/(1.0+max_total)。比如缓存容量设 14,因子就是约 0.1333;设 30 则约 0.0645,容量越大加权越慢。 CalcEma 的逻辑是返回两项之和:最新价乘平滑因子,加上前一 EMA 乘(1-因子)。若前值还没算过,就直接拿缓存 0 号柱的收盘价当种子值,避免首根 K 线输出空值。 下方代码只截到 OnSetMaxTotal 的实现,已能验证因子随容量动态变化。开 MT5 建个 EA 把 CRiEMA 包进去,改 max_total 从 10 到 50,用 Print 打出 m_smoth_factor 就能确认这条曲线。外汇和贵金属波动大,这类均值仅作概率参考,实盘须控仓。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| RingBuffer.mqh | class=class="str">"cmt">//| 版权所有 class="num">2016, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2016, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#include "RiBuffDbl.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算环形缓存中的指数移动平均值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CRiEMA : class="kw">public CRiBuffDbl { class="kw">private: class="type">class="kw">double m_prev_ema; class=class="str">"cmt">// 前一个 EMA 数值 class="type">class="kw">double m_last_value; class=class="str">"cmt">// 最后的价格值 class="type">class="kw">double m_smoth_factor; class=class="str">"cmt">// 平滑因子 class="type">bool m_calc_first_v; class=class="str">"cmt">// 计算第一个数值的指示标志 class="type">class="kw">double CalcEma(); class=class="str">"cmt">// 直接平均计算 class="kw">protected: class="kw">virtual class="type">void OnAddValue(class="type">class="kw">double value); class="kw">virtual class="type">void OnChangeValue(class="type">int index, class="type">class="kw">double del_value, class="type">class="kw">double new_value); class="kw">virtual class="type">void OnSetMaxTotal(class="type">int max_total); class="kw">public: CRiEMA(class="type">void); class="type">class="kw">double EMA(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 订阅数值增加/更改通知 | class=class="str">"cmt">//+------------------------------------------------------------------+ CRiEMA::CRiEMA(class="type">void) : m_prev_ema(EMPTY_VALUE), m_last_value(EMPTY_VALUE), m_calc_first_v(class="kw">false) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 根据 MetaQuotes EMA 方程计算平滑因子 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiEMA::OnSetMaxTotal(class="type">int max_total) { m_smoth_factor = class="num">2.0/(class="num">1.0+max_total); } class=class="str">"cmt">//+------------------------------------------------------------------+