格兹尔算法循环分析实战
用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的复数矩阵运算,只需维护两个状态变量。
算法与MESA及FFT的对比
离散傅立叶变换能检测的频率范围受样本数N限制,可分辨频带间距为1/N,这意味着短窗口下频率分辨率很差。J.Ehlers提出的最大熵谱分析(MESA)缓解了此问题。但D.Meyers的研究指出,在信号含噪较多时,格兹尔算法反而可能获得优于MESA的光谱分辨率。金融市场时间序列天然带有噪声,这正是格兹尔的潜在优势。
- FFT:一次性算全部频率,适合离线全谱分析,计算重
- 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参数即控制此步骤。预处理顺序应为:先去趋势,再加窗,最后送入算法。
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。
- 下载ZIP,解压保留目录结构
- 检查include路径是否被IDE识别
- 先空载回测观察频谱输出,再接信号逻辑
- 警惕过度拟合:周期参数需跨品种验证
总结与进阶方向
格兹尔算法为MT5交易者提供了轻量周期检测工具。相较MESA,它在噪声环境有优势,但需严谨预处理。通过CGoertzelCycle,我们能够快速搭建滤波波形与自适应指标。后续可探索:结合多时间框架周期共识、将输出用于EA开平仓、或融合SSA等其它谱方法提升鲁棒性。原文代码见 https://www.mql5.com/zh/articles/975 附件。