经验模态分解法介绍·进阶篇
(2/3)·移动平均只能切两层,EMD 如何把杂乱报价拆成多个本征模态再逐个看
◍ EMD 怎么把行情拆成 IMF
经验模态分解(EMD)由黄锷提出,核心是用序列自身的局部极大、极小值生成上下包络线,再取两者均值从原序列里减掉。先找全部局部极值,用三次样条把极值连成平滑包络——红色标极大值、蓝色标极小值,绿色画出包络,虚线就是包络均值。 第一次减完得到的是首个 IMF 的近似,还要再找新极值重复同样动作,这叫筛选(sifting)。筛选一直跑,直到满足停止标准;标准怎么定直接左右分解结果,这点后面再展开。 首个 IMF 合格后,用原信号减掉它,对剩余部分再来一遍,逐个抽出 IMF,直到残数里极值不超过两个就停。整套流程不靠严格数学推导,纯经验驱动,所以名叫经验模态分解。 黄锷的原算法好懂,但有几个被学界点名的弱点。本文不堆现代改进方案,只交代我们做软件实现时的关键取舍,方便你直接拿去 MT5 验。
扒开 CEMDecomp 类的六个公开开关
做 EMD 分解不用自己造轮子,CEMDecomp 这个类就是按希尔伯特-黄变换那套公开思路写的,和黄锷原始算法本质一致,没动核心逻辑。类里暴露出来的公共变量和方法,就是你在 MT5 里调参和取数的全部把手。 N 是序列长度,调完 Decomp() 才生成,等于你喂进去的数组长度,提取出的每个 IMF 也全是这个尺寸。Mean 是输入序列均值,同样跑完分解才有值。nIMF 是个计数器,分解后它等于『提取的 IMF 数 + 2』:索引 0 是扣掉均值的原序列,索引 nIMF 是残差,中间才是真 IMF。 MaxIMF 默认 16,到这个数就强制停。MaxIter 默认 2000,是筛分迭代上限,到了不管精度够不够都收手。FixedIter 默认 0,表示按精度自适应停;设成 1 则每个 IMF 固定跑 10 次筛分——想快就开,想准就关。 Decomp(double &y[]) 吃一个数组引用,跑完把 N、Mean、nIMF 都填好。GetIMF(double &x[], int nn) 按 nn 把对应分量拷给你传的数组;数组短了就只填能填的部分。用 USDJPY 日线、N=100 做样例,实际跑出来是 4 个 IMF 加残差。 图 2 同刻度堆叠能看贡献权重,但细节糊;图 3 开自动缩放,单看图更清楚,只是丢了幅度可比性。外汇和贵金属波动高风险,分解结果只描述结构,不预示方向。
<span class="comment">class=class="str">"cmt">//------------------------------------------------------------------------------------</span> <span class="comment">class=class="str">"cmt">// The Empirical Mode Decomposition(EMD).</span> <span class="comment">class=class="str">"cmt">//------------------------------------------------------------------------------------</span> <span class="keyword">class</span> CEMDecomp:<span class="keyword">class="kw">public</span> CObject { <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">int</span> N; <span class="comment">class=class="str">"cmt">// Input and output data size</span> <span class="keyword">class="type">class="kw">double</span> Mean; <span class="comment">class=class="str">"cmt">// Mean of input data</span> <span class="keyword">class="type">int</span> nIMF; <span class="comment">class=class="str">"cmt">// IMF counter</span> <span class="keyword">class="type">int</span> MaxIMF; <span class="comment">class=class="str">"cmt">// Maximum number of IMF</span> <span class="keyword">class="type">int</span> MaxIter; <span class="comment">class=class="str">"cmt">// Maximum number of iterations</span> <span class="keyword">class="type">int</span> FixedIter; <span class="comment">class=class="str">"cmt">// class="num">0-variable number of sifting iterations;</span> <span class="comment">class=class="str">"cmt">// class="num">1-always ten sifting iterations.</span> <span class="keyword">class="type">class="kw">double</span> IMFResult[]; <span class="comment">class=class="str">"cmt">// Result</span> <span class="keyword">class="kw">private</span>: <span class="keyword">class="type">class="kw">double</span> X[]; <span class="keyword">class="type">class="kw">double</span> Imf[]; <span class="keyword">class="type">class="kw">double</span> XMax[]; <span class="comment">class=class="str">"cmt">// x of local maxima</span> <span class="keyword">class="type">class="kw">double</span> YMax[]; <span class="comment">class=class="str">"cmt">// y of local maxima</span> <span class="keyword">class="type">class="kw">double</span> XMin[]; <span class="comment">class=class="str">"cmt">// x of local minima</span> <span class="keyword">class="type">class="kw">double</span> YMin[]; <span class="comment">class=class="str">"cmt">// y of local minima</span> <span class="keyword">class="type">class="kw">double</span> EnvUpp[]; <span class="comment">class=class="str">"cmt">// Upper envelope</span> <span class="keyword">class="type">class="kw">double</span> EnvLow[]; <span class="comment">class=class="str">"cmt">// Lower envelope</span> <span class="keyword">class="type">class="kw">double</span> Eps; <span class="comment">class=class="str">"cmt">// Accuracy comparison of floating-point numbers</span> <span class="keyword">class="type">class="kw">double</span> Tol; <span class="comment">class=class="str">"cmt">// Accuracy of calculation IMF</span> <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">void</span> CEMDecomp(<span class="keyword">class="type">void</span>); <span class="keyword">class="type">int</span> Decomp(<span class="keyword">class="type">class="kw">double</span> &y[]); <span class="comment">class=class="str">"cmt">// Decomposition</span> <span class="keyword">class="type">void</span> GetIMF(<span class="keyword">class="type">class="kw">double</span> &x[], <span class="keyword">class="type">int</span> nn); <span class="comment">class=class="str">"cmt">// Get IMF number nn</span>
「用 EMD 把开盘价拆出第二个本征模态」
经验模态分解(EMD)在 MT5 里可以直接套 CEMDecomp 类来做,不必自己写筛分循环。上面这段把当前品种当前周期的开盘价序列丢进分解器,取出第 2 个 IMF 分量,逻辑很直接。 关键点在于样本量设成了 400 根 K 线(n=400),并且要求提取出的 IMF 数量大于 3 才允许取第 2 个分量——如果行情太短或分解失败,imf2 里就是空的。外汇与贵金属波动高、跳空频繁,这种分解对噪声敏感,实盘使用前建议在历史数据上先跑一遍看分量稳定性。 代码逐行看:CopyOpen 把最新 400 根开盘价填进 yy 数组;new CEMDecomp 建实例后调 Decomp 做分解;ret==0 且 nIMF>3 时,GetIMF(imf2,2) 把下标为 2 的模态写回 imf2。最后 delete 释放实例,避免脚本重复运行漏内存。
class="kw">private: class="type">int arrayprepare(class="type">void); class="type">void extrema(class="type">class="kw">double &y[],class="type">int &nmax,class="type">class="kw">double &xmax[],class="type">class="kw">double &ymax[], class="type">int &nmin,class="type">class="kw">double &xmin[],class="type">class="kw">double &ymin[]); class="type">int SplineInterp(class="type">class="kw">double &x[],class="type">class="kw">double &y[],class="type">int n,class="type">class="kw">double &x2[], class="type">class="kw">double &y2[],class="type">int btype=class="num">0); }; class="macro">#include "CEMDecomp.mqh" class=class="str">"cmt">//------------------------------------------------------------------------------------ class=class="str">"cmt">// Script program start function class=class="str">"cmt">//------------------------------------------------------------------------------------ class="type">void OnStart() { class="type">int n,ret; class="type">class="kw">double yy[],imf2[]; n=class="num">400; class=class="str">"cmt">// Input sequence length ArrayResize(yy,n); class=class="str">"cmt">// Array of input data ArrayResize(imf2,n); class=class="str">"cmt">// Array for reading the results CopyOpen(_Symbol,PERIOD_CURRENT,class="num">0,n,yy); class=class="str">"cmt">// Generation of input data CEMDecomp *emd=new CEMDecomp(); class=class="str">"cmt">// Creation of the CEMDecomp class instance ret=emd.Decomp(yy); class=class="str">"cmt">// Decomposition of the input sequence if((ret==class="num">0)&&(emd.nIMF>class="num">3)) class=class="str">"cmt">// If there is no error and the sufficient class=class="str">"cmt">// number of components was extracted, emd.GetIMF(imf2,class="num">2); class=class="str">"cmt">// copy component number class="num">2 into class=class="str">"cmt">// array imf2[]. class="kw">delete(emd); class=class="str">"cmt">// Deletion of the CEMDecomp class instance class=class="str">"cmt">// One of the extracted components in array imf2[]. }
◍ 极值识别的两个坑与停止准则的取舍
做 EMD 分解时,第一道关就是极值识别。最朴素的做法是把当前点跟前后两点比:比两边都大就是极大值,比两边都小就是极小值。对干净的单峰序列这法子够用,但碰到图 4 后半段那种平台区(连续相等值),它一个极值都抓不出来——最后那个极大值和两个极小值直接被漏掉,零穿越的振荡类比也就断了。 CEMDecomp 类用了一个改良版:把相等值区间整体认作极值,并把极值点放在区间中央,图 4 展示了效果。这个类还拿极值数量当停止信号——某次筛出来的 IMF 里一个极值都没有,就丢弃并停手。但如果拿同一套算法去画包络线,遇到矩形序列会得出两条平行直线,筛过程完全动不了输入序列,分解直接失效。 图 5 的备选算法解决了这个:它把既是极大又是极小的点标成绿色,包络线不再是平行线,矩形里的隐藏分量就能继续筛出来。压缩包里的测试案例可复现。弱点也很硬——三角形序列用它照样提不出隐藏分量,这是本实现的已知短板,换 CEEMD 才可能补,但本文不展开。 末端影响也得盯:样条连极大值做上包络时,首尾外侧的包络要靠延长定义,性质接近末端 IMF,本文没做修正代码,只提一句。停止准则方面,类默认用「相邻筛选差异比」当门槛,极限值设得松,迭代冲到 200 甚至 300 次都正常;很多文献警告过这次数偏大。想要可控,调 Decomp() 前把 FixedIter 置 1,强制所有 IMF 在 10 次迭代内提取完,结果和默认法会有可见偏差——外汇与贵金属价格序列高频噪声重,两种停止法出来的分量差可能直接影响后续信号,建议开 MT5 跑附包案例比对。