预测时间序列(第 1 部分):经验分解模式(EMD)方法(基础篇)
📘

预测时间序列(第 1 部分):经验分解模式(EMD)方法(基础篇)

第 1/2 篇

用 EMD 把行情拆成几条本征模态

经验模态分解(EMD)的核心思路,是把一段非平稳的价格序列逐层剥离出若干本征模态函数(IMF)和一个残余趋势项。每一层 IMF 对应不同时间尺度的波动,高频层贴近噪声明细,低频层接近趋势骨架。对 MT5 用户来说,这意味着可以把一根乱糟糟的 H1 收盘价曲线,拆成 5~8 个分量再分别判断结构。 原文作者在 2020 年 6 月发布的这套方法,实测在 EURUSD H1 上能把原序列分解为 7 个 IMF 加 1 个残差,最高频 IMF 周期约 4~6 根 K 线。这种拆解不预设基函数,完全靠极值点包络迭代,因此比固定窗口的均线族更贴合局部形态。 开 MT5 自建脚本时,重点盯住筛分停止条件:通常设定标准差阈值 0.2~0.3 之间,迭代到相邻两次 IMF 差异小于该值即停。阈值设太低会过度分解引入伪分量,设太高则残留混模,建议先用历史段跑一遍看分量数是否稳定在 6~9 个。

◍ 预测工具箱里还缺哪两块

交易者能否盈利,核心在于对一段周期后价格走向的推断质量,而不是指标堆得多花哨。要支撑这种推断,既需要紧跟基准市场特征,也需要技术分析算法,乃至把价格、波动率、宏观指数、账户权益等都当成时间序列来建模。 MQL5 社区里预测类资料不少,2008 年那篇《预测财经时间序列》算早期严谨文献,代码库也提供了指数平滑、DTW 历史形态匹配、BPNN、SOM 等可直接在 MT5 跑的工具。 本系列往现有列表里补两块:其一是经验模式分解(EMD),前文只讲过原理没拿来做预测,第一篇先啃它;其二是最小二乘支持向量机(LS-SVM),放到第二篇。外汇与贵金属杠杆高、滑点突发行情多,任何预测都只是概率倾向,开 MT5 前先想清楚爆仓距离。

「给 EMD 类加上外推预测能力」

经验模式分解把价格序列拆成一堆本征模式函数(IMF),每种都是对序列极值做样条插值得到的分量。直观上像傅立叶展开,但 IMF 不是固定频率的谐波,而是随数据自适应。原始 CEMD 类只做分解,要拿它预测就得改 decomp 方法,加一个外推参数决定向前多推几根柱线。 直接在 decomp 里把数组长度 N 加上 extrapolate,同时用局部变量 Nf 存住真实序列长度。代码里标「+」的是新增,「*」的是改写:用 MathMin(i, Nf-1) 取最后一个已知值去填外推段,均值也只按前 Nf 个点算。这样 IMF 能顺着最后已知点往后外推,但预测深度最好压在几根柱线内——离已知区越远,IMF 越失真。 这种改法存成 EMDloose.mqh 后有个坑:极值搜索 extrema 会把这些假未来点当真,样条互相平滑,预测变成自平衡振荡,不再表征原序列。外汇和贵金属波动大、跳空频繁,拿这种自平滑线当信号容易误判,只能当参考。 正经做法是把 Nf 提为对象变量,extrema 里仍用 Nf 找极值,只有真实点参与。最终 EMD.mqh 和 loose 版可并列挂进指标比效果。另外给 getIMF 加了 reverse 参数,逆序排缓冲方便指标第 0 根是最新价。下面这段是改动核心: #include <EMD.mqh> #define BUF_NUM 18 // 16 IMF maximum (including input at 0-th index) + residue + reconstruction #property indicator_separate_window #property indicator_buffers BUF_NUM #property indicator_plots BUF_NUM #include <IndArray.mqh> IndicatorArray buffers(BUF_NUM); IndicatorArrayGetter getter(buffers); class CEMD { private: int N; // Input and output data size double IMFResult[]; // Result double X[]; // X-coordinate for the TimeSeries. X[]=0,1,2,...,N-1. ... public: int N; // Input and output data size double Mean; // Mean of input data ... int decomp(double &y[]) { ... N = ArraySize(y); arrayprepare(); for(i = 0; i < N; i++) X[i] = i; Mean = 0; for(i = 0; i < N; i++) Mean += (y[i] - Mean) / (i + 1.0); // Mean (average) of input data for(i = 0; i < N; i++) { a = y[i] - Mean; Imf[i] = a; IMFResult[i] = a; } // The loop of decomposition ... extrema(...); ... ... } private: int arrayprepare(void) { if(ArrayResize(IMFResult, N) != N) return (-1); ... } }; int decomp(const double &y[], const int extrapolate = 0) // * { ... N = ArraySize(y); int Nf = N; // + preserve actual number of input data points N += extrapolate; // + arrayprepare(); for(i = 0; i < N; i++) X[i] = i; Mean = 0; for(i = 0; i < Nf; i++) // * was N Mean += (y[i] - Mean) / (i + 1.0); for(i = 0; i < N; i++) { a = y[MathMin(i, Nf - 1)] - Mean; // * was y[i] Imf[i] = a; IMFResult[i] = a; } // The loop of decomposition ... extrema(...); ... for(i = 0; i < N; i++) { IMFResult[i + N * nIMF] = IMFResult[i]; IMFResult[i] = y[MathMin(i, Nf - 1)] - Mean; // * was y[i] } } class CEMD { private: int N; // Input and output data size int Nf; // + public: int decomp(const double &y[], const int extrapolate = 0) { ... N = ArraySize(y); Nf = N; // + preserve actual number of input data points in the object N += extrapolate; // + ... } };

MQL5 / C++
class="macro">#include <EMD.mqh>
class="macro">#define BUF_NUM class="num">18 class=class="str">"cmt">// class="num">16 IMF maximum(including class="kw">input at class="num">0-th index) + residue + reconstruction
class="macro">#class="kw">property indicator_separate_window
class="macro">#class="kw">property indicator_buffers BUF_NUM
class="macro">#class="kw">property indicator_plots   BUF_NUM
class="macro">#include <IndArray.mqh>
IndicatorArray buffers(BUF_NUM);
IndicatorArrayGetter getter(buffers);
class CEMD
{
class="kw">private:
class="type">int N;              class=class="str">"cmt">// Input and output data size
class="type">class="kw">double IMFResult[]; class=class="str">"cmt">// Result
class="type">class="kw">double X[];         class=class="str">"cmt">// X-coordinate for the TimeSeries. X[]=class="num">0,class="num">1,class="num">2,...,N-class="num">1.
...
class="kw">public:
class="type">int N;              class=class="str">"cmt">// Input and output data size
class="type">class="kw">double Mean;        class=class="str">"cmt">// Mean of class="kw">input data
...
class="type">int decomp(class="type">class="kw">double &y[])
{
...
N = ArraySize(y);
arrayprepare();
for(i = class="num">0; i < N; i++)
X[i] = i;
Mean = class="num">0;
for(i = class="num">0; i < N; i++)
Mean += (y[i] - Mean) / (i + class="num">1.0); class=class="str">"cmt">// Mean(average) of class="kw">input data
for(i = class="num">0; i < N; i++)
{
a = y[i] - Mean;
Imf[i] = a;
IMFResult[i] = a;
}
class=class="str">"cmt">// The loop of decomposition
...
extrema(...);
...
...
}
class="kw">private:
class="type">int arrayprepare(class="type">void)
{
if(ArrayResize(IMFResult, N) != N) class="kw">return (-class="num">1);
...
}
};
class="type">int decomp(class="kw">const class="type">class="kw">double &y[], class="kw">const class="type">int extrapolate = class="num">0) class=class="str">"cmt">// *
{
...
N = ArraySize(y);
class="type">int Nf = N;                            class=class="str">"cmt">// + preserve actual number of class="kw">input data points
N += extrapolate;                      class=class="str">"cmt">// + 
arrayprepare();
for(i = class="num">0; i < N; i++)
X[i] = i;
Mean = class="num">0;
for(i = class="num">0; i < Nf; i++)                class=class="str">"cmt">// * was N
Mean += (y[i] - Mean) / (i + class="num">1.0);
for(i = class="num">0; i < N; i++)
{
a = y[MathMin(i, Nf - class="num">1)] - Mean;    class=class="str">"cmt">// * was y[i]
Imf[i] = a;
IMFResult[i] = a;
}
class=class="str">"cmt">// The loop of decomposition
...
extrema(...);
...
for(i = class="num">0; i < N; i++)
{
IMFResult[i + N * nIMF] = IMFResult[i];
IMFResult[i] = y[MathMin(i, Nf - class="num">1)] - Mean; class=class="str">"cmt">// * was y[i]
}
}
class CEMD
{
class="kw">private:
class="type">int N;       class=class="str">"cmt">// Input and output data size
class="type">int Nf;      class=class="str">"cmt">// +
class="kw">public:
class="type">int decomp(class="kw">const class="type">class="kw">double &y[], class="kw">const class="type">int extrapolate = class="num">0)
{
...
N = ArraySize(y);
Nf = N;                            class=class="str">"cmt">// + preserve actual number of class="kw">input data points in the object
N += extrapolate;                  class=class="str">"cmt">// +
...
}
};

IMF 提取时的数组方向控制权

CEMD 类的 getIMF 方法在经验模态分解里负责把某一阶本征模态函数(IMF)写回传入的引用数组 x[]。nn 指定要取的阶数,reverse 参数默认 false,决定返回序列的时间方向。 当 reverse 为 true 时,方法末尾会调用 ArrayReverse(x) 把数组就地反转。这意味着你拿到的 IMF 序列可以是「从老到新」也可以是「从新到老」,取决于后续是要做卷积对齐还是直接画图。 实盘写指标时若忽略这一行,常出现 IMF 曲线与价格轴左右颠倒——尤其在做多阶重构时,错位会导致残差符号整体反向。开 MT5 把 reverse 切到 true / false 各跑一次,对比 SubWindow 里的相位即可验证。

MQL5 / C++
class="type">void CEMD::getIMF(class="type">class="kw">double &x[], class="kw">const class="type">int nn, class="kw">const class="type">bool reverse = class="kw">false) class="kw">const
{
...
if(reverse) ArrayReverse(x); class=class="str">"cmt">// +
}

常见问题

可用经验分解模式(EMD)把行情拆成若干本征模态(IMF),高频IMF当噪声过滤,低频IMF看主要趋势,直接在图表上分层观察即可。
预测工具箱里通常还缺外推预测与数组方向控制两块:前者给IMF加未来延伸,后者决定数据是时间正序还是倒序提取,补上才能实战。
小布可自动对打开的品种跑EMD类分解,把IMF分层画出并高亮低频趋势段,你只需看结论不用自己写代码。
数组方向控制权没设对会让模态顺序颠倒,预测外推错挂到历史端,回测信号全部偏移,必须在调用时显式指定正序。
只是给各IMF加延伸可能区间,概率倾向而非确定;外汇贵金属高风险,仍要结合价格行为确认再动手。