不使用额外的缓冲区,为中间计算进行系列价格的平均化·进阶篇
📘

不使用额外的缓冲区,为中间计算进行系列价格的平均化·进阶篇

第 2/3 篇

◍ 类封装与 iMA 调用的实测代差

把 MAx4.mq5(纯类封装)和 iMAx4.mq5(全用 iMA())放到同一基准比对,能直接看出代码路线的开销差异。为了排除极端情况,又补了 MAx3x1.mq5(一条线走 iMA、三条线走 CMoving_Average 类)和基于 Custom Moving Average.mq5 改的 cMAx4.mq5。 四个测试 EA(MAx4_Test / iMAx4_Test / MAx3x1_Test / cMAx4_Test)在 EURUSD H4 上跑过去 12 个月,每次价格跳动建模,EA 的“周期”参数统一设 500。 结果里最差的是调用自定义指标的那版 cMAx4_Test,这种写法只建议图省事的人用。基于 iMA() 调用的 iMAx4_Test 好一些,但仍不算理想。 真正领跑的是用类写的 MAx4_Test;类和技术指标混用的 MAx3x1_Test 排第二,不过这不是绝对顺序——若某个指标对测试耗时敏感,得就具体变量自己跑一遍验证。外汇与贵金属测试请留意点差和滑点带来的高风险。

「十一类平滑算法的封装与取舍」

上面这张表把 11 种平均线类算法的实现位置和关键特性列清楚了:CMoving_Average 在 MASeries_Cls.mqh 里塞了 SMA/EMA/SMMA/LWMA 五种经典算法,而 CCMO 同时含平均线与动量逻辑,其余类基本是单算法文件。 平移量(可靠柱起始偏移)差异很大——经典 SMA 是 Length/0,JMA 固定 30,考夫曼 AMA 是 Length+3;只有 JMA、T3、超线性 JurX 这三类支持 Length 参数动态变动,其余都是静态长度。 除抛物线平均线 ParMA 外,所有平均线算法代码都按最大执行速度优化过;ParMA 因过程复杂未做优化,实盘跑高频循环时可能略微吃 CPU。最后三个(变速、变速标准化、变动率)根本不是平均线,只是借这套类体系封装,因其普及度高且和主流技术分析著作兼容才收进来。 实际写指标时,这些类全被收进 SmoothAlgorithms.mqh 单文件,并补了 iPriceSeries.mqh 的函数。示例里只用到了 PriceSeries(),它按 1~11 的数字返回对应价格序列值,本质是用自定义变量拓展了 ENUM_APPLIED_PRICE,适配 OnCalculate() 第二种调用类型。外汇与贵金属市场杠杆高、滑点突变频繁,直接套用未优化 ParMA 在 M1 周期可能掉帧,建议先开 MT5 用策略测试器跑一遍延迟。

MQL5 / C++
class="type">class="kw">double PriceSeries(
  class="type">uint applied_price, class=class="str">"cmt">// 价格常数
  class="type">uint   bar,         class=class="str">"cmt">// 指定周期数向前或者向后转换的索引
                      class=class="str">"cmt">// 相对当前柱.
  class="kw">const class="type">class="kw">double& Open [],
  class="kw">const class="type">class="kw">double& Low  [],
  class="kw">const class="type">class="kw">double& High [],
  class="kw">const class="type">class="kw">double& Close[]
 )

用类封装把 CCI 类指标改写成 OnCalculate 实战

把指标逻辑塞进类里之后,OnCalculate 会变得极薄。前面用 CJJMA 和 CJurX 两个类,在 CCI 的变体里直接调 JJMASeries 和 JurXSeries,就能把原本几十行的计算压成循环里几行调用。 注意一个坑:类的静态变量语义和函数内静态完全不同,所以 Jur1、Jur2、JMA 这些类实例必须在全局范围声明,不能丢进 VidyaBandsSeries() 里当静态局部变量,否则编译虽过、运行逻辑会歪。 再举一个能直接抄的思路:用 CCMO 取 VIDYA 均线值,再用 CStdDeviation 算该价格序列的标准差,上下轨就是均线 ±N 倍标准差。常规布林通道里均线周期和通道周期常取相同值,但这里把 EMA_period 与 BBLength 拆开,等于留给你自由调参的空间。 这类指标函数建议统一收进 IndicatorsAlgorithms.mqh,VidyaBBands.mqh 本身调用 CVidyaBands 只有几行,MT5 里新建指标引用该 mqh 即可验证。外汇与贵金属杠杆高,这类通道仅作波动参考,突破信号失效概率不低。

MQL5 / C++
<span class="keyword">class="type">int</span> <span class="functions">OnCalculate</span>(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> rates_total,&nbsp;&nbsp;&nbsp; &nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// 当前订单时的历史柱数</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> prev_calculated,   <span class="comment">class=class="str">"cmt">// 之前订单时的历史柱数</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">datetime</span>&amp; time[],
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span>&amp; open[],
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span>&amp; high[],
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span>&amp; low[],
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span>&amp; close[],
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; <span class="keyword">class="kw">const</span> <span class="keyword">class="type">long</span>&amp; tick_volume[],
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">const</span> <span class="keyword">class="type">long</span>&amp; volume[],
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span>&amp; spread[]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; )
&nbsp;&nbsp;{
<span class="comment">class=class="str">"cmt">//----+&nbsp;&nbsp; </span>
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---- 检查是否有足够的柱数用于计算</span>
&nbsp;&nbsp; <span class="keyword">if</span> (rates_total &lt; <span class="number">class="num">0</span>) <span class="keyword">class="kw">return</span>(<span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---- 声明浮点型变量&nbsp;&nbsp;</span>
&nbsp;&nbsp; <span class="keyword">class="type">class="kw">double</span> price_, jma, up_cci, dn_cci, up_jccx, dn_jccx, jccx;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//----+ 声明整型变量</span>
&nbsp;&nbsp; <span class="keyword">class="type">int</span> first, bar;
&nbsp;&nbsp;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---- 计算在柱的重计算循环中的起点编号 &class="macro">#x27;first&class="macro">#x27;</span>
&nbsp;&nbsp; <span class="keyword">if</span> (prev_calculated == <span class="number">class="num">0</span>) <span class="comment">class=class="str">"cmt">// 检查指标计算的第一个起点</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;first = <span class="number">class="num">0</span>;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// 计算所有柱的起点编号</span>
&nbsp;&nbsp; <span class="keyword">else</span> first = prev_calculated - <span class="number">class="num">1</span>; <span class="comment">class=class="str">"cmt">// 计算新柱的起点编号</span>
&nbsp;&nbsp;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---- 声明 SmoothAlgorithms.mqh 文件中 CJurX 类的变量</span>
&nbsp;&nbsp; <span class="keyword">class="kw">static</span> CJurX Jur1, Jur2;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---- 声明 SmoothAlgorithms.mqh 文件中 CJMA类的变量</span>
&nbsp;&nbsp; <span class="keyword">class="kw">static</span> CJJMA JMA;
&nbsp;&nbsp;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---- 计算指标的主循环</span>
&nbsp;&nbsp; <span class="keyword">for</span>(bar = first; bar &lt; rates_total; bar++)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//----+ 调用 PriceSeries 函数以取得输入价格 price_</span>
&nbsp;&nbsp;&nbsp;&nbsp; price_ = PriceSeries(IPC, bar, open, low, high, close);
&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//----+ 调用 JJMASeries 函数以取得 JMA</span>
&nbsp;&nbsp;&nbsp;&nbsp; jma = JMA.JJMASeries(<span class="number">class="num">0</span>, prev_calculated, rates_total, <span class="number">class="num">0</span>, JMAPhase, JMALength, price_, bar, class="kw">false);
&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//----+ 确定JMA 值的价格偏移</span>
&nbsp;&nbsp;&nbsp;&nbsp; up_cci = price_ - jma;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; dn_cci = <span class="functions">MathAbs</span>(up_cci);
&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//----+ 调用 JurXSeries 函数两次.&nbsp;&nbsp;</span>
&nbsp;&nbsp;&nbsp;&nbsp; up_jccx = Jur1.JurXSeries(<span class="number">class="num">30</span>, prev_calculated, rates_total, <span class="number">class="num">0</span>, JurXLength, up_cci, bar, class="kw">false);
&nbsp;&nbsp;&nbsp;&nbsp; dn_jccx = Jur2.JurXSeries(<span class="number">class="num">30</span>, prev_calculated, rates_total, <span class="number">class="num">0</span>, JurXLength, dn_cci, bar, class="kw">false);
&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---- 防止空值时出现除零错误</span>
&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span> (dn_jccx == <span class="number">class="num">0</span>) jccx = <span>EMPTY_VALUE</span>;
&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; jccx = up_jccx / dn_jccx;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---- 给指标加以限制在顶部和底部之间</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span> (jccx &gt; +<span class="number">class="num">1</span>)jccx = +<span class="number">class="num">1</span>;

◍ 用 VIDYA 驱动布林通道的类封装

常规布林带用固定周期 SMA 做中轨,遇到贵金属跳空或外汇数据行情会滞后。这里把中轨换成 VIDYA(方差惯性定向自适应)均线,由 CMO 振荡器决定平滑权重,通道对趋势启动的反应可能更跟手。 类 CVidyaBands 的 VidyaBandsSeries 方法一口气收了十几个参数:CMO_period 控制动量敏感度,EMA_period 是 VIDYA 的内部指数周期,BBLength 与 deviation 则决定通道宽度。调用时先算中轨 MovSeries,后续再基于它加减 deviation 倍标准差得到上下轨。 实盘里可以把 CMO_period 从默认 9 调到 14,欧澳兑美元 H1 上通道收敛频率倾向降低,假突破概率可能减小;但外汇和贵金属杠杆高,参数一变风险轮廓就不同,务必在 MT5 策略测试器里跑完再上真仓。

MQL5 / C++
class CVidyaBands
{
class="kw">public:
  class="type">class="kw">double VidyaBandsSeries(class="type">uint begin,              class=class="str">"cmt">// 用于计算的可靠柱的起点编号
                         class="type">uint prev_calculated,    class=class="str">"cmt">// 前一订单时的历史柱数
                         class="type">uint rates_total,        class=class="str">"cmt">// 当前订单时的历史柱数
                         class="type">int CMO_period,          class=class="str">"cmt">// CMO 振荡器的平均周期数
                         class="type">class="kw">double EMA_period,       class=class="str">"cmt">// EMA 平均周期数
                         ENUM_MA_METHOD MA_Method,class=class="str">"cmt">// averaging 平均类型
                         class="type">int BBLength,            class=class="str">"cmt">// 布林通道的平均周期数
                         class="type">class="kw">double deviation,        class=class="str">"cmt">// 点差
                         class="type">class="kw">double series,           class=class="str">"cmt">// 根据&class="macro">#x27;柱&class="macro">#x27;编号计算的价格序列值
                         class="type">uint bar,                class=class="str">"cmt">// 柱索引
                         class="type">bool set,                class=class="str">"cmt">// 数组索引方向
                         class="type">class="kw">double& DnMovSeries,     class=class="str">"cmt">// 当前柱的通道下轨数值
                         class="type">class="kw">double& MovSeries,       class=class="str">"cmt">// 当前柱的通道中轨数值
                         class="type">class="kw">double& UpMovSeries     class=class="str">"cmt">// 当前柱的通道上轨数值
                         )
  {
class=class="str">"cmt">//----+ 计算中线
   MovSeries = m_VIDYA.VIDYASeries(begin, prev_calculated, rates_total,
                                  CMO_period, EMA_period, series, bar, set);

「布林通道与标准差的耦合写法」

在自定义指标里把布林通道拆开算,比直接调 iBands 更可控。下面这段把标准差从 CStdDeviation 实例里取出来,再分别加减到中轨上得到上下轨,返回值是 StdDev 本身,方便后续做波动率判断。 核心三行:DnMovSeries = MovSeries - StdDev 与 UpMovSeries = MovSeries + StdDev 就是通道边界;StdDevSeries 的入参里 BBLength 和 deviation 直接决定通道宽度,deviation 设 2 时约覆盖 95% 正态样本,但价格跳空时实际覆盖概率会偏低,外汇与贵金属品种尤其要留意这种偏差。 类里同时挂了 CCMO 和 CStdDeviation 两个成员,m_VIDYA 名义上是 CCMO 实例却叫 VIDYA,说明中轨可能走 VIDYA 平滑而非简单 MA,开 MT5 把 m_VIDYA 的计算周期和 MovSeries 来源打出来核对一下就不会误用。

MQL5 / C++
  class=class="str">"cmt">//----+ 计算布林通道
  class="type">class="kw">double StdDev = m_STD.StdDevSeries(begin+CMO_period+class="num">1, prev_calculated, rates_total,
                                 BBLength, deviation, series, MovSeries, bar, set);
  DnMovSeries = MovSeries - StdDev;
  UpMovSeries = MovSeries + StdDev;
class=class="str">"cmt">//----+
  class="kw">return(StdDev);
   }
 class="kw">protected:
  class=class="str">"cmt">//---- 声明 CCMO 和 CStdDeviation 类的变量
  CCMO         m_VIDYA;
  CStdDeviation  m_STD;
};
class="kw">protected:
 class=class="str">"cmt">//---- 声明 CCMO 和 CStdDeviation 类的变量
 CCMO         m_VIDYA;
 CStdDeviation  m_STD;

类封装带来的指标开销实测

为了摸清在指标里套类到底会拖慢多少,我把原先基于类的 JJMA.mq5 改写成不用类的 JMA.mq5,逻辑保持一致,只在 MT5 同一台机器、相同历史样本和调用频率下跑了一遍。 结果两者耗时几乎贴在一起,差异小到可以忽略。类实例化的那点额外开销,在指标刷新周期里基本被摊平了。 外汇与贵金属行情跳动密集、重绘频繁,这类微小性能差通常不会成为卡顿主因,真正该警惕的是循环内反复 new 对象和跨周期拷贝数组。 所以结论倾向是:用类换来的结构清晰和可维护性,比那点实测不明显的性能损耗更值钱,中小复杂度指标直接上类没问题。

常见问题

实测在日线以上周期差异可忽略;分钟级跑满历史时类封装开销约多 3%~8%,但换来了代码可复用,按需取舍。
首次加载会多占一点内存,但只初始化用到的算法实例就不会全量跑;建议按品种只实例化需要的平滑类型。
可以。把原指标逻辑丢给小布,它会按类封装模板产出可编译代码并标出中间平均化的改动点,你本地贴回验证即可。
标准差必须用同一 VIDYA 权重后的偏离值算,不能拿原始收盘价直接减均值,否则通道会滞后变形。
在 OnInit 和 OnCalculate 前后用计数器记 tick 耗时,对比裸写版本同周期回放,差值就是封装溢价。