MQL5 酷宝典 - 创建的环形缓存用于快速计算滑动窗口中的指标·进阶篇
📘

MQL5 酷宝典 - 创建的环形缓存用于快速计算滑动窗口中的指标·进阶篇

第 2/3 篇

「双精度环形缓存的索引与读写逻辑」

在 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 根收盘价做均值偏离。外汇与贵金属波动剧烈,环形缓存只解决存储效率,信号真伪仍须多周期验证。

MQL5 / C++
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,旧数据是否保留、统计量如何重置,就靠这两个虚函数拿捏。外汇与贵金属行情跳变频繁,这类缓存若容量设太小,滚动窗口会截断近期波动,高风险下误判概率会上升。

MQL5 / C++
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)。

MQL5 / C++
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() 即得均值。

MQL5 / C++
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();
}
上面两段是类的核心:OnChangeValue 负责维护滑动和,SMA 只做除法。注意 m_sum 不重新遍历价格数组,复杂度为 O(1) 而非 O(n)。 真正挂到图表上的指标文件用 #property 声明了 1 个画线缓冲区、颜色 clrRed,并 include 了 RingBuffer\RiSMA.mqh。输入参数 MaPeriod 默认 13,意味着窗口长度 13 根棒。
MQL5 / C++
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);
}
OnInit 里 Sma.SetMaxTotal(MaPeriod) 把环形缓冲容量锁死为 13;OnCalculate 从 prev_calculated 续算,只对新增棒调用 Sma.AddValue 和 buff[i]=Sma.SMA()。外汇与贵金属波动剧烈,13 期均线仅反映短期倾向,实盘使用前请在 MT5 策略测试器以历史数据验证其滞后表现。

MQL5 / C++
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) 告知终端只更新到末位索引,避免无谓的重绘。

MQL5 / C++
  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 就能确认这条曲线。外汇和贵金属波动大,这类均值仅作概率参考,实盘须控仓。

MQL5 / C++
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">//+------------------------------------------------------------------+

常见问题

维护一个定长双精度数组作环形缓冲,新 tick 只写入头索引并后移指针,窗口和减去被覆盖的老值、加上新值即可,避免全量循环。
用 idx = counter % size 取写入位,读时按 (idx + i) % size 回退取历史;计数器只增不减,取模代替归零最稳。
可以,把代码贴给小布,它会逐行核对取模逻辑、老值扣除与窗口和更新,并提示轻量模式适用的品种周期。
多数漏在初始化未填满整窗就除长度,或虚函数钩子没在reset时清计数器;先打点打印前size根BAR确认缓冲满窗。
逻辑等价,差别在速度;类里存上一期EMA与权重,新值按 α·price + (1-α)·prev 递推,省掉历史遍历。