使用格兹尔算法的循环分析·进阶篇
「进 Goertzel 前的去趋势与窗口化」
价格序列不能直接塞进 Goertzel 做频域变换。该算法本质是 DFT 的子集,非周期数据尤其要先做预处理,否则频谱泄漏的概率会明显抬高。文献里常见做法是端点平坦化:用序列首尾值 a、b 构造窗函数把末端收细,公式形式为 flat(i) = 原序列(i) - 线性插值(a,b),这一步放在窗口化之前。 比窗口化更前置的是去趋势和剔异常值。若不对原始报价做去趋势,趋势成分会失真并直接带进频域表达里。去趋势方法很多,本文用最小二乘直线拟合,只在对样本有必要才做,避免过度处理引入噪声。 下面这段是 MT5 里可直接抄用的去趋势实现。它先算 x 均值(样本中点索引),再遍历求斜率 m_slope,最后把每条数据减去「(i-中点)*斜率」得到去趋势后序列。开 MT5 新建类方法粘进去,传报价数组就能跑。 别把正态当圣经:去趋势只是预处理的一环,窗函数选型和异常值处理没做好的话,后面周期检测依然可能偏。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| helper method for detrending data | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGoertzelCycle::detrend(const class="type">class="kw">double &in_data[], class="type">class="kw">double &out_data[]) { class="type">uint i ; class="type">class="kw">double xmean, ymean, x, y, xy, xx ; xmean=ymean=x=y=xx=xy=class="num">0.0; xmean=class="num">0.5*class="type">class="kw">double(in_data.Size()-class="num">1); if(out_data.Size()!=in_data.Size()) ArrayResize(out_data,in_data.Size()); for(i=class="num">0; i<out_data.Size(); i++) { x = class="type">class="kw">double(i) - xmean ; y = in_data[i] - ymean ; xx += x * x ; xy += x * y ; } m_slope=xy/xx; m_pivot=xmean; for(i=class="num">0; i<out_data.Size(); i++) out_data[i]=in_data[i]-(class="type">class="kw">double(i)-xmean)*m_slope; }
Goertzel 周期类的接口与参数边界
做 MT5 自定义指标时,若想从报价里扒出隐藏周期,CGoertzelCycle 是直接可用的封装类。它用 Goertzel 算法做离散频谱估计,头文件 GoertzelCycle.mqh 里包含两个构造函数:无参版走默认配置,参数化版允许你显式设定去趋势、振幅呈现方式、窗函数与周期扫描区间。 参数化构造有 5 个输入:detrend 控制是否剔除线性趋势;apply_window 决定是否加窗以减少频谱泄漏;min_period 是算法解析的最短周期,硬性下限为 2,设更低会直接失败;max_period 是最长周期,且必须严格大于 min_period;squared_amp 为 false 时振幅按平方根输出,否则输出平方幅值。 对外暴露 6 个方法,核心是两个重载的 GetSpectrum() 与 GetDominantCycle()。GetSpectrum() 第一个重载吃原始序列与输出振幅数组,配置依赖构造时设定;第二个重载把构造参数也当入参,两者成功返 true、失败返 false,错误细节进终端日志。GetDominantCycle() 类似,但多一个 use_cycle_stngth 布尔量:false 取振幅最大频率,true 则按循环强度公式排定主周期,结果降序写入输出数组末尾。 给指标用的 CalculateDominantCycles() 与 CalculateWave() 更贴近实时刷图。前者需传 prev(已算柱数)、total(图表总柱数)、in_data(报价)、out_signal(指标缓冲);后者靠 max_cycles 限制重构滤波值所用主频分量数。外汇与贵金属波动受事件驱动,周期结构可能突变,这类工具仅提供概率倾向,实盘需自担高风险。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| CGoertzelCycle class for cycle using the Goertzel Algorithm |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class</span> CGoertzelCycle { <span class="keyword">class="kw">private</span>: CGoertzel*m_ga; <span class="keyword">class="type">class="kw">double</span> m_pivot; <span class="keyword">class="type">class="kw">double</span> m_slope; <span class="keyword">class="type">bool</span> m_detrend; <span class="keyword">class="type">bool</span> m_squaredamp; <span class="keyword">class="type">bool</span> m_flatten; <span class="keyword">class="type">uint</span> m_cycles; <span class="keyword">class="type">uint</span> m_maxper,m_minper; <span class="keyword">class="type">class="kw">double</span> m_amplitude[]; <span class="keyword">class="type">class="kw">double</span> m_peaks[]; <span class="keyword">class="type">class="kw">double</span> m_cycle[]; <span class="keyword">class="type">class="kw">double</span> m_phase[]; <span class="keyword">class="type">uint</span> m_peaks_index[]; <span class="keyword">class="type">class="kw">double</span> m_detrended[]; <span class="keyword">class="type">class="kw">double</span> m_flattened[]; <span class="keyword">class="type">class="kw">double</span> m_raw[]; <span class="keyword">class="type">void</span> detrend(<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &in_data[], <span class="keyword">class="type">class="kw">double</span> &out_data[]); <span class="keyword">class="type">class="kw">double</span> wavepoint(<span class="keyword">class="type">bool</span> use_cycle_strength,<span class="keyword">class="type">uint</span> max_cycles); <span class="keyword">class="type">bool</span> spectrum(<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &in_data[],<span class="keyword">class="type">int</span> shift=-<span class="number">class="num">1</span>); <span class="keyword">class="type">uint</span> cyclepeaks(<span class="keyword">class="type">bool</span> use_cycle_strength); <span class="keyword">class="kw">public</span> : CGoertzelCycle(<span class="keyword">class="type">void</span>); CGoertzelCycle(<span class="keyword">class="type">bool</span> detrend,<span class="keyword">class="type">bool</span> squared_amp,<span class="keyword">class="type">bool</span> apply_window, <span class="keyword">class="type">uint</span> min_period,<span class="keyword">class="type">uint</span> max_period); ~CGoertzelCycle(<span class="keyword">class="type">void</span>); <span class="keyword">class="type">bool</span> GetSpectrum(<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &in_data[],<span class="keyword">class="type">class="kw">double</span> &out_amplitude[]); <span class="keyword">class="type">bool</span> GetSpectrum(<span class="keyword">class="type">bool</span> detrend,<span class="keyword">class="type">bool</span> squared_amp,<span class="keyword">class="type">bool</span> apply_window, <span class="keyword">class="type">uint</span> min_period,<span class="keyword">class="type">uint</span> max_period,<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &in_data[],<span class="keyword">class="type">class="kw">double</span> &out_amplitude[]);
◍ Goertzel 周期类的构造与频谱取数
CGoertzelCycle 这个类把 Goertzel 算法包了一层,专门在 MT5 里做主导周期提取。它有两个构造路径:默认构造只把 pivot、slope 清零,周期上下限和数组都置空;带参构造则直接吃进 detrend、squared_amp、apply_window 以及 min_period / max_period,并在 max_period 非零时把振幅、相位、周期、峰值等五个动态数组一次性 ArrayResize 到最大周期长度。 带参构造里那句 if(m_maxper) 很关键——若你传了 max_period=0,后面五个 ArrayResize 全部跳过,GetSpectrum 调 spectrum() 时大概率因数组未初始化直接返 false。实盘接 EURUSD 日线前,先确认 max_period 至少大于你关心的波段长度,比如设 120 就覆盖约半年日线周期。 取数接口 GetSpectrum 很薄:内部跑完 spectrum(in_data) 后,用 ArrayCopy 把 m_amplitude 拷到外部 out_amplitude,成功返 true。想看哪些周期能量强,直接接这个函数拿振幅谱,再自己挑前 N 个峰值即可,外汇与贵金属波动受事件驱动,周期 dominant 可能随流动性突变而漂移,属正常高风险现象。
class="type">uint GetDominantCycles(class="type">bool use_cycle_strength,const class="type">class="kw">double &in_data[],class="type">class="kw">double &out_cycles[]); class="type">uint GetDominantCycles(class="type">bool use_cycle_strenght,class="type">bool detrend,class="type">bool squared_amp,class="type">bool apply_window, class="type">uint min_period,class="type">uint max_period,const class="type">class="kw">double &in_data[],class="type">class="kw">double &out_cycles[]); class="type">void CalculateDominantCycles(class="type">uint prev,class="type">uint total,class="type">bool use_cycle_strength,const class="type">class="kw">double &in_data[], class="type">class="kw">double &out_signal[]); class="type">void CalculateWave(class="type">uint prev,class="type">uint total,class="type">uint max_cycles, class="type">bool use_cycle_strength,const class="type">class="kw">double &in_data[], class="type">class="kw">double &out_signal[]); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Default Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CGoertzelCycle::CGoertzelCycle(class="type">void) { m_pivot=m_slope=class="num">0.0; m_maxper=m_minper=m_cycles=class="num">0; m_detrend=m_squaredamp=m_flatten=false; m_ga=new CGoertzel(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Parametric Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CGoertzelCycle::CGoertzelCycle(class="type">bool detrend,class="type">bool squared_amp,class="type">bool apply_window, class="type">uint min_period,class="type">uint max_period) { m_pivot=m_slope=class="num">0.0; m_cycles=class="num">0; m_maxper=max_period; m_minper=min_period; m_detrend=detrend; m_squaredamp=squared_amp; m_flatten=apply_window; m_ga=new CGoertzel(); if(m_maxper) { ArrayResize(m_amplitude,m_maxper); ArrayResize(m_phase,m_maxper); ArrayResize(m_cycle,m_maxper); ArrayResize(m_peaks,m_maxper); ArrayResize(m_peaks_index,m_maxper); } } class=class="str">"cmt">//+-------------------------------------------------------------------+ class=class="str">"cmt">//| class="kw">public method to access the amplitude values | class=class="str">"cmt">//+-------------------------------------------------------------------+ class="type">bool CGoertzelCycle::GetSpectrum(const class="type">class="kw">double &in_data[],class="type">class="kw">double &out_amplitude[]) { if(spectrum(in_data)) { ArrayCopy(out_amplitude,m_amplitude); class="kw">return true; } class="kw">return false; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| class="kw">public method to access the amplitude values | class=class="str">"cmt">//+------------------------------------------------------------------+
「从频谱里抠出主周期的两套入口」
Goertzel 类对外暴露了两种拿主周期的方式:先算频谱再调 GetDominantCycles,或一次性把去趋势、平方振幅、加窗等参数都塞进重载版直接出结果。后者在内部重置了 m_pivot、m_slope 为 0.0,m_cycles 清 0,并按 max_period 把五个内部数组(振幅、相位、周期、峰值、峰值索引)全 resize 一遍,避免上根 K 线的残留污染本次计算。 看 GetSpectrum 的返回逻辑:若 spectrum(in_data) 为真,就把 m_amplitude 拷给 out_amplitude 并返回 true;否则返回 false。这意味着调用方必须判返回值,不能直接信 out_amplitude 有数——实盘里若行情停牌或 in_data 长度不足,数组可能是空的。 GetDominantCycles 的非重载版先跑 spectrum,失败就 ArrayInitialize(out_cycles,0.0) 且返回 0;成功才调 cyclepeaks 找峰,并按 m_cycles 调整 out_cycles 大小,用循环把 m_cycle[m_peaks_index[i]] 依次填进去。注意它返回的是 dominant cycle 的个数,不是周期值本身,调用方拿这个数去遍历 out_cycles 才拿到具体周期。 CalculateDominantCycles 多带了 prev 和 total 参数,明显是给指标逐根刷新用的:limit 初始为 0,并用 ArrayGetAsSeries(out_signal) 探一下输出数组的时间序列方向,决定后面填充时下标怎么走。外汇与贵金属波动受消息扰动大,周期峰值可能频繁跳变,这类接口在高波动品种上需限定 min_period 不低于 8~10 根以免噪声被当主周期。
class="type">bool CGoertzelCycle::GetSpectrum(class="type">bool detrend,class="type">bool squared_amp,class="type">bool apply_window,class="type">uint min_period,class="type">uint max_period,const class="type">class="kw">double &in_data[],class="type">class="kw">double &out_amplitude[]) { m_pivot=m_slope=class="num">0.0; m_cycles=class="num">0; m_maxper=max_period; m_minper=min_period; m_detrend=detrend; m_squaredamp=squared_amp; m_flatten=apply_window; if(m_maxper) { ArrayResize(m_amplitude,m_maxper); ArrayResize(m_phase,m_maxper); ArrayResize(m_cycle,m_maxper); ArrayResize(m_peaks,m_maxper); ArrayResize(m_peaks_index,m_maxper); } if(spectrum(in_data)) { ArrayCopy(out_amplitude,m_amplitude); class="kw">return true; } class="kw">return false; } class=class="str">"cmt">//+----------------------------------------------------------------------------+ class=class="str">"cmt">//|class="kw">public method to get the dominant cycle periods arranged in descending order| class=class="str">"cmt">//+----------------------------------------------------------------------------+ class="type">uint CGoertzelCycle::GetDominantCycles(class="type">bool use_cycle_strength,const class="type">class="kw">double &in_data[],class="type">class="kw">double &out_cycles[]) { if(!spectrum(in_data)) { ArrayInitialize(out_cycles,class="num">0.0); class="kw">return(class="num">0); } cyclepeaks(use_cycle_strength); if(out_cycles.Size()!=m_cycles) ArrayResize(out_cycles,m_cycles); for(class="type">uint i=class="num">0; i<m_cycles; i++) out_cycles[i]=m_cycle[m_peaks_index[i]]; class="kw">return m_cycles; } class=class="str">"cmt">//+----------------------------------------------------------------------------+ class=class="str">"cmt">//|class="kw">public method to get the dominant cycle periods arranged in descending order| class=class="str">"cmt">//+----------------------------------------------------------------------------+ class="type">uint CGoertzelCycle::GetDominantCycles(class="type">bool use_cycle_strength,class="type">bool detrend,class="type">bool squared_amp,class="type">bool apply_window,class="type">uint min_period,class="type">uint max_period,const class="type">class="kw">double &in_data[],class="type">class="kw">double &out_cycles[]) { m_pivot=m_slope=class="num">0.0; m_cycles=class="num">0; m_maxper=max_period; m_minper=min_period; m_detrend=detrend; m_squaredamp=squared_amp; m_flatten=apply_window; if(m_maxper) { ArrayResize(m_amplitude,m_maxper); ArrayResize(m_phase,m_maxper); ArrayResize(m_cycle,m_maxper); ArrayResize(m_peaks,m_maxper); ArrayResize(m_peaks_index,m_maxper); } class="kw">return(GetDominantCycles(use_cycle_strength,in_data,out_cycles)); } class=class="str">"cmt">//+-----------------------------------------------------------------------+ class=class="str">"cmt">//|method used to access calculated dominant cycles , for use in indcators| class=class="str">"cmt">//+-----------------------------------------------------------------------+ class="type">void CGoertzelCycle::CalculateDominantCycles(class="type">uint prev,class="type">uint total,class="type">bool use_cycle_strength,const class="type">class="kw">double &in_data[], class="type">class="kw">double &out_signal[]) { class="type">uint limit =class="num">0; class="type">bool indexOut=ArrayGetAsSeries(out_signal);
波形重建时的数组索引与边界处理
在 MT5 自定义指标里调用 CGoertzelCycle::CalculateWave 时,先靠 ArrayGetAsSeries 判定输入输出数组是时间序(最新 Bar 在 0 号)还是普通序。这一步直接决定后面循环的方向和索引映射,写错就会把信号画到历史反方向。 首次计算(prev<=0)要预留足够历史:若输出为时间序,limit 取 total - m_maxper*3,首值写在 limit+1;否则 limit 取 m_maxper*3,首值写在 limit-1。也就是至少留出最大周期 3 倍的缓冲,否则频谱在头部会塌掉。 非首次只跑新增部分:limit = (indexOut)? total-prev : prev,避免每 tick 重算全量。下面这段是时间序分支的核心循环,注意 spectrum 的索引做了翻转兼容。
class="type">bool indexIn=ArrayGetAsSeries(in_data); if(prev<=class="num">0) { class="type">uint firstindex=class="num">0; if(indexOut) { limit=total-(m_maxper*class="num">3); firstindex=limit+class="num">1; } else { limit=m_maxper*class="num">3; firstindex=limit-class="num">1; } out_signal[firstindex]=class="num">0; } else { limit=(indexOut)?total-prev:prev; } class="type">uint found_cycles=class="num">0; if(indexOut) { for(class="type">int ibar=(class="type">int)limit; ibar>=class="num">0; ibar--) { spectrum(in_data,(indexIn)?ibar:total-ibar-class="num">1); out_signal[ibar]=out_signal[ibar+class="num">1]+wavepoint(use_cycle_strength,max_cycles); } }
class="type">bool indexIn=ArrayGetAsSeries(in_data); if(prev<=class="num">0) { class="type">uint firstindex=class="num">0; if(indexOut) { limit=total-(m_maxper*class="num">3); firstindex=limit+class="num">1; } else { limit=m_maxper*class="num">3; firstindex=limit-class="num">1; } out_signal[firstindex]=class="num">0; } else { limit=(indexOut)?total-prev:prev; } class="type">uint found_cycles=class="num">0; if(indexOut) { for(class="type">int ibar=(class="type">int)limit; ibar>=class="num">0; ibar--) { spectrum(in_data,(indexIn)?ibar:total-ibar-class="num">1); out_signal[ibar]=out_signal[ibar+class="num">1]+wavepoint(use_cycle_strength,max_cycles); } }
◍ 逐根 K 线回填信号数组的写法
这段循环是频谱合成落到图表上的最后一步:从 limit 到 total 逐根处理,把每个 bar 的波点累加进 out_signal。 for(int ibar=(int)limit; ibar<(int)total; ibar++) 定义遍历区间,limit 通常为已计算起点,total 是数据总数。spectrum(...) 按正向或反向索引取当前 bar 的频谱分量;out_signal[ibar]=out_signal[ibar-1]+wavepoint(...) 则把上一根的信号值加上本根波点,形成连续累加曲线。 在 MT5 里把这段接在你自己的指标 OnCalculate 末尾,改 limit 为已算 bars 数,就能只刷新增量、不重算全历史,回测时 CPU 占用会明显掉一截。外汇与贵金属杠杆高,信号曲线仅作概率参考,实盘须自担风险。
for(class="type">int ibar=(class="type">int)limit; ibar<(class="type">int)total; ibar++) { spectrum(in_data,(indexIn)?total-ibar-class="num">1:ibar); out_signal[ibar]=out_signal[ibar-class="num">1]+wavepoint(use_cycle_strength,max_cycles); } } } class=class="str">"cmt">//+------------------------------------------------------------------+