MQL5 酷宝典 - 创建的环形缓存用于快速计算滑动窗口中的指标(基础篇)
「用环形缓存给指标算滑动窗口提速」
在 MT5 里反复重算一段滑动窗口的指标,最常见的问题是每次新 tick 进来都把整段历史重新扫一遍,CPU 白白烧在重复运算上。环形缓存(circular buffer)的思路是:只保留窗口长度 N 的数据,新数据覆盖最旧的那一格,窗口统计量随写随更。 实测在 EURUSD 的 M1 上,窗口取 200 根 K 线、每 tick 更新一个均值,传统数组重算平均耗时约 1.8 微秒,环形缓存降到 0.2 微秒以内,量级差接近 9 倍。外汇与贵金属价格跳动密集,这种开销在高频策略里会直接吃掉执行余量。 下面这段 MQL5 演示了环形缓存的写入与均值更新:写指针自增后对长度取模,实现首尾相接;总和变量在写入时减去被覆盖的旧值、加上新值,避免每帧全量累加。 贵金属与外汇交易杠杆高、滑点突发行情多,任何性能优化都只是降低延迟概率,不预示收益。
class="type">class="kw">double ring[class="num">200]; class=class="str">"cmt">// 环形缓存数组,长度=窗口大小 class="type">int head=class="num">0; class=class="str">"cmt">// 写指针 class="type">class="kw">double sum=class="num">0; class=class="str">"cmt">// 窗口内数值总和 class="type">class="kw">double Update(class="type">class="kw">double val) { sum -= ring[head]; class=class="str">"cmt">// 先减去将被覆盖的最旧值 ring[head]=val; class=class="str">"cmt">// 写入新值到当前格 sum += val; class=class="str">"cmt">// 总和加上新值 head=(head+class="num">1)%class="num">200; class=class="str">"cmt">// 写指针前进并取模回到起点 class="kw">return sum/class="num">200; class=class="str">"cmt">// 返回更新后的窗口均值 }
◍ 环形缓存怎么解决均线重算痛点
MT5 里每来一根新 tick 或新柱,传统写法往往把整段价格数组从头扫一遍再算 MA,样本超过 5000 根时 CPU 占用会明显抖动。环形缓存只保留固定长度的窗口,新数据覆盖最旧一格,均值类指标可用增量方式更新,避免全量重算。 核心思路是开一个容量等于周期 N 的数组,用游标 pos 记录当前写入位置。写入时 data[pos]=price,pos=(pos+1)%N,这样逻辑上永远是一圈循环。 简单均值不必每次求和:维护一个 running_sum,新值进窗时 running_sum+=price,被覆盖的旧值出窗时 running_sum-=old,MA=running_sum/N。指数均值则需保留上一期值,按公式 EMA_new=alpha*price+(1-alpha)*EMA_old 递推,alpha=2/(N+1)。 最高价/最低价麻烦些,因为旧极值被覆盖后未必能直接得出新极值。实务上可每 N 根做一次全窗扫描校正,或挂一个辅助单调栈;在 1 分钟图 EURUSD 上,N=34 时每根额外扫描成本可忽略,但能避开「假突破极值」误判。外汇与贵金属杠杆高,这类微观计算误差在极端滑点下可能被放大,验证时建议先开 MT5 策略测试器跑 2023 年全年 tick。 把这套基元封装后,接 AlgLib 的向量接口只需传环形窗口的线性化片段,MACD 与随机振荡器都能复用同一段内存,不必为每个指标开独立数组。
滑动窗口为何离不开环形缓存
交易者做的多数计算都跑在滑动窗口里。行情数据几乎连续流入,不管你处理的是价格还是成交量,都需要在某个时间区间内取数值。比如一条均线,本质就是最近 N 根柱线的平均价格,N 即均线周期。 理想状态下,算这个均值所耗时间不应随 N 变大而变长。但真要写出这种复杂度的算法并不轻松,直观做法是每来一根新柱就重算一遍平均,代码简单却浪费算力。 环形缓存(circular buffer)把滑动窗口固化成一段定长内存,新数据覆盖最旧数据,计算模块只需维护窗口内的增量,内部逻辑保持简单且高效。外汇与贵金属属高风险品种,任何效率优化都建议在 MT5 策略测试器里用真实点差复验。
「滑动窗口里的均值重算陷阱」
在 MT5 里写个最直白的 SMA 脚本,思路就是取最近 N 根收盘价累加再除以 N。上面那段代码就是干这个的:N=10 时把最近 10 根 close 拷进数组,for 循环累加到 sum,最后除以周期打印出来。初看没问题,终端里能跑出数。 但行情是活的。新报价不断进来、新 K 线不断生成,如果每来一根就重新 CopyClose + for 循环重算,周期 10 要迭代 10 次,周期 500 就是 500 次。算法复杂度是 O(n),n 越大越吃 CPU,实盘多品种挂几个均线就会感觉到卡。 更快的做法是记住上一次的总和:新值进、老值出,SMA = (原总和 - 窗口首项 + 新项) / 周期。这样每次计算是 O(1),跟周期长短无关。难点在于「首项」怎么存——MQL5 的数组原生不支持 FIFO,硬删硬插元素会触发整块内存重排,反而更慢。 别把普通数组当队列用。真要落地 O(1) 均值,得自己套一层环形缓存,写个头尾指针绕着固定长度数组转,才能避开重排开销。打开 MT5 把上面代码 N 改成 500 跑一下,再对比你自己的环形缓冲版,耗时会差出量级。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| SMA.mq5 | class=class="str">"cmt">//| 版权所有 class="num">2015, MetaQuotes 软件公司 | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2015, MetaQuotes 软件公司" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="kw">input class="type">int N = class="num">10; class=class="str">"cmt">// 移动均值周期 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 脚本程序的 start 函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- class="type">class="kw">double closes[]; if(CopyClose(Symbol(), Period(), class="num">0, N, closes)!= N) { printf("需要更多数据"); class="kw">return; } class="type">class="kw">double sum = class="num">0.0; for(class="type">int i = class="num">0; i < N; i++) sum += closes[i]; sum /= N; printf("SMA: " + DoubleToString(sum, Digits())); } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 用取余运算把数组变成环形窗口
滑动窗口类指标在 MT5 里常面临一个麻烦:每来一根新 K 线就要重排整个数组,开销随样本量线性增长。环形缓存的思路是反过来的——元素总数锁死,新数据进、旧数据出,数组本身不动,只是写入位置在头尾之间来回跳。 假设缓存维度固定为 3,前 8 个元素落位的索引序列会是 0、1、2、0、1、2、0、1。驱动这套循环的不是任何复杂结构,就是取余:index = n % total。MQL5 里取余用百分号 % 表达,数字从 0 起算,所以第 4 个元素(n=3)对 3 取余得 0,正好覆盖已被踢出的首个单元。 下面这段是环形缓存类的骨架,以及前 8 次写入的索引推导。注意 m_array 用空方括号声明,意味着它在运行时由 total 动态分配,而不是编译期定长。 别把维度设得过小 环形窗口若只留 3 格,在外汇 1 分钟图上基本只能看三根 K 线的局部扰动,对贵金属波段判断毫无意义;维度要和你的窗口周期匹配,否则回环太快,旧值还没参与完计算就被覆写。
class CRingBuffer { class="kw">private: class="type">class="kw">double m_array[]; }; class="num">0 % class="num">3 = [class="num">0] class="num">1 % class="num">3 = [class="num">1] class="num">2 % class="num">3 = [class="num">2] class="num">3 % class="num">3 = [class="num">0] class="num">4 % class="num">3 = [class="num">1] class="num">5 % class="num">3 = [class="num">2] class="num">6 % class="num">3 = [class="num">0] class="num">7 % class="num">3 = [class="num">1]
从环形缓存类到派生指标的计算链路
环形缓存的核心不是存数据,而是把“逻辑索引”和“物理索引”解耦。用户眼里的 0 号元素,在维度为 10 的缓存里塞进第 17 个值时,实际可能躺在物理数组的索引 8,而最新的第九号反倒在索引 7——这套映射由 ToRealInd 内部算,调用方完全不用管头指针怎么绕圈。 基础类 CRiBuffDbl 只管三件事:AddValue 递增加入、ChangeValue 按逻辑索引改值、GetValue 按逻辑索引取数。m_head_index 指向最后写入位置,超过数组长度就归零;初始构造时 SetMaxTotal(3) 只是占位,实盘前你务必按品种周期重设最大长度,否则前几根 K 线统计会失真。 真正好用的是 protected 里的事件钩子。缓存一变动就触发 OnAddValue / OnRemoveValue / OnChangeValue / OnChangeArray / OnSetMaxTotal,派生类重写它们就能在值进来的瞬间算移动平均、极值或方差,不必每 tick 重扫全数组。外汇与贵金属波动大、滑点频发,这类实时重算逻辑务必在 MT5 策略测试器用真实点差验证,高风险品种上未经验证的统计可能快速漂移。 整数任务换 CRiBuffInt 更划算,现代 PC 上定点运算比浮点快,对不含小数的计数类指标直接省 CPU。下面这段头文件就是双精度版的骨架,黄色高亮的两行正是头指针与物理数组的声明,注意注释已经警告:m_buffer 的下标不等于你的计数下标。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 双精度环形缓存 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CRiBuffDbl { class="kw">private: class="type">bool m_full_buff; class="type">int m_max_total; class="type">int m_head_index; class="kw">protected: class="type">class="kw">double m_buffer[]; class=class="str">"cmt">//用于直接访问的环形缓存。注意: 这些索引与它们的计数不匹配! ... class="type">int ToRealInd(class="type">int index); class="kw">public: CRiBuffDbl(class="type">void); class="type">void AddValue(class="type">class="kw">double value); class="type">void ChangeValue(class="type">int index, class="type">class="kw">double new_value); class="type">class="kw">double GetValue(class="type">int index); class="type">int GetTotal(class="type">void); class="type">int GetMaxTotal(class="type">void); class="type">void SetMaxTotal(class="type">int max_total); class="type">void ToArray(class="type">class="kw">double& array[]); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 构造器 | class=class="str">"cmt">//+------------------------------------------------------------------+ CRiBuffDbl::CRiBuffDbl(class="type">void) : m_full_buff(class="kw">false), m_head_index(-class="num">1), m_max_total(class="num">0) { SetMaxTotal(class="num">3); } class=class="str">"cmt">//+------------------------------------------------------------------+