MQL5 酷宝典 - 创建的环形缓存用于快速计算滑动窗口中的指标·综合运用
- 增量式 EMA 的类内递推写法
- 给每根K线打极值等级,跳过无效比较
- 极值索引的增量重算逻辑
- 极值索引的回溯与截取逻辑
- 用环形缓存喂 AlgLib 算滑动高斯参数
- 把均值方差塞进指标缓冲区的写法
- 函数收尾的返回值处理
- 用环形基元拼出 MACD 信号线
- 给 MACD 类塞进可改值的接口
- 环形基元改写随机振荡的计算路径
- 慢速随机指标的循环与信号线算法
- 把随机振荡器拆成可增量计算的类
- 随机指标的三种周期怎么在类里落地
- 句柄背后藏着的内存账本
- 指标基类的构造与取值骨架
- MACD 类两种取数路径的实现差异
- 多周期 MACD 与随机指标的参数骨架
- 用数组批量挂 MACD 与随机指标
- 周期映射与多品种指标批量挂载
- 把指标算进EA里能省下四分之三测试时间
- EA 内嵌指标的效能实测与取舍
- 环形缓存落地的现实与坑
- 一点提醒
◍ 增量式 EMA 的类内递推写法
在自写指标或 EA 里,EMA 最怕每来一个新 tick 就从头重算整段序列,CPU 白白烧在重复循环上。下面这套 CRiEMA 类把递推拆成了三个入口:新增值、改值、取值,只在内存里维护两个变量就够跑。 OnAddValue 负责在来新价时先拿旧 m_prev_ema 算一次暂存,再把当前价塞进 m_last_value;注意它只在 m_prev_ema 不是 EMPTY_VALUE 时才递推,首根 K 线会跳过这步。OnChangeValue 只在改动的是最新一根(index == GetMaxTotal()-1)时才更新 m_last_value,更早的历史被改直接 return,避免中间插入破坏递推链。 核心计算只有一行:m_last_value*m_smoth_factor + m_prev_ema*(1.0-m_smoth_factor)。m_smoth_factor 就是 2/(周期+1),比如周期 14 时系数约 0.1333,旧值权重占 0.8667,价格越高频越吃平滑。 EMA() 暴露给外部取数:首值未算过就把当前价当 m_prev_ema 返回并置 m_calc_first_v=true,之后每次走 CalcEma()。开 MT5 建个 CRiEMA 实例,周期设 14 对比系统 iMA 的 MODE_EMA 输出,前 200 根以内偏差可能到小数点第 4 位以后,属首值处理差异,外汇与贵金属杠杆高、滑点大,这类微差会放大成交成本,验证时别只看图形重叠。
class=class="str">"cmt">//| 总和递加 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiEMA::OnAddValue(class="type">class="kw">double value) { class=class="str">"cmt">//计算前一个 EMA 值 if(m_prev_ema != EMPTY_VALUE) m_prev_ema = CalcEma(); class=class="str">"cmt">//保存当前价格 m_last_value = value; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 调整 EMA | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiEMA::OnChangeValue(class="type">int index,class="type">class="kw">double del_value,class="type">class="kw">double new_value) { if(index != GetMaxTotal()-class="num">1) class="kw">return; m_last_value = new_value; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 直接 EMA 计算 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CRiEMA::CalcEma(class="type">void) { class="kw">return m_last_value*m_smoth_factor+m_prev_ema*(class="num">1.0-m_smoth_factor); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取简单移动均值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CRiEMA::EMA(class="type">void) { if(m_calc_first_v) class="kw">return CalcEma(); else { m_prev_ema = m_last_value; m_calc_first_v = true; } class="kw">return m_prev_ema; }
给每根K线打极值等级,跳过无效比较
在滑动窗口里算最高/最低价,如果每次都用 ArrayMaximum、ArrayMinimum 从头扫一遍,环形缓存带来的增量更新优势就全废了。真正省算力的做法是:每往缓存压入一个新值,就顺手记两个附加计数——之前有多少个元素低于它(用于找最高价),以及之前有多少个高于它(用于找最低价)。 以正常价格柱为例,给每根柱线标一个「极值指数」:第一根永远是 0;第二根比第一根高,指数为 1;第三根比前两根都高,指数为 2;随后连续三根每根都比前一根低,它们都低于第三根,指数全是 0;第七根高于前三个但低于第四根,指数为 3。每次新柱进来,靠前面已算好的指数就能直接定位比较对象。 图例里的红色新柱编号 9(从 0 计),先和 #8 比变高,指数 1;再和 #7 比也高,而 #7 已知高于前四根,于是直接跳到 #3 比——省掉了四个中间柱的比较。外汇和贵金属波动快、跳空多,这种跳过机制在 M1/M5 上处理长周期极值时,CPU 占用可能明显低于全量扫描。 源码里 CRiMaxMin 类用两个 CRiBuffInt 辅助缓存分别存最高/最低价指数,并且记忆当前极值:只要极值没被刷新,MaxValue / MinValue 直接返回缓存值,不再算。MaxIndex、MinIndex 还支持传入小于缓存总长的周期参数,实现「窗口内再切短区间」的极值查询。 [CODE] 片段里的类骨架已经把这套结构立好了:私有成员 m_max、m_min 是整型环形缓存,m_max_ind / m_min_ind 记当前极值位置;构造函数把 m_full 置 false、两个索引清 0,后续 OnAddValue 里才会逐步填充极值等级。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| RingBuffer.mqh |</span> <span class="comment">class=class="str">"cmt">//| 版权所有 class="num">2016, Vasiliy Sokolov. |</span> <span class="comment">class=class="str">"cmt">//| [MQL5官方文档] |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> class="macro">#class="kw">property copyright <span class="class="type">class="kw">string">"版权所有 class="num">2016, Vasiliy Sokolov."</span> class="macro">#class="kw">property link <span class="class="type">class="kw">string">"[MQL5官方文档] class="macro">#include <span class="class="type">class="kw">string">"RiBuffDbl.mqh"</span> class="macro">#include <span class="class="type">class="kw">string">"RiBuffInt.mqh"</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| 计算环形存中的指数移动平均值 |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class</span> CRiMaxMin : <span class="keyword">class="kw">public</span> CRiBuffDbl { <span class="keyword">class="kw">private</span>: CRiBuffInt m_max; CRiBuffInt m_min; <span class="keyword">class="type">bool</span> m_full; <span class="keyword">class="type">int</span> m_max_ind; <span class="keyword">class="type">int</span> m_min_ind; <span class="keyword">class="kw">protected</span>: <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> OnAddValue(<span class="keyword">class="type">class="kw">double</span> <span class="keyword">value</span>); <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> OnCalcValue(<span class="keyword">class="type">int</span> index); <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> OnChangeValue(<span class="keyword">class="type">int</span> index, <span class="keyword">class="type">class="kw">double</span> del_value, <span class="keyword">class="type">class="kw">double</span> new_value); <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> OnSetMaxTotal(<span class="keyword">class="type">int</span> max_total); <span class="keyword">class="kw">public</span>: CRiMaxMin(<span class="keyword">class="type">void</span>); <span class="keyword">class="type">int</span> MaxIndex(<span class="keyword">class="type">int</span> max_period = <span class="number">class="num">0</span>); <span class="keyword">class="type">int</span> MinIndex(<span class="keyword">class="type">int</span> min_period = <span class="number">class="num">0</span>); <span class="keyword">class="type">class="kw">double</span> MaxValue(<span class="keyword">class="type">int</span> max_period = <span class="number">class="num">0</span>); <span class="keyword">class="type">class="kw">double</span> MinValue(<span class="keyword">class="type">int</span> min_period = <span class="number">class="num">0</span>); <span class="keyword">class="type">void</span> GetMaxIndexes(<span class="keyword">class="type">int</span>& array[]); <span class="keyword">class="type">void</span> GetMinIndexes(<span class="keyword">class="type">int</span>& array[]); }; CRiMaxMin::CRiMaxMin(<span class="keyword">class="type">void</span>) { m_full = <span class="keyword">class="kw">false</span>; m_max_ind = <span class="number">class="num">0</span>; m_min_ind = <span class="number">class="num">0</span>; }
「极值索引的增量重算逻辑」
CRiMaxMin 类里极值索引不是每次全量扫描,而是靠几个内部方法做增量维护。GetMaxIndexes 和 GetMinIndexes 只是把 m_max / m_min 两个缓存直接导成数组,对外暴露极值点位置。 OnSetMaxTotal 在主体缓存改尺寸时被调用,同步把 m_max 与 m_min 的容量上限设为同一个 max_total,避免两侧长度漂移。 真正干活的是 OnAddValue:每来一个新值,先把 m_max_ind 和 m_min_ind 各减 1(旧极值相对位移),再判断新值是否击穿原极值,若 value >= GetValue(m_max_ind) 就把 m_max_ind 指向末位 last,最低价同理。随后调 OnCalcValue(last) 算该位置的极值跨度。 OnCalcValue 里的双 while 是核心。以最高价搜索为例:从 index-1 往前比,只要当前值仍大于等于历史值,就累加 m_max 里存过的极值跨度 extr 并 +1,再把 p_ind 跳到 GetTotal()-1-max-1 继续。最低价分支完全对称。算完 max / min 后分别 AddValue 进缓存,并在首次填满时把 m_full 置 true。 这套写法的好处是单次插入接近 O(k),k 为连续同向极值段数,而非 O(n) 全扫。外汇与贵金属波动跳变频繁,k 多数时候是个位数,MT5 上挂这类结构做实时极值跟踪,CPU 占用倾向明显低于朴素循环。
class="type">void CRiMaxMin::GetMaxIndexes(class="type">int& array[]) { m_max.ToArray(array); } class="type">void CRiMaxMin::GetMinIndexes(class="type">int& array[]) { m_min.ToArray(array); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 依据主要缓存的新尺寸 | class=class="str">"cmt">//| 修改内部缓存的尺寸 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiMaxMin::OnSetMaxTotal(class="type">int max_total) { m_max.SetMaxTotal(max_total); m_min.SetMaxTotal(max_total); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算最大/最小指数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiMaxMin::OnAddValue(class="type">class="kw">double value) { m_max_ind--; m_min_ind--; class="type">int last = GetTotal()-class="num">1; if(m_max_ind > class="num">0 && value >= GetValue(m_max_ind)) m_max_ind = last; if(m_min_ind > class="num">0 && value <= GetValue(m_min_ind)) m_min_ind = last; OnCalcValue(last); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算最大/最小指数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiMaxMin::OnCalcValue(class="type">int index) { class="type">int max = class="num">0, min = class="num">0; class="type">int offset = m_full ?class="num">1 : class="num">0; class="type">class="kw">double value = GetValue(index); class="type">int p_ind = index-class="num">1; class=class="str">"cmt">//搜索最高价 class="kw">while(p_ind >= class="num">0 && value >= GetValue(p_ind)) { class="type">int extr = m_max.GetValue(p_ind+offset); max += extr + class="num">1; p_ind = GetTotal() - class="num">1 - max - class="num">1; } p_ind = GetTotal()-class="num">2; class=class="str">"cmt">//搜索最低价 class="kw">while(p_ind >= class="num">0 && value <= GetValue(p_ind)) { class="type">int extr = m_min.GetValue(p_ind+offset); min += extr + class="num">1; p_ind = GetTotal() - class="num">1 - min - class="num">1; } m_max.AddValue(max); m_min.AddValue(min); if(!m_full && GetTotal() == GetMaxTotal()) m_full = true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 修改任意位置的数值之后 | class=class="str">"cmt">//| 重新计算最高价/最低价指数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiMaxMin::OnChangeValue(class="type">int index, class="type">class="kw">double del_value, class="type">class="kw">double new_value) { if(m_max_ind >= class="num">0 && new_value >= GetValue(m_max_ind))
◍ 极值索引的回溯与截取逻辑
CRiMaxMin 里 MaxIndex 与 MinIndex 负责在已登记的极值序列中回找最近的有效位置。传入 max_period 大于 0 时,函数先把 limit 设成总数减周期,相当于只在最近 N 根内找极值,避免拿太早的历史高点干扰信号。 若 m_max_ind 已大于等于 0 说明前面算过且未失效,直接返回缓存索引,省一次循环。否则从序列末尾往前跳:每次取 GetValue(c_max) 得到该位置距上一个极值的偏移 ext,若 (c_max - ext) 已落在 limit 内就返回当前 c_max,不然 c_max 减 ext 再减 1 继续往前。 MaxValue / MinValue 只是套一层 MaxIndex / MinIndex 再调 GetValue 取数。你在 MT5 里若想只盯最近 20 根的高低点,传参 20 即可,返回索引越界会倾向拿到 limit 边界值,需注意外汇与贵金属品种跳空时极值可能失真,属高风险场景。
m_max_ind = index; if(m_min_ind >= class="num">0 && new_value >= GetValue(m_min_ind)) m_min_ind = index; for(class="type">int i = index; i < GetTotal(); i++) OnCalcValue(i); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取最大元素索引 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CRiMaxMin::MaxIndex(class="type">int max_period = class="num">0) { class="type">int limit = class="num">0; if(max_period > class="num">0 && max_period <= m_max.GetTotal()) { m_max_ind = -class="num">1; limit = m_max.GetTotal() - max_period; } if(m_max_ind >=class="num">0) class="kw">return m_max_ind; class="type">int c_max = m_max.GetTotal()-class="num">1; class="kw">while(c_max > limit) { class="type">int ext = m_max.GetValue(c_max); if((c_max - ext) <= limit) class="kw">return c_max; c_max = c_max - ext - class="num">1; } class="kw">return limit; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取最小元素索引 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CRiMaxMin::MinIndex(class="type">int min_period = class="num">0) { class="type">int limit = class="num">0; if(min_period > class="num">0 && min_period <= m_min.GetTotal()) { limit = m_min.GetTotal() - min_period; m_min_ind = -class="num">1; } if(m_min_ind >=class="num">0) class="kw">return m_min_ind; class="type">int c_min = m_min.GetTotal()-class="num">1; class="kw">while(c_min > limit) { class="type">int ext = m_min.GetValue(c_min); if((c_min - ext) <= limit) class="kw">return c_min; c_min = c_min - ext - class="num">1; } class="kw">return limit; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取最大元素值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CRiMaxMin::MaxValue(class="type">int max_period = class="num">0) { class="kw">return GetValue(MaxIndex(max_period)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取最小元素值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CRiMaxMin::MinValue(class="type">int min_period = class="num">0) { class="kw">return GetValue(MinIndex(min_period)); }
用环形缓存喂 AlgLib 算滑动高斯参数
统计类算法大多按整段样本算矩,本身不理会滑动窗口。要把均值、标准差、偏态、峰度做成随 K 线滚动的指标,传统写法得在每次刷新时手动拼一个截断数组,既啰嗦又容易越界。 环形缓存把这件事接管了:新数据进 AddValue,旧数据自动被覆盖,窗口长度由缓存容量固定。我们只需要在数组变动的回调里调一次 AlgLib 的静态方法,就能拿到最新的一组高斯矩。 上面这段代码里 CRiGaussProperty 继承自双精度环形缓存类,四个私有字段分别存均值、方差、偏态、峰度。OnChangeArray 中先 ToArray 倒出当前窗口,再丢给 CAlglib::SampleMoments 一口气算出四个量,StdDev 直接由方差开根得到。 实盘里把指标挂到子窗口,设置里切到标准偏差绘制,就能看到随价格滚动的波动带。外汇与贵金属波动跳变频繁,这类滑动矩仅描述已发生样本的分布特征,对后续走势只具概率层面的参考意义,杠杆品种高风险依旧。
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_separate_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\RiBuffDbl.mqh> class="macro">#include <Math\AlgLib\AlgLib.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算高斯分布的主要参数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CRiGaussProperty : class="kw">public CRiBuffDbl { class="kw">private: class="type">class="kw">double m_mean; class=class="str">"cmt">// 均值 class="type">class="kw">double m_variance; class=class="str">"cmt">// 方差 class="type">class="kw">double m_skewness; class=class="str">"cmt">// 偏态 class="type">class="kw">double m_kurtosis; class=class="str">"cmt">// 峰度 class="kw">protected: class="kw">virtual class="type">void OnChangeArray(class="type">void); class="kw">public: class="type">class="kw">double Mean(class="type">void){ class="kw">return m_mean;} class="type">class="kw">double StdDev(class="type">void){class="kw">return MathSqrt(m_variance);} class="type">class="kw">double Skewness(class="type">void){class="kw">return m_skewness;} class="type">class="kw">double Kurtosis(class="type">void){class="kw">return m_kurtosis;} }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 数组产生任何变化的情况下执行计算 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiGaussProperty::OnChangeArray(class="type">void) { class="type">class="kw">double array[]; ToArray(array); CAlglib::SampleMoments(array, m_mean, m_variance, m_skewness, m_kurtosis); } class=class="str">"cmt">//+------------------------------------------------------------------+
「把均值方差塞进指标缓冲区的写法」
想在 MT5 自定义指标里直接画出某段价格序列的高斯属性,核心是先定义一个枚举把四种统计量列清楚:均值、标准差、偏态、峰度。这样在 input 里切一下 Property,就能让同一套缓存输出不同的分布特征,不用写四个指标。 下面这段代码里 BPeriod 默认 13,意味着滚动窗口取最近 13 根价格。CRiGaussProperty 是封装好的高斯计算器,SetMaxTotal(BPeriod) 先限定它最多记多少个值,超了就丢最早的。 OnCalculate 里分两种跑法:如果是新 K 线进来(i 从 prev_calculated 往后),就 AddValue 把价格喂进去,再取对应属性写进 buff;如果这根 K 线只是价格跳动没新柱,就走 ChangeValue 改掉窗口里最老那个值,避免重算整段。外汇和贵金属波动跳变多,这种增量更新能省不少 CPU,但高频下仍属高风险环境,参数乱调可能画出失真曲线。 GetGaussValue 就是个 switch 分发,外部传进来的 property 决定调 Mean / StdDev / Skewness / Kurtosis 哪一个。复制时留意原文枚举名是 Property 大写,函数参数却叫 property 小写,编译不过就改一致。
enum ENUM_GAUSS_PROPERTY { GAUSS_MEAN, class=class="str">"cmt">// 均值 GAUSS_STDDEV, class=class="str">"cmt">// 方差 GAUSS_SKEWNESS, class=class="str">"cmt">// 偏态 GAUSS_KURTOSIS class=class="str">"cmt">// 峰度 }; class="kw">input class="type">int BPeriod = class="num">13; class=class="str">"cmt">//周期 class="kw">input ENUM_GAUSS_PROPERTY Property; class="type">class="kw">double buff[]; CRiGaussProperty RiGauss; class="type">int OnInit() { SetIndexBuffer(class="num">0, buff, INDICATOR_DATA); RiGauss.SetMaxTotal(BPeriod); class="kw">return(INIT_SUCCEEDED); } 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="type">bool calc = class="kw">false; for(class="type">int i = prev_calculated; i < rates_total; i++) { RiGauss.AddValue(price[i]); buff[i] = GetGaussValue(Property); calc = true; } if(!calc) { RiGauss.ChangeValue(BPeriod-class="num">1, price[rates_total-class="num">1]); buff[rates_total-class="num">1] = GetGaussValue(Property); } class="kw">return(rates_total-class="num">1); } class="type">class="kw">double GetGaussValue(ENUM_GAUSS_PROPERTY class="kw">property) { class="type">class="kw">double value = EMPTY_VALUE; class="kw">switch(Property) { case GAUSS_MEAN: value = RiGauss.Mean(); class="kw">break; case GAUSS_STDDEV: value = RiGauss.StdDev(); class="kw">break; case GAUSS_SKEWNESS: value = RiGauss.Skewness(); class="kw">break; case GAUSS_KURTOSIS: value = RiGauss.Kurtosis(); class="kw">break; }
◍ 函数收尾的返回值处理
在 MQL5 自定义函数里,末尾用 return 把计算结果交回调用处是最基础的动作。少写这一句,编译器会报“not all control paths return a value”,函数无法编译通过。 实际调试时,若函数在条件分支里已 return,末尾再补一个默认 return 能避免逻辑漏路。打开 MT5 按 F4 进编辑器,随便建个 int 型函数试两行就能验证。
class="kw">return value;
}用环形基元拼出 MACD 信号线
前面已经攒下简单/指数均线和极值两类环形基元,靠它们能直接组合出标准 MACD:快、慢两条 EMA 之差进第三个环形缓存,再对这个差值序列跑 SMA 就是信号线。 CRiMACD 没有继承环形缓存基类,而是把 CRiEMA、CRiSMA 实例塞进 private 区做内聚。类本身不维护自己的计算缓冲,只在零号柱上跟踪最后一个元素的变动,其余交给包含的缓存自行滑动。 Macd() 和 Signal() 两个方法分别吐出柱值和信号线,每个缓存各管各的周期。实测在 MT5 里渲染出来的曲线与终端自带 MACD 视觉一致,零号柱刷新逻辑也对齐。 开 MT5 把下面代码塞进你的 RingBuffer.mqh 体系,改 SetFastPeriod(12)、SetSlowPeriod(26)、SetSignalPeriod(9) 即可对照验证。外汇与贵金属波动剧烈,指标仅作概率参考,高风险。
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="macro">#include "RiSMA.mqh" class="macro">#include "RiEMA.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 计算环形缓存中的移动平均值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CRiMACD { class="kw">private: CRiEMA m_slow_macd; class=class="str">"cmt">// 快速指数均线 CRiEMA m_fast_macd; class=class="str">"cmt">// 慢速指数均线 CRiSMA m_signal_macd; class=class="str">"cmt">// 信号线 class="type">class="kw">double m_delta; class=class="str">"cmt">// 快速和慢速 EMA 之间的差值 class="kw">public: class="type">class="kw">double Macd(class="type">void); class="type">class="kw">double Signal(class="type">void); class="type">void ChangeLast(class="type">class="kw">double new_value); class="type">void SetFastPeriod(class="type">int period); class="type">void SetSlowPeriod(class="type">int period); class="type">void SetSignalPeriod(class="type">int period); class="type">void AddValue(class="type">class="kw">double value); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 重新计算 MACD | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiMACD::AddValue(class="type">class="kw">double value) { m_slow_macd.AddValue(value); m_fast_macd.AddValue(value); m_delta = m_slow_macd.EMA() - m_fast_macd.EMA(); m_signal_macd.AddValue(m_delta); } class=class="str">"cmt">//+------------------------------------------------------------------+
「给 MACD 类塞进可改值的接口」
在自研指标类里,实时改写末根柱子比重新算整列更省资源。下面这组方法把慢线、快线、信号线的末值单独暴露出来,回测时想制造特定形态直接调 ChangeLast 即可。 ChangeLast 先把慢速序列和快速序列的最后一格换成传入值,再用两者 EMA 之差重算 m_delta,最后把差值写进信号序列末尾。注意它调用的是 GetTotal 和 GetMaxTotal 两种不同的长度接口,说明慢快两段缓冲区管理方式并不对称。 取数接口很薄:Macd() 直接返回 m_delta,Signal() 返回信号序列的 SMA。设置周期的三个方法实际只改了各自缓冲区上限,没有触发重算,调用后需自行保证数据一致。 外汇与贵金属波动跳变频繁,这类手动改末值的做法仅适合离线验证,实盘误用可能扭曲信号,概率上会放大错单风险。
class=class="str">"cmt">//| 修改 MACD | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiMACD::ChangeLast(class="type">class="kw">double new_value) { m_slow_macd.ChangeValue(m_slow_macd.GetTotal()-class="num">1, new_value); class=class="str">"cmt">// 把慢速EMA序列最后一根改为新值 m_fast_macd.ChangeValue(m_fast_macd.GetMaxTotal()-class="num">1, new_value); class=class="str">"cmt">// 把快速EMA序列最后一根改为新值 m_delta = m_slow_macd.EMA() - m_fast_macd.EMA(); class=class="str">"cmt">// 用改过的两条线重算差值 m_signal_macd.ChangeValue(m_slow_macd.GetTotal()-class="num">1, m_delta); class=class="str">"cmt">// 把差值写进信号线末位 } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取 MACD 直方图 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CRiMACD::Macd(class="type">void) { class="kw">return m_delta; class=class="str">"cmt">// 直接返回快慢线EMA差 } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取信号线 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CRiMACD::Signal(class="type">void) { class="kw">return m_signal_macd.SMA(); class=class="str">"cmt">// 返回信号序列的简单移动平均 } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置快速周期 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiMACD::SetFastPeriod(class="type">int period) { m_slow_macd.SetMaxTotal(period); class=class="str">"cmt">// 设慢线缓冲区上限 } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置慢速周期 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiMACD::SetSlowPeriod(class="type">int period) { m_fast_macd.SetMaxTotal(period); class=class="str">"cmt">// 设快线缓冲区上限 } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置信号线周期 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiMACD::SetSignalPeriod(class="type">int period) { m_signal_macd.SetMaxTotal(period); class=class="str">"cmt">// 设信号线缓冲区上限 }
◍ 环形基元改写随机振荡的计算路径
随机振荡器依赖三根价格序列:每根柱线的最高价、最低价与收盘价。先框定 K% 周期窗口内的最高值与最低值,再算 (收盘价-最低价)/((最高价-最低价)*100.0%),最后对该比值做 N 周期 SMA 得到 K%;对 K% 再做一次 D% 周期 SMA 得到信号线。标准例程 Stochastic.mq5 把这套逻辑拆成 8 个 for 循环,其中 3 个嵌套,最高/最低价搜索、K% 与 D% 各跑一轮双重迭代,主计算量随 K%、Slowing、D% 三个参数线性放大。 没有环形缓存时,计算被迫分阶段落地:先写两个附加缓冲区存极值,再回头算 K%,最后算 D%,代码可读性明显下降。CRiStoch 类把方程直接套到已提供的数值上,极值搜索与均线都由环形基元内部完成,主方法里不再出现中间循环。 附一段标准版 OnCalculate 的核心片段,黄色背景处就是嵌套搜索极值的主循环:外层按柱线推进,内层在 K% 周期内逐根比对 low/high。把它和 CRiStoch 并排跑,输出曲线应完全重合,可用 MT5 自带 Stochastic 与自写版本叠加验证。外汇与贵金属波动剧烈,指标仅描述概率性超买超卖,实盘须控仓。
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">class="kw">datetime &time[], class="kw">const class="type">class="kw">double &open[], class="kw">const class="type">class="kw">double &high[], class="kw">const class="type">class="kw">double &low[], class="kw">const class="type">class="kw">double &close[], class="kw">const class="type">long &tick_volume[], class="kw">const class="type">long &volume[], class="kw">const class="type">int &spread[]) { class="type">int i,k,start; class=class="str">"cmt">//--- 检查柱线计数 if(rates_total<=InpKPeriod+InpDPeriod+InpSlowing) class="kw">return(class="num">0); class=class="str">"cmt">//--- start=InpKPeriod-class="num">1; if(start+class="num">1<prev_calculated) start=prev_calculated-class="num">2; else { for(i=class="num">0;i<start;i++) { ExtLowesBuffer[i]=class="num">0.0; ExtHighesBuffer[i]=class="num">0.0; } } class=class="str">"cmt">//--- 计算 HighesBuffer[] 和 ExtHighesBuffer[] for(i=start;i<rates_total && !IsStopped();i++) { class="type">class="kw">double dmin=class="num">1000000.0; class="type">class="kw">double dmax=-class="num">1000000.0; for(k=i-InpKPeriod+class="num">1;k<=i;k++) { if(dmin>low[k]) dmin=low[k]; if(dmax<high[k]) dmax=high[k]; } ExtLowesBuffer[i]=dmin; ExtHighesBuffer[i]=dmax; } class=class="str">"cmt">//--- %K start=InpKPeriod-class="num">1+InpSlowing-class="num">1; if(start+class="num">1<prev_calculated) start=prev_calculated-class="num">2; else {
慢速随机指标的循环与信号线算法
慢速随机指标在主计算循环里先用两层 for 把每个 bar 的 %K 原始值算出来:内层遍历 i-InpSlowing+1 到 i 的窗口,累加 close 与局部最低价的差作为 sumlow,以及局部最高价与最低价的差作为 sumhigh。若 sumhigh 为 0 则直接给 ExtMainBuffer[i] 赋 100.0,否则取 sumlow/sumhigh*100,这段就是慢速 %K 的核心。 信号线 ExtSignalBuffer 走的是对 ExtMainBuffer 做 InpDPeriod 周期简单平均:从 start 位置起,每个 i 把前 InpDPeriod 个 %K 加总除以周期数。注意 start 的判定——若 start+1 小于 prev_calculated 则回退到 prev_calculated-2,避免历史重算拖慢 EA 在 MT5 上的实时刷新。
for(i=class="num">0;i<start;i++) ExtMainBuffer[i]=class="num">0.0; class=class="str">"cmt">// 初始化阶段:把起始前的历史缓冲区清零 for(i=start;i<rates_total && !IsStopped();i++) class=class="str">"cmt">// 主循环:从 start 扫到最新 bar,IsStopped 保证终端关闭时退出 class="type">class="kw">double sumlow=class="num">0.0; class="type">class="kw">double sumhigh=class="num">0.0; class=class="str">"cmt">// 定义慢速窗口内的分子和分母累加器 for(k=(i-InpSlowing+class="num">1);k<=i;k++) class=class="str">"cmt">// 内层窗口循环:长度由 InpSlowing 控制 sumlow +=(close[k]-ExtLowesBuffer[k]); class=class="str">"cmt">// 收盘价减局部最低,累加到分子 sumhigh+=(ExtHighesBuffer[k]-ExtLowesBuffer[k]); class=class="str">"cmt">// 局部高低差,累加到分母 if(sumhigh==class="num">0.0) ExtMainBuffer[i]=class="num">100.0; else ExtMainBuffer[i]=sumlow/sumhigh*class="num">100; class=class="str">"cmt">// 防零除并算出慢速 %K for(i=class="num">0;i<start;i++) ExtSignalBuffer[i]=class="num">0.0; class=class="str">"cmt">// 信号线历史区清零 for(k=class="num">0;k<InpDPeriod;k++) sum+=ExtMainBuffer[i-k]; class=class="str">"cmt">// 对 %K 做周期求和 ExtSignalBuffer[i]=sum/InpDPeriod; class=class="str">"cmt">// 除以周期得 %D 信号线
for(i=class="num">0;i<start;i++) ExtMainBuffer[i]=class="num">0.0; for(i=start;i<rates_total && !IsStopped();i++) { class="type">class="kw">double sumlow=class="num">0.0; class="type">class="kw">double sumhigh=class="num">0.0; for(k=(i-InpSlowing+class="num">1);k<=i;k++) { sumlow +=(close[k]-ExtLowesBuffer[k]); sumhigh+=(ExtHighesBuffer[k]-ExtLowesBuffer[k]); } if(sumhigh==class="num">0.0) ExtMainBuffer[i]=class="num">100.0; else ExtMainBuffer[i]=sumlow/sumhigh*class="num">100; } for(i=class="num">0;i<start;i++) ExtSignalBuffer[i]=class="num">0.0; for(i=start;i<rates_total && !IsStopped();i++) { class="type">class="kw">double sum=class="num">0.0; for(k=class="num">0;k<InpDPeriod;k++) sum+=ExtMainBuffer[i-k]; ExtSignalBuffer[i]=sum/InpDPeriod; }
「把随机振荡器拆成可增量计算的类」
在 MT5 里写自己的随机指标,不必每次重算整段序列。下面这个类把最高、最低、K 平滑、D 均线拆成四个独立缓冲,每来一根 Bar 只做增量更新,回测时省掉大量重复遍历。 核心计算落在 AddValue:先把当前 high/low 喂给极值跟踪器,取周期内 max 与 min,再用 (close-min)/(max-min)*100 得到原始 %K。若区间高度为 0 则 K 直接记 0,避免除零。 得到的 K 先送进 m_slowed_k 做 SMA 平滑,再从平滑后的 K 序列取均值得到 %D。这样 SetPeriodK / SetPeriodD / SetSlowedPeriodK 三个周期参数可以独立调,黄金 15 分钟图上把 K 周期设 5、D 设 3、慢速 K 设 3 是常见的紧凑组合,但外汇与贵金属波动剧烈,参数过紧可能频繁假交叉,实盘前务必在 MT5 策略测试器跑一遍。 别把正态当圣经:这个类只负责算值,信号过滤得你自己接一层,比如要求 D 在 20 以下金叉才视为超卖反弹倾向。
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="macro">#include "RiSMA.mqh" class="macro">#include "RiMaxMin.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 随机振荡指标类 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CRiStoch { class="kw">private: CRiMaxMin m_max; class=class="str">"cmt">// 最高价指标 CRiMaxMin m_min; class=class="str">"cmt">// 最低价指标 CRiSMA m_slowed_k; class=class="str">"cmt">// K% 平滑 CRiSMA m_slowed_d; class=class="str">"cmt">// D% 移动均线 class="kw">public: class="type">void ChangeLast(class="type">class="kw">double new_value); class="type">void AddValue(class="type">class="kw">double close, class="type">class="kw">double high, class="type">class="kw">double low); class="type">void AddHighValue(class="type">class="kw">double value); class="type">void AddLowValue(class="type">class="kw">double value); class="type">void AddCloseValue(class="type">class="kw">double value); class="type">void SetPeriodK(class="type">int period); class="type">void SetPeriodD(class="type">int period); class="type">void SetSlowedPeriodK(class="type">int period); class="type">class="kw">double GetStochK(class="type">void); class="type">class="kw">double GetStochD(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 添加新值并计算随机振荡 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiStoch::AddValue(class="type">class="kw">double close, class="type">class="kw">double high, class="type">class="kw">double low) { m_max.AddValue(high); class=class="str">"cmt">// 添加新的最高价 m_min.AddValue(low); class=class="str">"cmt">// 添加新的最低价 class="type">class="kw">double c = close; class="type">class="kw">double max = m_max.MaxValue() class="type">class="kw">double min = m_min.MinValue(); class="type">class="kw">double delta = max - min; class="type">class="kw">double k = class="num">0.0; if(delta != class="num">0.0) k = (c-min)/delta*class="num">100.0; class=class="str">"cmt">// 使用方程搜索 K% m_slowed_k.AddValue(k); class=class="str">"cmt">// 平滑 K% (K% 慢速) m_slowed_d.AddValue(m_slowed_k.SMA()); class=class="str">"cmt">// 从平滑的 K% 中搜索 %D }
◍ 随机指标的三种周期怎么在类里落地
封装随机指标时,快线、慢线和信号线其实是三套独立的长度窗口,分别由 SetPeriodK、SetSlowedPeriodK、SetPeriodD 三个方法写入。它们各自调用对应内部对象的 SetMaxTotal,决定滚动样本总数,而不是在计算公式里临时传参。 从代码看,m_max 与 m_min 共用快线周期 period,说明 %K 的原始高低点滚动区间长度一致;m_slowed_k 单独吃慢线周期,m_slowed_d 再吃信号线周期。三者解耦后,你可以在 EA 初始化时把快线设 5、慢线设 3、信号线设 3 这类非对称组合直接灌进去。 取数接口只有两个:GetStochK 返回 m_slowed_k.SMA(),GetStochD 返回 m_slowed_d.SMA()。也就是说你拿到的 %K 和 %D 都已经过了慢速平滑,不是裸随机值。外汇与贵金属波动大,这种二次平滑在震荡市可能过滤假交叉,趋势市则可能滞后,实盘前务必在 MT5 用历史数据验证信号节奏。 别把周期当摆设 改一个周期参数就能让同一段行情的交叉信号从 12 次变成 4 次,建议用策略测试器跑 EURUSD 的 M15,对比快线 5 与 14 的触发密度再定参数。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置快速周期 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiStoch::SetPeriodK(class="type">int period) { m_max.SetMaxTotal(period); m_min.SetMaxTotal(period); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置慢速周期 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiStoch::SetSlowedPeriodK(class="type">int period) { m_slowed_k.SetMaxTotal(period); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 设置信号线周期 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CRiStoch::SetPeriodD(class="type">int period) { m_slowed_d.SetMaxTotal(period); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取 %K 数值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CRiStoch::GetStochK(class="type">void) { class="kw">return m_slowed_k.SMA(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取 %D 数值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CRiStoch::GetStochD(class="type">void) { class="kw">return m_slowed_d.SMA(); }
句柄背后藏着的内存账本
指标句柄本身只是个 64 位整数,占用极小,但 MetaTrader 5 在后台为它分配的计算模块和数据缓存才是吃内存的大头。用 iMACD 这类标准指标拿句柄再 CopyBuffer,比在 EA 内部直接算要慢且费内存,调用五六个指标时差距就明显了。外汇和贵金属交易多品种多周期扫描属高风险操作,资源占用失控会让终端直接卡死。 一个信息面板若覆盖 17 个品种、9 套参数,就需要 17×9=153 个指标;若再扫全部 21 个时间帧,数量会飙到 3213 个,全盘安置必然耗尽内存。 实测用 EA 做负载测试:4 个品种(EURUSD、GBPUSD、USDCHF、USDJPY)×21 周期×2 指标(MACD、随机)×3 参数集 = 504 个数值。调用系统指标模式占 11.9 GB,环形缓存内部计算模式占 2.9 GB(测试机 16 GB 内存),省下的主要来自把计算搬进 EA 而非句柄本身。 把 21 个周期砍到只留 PERIOD_M1,内部计算 504 个指标仅需 548 MB,系统指标模式也降到 1.9 GB。下载计算所需行情数据才是消耗主体,终端自身约占 100 MB。四倍内存压缩是实打实可复现的。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| RiIndLoad.mq5 | class=class="str">"cmt">//| 版权所有 class="num">2017, 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">#include <Arrays\ArrayObj.mqh> class="macro">#include "NewBarDetector.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 创建的指标类型 | class=class="str">"cmt">//+------------------------------------------------------------------+ enum ENUM_INDICATOR_TYPE { INDICATOR_SYSTEM, class=class="str">"cmt">// 系统指标 INDICATOR_RIBUFF class=class="str">"cmt">// 环形缓存指标 }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 指标容器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CIndBase : class="kw">public CObject { class="kw">protected: class="type">int m_handle; class=class="str">"cmt">// 指标句柄 class="type">class="kw">string m_symbol; class=class="str">"cmt">// 指标计算品种 ENUM_INDICATOR_TYPE m_ind_type; class=class="str">"cmt">// 指标类型 ENUM_TIMEFRAMES m_period; class=class="str">"cmt">// 指标计算周期
「指标基类的构造与取值骨架」
在 MT5 里写可复用的指标容器,第一步往往是抽一个基类把品种、周期和句柄管起来。下面这段 CIndBase 的受保护构造器就干了这件事:把传进来的 symbol、period、ind_type 存进成员变量,同时给内部的新柱线探测器 m_bar_detect 同步品种和周期。 CIndBase::CIndBase(string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR_TYPE ind_type) { m_handle = INVALID_HANDLE; m_symbol = symbol; m_period = period; m_ind_type = ind_type; m_bar_detect.Symbol(symbol); m_bar_detect.Timeframe(period); } 逐行看:第一行先把指标句柄置为 INVALID_HANDLE,避免野指针;第二三行落库品种与周期;第四行记指标类型;最后两行把同样参数喂给柱线探测器,保证后续判断“是否出新 K 线”时用的是同一上下文。 基类里 GetLastValue 只是个虚壳,直接 return EMPTY_VALUE,真正的计算留给子类重写。比如 CIndMacd 继承后,用 CRiMACD 环形缓存接住 MACD 的三参数(快 12 / 慢 26 / 信号 9 是常见默认,但构造器允许你传任意整数),再覆盖 GetLastValue 从指定 buffer 取数。 外汇与贵金属波动大、杠杆高,这类封装只是降低代码耦合,不预示任何方向。开 MT5 把 RiIndBase.mqh 和 RiMACD.mqh 放好,改 fast_period 从 12 调到 5,回测 EURUSD 的 M15,能直接看到信号触发频率变密。
CBarDetector m_bar_detect; class=class="str">"cmt">// 新柱线探测器 CIndBase(class="type">class="kw">string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR_TYPE ind_type); class="kw">public: class="type">class="kw">string Symbol(class="type">void){class="kw">return m_symbol;} ENUM_TIMEFRAMES Period(class="type">void){class="kw">return m_period;} class="kw">virtual class="type">class="kw">double GetLastValue(class="type">int index_buffer); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 受保护构造器需要指定指标的 | class=class="str">"cmt">//| 品种, 时间帧和时间 | class=class="str">"cmt">//+------------------------------------------------------------------+ CIndBase::CIndBase(class="type">class="kw">string symbol,ENUM_TIMEFRAMES period,ENUM_INDICATOR_TYPE ind_type) { m_handle = INVALID_HANDLE; m_symbol = symbol; m_period = period; m_ind_type = ind_type; m_bar_detect.Symbol(symbol); m_bar_detect.Timeframe(period); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取最后的指标值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CIndBase::GetLastValue(class="type">int index_buffer) { class="kw">return EMPTY_VALUE; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| RiIndLoad.mq5 | class=class="str">"cmt">//| 版权所有 class="num">2017, 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">#include <RingBuffer\RiMACD.mqh> class="macro">#include "RiIndBase.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 指标容器 | class=class="str">"cmt">//+------------------------------------------------------------------+ class CIndMacd : class="kw">public CIndBase { class="kw">private: CRiMACD m_macd; class=class="str">"cmt">// 指标环形缓存 class="kw">public: CIndMacd(class="type">class="kw">string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR_TYPE ind_type, class="type">int fast_period, class="type">int slow_period, class="type">int signal_period); class="kw">virtual class="type">class="kw">double GetLastValue(class="type">int index_buffer); };
◍ MACD 类两种取数路径的实现差异
在 MT5 自建指标封装里,MACD 可以走系统句柄,也可以走自算缓冲。CIndMacd 构造函数用 ind_type 分流:INDICATOR_SYSTEM 直接调 iMACD 拿句柄,INDICATOR_RIBUFF 则只把快慢线周期存进 m_macd 对象,不碰系统接口。 系统模式的句柄创建一旦返回 INVALID_HANDLE,代码会向终端打印失败日志,包含品种与周期字符串,方便你开 MT5 后直接在 Experts 标签里抓错。 GetLastValue 的取数逻辑分两头。句柄有效时用 CopyBuffer 从 buffer 1 取 1 根柱线,返回 array[0];失败给 EMPTY_VALUE。无句柄则依赖 IsNewBar 判新柱,CopyClose 取收盘价喂给 m_macd.AddValue,再按 index_buffer 0/1 分别吐 Macd 或 Signal 值。 实盘里若你发现 MACD 信号延迟一根,优先查 INDICATOR_RIBUFF 分支是否漏了 AddValue 调用——这是自算模式最常见的断点。外汇与贵金属波动大,这类自封装指标误触发可能放大滑点风险,参数上线前务必在策略测试器跑历史 Tick 验证。
class=class="str">"cmt">//| 创建 MACD 指标 | class=class="str">"cmt">//+------------------------------------------------------------------+ CIndMacd::CIndMacd(class="type">class="kw">string symbol, ENUM_TIMEFRAMES period, ENUM_INDICATOR_TYPE ind_type, class="type">int fast_period,class="type">int slow_period,class="type">int signal_period) : CIndBase(symbol, period, ind_type) { if(ind_type == INDICATOR_SYSTEM) { m_handle = iMACD(m_symbol, m_period, fast_period, slow_period, signal_period, PRICE_CLOSE); if(m_handle == INVALID_HANDLE) printf("创建 iMACD 句柄失败。品种: " + symbol + " 周期: " + EnumToString(period)); } else if(ind_type == INDICATOR_RIBUFF) { m_macd.SetFastPeriod(fast_period); m_macd.SetSlowPeriod(slow_period); m_macd.SetSignalPeriod(signal_period); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取最后的指标值 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CIndMacd::GetLastValue(class="type">int index_buffer) { if(m_handle != INVALID_HANDLE) { class="type">class="kw">double array[]; if(CopyBuffer(m_handle, index_buffer, class="num">1, class="num">1, array) > class="num">0) class="kw">return array[class="num">0]; class="kw">return EMPTY_VALUE; } else { if(m_bar_detect.IsNewBar()) { class=class="str">"cmt">//printf("收到新柱线 " + m_symbol + " 周期 " + EnumToString(m_period)); class="type">class="kw">double close[]; CopyClose(m_symbol, m_period, class="num">1, class="num">1, close); m_macd.AddValue(close[class="num">0]); } class="kw">switch(index_buffer) { case class="num">0: class="kw">return m_macd.Macd(); case class="num">1: class="kw">return m_macd.Signal(); } class="kw">return EMPTY_VALUE; } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TestIndEA.mq5 | class=class="str">"cmt">//| 版权所有 class="num">2017, Vasiliy Sokolov. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2017, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档]
多周期 MACD 与随机指标的参数骨架
做跨品种多周期扫描,第一步是把指标参数结构化。下面这段把 MACD 和随机振荡拆成两个 struct,分别存三套参数,方便后面按品种和周期批量挂实例。 MACD 三套取值分别是快/慢/信号:(3,13,6)、(9,26,12)、(18,52,24);随机振荡对应 K/K平滑/D:(6,3,3)、(12,5,6)、(24,7,12)。这种分层写法比写死在指标调用里更利于回测时改一组数就换一种风格。 初始化函数里先填完这 6 个数组,再准备用三层循环去实例化:外层扫 4 个货币对(EURUSD、GBPUSD、USDCHF、USDJPY),中层走 21 个周期,内层走 3 套参数组合。注释提到最终会生成 504 个指标对象,也就是 4×21×3×2 的体量,MT5 终端跑这种规模需留意内存和刷新开销。外汇与贵金属杠杆高,这类多实例逻辑只解决信号覆盖,不预示任何方向胜率。
class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <Object.mqh> class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include "RiIndBase.mqh" class="macro">#include "RiIndMacd.mqh" class="macro">#include "RiIndStoch.mqh" class="macro">#include "NewBarDetector.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MACD 参数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct CMacdParams { class="type">int slow_period; class="type">int fast_period; class="type">int signal_period; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 随机振荡参数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct CStochParams { class="type">int k_period; class="type">int k_slowed; class="type">int d_period; }; class="kw">input ENUM_INDICATOR_TYPE IndType = INDICATOR_SYSTEM; class=class="str">"cmt">// 指标类型 class="type">class="kw">string Symbols[] = {"EURUSD", "GBPUSD", "USDCHF", "USDJPY"}; CMacdParams MacdParams[class="num">3]; CStochParams StochParams[class="num">3]; CArrayObj ArrayInd; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 专家系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { MacdParams[class="num">0].fast_period = class="num">3; MacdParams[class="num">0].slow_period = class="num">13; MacdParams[class="num">0].signal_period = class="num">6; MacdParams[class="num">1].fast_period = class="num">9; MacdParams[class="num">1].slow_period = class="num">26; MacdParams[class="num">1].signal_period = class="num">12; MacdParams[class="num">2].fast_period = class="num">18; MacdParams[class="num">2].slow_period = class="num">52; MacdParams[class="num">2].signal_period = class="num">24; StochParams[class="num">0].k_period = class="num">6; StochParams[class="num">0].k_slowed = class="num">3; StochParams[class="num">0].d_period = class="num">3; StochParams[class="num">1].k_period = class="num">12; StochParams[class="num">1].k_slowed = class="num">5; StochParams[class="num">1].d_period = class="num">6; StochParams[class="num">2].k_period = class="num">24; StochParams[class="num">2].k_slowed = class="num">7; StochParams[class="num">2].d_period = class="num">12; class=class="str">"cmt">// class="num">504 MACD 和 随机振荡指标在此已被创建 for(class="type">int symbol = class="num">0; symbol < ArraySize(Symbols); symbol++) { for(class="type">int period = class="num">1; period <=class="num">21; period++) { for(class="type">int i = class="num">0; i < class="num">3; i++) {
「用数组批量挂 MACD 与随机指标」
在 MT5 的 EA 初始化阶段,可以用 CIndMacd 与 CIndStoch 的指针把多周期、多参数组合的指标一次性塞进一个 CArrayObj 容器,而不是逐个写死全局变量。下面这段逻辑里,外层遍历交易品种与周期索引,内层遍历参数表,每轮 new 出指标后直接 ArrayInd.Add() 进数组。
创建完成后用 printf 打出 创建 N 指标成功,N 来自 ArrayInd.Total()。若你挂了 3 个品种 × 2 个周期 × 2 套参数,终端会显示创建了 12 个指标——这个数字直接反映内存里活着的指标对象数,可用于排查重复实例化。
OnTick 里只示范了遍历:取 ArrayInd.At(i) 拿到基类指针,再 GetLastValue(0) 读主值、GetLastValue(1) 读信号线值。注意这里没有做有效性判断,实盘跑外汇或贵金属前应当先确认 ind 非空且 Bars 足够,否则可能返回空值触发异常。
PeriodByIndex 把 0~11 映射成 PERIOD_CURRENT 到 PERIOD_M30,相当于用整数当周期菜单。想扩到 H1 以上,继续补 case 12 返回 PERIOD_H1 即可,不用改调用处的其他代码。
CIndMacd* macd = new CIndMacd(Symbols[symbol], PeriodByIndex(period), IndType, MacdParams[i].fast_period, MacdParams[i].slow_period, MacdParams[i].signal_period); CIndStoch* stoch = new CIndStoch(Symbols[symbol], PeriodByIndex(period), IndType, StochParams[i].k_period, StochParams[i].k_slowed, StochParams[i].d_period); ArrayInd.Add(macd); ArrayInd.Add(stoch); } } } printf("创建 " + (class="type">class="kw">string)ArrayInd.Total() + " 指标成功"); class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 专家系统即时报价函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { for(class="type">int i = class="num">0; i < ArrayInd.Total(); i++) { CIndBase* ind = ArrayInd.At(i); class="type">class="kw">double value = ind.GetLastValue(class="num">0); class="type">class="kw">double value_signal = ind.GetLastValue(class="num">1); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 通过其索引获取时间帧 | class=class="str">"cmt">//+------------------------------------------------------------------+ ENUM_TIMEFRAMES PeriodByIndex(class="type">int index) { class="kw">switch(index) { case class="num">0: class="kw">return PERIOD_CURRENT; case class="num">1: class="kw">return PERIOD_M1; case class="num">2: class="kw">return PERIOD_M2; case class="num">3: class="kw">return PERIOD_M3; case class="num">4: class="kw">return PERIOD_M4; case class="num">5: class="kw">return PERIOD_M5; case class="num">6: class="kw">return PERIOD_M6; case class="num">7: class="kw">return PERIOD_M10; case class="num">8: class="kw">return PERIOD_M12; case class="num">9: class="kw">return PERIOD_M15; case class="num">10: class="kw">return PERIOD_M20; case class="num">11: class="kw">return PERIOD_M30;
◍ 周期映射与多品种指标批量挂载
上面这段 switch 的 case 12 到 21,把整型编号直接翻译成 MT5 的内置周期常量:12 对应 H1,13 是 H2,依此类推到 21 的月线 MN1,未匹配的任何值都落到 default 返回 PERIOD_CURRENT。写 EA 时若用数字下标管理周期,这套映射能避免手写字符串判等。 后面的三重 for 循环才是实跑部分:外层扫 Symbols 数组里的每个品种,中层把 period 从 1 跑到 21 全覆盖,内层 i 循环 3 次,每次 new 一个 CIndMacd 和一个 CIndStoch,都挂在 PERIOD_M1 上,只是 fast/slow/signal 或 k/d 参数取 MacdParams[i]、StochParams[i] 三套组合。 注意一个细节:period 变量在循环里根本没传给指标构造函数,所有实例都写死 PERIOD_M1,也就是说 21 个周期层只是空转计数,实际只加载了 M1 周期的 3×品种数×2 个指标对象。外汇与贵金属杠杆高、滑点跳空频繁,这种冗余构造会吃掉多余内存,开 MT5 用 iCustom 或 CCustomIndicator 自查句柄数就能验证。
case class="num">12: class="kw">return PERIOD_H1; case class="num">13: class="kw">return PERIOD_H2; case class="num">14: class="kw">return PERIOD_H3; case class="num">15: class="kw">return PERIOD_H4; case class="num">16: class="kw">return PERIOD_H6; case class="num">17: class="kw">return PERIOD_H8; case class="num">18: class="kw">return PERIOD_H12; case class="num">19: class="kw">return PERIOD_D1; case class="num">20: class="kw">return PERIOD_W1; case class="num">21: class="kw">return PERIOD_MN1; class="kw">default: class="kw">return PERIOD_CURRENT; } } class=class="str">"cmt">//+------------------------------------------------------------------+ ... for(class="type">int symbol = class="num">0; symbol < ArraySize(Symbols); symbol++) { for(class="type">int period = class="num">1; period <=class="num">21; period++) { for(class="type">int i = class="num">0; i < class="num">3; i++) { CIndMacd* macd = new CIndMacd(Symbols[symbol], PERIOD_M1, IndType, MacdParams[i].fast_period, MacdParams[i].slow_period, MacdParams[i].signal_period); CIndStoch* stoch = new CIndStoch(Symbols[symbol], PERIOD_M1, IndType, StochParams[i].k_period, StochParams[i].k_slowed, StochParams[i].d_period); ArrayInd.Add(macd); ArrayInd.Add(stoch); } } }
把指标算进EA里能省下四分之三测试时间
MT5 策略测试器里跑多品种 EA 时,最拖后腿的往往不是行情回放,而是反复向终端要指标缓冲区。若 EA 依赖系统指标跨品种取数,光环境同步虽快(实测 0.078 秒),但整体测试常被外部计算拖垮。 我们用前一例的 EA 在 Intel Core i7 870 2.9GHz 上做对照:EURUSD M1、仅开盘价模式、测最近一个月。调用系统指标计算时,114357 ticks、28647 bars 跑完花了 57.923 秒。 把同样的逻辑改成 EA 内部计算后,tick 与 bar 数量完全一致,测试时间掉到 12.292 秒。也就是说,省掉外部指标访问这一步,回测耗时大约降到原来的 21%,对需要反复调参的多品种策略,这个差距会直接体现在迭代效率上。 外汇与贵金属品种点差和滑点敏感,回测快不等于实盘稳,缩短测试时间只是让你更早拿到可验证的假设,不代表胜率提升。
「EA 内嵌指标的效能实测与取舍」
我们在开发指标时压测过内存与执行速度,对比两种运行模式:基于环形缓存的内部计算,能把内存占用压下来,整体性能提升数倍。当然这套基准是刻意造出来的极端场景——同时拉起 500 个指标、覆盖所有时间帧跑测试,普通程序员基本碰不到。但这种压力测试的价值在于,它能暴露最烧资源的机制,逼你少碰。 把指标计算模块直接塞进 EA 里,回测时省时间和内存最直观。跨时间帧取数的请求能免就免,只用最低周期(比如 M1)算完,再聚合成 H1 用,写法麻烦点,但内存省一大截。环形缓存值得认真用:它占的内存量跟指标本身计算所需几乎一致,还能顺手优化极值查找这类算法。 接一个通用指标接口来统一取数。内部不好写的指标就走接口调外部 MT5 指标;能内嵌的模块也挂到这接口上,EA 主体改动最小。不过别盲目重构——单个品种只挂一个指标时,转内部计算的开发耗时,可能比那点性能增益更贵。外汇和贵金属交易本就高杠杆高风险,这类优化只解决资源开销,不解决胜率。
◍ 环形缓存落地的现实与坑
环形缓存思路在 MT5 指标里最大的卖点是省内存、算得快,而且大多顺着 FIFO 逻辑走,代码写起来比常规数组指标轻。可实际在 MQL 社区里,这类结构长期没人系统整理,自己造轮子容易在索引换算上栽跟头。
| 有用户反馈,在 `ToRealInd(int index)` 里若写成 `if(index >= iNumElements() | index < 0) return iBufferSize-1;` 漏掉 `-1`,新 K 线到来时高低值会完全错位;改成 `if(index >= GetTotal() | index < 0) return m_max_total-1;` 才稳。外汇与贵金属波动剧烈,这类索引 bug 会让信号在实时行情中失真,实盘前务必在策略测试器用历史数据跑一遍。 |
|---|
附带的 RiMaxMin 类可做极值检索,下面这段测试代码把环形与非环形两条路径都留了口子,把 ringBuffer 输入切到 false 就能对比常规数组写法。
<span class="keyword">if</span>(m_min_ind >= <span class="number">class="num">0</span> && new_value >= GetValue(m_min_ind)) m_min_ind = index; <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">copyright</span> <span class="class="type">class="kw">string">"Copyright class="num">2020, MetaQuotes Software Corp."</span> <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">link</span> <span class="class="type">class="kw">string">"[MQL5官方文档] <span class="preprocessor">class="macro">#class="kw">property </span><span class="macro">version</span> <span class="class="type">class="kw">string">"class="num">1.00"</span> <span class="preprocessor">class="macro">#include </span><RingBuffer\RiMaxMin.mqh> <span class="keyword">class="kw">input</span> group <span class="class="type">class="kw">string">"通过环形缓冲器检查是真的"</span> <span class="keyword">class="kw">input</span> <span class="keyword">class="type">bool</span> ringBuffer=<span class="macro">true</span>; <span class="keyword">class="kw">input</span> group <span class="class="type">class="kw">string">"缓冲区大小"</span>。 <span class="keyword">class="kw">input</span> <span class="keyword">class="type">int</span> pTest=<span class="number">class="num">10</span>; <span class="keyword">class="type">class="kw">double</span> minValue,maxValue; <span class="keyword">class="type">int</span> minIndex,maxIndex,lastIndex,indArr; <span class="keyword">class="type">class="kw">double</span> arr[]; CRiMaxMin minMaxTest; <span class="keyword">class="type">int</span> <span class="functions">OnInit</span>() { <span class="keyword">if</span>(ringBuffer)minMaxTest.SetMaxTotal(pTest); <span class="keyword">else</span> { indArr=-<span class="number">class="num">1</span>; lastIndex=pTest-<span class="number">class="num">1</span>; <span class="functions">ArraySetAsSeries</span>(arr,<span class="macro">true</span>); <span class="functions">ArrayResize</span>(arr,pTest); } <span class="keyword">class="kw">return</span>(<span class="macro">INIT_SUCCEEDED</span>); } <span class="keyword">class="type">void</span> <span class="functions">OnDeinit</span>(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> reason) { } <span class="keyword">class="type">void</span> <span class="functions">OnTick</span>() { <span class="keyword">if</span>(ringBuffer) { minMaxTest.AddValue(<span class="functions">rand</span>()); minIndex=minMaxTest.MinIndex(); minValue=minMaxTest.GetValue(minIndex); <span class="comment">class=class="str">"cmt">//minValue=minMaxTest.MinValue();</span> maxIndex=minMaxTest.MaxIndex(); maxValue=minMaxTest.GetValue(maxIndex); <span class="comment">class=class="str">"cmt">//maxValue=minMaxTest.MaxValue();</span> } <span class="keyword">else</span> { <span class="comment">class=class="str">"cmt">//arr[class="num">0]=rand();</span> <span class="comment">/ArrayCopy(arr,arr,class="num">1,class="num">0,lastIndex);</span> indArr++; <span class="keyword">if</span>(indArr>lastIndex)indArr=<span class="number">class="num">0</span>; arr[indArr]=<span class="functions">rand</span>();
一点提醒
上面这段 MQL5 片段演示了在 pTest 长度窗口内用 ArrayMinimum / ArrayMaximum 取极值,再通过 minValue>maxValue 的防御判断直接 ExpertRemove 退出。实盘里若数组里混入了异常 NaN 或排序错乱,这种自检能在首根 K 线就拦住 EA 继续跑。 外汇与贵金属杠杆高、跳空频繁,这类极值校验建议每次初始化数组后都跑一遍,别等爆仓才怀疑数据源。 把这段代码贴进 MT5 的 OnInit 或 OnTick 头部,改 pTest 为你的样本量,Alert 弹窗会直接告诉你当前极值边界。
minIndex=ArrayMinimum(arr,class="num">0,pTest); minValue=arr[minIndex]; class=class="str">"cmt">//minValue=arr[ArrayMinimum(arr,class="num">0,pTest)]; maxIndex=ArrayMaximum(arr,class="num">0,pTest); maxValue=arr[maxIndex]; class=class="str">"cmt">//maxValue=arr[ArrayMaximum(arr,class="num">0,pTest)]; } Alert("minValue ",DoubleToString(minValue)," -- maxValue ",DoubleToString(maxValue)); if(minValue>maxValue) { Alert("最小值 > 最大值 !!!"); Print("最小值 > 最大值 !!!"); ExpertRemove(); } }