经验模态分解法介绍·进阶篇
📊

经验模态分解法介绍·进阶篇

(2/3)·移动平均只能切两层,EMD 如何把杂乱报价拆成多个本征模态再逐个看

新手友好 第 2/3 篇
很多人画一条 MA 再减残差就以为完成了行情分解,其实只拿到两层粗糙分量。真正复杂的货币报价混着多周期扰动,硬切反而遮住隐藏形态。先理解 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 开自动缩放,单看图更清楚,只是丢了幅度可比性。外汇和贵金属波动高风险,分解结果只描述结构,不预示方向。

MQL5 / C++
<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
&nbsp;&nbsp;{
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp; N;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Input and output data size</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;Mean;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Mean of input data</span>
&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp; nIMF;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// IMF counter</span>
&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp; MaxIMF;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Maximum number of IMF</span>
&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp; MaxIter;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Maximum number of iterations</span>
&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp; FixedIter;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// class="num">0-variable number of sifting iterations;</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;  <span class="comment">class=class="str">"cmt">// class="num">1-always ten sifting iterations.</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;IMFResult[];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Result</span>
<span class="keyword">class="kw">private</span>:
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;X[];
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;Imf[];
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;XMax[];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// x of local maxima</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;YMax[];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// y of local maxima</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;XMin[];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// x of local minima</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;YMin[];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// y of local minima</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;EnvUpp[];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Upper envelope</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;EnvLow[];&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Lower envelope</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;Eps;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Accuracy comparison of floating-point numbers</span>
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span>&nbsp;&nbsp;Tol;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">// Accuracy of calculation IMF</span>
<span class="keyword">class="kw">public</span>:&nbsp;&nbsp;
&nbsp;&nbsp;<span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;CEMDecomp(<span class="keyword">class="type">void</span>);
&nbsp;&nbsp;<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp; Decomp(<span class="keyword">class="type">class="kw">double</span> &amp;y[]);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">// Decomposition</span>
&nbsp;&nbsp;<span class="keyword">class="type">void</span>&nbsp;&nbsp;&nbsp;&nbsp;GetIMF(<span class="keyword">class="type">class="kw">double</span> &amp;x[], <span class="keyword">class="type">int</span> nn);&nbsp;&nbsp;<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 释放实例,避免脚本重复运行漏内存。

MQL5 / C++
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 跑附包案例比对。

交给小布盯盘拆分量
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到不同分量的波动贡献,你不必手算 EMD。

常见问题

MA 用固定长度的均线做减差,基函数是预设的;EMD 依据数据自身极值自适应提取本征模态,不依赖外部频率假设,更适合非平稳的报价序列。
取决于采样长度和筛选次数,通常几分钟周期的历史重算开销可控,但实时逐 tick 跑需注意线程阻塞,建议放在独立计算周期。
EMD 不追求物理正交,分量是为分析便利人工构造的函数,重点在看形态而非严格频谱意义,负向摆动仅代表局部偏离基线。
可以,小布盯盘的品种页内置了这类分解视图,自动标注各模态贡献度,省去你自己写 MQL5 类的麻烦。
原文说明里提到对序列两端做镜像或极值延拓来削弱边界漂移,具体参数要在回测里按品种波动特征微调。