格兹尔算法循环分析实战
📊

格兹尔算法循环分析实战

用MQL5提取金融时间序列主导周期

技术分析基础 难度 · 进阶 2026-03-13 7 分钟阅读
#格兹尔算法#循环分析#MQL5#周期检测

什么是格兹尔算法

格兹尔算法(Goertzel algorithm)是一种数字信号处理技术,以高效检测特定频率分量而著称。它由Gerald Goertzel于1958年提出,原本用于计算离散傅立叶变换(DFT)中的个别项。与快速傅立叶变换(FFT)不同,格兹尔算法在只需关注少数频率分量时计算量更小,非常适合实时性要求高、计算资源受限的场景。在金融领域,价格序列往往混杂噪声,且我们常只关心几个主导周期,因此该算法具备实用价值。

从数学上看,格兹尔算法通过迭代递推计算特定频率仓k的实部与虚部。其核心公式为:X[k]=x[n]+coeff*X[k-1]-X[k-2],其中coeff=2*cos(2πk/N)。迭代完成后,实部约等于X[k-1]-0.5*coeff*X[k-2],虚部约等于X[k-2]*sin(2πk/N)。这种结构避免了完整FFT的复数矩阵运算,只需维护两个状态变量。

✦ 适用边界
当您只想监控5至72根K线范围内的周期,而非全频谱时,格兹尔比FFT更轻量。若需连续频谱或极高分辨率,应考虑MESA等技术。

算法与MESA及FFT的对比

离散傅立叶变换能检测的频率范围受样本数N限制,可分辨频带间距为1/N,这意味着短窗口下频率分辨率很差。J.Ehlers提出的最大熵谱分析(MESA)缓解了此问题。但D.Meyers的研究指出,在信号含噪较多时,格兹尔算法反而可能获得优于MESA的光谱分辨率。金融市场时间序列天然带有噪声,这正是格兹尔的潜在优势。

CGoertzel类的基础实现

原文提供的CGoertzel类封装了核心变换。它通过SetMinMaxPeriodLength设定感兴趣的周期带(注意最小周期不能低于2,且必须小于最大周期)。Dft方法支持两种输出:分离的实数/虚数数组,或complex复数数组。内部要求输入数据长度至少3倍于最大周期,以保证移动窗口有效。

class CGoertzel
  {
private:
   uint              m_minPeriod;
   uint              m_maxPeriod;
public:
   CGoertzel(void) { m_minPeriod=m_maxPeriod=0; }
   ~CGoertzel(void) {}
   uint GetMinPeriodLength(void) { return m_minPeriod;}
   uint GetMaxPerodLength(void) { return m_maxPeriod;}
   bool SetMinMaxPeriodLength(const uint min_wavePeriod, const uint max_wavePeriod);
   bool Dft(const double &in_data[],complex &out[]);
   bool Dft(const double &in_data[], double &out_real[], double &out_imaginary[]);
  };
bool CGoertzel::SetMinMaxPeriodLength(const uint min_wavePeriod,const uint max_wavePeriod)
  {
   if(min_wavePeriod<2 || min_wavePeriod>=max_wavePeriod)
     {
      Print("Critical error min_wavePeriod cannot be less than max_wavePeriod or less than 2");
      return false;
     }
   m_minPeriod=min_wavePeriod;
   m_maxPeriod=max_wavePeriod;
   return true;
  }

Dft实现中,对每一个周期i(从最小到最大),计算对应频率freq=1/i,系数coeff=2*cos(2π*freq),然后从尾部向头部迭代样本更新状态变量v0、v1、v2,最终导出实部与虚部。这段代码直接体现了前文公式,是理解整个库的关键。

bool CGoertzel::Dft(const double &in_data[],complex &out[])
  {
   uint minsize=(3*m_maxPeriod);
   uint fullsize=in_data.Size();
   if(fullsize<minsize) { Print("Sample size too small"); return false; }
   if(m_minPeriod>=m_maxPeriod || m_minPeriod<2) { Print("Invalid parameters"); return false; }
   if(out.Size()!=m_maxPeriod) ArrayResize(out,m_maxPeriod);
   double v0,v1,v2,freq,coeff,real,imag;
   for(uint i=0; i<m_maxPeriod; i++)
     {
      if(i<m_minPeriod) { out[i].imag=out[i].real=0.0; continue; }
      v0=v1=v2=0.0;
      freq=MathPow(i,-1);
      coeff=2.0*MathCos(2.0*M_PI*freq);
      for(uint k=minsize-1; k>0; k--)
        {
         v0=coeff*v1-v2+in_data[k];
         v2=v1; v1=v0;
        }
      real=v1-v2*0.5*coeff;
      imag=v2*MathSin(2*M_PI*freq);
      out[i].real=real; out[i].imag=imag;
     }
   return true;
  }

数据预处理:去趋势与端点平坦化

价格序列是非周期且带趋势的,直接做DFT会产生严重频谱泄漏。因此预处理必不可少。第一步是去趋势,原文采用最小二乘拟合:计算斜率m_slope=(Σx*y)/(Σx*x),然后从原数据减去线性趋势。注意过度去趋势会扭曲周期结构,仅在明显趋势时启用。

void CGoertzelCycle::detrend(const double &in_data[], double &out_data[])
  {
   uint i ;
   double xmean, ymean, x, y, xy, xx ;
   xmean=ymean=x=y=xx=xy=0.0;
   xmean=0.5*double(in_data.Size()-1);
   if(out_data.Size()!=in_data.Size()) ArrayResize(out_data,in_data.Size());
   for(i=0; i<out_data.Size(); i++)
     {
      x = double(i) - xmean ;
      y = in_data[i] - ymean ;
      xx += x * x ; xy += x * y ;
     }
   m_slope=xy/xx; m_pivot=xmean;
   for(i=0; i<out_data.Size(); i++)
      out_data[i]=in_data[i]-(double(i)-xmean)*m_slope;
  }

第二步是端点平坦化(窗口函数),公式flat(i)=原序列 - [a + (b-a)*i/(N-1)],其中a、b为首尾值。这能把序列两端逐渐收拢到零,抑制泄漏。CGoertzelCycle的apply_window参数即控制此步骤。预处理顺序应为:先去趋势,再加窗,最后送入算法。

⚠ 常见坑
若最小周期设为2以下,或最大周期小于等于最小周期,类会返回false并打印错误。另外样本数不足3倍最大周期也会导致Dft失败,实盘指标需预留足够历史 bars。

CGoertzelCycle类与核心方法

CGoertzelCycle在CGoertzel基础上整合了去趋势、加窗、振幅计算与主导周期提取。其参数化构造函数接收detrend、squared_amp、apply_window、min_period、max_period。squared_amp为true时振幅取平方值,false时取根号。该类提供GetSpectrum获取振幅谱,GetDominantCycles获取按强度或振幅排序的周期,CalculateDominantCycles与CalculateWave供指标逐根K线调用。

class CGoertzelCycle
  {
private:
   CGoertzel*m_ga;
   double m_pivot,m_slope;
   bool m_detrend,m_squaredamp,m_flatten;
   uint m_cycles,m_maxper,m_minper;
   // ... arrays and helpers
public :
   CGoertzelCycle(void);
   CGoertzelCycle(bool detrend,bool squared_amp,bool apply_window, uint min_period,uint max_period);
   bool GetSpectrum(const double &in_data[],double &out_amplitude[]);
   uint GetDominantCycles(bool use_cycle_strength,const double &in_data[],double &out_cycles[]);
   void CalculateDominantCycles(uint prev,uint total,bool use_cycle_strength,const double &in_data[], double &out_signal[]);
   void CalculateWave(uint prev,uint total,uint max_cycles,bool use_cycle_strength,const double &in_data[], double &out_signal[]);
  };

GetDominantCycles的use_cycle_strength参数决定排序依据:false用纯振幅,true用循环强度公式(原文未给全,但类内已封装)。CalculateWave则根据前若干主导周期重构滤波波形,相当于把噪声周期剥离后的价格曲线,可用于反转或突破信号。

实战指标一:NCycleGoertzelDft

该指标部分实现Meyers的白皮书策略:用移动窗口提取前10个主导周期,重构波形(Wave缓冲区),并以相对于近期峰谷的百分比阈值pntup/pntdn触发多空箭头。输入参数包含Detrend、EndFlatten、Minper=5、Maxper=72、MaxCycles=10。OnInit中创建CGoertzelCycle实例,OnCalculate调用CalculateWave填充Wave。

#include<GoertzelCycle.mqh>
input uint Maxper=72; input uint Minper=5; input uint MaxCycles=10;
double Wave[],Peak[],Trough[],Long[],Short[];
CGoertzelCycle *Gc;
int OnInit(){
   SetIndexBuffer(0,Wave,INDICATOR_DATA);
   Gc=new CGoertzelCycle(Detrend,SquaredAmp,EndFlatten,Minper,Maxper);
   return(INIT_SUCCEEDED);
}
int OnCalculate(const int rates_total,const int prev_calculated,const int begin,const double &price[]){
   Gc.CalculateWave(prev_calculated,rates_total,MaxCycles,UseCycleStrength,price,Wave);
   return(rates_total);
}

策略逻辑上,波形上穿近期谷底一定百分比做多,下穿近期峰值一定百分比做空。因原文截断,具体交易规则见Meyers白皮书。但指标已演示如何将格兹尔输出转化为可视信号。

实战指标二:AdaptiveGARSI自适应RSI

第二个示例将格兹尔用于使RSI自适应:先用CalculateDominantCycles得到当前主周期,再以此周期长度作为RSI回看窗口动态计算RSI。这类似Ehlers的自适应思路,但用格兹尔测周期。OnCalculate中先算DomPeriodBuffer,再循环用该周期算涨跌和。

#include<GoertzelCycle.mqh>
input uint Maxper=72; input uint Minper=5;
double RSiBuffer[], DomPeriodBuffer[];
CGoertzelCycle *Gc;
int OnInit(){ 
   SetIndexBuffer(0,RSiBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,DomPeriodBuffer,INDICATOR_CALCULATIONS);
   Gc=new CGoertzelCycle(Detrend,SquaredAmp,EndFlatten,Minper,Maxper);
   return(INIT_SUCCEEDED);
}
int OnCalculate(... const double &close[] ...){
   Gc.CalculateDominantCycles(prev_calculated,rates_total,UseCycleStrength,close,DomPeriodBuffer);
   for(int i=lim;i<rates_total;i++){
      double p=DomPeriodBuffer[i]; /* 用p作为RSI周期计算 */
   }
   return(rates_total);
}

部署步骤与注意事项

将Goertzel.mqh与GoertzelCycle.mqh放入MQL5/Include,指标mq5放入MQL5/Indicators,编译后在MT5导航器加载。建议先在EURUSD小时图测试参数,观察Dominant Cycle是否稳定在20-60区间。若周期跳变剧烈,可放宽Minper/Maxper或开启UseCycleStrength。

⚠ 实盘弱点
金融市场主周期常突变,格兹尔一次只解析有限频带,难实时锁定周期结束点。务必配合风控与人工研判,不宜单独依赖。

总结与进阶方向

格兹尔算法为MT5交易者提供了轻量周期检测工具。相较MESA,它在噪声环境有优势,但需严谨预处理。通过CGoertzelCycle,我们能够快速搭建滤波波形与自适应指标。后续可探索:结合多时间框架周期共识、将输出用于EA开平仓、或融合SSA等其它谱方法提升鲁棒性。原文代码见 https://www.mql5.com/zh/articles/975 附件。

常见问题

CGoertzel::SetMinMaxPeriodLength规定min_wavePeriod不能小于2,且必须小于max_wavePeriod,否则返回false并打印错误,实际设参应>=2。
根据D.Meyers研究,在含噪信号中格兹尔光谱分辨率可能优于MESA;MT5实现通过CGoertzelCycle类可方便开启去趋势与加窗以应对噪声。
价格有趋势且非周期,直接变换会频谱泄漏。类内detrend用最小二乘去线性趋势,apply_window做端点平坦化,顺序为先去趋势后加窗。
构造CGoertzelCycle实例,在OnCalculate中用CalculateDominantCycles(prev,total,use_strength,close,buffer)逐根K线填充周期缓冲区,再取buffer值用做动态参数。
Dft要求数据长度>=3*max_period。指标里需预留至少最大周期三倍bars,如Maxper=72则最少216根历史,否则返回false。