频域中的滤波和特征提取·综合运用
(3/3)·从预处理到逆变换,圆角滤波器如何在 MT5 里把噪声频段悄悄抹平
频域低通滤波的构造函数与去趋势处理
在 MT5 里做频率域滤波,第一步是把原始序列送进 CFilter 构造函数。它接收 double 数组引用、最小补零长度 min_padding 以及 detrend 开关,默认 detrend=true。若传入数组长度为 0,直接 Print 报错并返回,不初始化。 补零长度按 2 的幂向上取整:从 m_padded_len=2 开始翻倍,直到大于等于 npts+pad 才 break。例如 npts=100、pad=0 时,实际 m_padded_len 会取到 128;若 pad=28,则取到 256。这一步决定后续 FFT 的运算规模。 当需要扩展补零时,detrend=true 会用首尾两点算截距和斜率,把线性趋势减掉;detrend=false 则减掉全序列均值。剩余补零区填 0.0。若无需补零,直接 ArrayCopy 且截距斜率置零。 初始化收尾调用 CFastFourierTransform::FFTR1D 把时域转成频域存进 m_dft,m_initialized 置 true。外汇与贵金属价格序列含强趋势,detrend 选错会让低频能量泄漏,FFT 结果失真,实盘使用前建议在策略测试器用历史数据验证两种模式差异。
CFilter::CFilter(class="type">class="kw">double &series[],class="type">uint min_padding=class="num">0,class="type">bool detrend=true) { class=class="str">"cmt">//---local variables m_initialized=false; class="type">int i; class="type">int npts = ArraySize(series); class="type">int pad = (class="type">int)MathAbs(min_padding); class=class="str">"cmt">//--- check size of series if(npts<=class="num">0) { Print("Input array is empty"); class="kw">return ; } class=class="str">"cmt">//--- m_length = npts ; for(m_padded_len=class="num">2 ; m_padded_len<INT_MAX ; m_padded_len*=class="num">2) { if(m_padded_len >= npts+pad) class="kw">break ; } class=class="str">"cmt">//--- if(m_padded_len<npts+pad) { Print("Warning, calculated length of modified series is too class="type">long"); class="kw">return; } class=class="str">"cmt">//--- m_half_padded_len = m_padded_len / class="num">2; class=class="str">"cmt">//--- ArrayResize(m_buffer,m_padded_len); class=class="str">"cmt">//--- if(m_padded_len > npts) class=class="str">"cmt">// Any padding needed? { if(detrend) { m_intercept = series[class="num">0] ; m_slope = (series[npts-class="num">1] - series[class="num">0]) / (npts-class="num">1) ; } else { m_intercept = m_slope = class="num">0.0 ; for(i=class="num">0 ; i<npts ; i++) m_intercept += series[i] ; m_intercept /= npts ; } for(i=class="num">0 ; i<npts ; i++) { m_buffer[i]=series[i] - m_intercept - m_slope * i ; } for(i=npts ; i<m_padded_len ; i++) { m_buffer[i]=class="num">0.0; } } else { ArrayCopy(m_buffer,series); m_intercept = m_slope = class="num">0.0 ; } class=class="str">"cmt">//---Compute the Fourier transform of the padded series CFastFourierTransform::FFTR1D(m_buffer,class="type">int(m_padded_len),m_dft); class=class="str">"cmt">//--- m_initialized = true; }
◍ 正交镜像滤波的频率窗与逆变换落点
QMF 这段和前面的实序列低通不同,它直接对复 DFT 系数动刀:以归一化频率 f = i / m_padded_len 为中心,用高斯权重 wt = exp(-dist*dist) 衰减频点,同时把镜像位置 m_padded_len-i 的实部虚部都置零,这是产生正交镜像输出的关键一步。 注意 dft_temp[0] 与半长点 m_half_padded_len 的特殊处理:前者实部清零、后者用 0.5 倍虚部乘高斯窗赋值,且两端虚部强制为 0,符合实变换的对称定义。漏掉这一句,逆变换后会混入非因果分量。 逆变换走 FFTC1DInv,输出长度裁回 m_length,每个复数值除以 m_half_padded_len 而非全长,这是 QMF 幅度标定的硬参数。在 MT5 里把 width 从 0.02 调到 0.05,频窗变宽,贵金属 1H 序列的边界振荡会明显减弱,但过渡带也会变钝,属典型权衡。 下面这段是 QMF 方法本体,逐行对应上面说的频窗与置零逻辑,可直接拷进自定义滤波器类验证。
class="type">void CFilter::Qmf(class="type">class="kw">double freq,class="type">class="kw">double width,complex &out[]) { class=class="str">"cmt">//--- class="type">int i ; class="type">class="kw">double f, dist, wt ; complex dft_temp[]; ArrayCopy(dft_temp,m_dft); class=class="str">"cmt">//--- for(i=class="num">1 ; i<m_half_padded_len ; i++) { f = (class="type">class="kw">double) i / (class="type">class="kw">double) m_padded_len ; class=class="str">"cmt">// This frequency dist = (f - freq) / width ; wt = exp(-dist * dist) ; dft_temp[i].real *= wt ; dft_temp[i].imag *= wt ; dft_temp[m_padded_len-i].real = dft_temp[m_padded_len-i].imag = class="num">0.0 ; class=class="str">"cmt">// Causes QMF outputs } class=class="str">"cmt">//--- dft_temp[class="num">0].real = class="num">0.0 ; dist = (class="num">0.5 - freq) / width ; dft_temp[m_half_padded_len].real = class="num">0.5 * dft_temp[m_half_padded_len].imag * exp(-dist * dist) ; class=class="str">"cmt">//--- dft_temp[class="num">0].imag = dft_temp[m_half_padded_len].imag = class="num">0.0 ; class=class="str">"cmt">// By definition of real transform class=class="str">"cmt">//--- CFastFourierTransform::FFTC1DInv(dft_temp,m_padded_len); ArrayResize(out,m_length); class=class="str">"cmt">//--- for(i=class="num">0 ; i<m_length ; i++) { out[i].real = dft_temp[i].real/class="type">class="kw">double(m_half_padded_len) ; out[i].imag = dft_temp[i].imag/class="type">class="kw">double(m_half_padded_len) ; } }
「用 AFD 把滤波器输出拆开看」
AFD 是一个 MT5 应用程序,核心用途是生成用户指定长度的随机序列,并实时绘制原始序列(蓝线)与经过 CFilter 滤波后的结果。序列长度、随机数种子、滤波器参数都能在 GUI 里直接调,省去了自己写脚本比对的过程。 同相分支里,低通输出反映输入序列在任意时刻的平均水平,高通输出把原始序列里偶发的高低点剥出来,两者合起来就是每个时间点的状态感知。带通输出则没那么直观——单看曲线谁都猜不出含义,但在峰值或低谷处,往往对应原始序列某种周期性特征,可能适合喂给预测模型。 把随机序列跑在不同频率的带通下,峰值高度直接表示那一频段的周期变化幅度:峰值低说明变化弱,峰值高说明变化强。下面这段取自 AFD.mq5 的代码,就是采样 QM 滤波器输出后算幅度和相位的做法。 不同频率的带通差异说明可提取的特征非常多样。想给机器学习算法喂独特特征,前提是把定义滤波器的参数摸透——外汇和贵金属市场高杠杆、高风险,这类特征仅作概率参考,不构成方向判定。
case ENUM_QMF_AMPLITUDE: { y_name = "Amplitude"; complex comp[]; filter.Qmf(freq,scale,comp); ArrayResize(m_output1,ArraySize(comp)); for(class="type">int i=class="num">0; i<ArraySize(m_output); i++) m_output1[i]=MathSqrt(comp[i].real*comp[i].real + comp[i].imag*comp[i].imag); } class="kw">break; case ENUM_QMF_PHASE: { y_name = "Phase"; complex comp[]; filter.Qmf(freq,scale,comp); ArrayResize(m_output1,ArraySize(comp)); for(class="type">int i=class="num">0; i<ArraySize(m_output); i++) m_output1[i]=(comp[i].real>=class="num">1.e-40 || comp[i].imag>=class="num">1.e-40)?atan2(comp[i].imag, comp[i].real):class="num">0.0; } class="kw">break;
width 如何同时框定频域与时域
频域分析有个绕不开的痛点:你能算出某个频率分量,却很难直接说清它在时间轴上摊了多宽。前面随机序列的例子里,带通输出取了 0.15、0.3、0.45 三个频率,width 都是 0.03,光看这个数容易没概念。 频率 0.15 对应的周期是 1/0.15 ≈ 6.67 次取样。width 决定频域分辨率:想抠出极窄的频带,就把 width 设小,0.01 一般算下限,还能更低。例子中 width=0.03,通带就是 0.12~0.18,这个范围里的频率放行,之外的几乎全截掉。 width 反过来也卡死了时域分辨率,两者成反比。估算时域受影响范围用 0.8/width:例子里 0.8/0.03 ≈ 27 次取样,意味着每个输出值都被它前 27 个和后 27 个观测值拽着走。 选 width 得跟着中心频率走。低频周期长,可牺牲时域分辨率用窄 width;高频周期短,更适合宽 width,代价是时域覆盖变短。若拿滤波结果喂预测模型,比如预测第 201 个点,只能喂末尾 27 个已知值的输出,再往后就会吃 DFT 的环绕效应。 实战里别所有频率都套同一 width。按策略挑频率和对应宽度分别采样,才可能捞到更多有用信息;外汇与贵金属波动受事件驱动,窄宽滤波易在跳空时失真,验证前请在 MT5 用历史数据跑一遍。
◍ 一点提醒
三类滤波器在序列处理里各管一摊:同相的负责把噪声拆开、留住有用分量,正交的专盯级别跳变区,两者叠加能挖出藏着的周期特征。频域这条路靠 FFT 跑得快,但粗心处理数据带来的失真,可能直接把结论带偏,实操时得绷着根弦。 附带的 CFilter 类在 Filter.mqh 里用频域实现基础数字滤波,RandomStationarySeries.mqh 能造不同特性的随机序列给 AFD 用;AFD.mq5 调了 EAF 图形库,编译时要让 EAF 最后包含,否则和 ALGLIB 命名撞车会挂。外汇与贵金属波动剧烈、杠杆高风险大,这类工具只辅助研判,别当信号圣经。 代码都随文给了,AFD.ex5 已编译成 353.06 KB 的 EA,想验证就丢进 MT5 的 Experts 目录跑一遍。