先进的自适应指标理论及在 MQL5 中的实施·进阶篇
📡

先进的自适应指标理论及在 MQL5 中的实施·进阶篇

(2/3)·复数相量和中值滤波如何把噪音周期变成可交易信号

实战向进阶 第 2/3 篇
多数交易者把指标周期当固定参数,行情一变就钝化。自适应思路是用希尔伯特变换实时测相角差,让周期随波动伸缩。先搞懂复数相量,才不会在代码里瞎调常数。

◍ 指标入口与数组初始化的坑

自定义指标跑起来第一道关就是 OnCalculate 的参数表,MT5 把每根 K 线的开高低收、时间、成交量、点差全以数组引用丢进来,rates_total 是已就绪的柱数,prev_calculated 是上一轮已算过的柱数,靠这两个值判断是冷启动还是增量刷新。 这段实现里先用 CopyTickVolume 抓最新 1 根柱的 tick 量,若取不到就直接 return(rates_total) 让系统下次再调,相当于跳过了这轮计算。随后把 high、low 设成倒序序列,并用 ArrayResize 按当前品种周期的总 Bars 数给 Smooth、Cycle、CyclePeriod 等一堆全局数组分配内存——注意这里没判断 Bars 是否变化,若品种切换或周期重载,旧数组长度可能不匹配。 冷启动或 tickCnt[0]==1 时会进入重算分支,nLimit 先取 rates_total-prev_calculated-1 作为循环起点,但马上被钳制到 rates_total-7 以内,说明算法至少需要前 7 根柱的历史才能稳定递推。 别把正态当圣经:这种 ArrayResize 放在 OnCalculate 里每次重算都调,虽能容错但会带来隐性开销;实盘贵金属或外汇短线高波动品种,建议在 OnInit 里按首屏 Bars 预分配一次,避免每 tick 反复申请内存。

MQL5 / C++
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],
                const class="type">class="kw">double &high[],
                const class="type">class="kw">double &low[],
                const class="type">class="kw">double &close[],
                const class="type">long &tick_volume[],
                const class="type">long &volume[],
                const class="type">int &spread[])
  {
class=class="str">"cmt">//---  
   class="type">long tickCnt[class="num">1];
   class="type">int i;
   class="type">int ticks=CopyTickVolume(Symbol(), class="num">0, class="num">0, class="num">1, tickCnt);
   if(ticks!=class="num">1) class="kw">return(rates_total);
   class="type">class="kw">double DC, MedianDelta;
   Comment(tickCnt[class="num">0]);
   if(prev_calculated==class="num">0 || tickCnt[class="num">0]==class="num">1)
     {
       class=class="str">"cmt">//--- 最近计数的柱将被重新计数
       class="type">int nLimit=rates_total-prev_calculated-class="num">1; class=class="str">"cmt">// 计算起点索引
       ArraySetAsSeries(high,true);
       ArraySetAsSeries(low,true);
       
       ArrayResize(Smooth,Bars(_Symbol,_Period));
       ArrayResize(Cycle,Bars(_Symbol,_Period));
       class=class="str">"cmt">//ArrayResize(Price,Bars(_Symbol,_Period));
       ArrayResize(CyclePeriod,Bars(_Symbol,_Period));
       ArrayResize(InstPeriod,Bars(_Symbol,_Period));
       ArrayResize(Q1,Bars(_Symbol,_Period));
       ArrayResize(I1,Bars(_Symbol,_Period));
       ArrayResize(DeltaPhase,Bars(_Symbol,_Period));
       
       if (nLimit>rates_total-class="num">7) class=class="str">"cmt">// 为最近的柱做调整
         nLimit=rates_total-class="num">7;  
       
       for(i=nLimit;i>=class="num">0 && !IsStopped();i--)  
       {
         Smooth[i] = (Price(i)+class="num">2*Price(i+class="num">1)+class="num">2*Price(i+class="num">2)+Price(i+class="num">3))/class="num">6.0;

         if (i<rates_total-class="num">7)
          {

「周期提取里的相位限幅与中值平滑」

这段逻辑紧接前面的平滑处理,核心是把价格二次差分后的循环分量 Cycle 转成可交易的周期读数。当处于初始化区间之外,Cycle 用带 InpAlpha 的带通加权递归,否则直接取 (Price(i)-2*Price(i+1)+Price(i+2))/4.0 作为裸差分,两种分支在 MT5 里切换时肉眼能看出前几根柱子数值跳变。 Q1 是 Cycle 跨 6 根柱子的加权组合再乘 (0.5+0.08*InstPeriod[i+1]),I1 简单取滞后 3 根的 Cycle。DeltaPhase 用反正切近似公式算相邻相位差,但代码把下限锁 0.1、上限锁 0.9——这意味着瞬时周期不会被极端噪声压到 7 以下或顶到 63 以上。 MedianDelta 取最近 5 根 DeltaPhase 的中值,若中值为 0 则 DC 硬性给 15.0,否则 DC = 6.28318/MedianDelta + 0.5。InstPeriod 再对 DC 做 0.33/0.67 的 EMA,CyclePeriod 用更慢的 0.15/0.85 平滑,Trigger 直接等于上一根 CyclePeriod。外汇与贵金属波动跳空多,这种限幅能避免假周期,但高频噪音下仍可能给出偏短周期,实盘需结合小布盯盘的 AIGC 滤波二次确认。 Median 函数把目标区间拷进临时数组并排序取中值,偶长度取中间两数平均。你可以把 m_len 从 5 改成 9 在 EURUSD 的 M15 上回看,DC 曲线会明显变钝,信号延迟约增加 2~3 根柱子。

MQL5 / C++
Cycle[i] = (class="num">1.0-class="num">0.5*InpAlpha) * (class="num">1.0-class="num">0.5*InpAlpha) * (Smooth[i]-class="num">2.0*Smooth[i+class="num">1]+Smooth[i+class="num">2])
      +class="num">2.0*(class="num">1.0-InpAlpha)*Cycle[i+class="num">1]-(class="num">1.0-InpAlpha)*(class="num">1.0-InpAlpha)*Cycle[i+class="num">2];
   } else
   {
      Cycle[i]=(Price(i)-class="num">2.0*Price(i+class="num">1)+Price(i+class="num">2))/class="num">4.0;
   }

   Q1[i] = (class="num">0.0962*Cycle[i]+class="num">0.5769*Cycle[i+class="num">2]-class="num">0.5769*Cycle[i+class="num">4]-class="num">0.0962*Cycle[i+class="num">6])*(class="num">0.5+class="num">0.08*InstPeriod[i+class="num">1]);
   I1[i] = Cycle[i+class="num">3];

   if (Q1[i]!=class="num">0.0 && Q1[i+class="num">1]!=class="num">0.0)
      DeltaPhase[i] = (I1[i]/Q1[i]-I1[i+class="num">1]/Q1[i+class="num">1])/(class="num">1.0+I1[i]*I1[i+class="num">1]/(Q1[i]*Q1[i+class="num">1]));
   if (DeltaPhase[i] < class="num">0.1)
      DeltaPhase[i] = class="num">0.1;
   if (DeltaPhase[i] > class="num">0.9)
      DeltaPhase[i] = class="num">0.9;

   MedianDelta = Median(DeltaPhase, i, class="num">5);

   if (MedianDelta == class="num">0.0)
      DC = class="num">15.0;
   else
      DC = (class="num">6.28318/MedianDelta) + class="num">0.5;

   InstPeriod[i] = class="num">0.33 * DC + class="num">0.67 * InstPeriod[i+class="num">1];
   CyclePeriod[i] = class="num">0.15 * InstPeriod[i] + class="num">0.85 * CyclePeriod[i+class="num">1];
   Trigger[i] = CyclePeriod[i+class="num">1];
   }
   }
class=class="str">"cmt">//--- 返回 prev_calculated 值用于下次调用
   class="kw">return(rates_total);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double Median(class="type">class="kw">double& arr[], class="type">int idx, class="type">int m_len)
{
   class="type">class="kw">double MedianArr[];
   class="type">int copied;
   class="type">class="kw">double result = class="num">0.0;

   ArraySetAsSeries(MedianArr, true);
   ArrayResize(MedianArr, m_len);

   copied = ArrayCopy(MedianArr, arr, class="num">0, idx, m_len);
   if (copied == m_len)
   {
      ArraySort(MedianArr);
      if (m_len %class="num">2 == class="num">0)
         result = (MedianArr[m_len/class="num">2] + MedianArr[(m_len/class="num">2)+class="num">1])/class="num">2.0;
      else
         result = MedianArr[m_len / class="num">2];

   }

中值复制失败的兜底处理

在 MQL5 里做中值计算时,若数组元素拷贝数目与预期不符,必须显式报错并立即返回结果,否则后续统计会基于残缺数据跑出误导性信号。 上面这段兜底逻辑用 Print 输出 __FILE__ 与 __FUNCTION__ 定位出错位置,并提示“中值错误 - 复制元素数目错误.”,随后直接 return result 中断流程。 实盘验证时可在 MT5 策略测试器故意传入长度不匹配的数组,观察日志是否精确打印文件名与函数名,从而确认拷贝分支的健壮性。外汇与贵金属波动剧烈,这类底层数组错误若不被捕获,可能让 EA 在极端行情下发出错误订单,属高风险环节。

MQL5 / C++
  else Print(__FILE__+__FUNCTION__+"中值错误 - 复制元素数目错误.");
  class="kw">return result;
}

◍ 循环周期线怎么在MT5里跑起来

循环周期指标本质是一个高通滤波器,只保留价格序列里的周期模式分量,两柱、三柱这类短周期组件再经有限脉冲响应低通滤波器平滑提取。它和同系列其他指标外观接近,但底层算法差异很大,属于价格行为里剥离趋势噪声看摆动的工具。 原始算法思路很直白:周期线上穿触发线时倾向买入,下穿时倾向卖出。外汇与贵金属波动受杠杆和消息面影响,这类信号可能出现假突破,实盘前务必在MT5策略测试器里验证。 下面这段MQL5把周期线和红色触发线画在独立窗口,默认alpha=0.07控制平滑强度,调大则线更灵敏、噪点也多。 //+------------------------------------------------------------------+

//CyberCycle.mq5
//Copyright 2011, Investeo.pl
//http://Investeo.pl

//+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http://Investeo.pl" #property version "1.00" #property indicator_separate_window #property description "CyberCycle 指标 - 由 John F. Ehlers 在" #property description " \"股票和期货的控制分析\"一书中介绍" #property description "本指标可以免费下载." #property indicator_buffers 2 #property indicator_plots 2 #property indicator_width1 1 #property indicator_width2 1 #property indicator_type1 DRAW_LINE #property indicator_type2 DRAW_LINE #property indicator_color1 Green #property indicator_color2 Red #property indicator_label1 "Cycle" #property indicator_label2 "Trigger Line" #define Price(i) ((high[i]+low[i])/2.0) double Smooth[]; double Cycle[]; double Trigger[]; input double InpAlpha=0.07; // alpha //+------------------------------------------------------------------+

//自定义指标初始化函数

//+------------------------------------------------------------------+ int OnInit() { //--- 指标缓冲区映射 ArraySetAsSeries(Cycle,true); ArraySetAsSeries(Trigger,true); ArraySetAsSeries(Smooth,true); SetIndexBuffer(0,Cycle,INDICATOR_DATA); SetIndexBuffer(1,Trigger,INDICATOR_DATA); PlotIndexSetDouble(0,PLOT_EMPTY_VALUE,0.0); PlotIndexSetDouble(1,PLOT_EMPTY_VALUE,0.0); return(0); } //+------------------------------------------------------------------+

//自定义指标迭代函数

//+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, 逐行拆解:

  • #property indicator_separate_window 让指标显示在副图,不和K线挤在一起。
  • #define Price(i) ((high[i]+low[i])/2.0) 取每根K线高低中点作为滤波输入,比收盘价更抗影线干扰。
  • input double InpAlpha=0.07 是平滑系数,0.07属于慢速档,欧元兑美元H1上约过滤掉小于3根K线的杂波。
  • OnInit 里三个 ArraySetAsSeries(...,true) 把数组倒序,使索引0对应最新柱,方便实时计算。
  • SetIndexBuffer 把Cycle和Trigger绑到绘图缓冲区,绿色周期线、红色触发线由此而来。

建议把alpha从0.07改成0.1~0.12,在XAUUSD的M15上对比穿越信号次数,感受灵敏度与噪点的权衡。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     CyberCycle.mq5 |
class=class="str">"cmt">//|          Copyright class="num">2011, Investeo.pl |
class=class="str">"cmt">//|             http://Investeo.pl |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2011, Investeo.pl"
class="macro">#class="kw">property link      "http:class=class="str">"cmt">//Investeo.pl"
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_separate_window
class="macro">#class="kw">property description "CyberCycle 指标 - 由 John F. Ehlers 在"
class="macro">#class="kw">property description " \"股票和期货的控制分析\"一书中介绍"
class="macro">#class="kw">property description "本指标可以免费下载."
class="macro">#class="kw">property indicator_buffers class="num">2
class="macro">#class="kw">property indicator_plots class="num">2
class="macro">#class="kw">property indicator_width1 class="num">1
class="macro">#class="kw">property indicator_width2 class="num">1
class="macro">#class="kw">property indicator_type1   DRAW_LINE
class="macro">#class="kw">property indicator_type2   DRAW_LINE
class="macro">#class="kw">property indicator_color1  Green
class="macro">#class="kw">property indicator_color2  Red
class="macro">#class="kw">property indicator_label1  "Cycle"
class="macro">#class="kw">property indicator_label2  "Trigger Line"
class="macro">#define Price(i) ((high[i]+low[i])/class="num">2.0)
class="type">class="kw">double Smooth[];
class="type">class="kw">double Cycle[];
class="type">class="kw">double Trigger[];
input class="type">class="kw">double InpAlpha=class="num">0.07; class=class="str">"cmt">// alpha
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标初始化函数                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 指标缓冲区映射 
   ArraySetAsSeries(Cycle,true);
   ArraySetAsSeries(Trigger,true);
   ArraySetAsSeries(Smooth,true);
   SetIndexBuffer(class="num">0,Cycle,INDICATOR_DATA);
   SetIndexBuffer(class="num">1,Trigger,INDICATOR_DATA);
   PlotIndexSetDouble(class="num">0,PLOT_EMPTY_VALUE,class="num">0.0);
   PlotIndexSetDouble(class="num">1,PLOT_EMPTY_VALUE,class="num">0.0);
   class="kw">return(class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标迭代函数                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,

「用逐笔成交量驱动重算的平滑周期滤波」

这段 OnCalculate 的核心不是算指标,而是先拿 CopyTickVolume 取最新 1 根 K 线的逐笔成交量 tickCnt[0],若不等于 1 就直接 return(rates_total) 跳过,等于 1 才认为新 tick 到达、需要重算最近一根柱。 重算起点 nLimit = rates_total - prev_calculated - 1,若 nLimit 超过 rates_total-4 则钳到 rates_total-4,保证 Smooth/Cycle 数组至少有 4 根前值的缓冲区。 Smooth[i] 用 (Price(i)+2*Price(i+1)+2*Price(i+2)+Price(i+3))/6.0 做对称加权,等价于 3 点三角平滑后再平滑一次;Cycle 对 i<rates_total-5 走 (1-0.5α)^2*(Δ²Smooth)+2(1-α)Cycle[i+1]-(1-α)^2*Cycle[i+2] 的带通递归,对最新 5 根则用 (Price(i)-2Price(i+1)+Price(i+2))/4.0 做冷启动近似。 Trigger[i] 直接取 Cycle[i+1],把上一根的周期分量当触发线。开 MT5 把 InpAlpha 从 0.07 调到 0.2,Cycle 振幅会明显变窄,外汇与贵金属波动下这种递归对滑点敏感,属高风险验证。

MQL5 / C++
const class="type">class="kw">datetime &time[],
const class="type">class="kw">double &open[],
const class="type">class="kw">double &high[],
const class="type">class="kw">double &low[],
const class="type">class="kw">double &close[],
const class="type">long &tick_volume[],
const class="type">long &volume[],
const class="type">int &spread[])
{
class=class="str">"cmt">//---  
  class="type">long tickCnt[class="num">1];
  class="type">int i;
  class="type">int ticks=CopyTickVolume(Symbol(), class="num">0, class="num">0, class="num">1, tickCnt);
  if(ticks!=class="num">1) class="kw">return(rates_total);
  Comment(tickCnt[class="num">0]);
  if(prev_calculated==class="num">0 || tickCnt[class="num">0]==class="num">1)
   {
     class=class="str">"cmt">//--- 最近计数的柱将被重新计数
     class="type">int nLimit=rates_total-prev_calculated-class="num">1; class=class="str">"cmt">// 计算起点索引
     ArraySetAsSeries(high,true);
     ArraySetAsSeries(low,true);
     ArrayResize(Smooth,Bars(_Symbol,_Period));
     ArrayResize(Cycle,Bars(_Symbol,_Period));
     
     if(nLimit>rates_total-class="num">4) class=class="str">"cmt">// 为最新的柱做调整
       nLimit=rates_total-class="num">4;
     for(i=nLimit;i>=class="num">0 && !IsStopped();i--)
      {
       Smooth[i]=(Price(i)+class="num">2*Price(i+class="num">1)+class="num">2*Price(i+class="num">2)+Price(i+class="num">3))/class="num">6.0;
       if(i<rates_total-class="num">5)
         {
          Cycle[i]=(class="num">1.0-class="num">0.5*InpAlpha)*(class="num">1.0-class="num">0.5*InpAlpha)*(Smooth[i]-class="num">2.0*Smooth[i+class="num">1]+Smooth[i+class="num">2])
                 +class="num">2.0*(class="num">1.0-InpAlpha)*Cycle[i+class="num">1]-(class="num">1.0-InpAlpha)*(class="num">1.0-InpAlpha)*Cycle[i+class="num">2];
         }
       else
         {
          Cycle[i]=(Price(i)-class="num">2.0*Price(i+class="num">1)+Price(i+class="num">2))/class="num">4.0;
         }
       class=class="str">"cmt">//Print(__FILE__+__FUNCTION__+" 接收数值: ",rCnt);
       Trigger[i]=Cycle[i+class="num">1];
      }
   }
class=class="str">"cmt">//--- 返回 prev_calculated 值用于下次调用
  class="kw">return(rates_total);
}

把静态周期换成动态读数

让指标具备自适应的核心,是用动态循环周期替代写死的参数。做法很直接:先挂上 CyclePeriod 自定义指标拿句柄,再在 OnCalculate() 里实时读它的缓冲,把当前主导周期喂给后续计算。 Ehlers 在自适应数码周期里,把主导循环周期当作指数平滑系数 alpha1 的分母项,关系是 alpha1 = 2/(周期+1)。这意味着行情周期缩短时平滑更灵敏,拉长时更迟钝,比固定长度均线更跟得上节奏。 实测对照里,自适应版相对非自适应版,买卖信号往往早发出一根柱。外汇与贵金属波动跳变频繁,这类早一柱的提示可能提高入场效率,但也放大假突破概率,属高风险用法,需自行在 MT5 回测验证。 下面这段是从初始化到取数的关键片段,直接拷进 MQ5 就能跑通句柄与周期读取:

MQL5 / C++
hCyclePeriod=iCustom(NULL,class="num">0,"CyclePeriod",InpAlpha);
   if(hCyclePeriod==INVALID_HANDLE)
     {
       Print("CyclePeriod 指标不可用!");
       class="kw">return(-class="num">1);
     }
class="type">int copied=CopyBuffer(hCyclePeriod,class="num">0,i,class="num">1,CyclePeriod);
     if(copied<=class="num">0)
       {
         Print("失败: 无法从 CyclePeriod 指标中取得数据.");
         class="kw">return -class="num">1;
       }
alpha1 = class="num">2.0/(CyclePeriod[class="num">0]+class="num">1.0);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  AdaptiveCyberCycle.mq5 |
class=class="str">"cmt">//|                                          Copyright class="num">2011, Investeo.pl |
class=class="str">"cmt">//|                                                                http://Investeo.pl |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2011, Investeo.pl"
class="macro">#class="kw">property link      "http:class=class="str">"cmt">//Investeo.pl"
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property indicator_separate_window
class="macro">#class="kw">property description "自适应 CyberCycle 指标 - 由 John F. Ehlers 在"
class="macro">#class="kw">property description " \"股票和期货的控制分析\"一书中介绍"
class="macro">#class="kw">property description "本指标可以免费下载."
class="macro">#class="kw">property indicator_buffers class="num">2
class="macro">#class="kw">property indicator_plots class="num">2
class="macro">#class="kw">property indicator_width1 class="num">1
class="macro">#class="kw">property indicator_width2 class="num">1
class="macro">#class="kw">property indicator_type1   DRAW_LINE
class="macro">#class="kw">property indicator_type2   DRAW_LINE
class="macro">#class="kw">property indicator_color1  Green
class="macro">#class="kw">property indicator_color2  Red
class="macro">#class="kw">property indicator_label1  "Cycle"
class="macro">#class="kw">property indicator_label2  "Trigger Line"
class="macro">#define Price(i) ((high[i]+low[i])/class="num">2.0)
class="type">class="kw">double Smooth[];
class="type">class="kw">double Cycle[];
class="type">class="kw">double Trigger[];
class="type">int hCyclePeriod;
input class="type">class="kw">double InpAlpha=class="num">0.07; class=class="str">"cmt">// 循环周期的 alpha
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 自定义指标初始化函数                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//--- 指标缓冲区映射 
   ArraySetAsSeries(Cycle,true);
   ArraySetAsSeries(Trigger,true);
   ArraySetAsSeries(Smooth,true);
   SetIndexBuffer(class="num">0,Cycle,INDICATOR_DATA);
   SetIndexBuffer(class="num">1,Trigger,INDICATOR_DATA);
把周期诊断交给小布盯盘
这些相角差和中值平滑的诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时自适应周期带,你只管判断边界反应。

常见问题

按 Ehlers 截断法,正交用当前柱方程,同相延迟三根柱价格,两者合成复数解析信号再做相角差分。
对应 6 到 63 根柱的周期窗口,过滤掉过长过短的无意义波动,且结果取非负避免反向折叠。
对尖刺型噪音中值五个样本取中位更能保边,SMA 会拉平真实跳变,自适应指标里常用前者。
可以,内置 AIGC 读取品种页就会输出基于相量差分的周期带,不用自己写希尔伯特变换函数。
前者只测当前主导周期长度,后者用测得周期动态收缩移动均线窗口,响应更快但需更多平滑。