
用 Python 揭秘均值回归策略:你的收益从何而来?
量化交易的生死线:用 Python 算出你的最优下注比例
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含 500 篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
在量化投资里,有一个「甜蜜的陷阱」几乎让每个人都栽过跟头——回测时业绩亮眼,一到实盘就原形毕露。罪魁祸首往往是「未来函数」(Lookahead Bias),也就是策略在计算某一天的信号时,偷偷用到了那一天之后才会出现的数据。
经验模态分解(Empirical Mode Decomposition,简称 EMD)是分析金融时间序列的一把利器,它能把杂乱无章的价格曲线拆解成不同时间尺度的「波」,让我们看清楚市场里同时叠加着的噪声、短线波动、中期周期和长期趋势。但传统 EMD 有个致命软肋:它在分解某个时点时,会用到整段历史(包括未来)的极值点,天然带着未来函数。
于是就有了本文的主角——因果经验模态分解(Causal EMD,简称 CEMD)。它是 EMD 的「在线版本」,在每个时点只用当下及之前的历史数据做分解,从根子上杜绝未来函数。本文将带你从原理到代码,一步步搞懂 CEMD,并在合成数据和六个真实资产(SPY、QQQ、TLT、GLD、BTC、IWM)上构建一套「CEMD–EMA」择时策略,最后用样本外测试检验它到底靠不靠谱。
一句话剧透:CEMD 不是能预测涨跌的水晶球,但它是一个非常好用的风控与市场结构分析工具。
EMD 是希尔伯特—黄变换(HHT)的核心部件。它把一段时间序列 (比如价格或收益率序列)分解为若干个自适应的振荡分量,这些分量叫做本征模态函数(Intrinsic Mode Function,IMF),每个 IMF 对应一个不同的特征时间尺度:
其中 是第 个本征模态函数, 是残差(通常代表长期趋势)。
一个合格的 IMF 要满足两个条件:其一,极值点的个数与过零点的个数最多相差 1;其二,由局部极大值和极小值分别构成的上下包络线,其均值在任何点都约等于 0。这两个条件保证了 IMF 表现得像一个局部对称的振荡,具有明确的瞬时频率。
把股价想象成几种以不同速度运行的隐藏力量叠加的结果,就很好理解了:
需要提醒的是:每个 IMF 的具体金融含义,会因资产、采样频率和分解参数的不同而变化,上面的对应关系是一种典型解读,不是铁律。
EMD 的优点在于:自适应分解(不需要预设滤波器)、能处理非平稳信号(这对波动率不断切换的金融市场很关键)、天然做多尺度分析、每个分量都可独立解读。
EMD 的致命缺点就是前面提到的未来函数:在完整历史数据上做分解时, 时刻的结果会被未来价格「污染」,导致回测虚高、信号不可实盘复现、实盘表现与历史模拟严重背离。
实际操作 EMD 需要安装 EMD-signal 这个包:
!pip install EMD-signalEMD 内部靠一个叫「筛分」(sifting)的迭代过程工作:找出局部极大值和极小值 → 构造上下包络线 → 计算平均包络 → 从信号中减去平均包络 → 重复,直到满足 IMF 条件。
下面是一个最小示例:
import numpy as npimport matplotlib.pyplot as pltfrom PyEMD import EMD, Visualisation# 构造一个示例输入信号t = np.arange(0, 3, 0.01)S = np.sin(13 * t + 0.2 * t**1.4) - np.cos(3 * t)# 执行 EMD 分解emd = EMD()emd.emd(S)imfs, res = emd.get_imfs_and_residue() # 取出各个 IMF 和残差# 可视化plt.plot(t, S)vis = Visualisation()vis.plot_imfs(imfs=imfs, residue=res, t=t, include_residue=True) # 画 IMFvis.plot_instant_freq(t, imfs=imfs) # 画瞬时频率vis.show()易错点提醒:EMD 本质上是串行算法,速度较慢且很难优化——因为筛分本身是迭代的,而且下一个 IMF 依赖上一个的计算结果,无法简单并行化。数据量大时要有心理准备。
CEMD 的思路其实很朴素:在 时刻,只用 时刻及之前的历史数据 做分解。实践中通常采用一个长度为 的滚动历史窗口:
在每个窗口上做完 CEMD 分解后,只保留最新一个样本点的各 IMF 数值,然后窗口向前滑动一格,重复整个过程。
它的完整工作流是这样的:收集历史数据并构造最近 个观测的滚动窗口 → 在窗口上做 CEMD 分解 → 只提取每个 IMF 和残差的最新一个值 → 根据时间尺度生成多尺度特征 → 组合特征生成交易信号 → 新数据到来时,丢掉最老的一个观测、追加最新的一个,重复上述过程。
由于每一次分解都只基于历史数据,之前生成的交易信号永远不会被「回头修改」——这正是它避免未来函数的关键。
下面这段代码,就是把标准 EMD 改造成滚动窗口在线分解的核心:
import numpy as npimport pandas as pdfrom PyEMD import EMDdef causal_emd(series, window=600, max_imfs=5): """ 滚动窗口的因果 EMD 在 t 时刻:IMF(t) = EMD(series[t-window:t]) 绝不使用任何未来观测值。 """ emd = EMD() causal_imfs = [] for t in range(len(series)): # 历史数据不够长,先用 NaN 占位 if t < window: causal_imfs.append([np.nan] * max_imfs) continue # 仅取历史窗口的数据 x = series.iloc[t - window:t].values decomposition = emd(x) # 提取当前各 IMF 的最新值 values = [] for i in range(max_imfs): if i < decomposition.shape[0]: values.append(decomposition[i, -1]) # 该 IMF 的最后一个值 else: values.append(np.nan) causal_imfs.append(values) return pd.DataFrame( causal_imfs, index=series.index, columns=[f"IMF_{i+1}" for i in range(max_imfs)] )CEMD 的优点:无未来函数、自适应分解、多尺度信息、非参数(不需要预设频率或平稳性假设)、能过滤噪声、天然识别市场状态、比黑箱机器学习模型更可解释。
CEMD 的代价(务必牢记):计算成本高,其复杂度约为 ,其中 是序列长度、 是窗口大小、 是 IMF 个数、 是每个 IMF 的平均筛分迭代次数;此外还有窗口长度敏感、边界效应、模态混叠(mode mixing)、IMF 顺序不保证、因果性带来的滞后等问题。最重要的一句话是:CEMD 是信号分解技术,不是预测模型。
为了验证 CEMD 是否真能还原出隐藏成分,我们先构造一个「答案已知」的合成价格序列——它由长期趋势、商业周期、短期周期、噪声四个成分叠加而成:
import numpy as npnp.random.seed(42)N = 2500t = np.arange(N)# IMF4:长期市场趋势trend = 0.0025 * t + 2.5 * np.sin(2 * np.pi * t / 1800)# IMF3:商业周期business_cycle = 1.4 * np.sin(2 * np.pi * t / 420) + 0.6 * np.sin(2 * np.pi * t / 700)# IMF2:短期市场周期short_cycle = 0.7 * np.sin(2 * np.pi * t / 80) + 0.35 * np.sin(2 * np.pi * t / 35)# IMF1:市场噪声 + 动量脉冲(AR(1) 过程模拟持续性动量)white = np.random.normal(scale=0.35, size=N)momentum = np.zeros(N)phi = 0.88eps = np.random.normal(scale=0.18, size=N)for i in range(1, N): momentum[i] = phi * momentum[i - 1] + eps[i]noise = white + momentum# 最终的合成金融时间序列price = 100 + trend + business_cycle + short_cycle + noise对这段合成数据做 EMD 分解并做滤波后,我们发现了一个漂亮的结果:去掉 IMF_1 后剩下被移除的分量,与真实的白噪声成分之间的皮尔逊相关系数高达 0.8153;去掉 IMF_1 + IMF_2 后,被移除分量与完整噪声成分 的相关系数为 0.6327。后者相关性略低是意料之中的——因为 里除了白噪声,还含有一个持续性的动量成分,这部分会被分散到多个 IMF 尺度上去。
方法论上的诚实提醒:皮尔逊相关衡量的是线性相似度,并不等于因果对应或精确还原。若要更严谨,应当补充 RMSE、方差解释率等指标,逐一对照每个已知的真实成分。
更关键的对比是:把标准 EMD 和 CEMD 在相同滤波配置下的输出相减,差异不到 1.5%,可以忽略不计。这说明 CEMD 并没有丢失标准 EMD 能捕捉到的重要信号结构,只是它诚实地不去偷看未来而已。
有了可靠的分解,我们就能构建第一个策略:把短期分量(IMF_1 + IMF_2)作为「快线」,长期分量(IMF_4 + IMF_5)作为「慢线」,快线上穿慢线做多、下穿则空仓。
import numpy as npfrom PyEMD import EMDdef causal_emd_group_crossover(price, window=500, fast_imfs=[0, 1], slow_imfs=[3, 4]): """ 因果 EMD 的 IMF 分组交叉策略 快线 = IMF1 + IMF2 慢线 = IMF4 + IMF5 """ n = len(price) fast_series = np.full(n, np.nan) slow_series = np.full(n, np.nan) emd = EMD() for t in range(window, n): x = price[t - window:t] # 因果滚动窗口 imfs = emd.emd(x) if imfs.shape[0] < 5: # 需要至少 5 个 IMF continue fast_component = np.sum(imfs[fast_imfs], axis=0) # 快线分量重构 slow_component = np.sum(imfs[slow_imfs], axis=0) # 慢线分量重构 fast_series[t] = fast_component[-1] # 只取最新值(因果) slow_series[t] = slow_component[-1] # 生成持仓信号 position = np.zeros(n) for t in range(1, n): if np.isnan(fast_series[t]): continue position[t] = 1 if fast_series[t] > slow_series[t] else 0 return fast_series, slow_series, position在计算收益时,务必把持仓信号向后平移一天(shift(1))再乘以次日收益——这是防止未来函数的关键一步:
import pandas as pdreturns = pd.Series(price).pct_change()live_position = pd.Series(position).shift(1).fillna(0) # 信号滞后一天执行strategy_returns = returns * live_positionequity = (1 + strategy_returns).cumprod() # 净值曲线样本内结果令人振奋:CEMD 策略实现了 19.51% 的总收益(买入持有仅 8.54%),同时波动率更低(4.99% 对 8.06%)、最大回撤更小(−5.92% 对 −7.78%),夏普比率从 0.143 提升到 0.385。妙就妙在,更高的收益并非靠承担更多风险换来的,而是波动率显著更低。
但这还只是样本内结果。真正的考验是样本外(Out-of-Sample,OOS)测试:把数据按 85% / 15% 切分,参数只在训练段确定,然后原封不动拿到从未见过的测试段上跑。样本外结果显示,CEMD 策略仅用 11.2% 的时间暴露在市场中,就取得了正收益,最大回撤仅 −1.78%(买入持有为 −5.39%),夏普比率 0.358 对 0.052。不过要清醒地认识到:这个样本外结论只基于 3 笔交易,属于初步证据,它更多说明的是「风险降低和选择性暴露」,而非确凿的超额收益能力。
接下来把方法搬到真实市场。这里的核心工具是「因果重构 + 双均线交叉」:先用滚动 CEMD 重构出一条平滑的价格信号(保留选定的 IMF 加残差),再在这条信号上算快、慢两条指数移动平均线(EMA),快线上穿慢线做多、下穿空仓。
from PyEMD import EMDimport numpy as npimport pandas as pddef causal_emd_reconstruct(series, window=500, keep_imfs=(2, 3, 4, 5)): """ 因果 EMD 价格重构(可自选保留哪些 IMF) 只用历史窗口,绝不使用未来观测。 """ n = len(series) reconstructed = np.full(n, np.nan) residual = np.full(n, np.nan) emd = EMD() values = series.values for t in range(window, n): x = values[t - window:t] # 因果窗口 imfs = emd.emd(x) n_imfs = imfs.shape[0] if n_imfs == 0: continue # 重构选定的 IMF smooth = np.zeros(imfs.shape[1]) for i in keep_imfs: if i < n_imfs: smooth += imfs[i] # 残差(趋势) reconstructed_imfs = np.sum(imfs, axis=0) res = x - reconstructed_imfs reconstructed[t] = smooth[-1] + res[-1] # 只取最新值 residual[t] = res[-1] return (pd.Series(reconstructed, index=series.index), pd.Series(residual, index=series.index))信号生成时同样要滞后一期执行,杜绝未来函数:
# 在重构信号上计算快、慢 EMAema_fast = reconstructed.ewm(span=fast_span, adjust=False).mean()ema_slow = reconstructed.ewm(span=slow_span, adjust=False).mean()# 快线在慢线上方则做多(1),否则空仓(0)signal = (ema_fast > ema_slow).astype(int)# 关键:信号滞后一期再乘以收益,防止未来函数strategy_returns = (signal.shift(1) * returns).fillna(0)通过对窗口长度、IMF 组合、快慢 EMA 跨度做网格搜索(以夏普比率排序选优),六个资产的样本内表现呈现出清晰的规律。这里挑几个典型说说:
一个反复出现的现象值得注意:这些策略的胜率往往很低(有的甚至不到 20%),单笔期望值为负,但总体表现却很好。这说明它们靠的是抓住少数几波大行情,而不是频繁地赢小钱——典型的趋势跟随特征。
前面的样本内结果再漂亮,也可能是「过拟合」的幻觉。真正的试金石是:参数只在训练段优化,冻结后原封不动地丢到从未见过的样本外数据上。这里所有资产都遵循同一套严格流程——训练段选参 → 冻结参数 → 用因果 CEMD 在全序列上生成信号 → 只截取样本外区间评估。
样本外的核心发现如下:
从训练段到样本外,业绩普遍出现回落,这完全正常且符合预期。真正有价值的信息是:在多数资产上,降低风险这个特性成功地延续到了未见过的数据上。
本文系统梳理了从标准 EMD 到因果 CEMD 的完整链条,并在合成数据与六大真实资产上做了从分解、滤波、构建策略到样本外验证的全流程实验。可以提炼出几条核心结论:
第一,CEMD 的价值在于「诚实地分解」,而非「预测未来」。它是一种自适应、可解释的多尺度信号分解框架,能把噪声、短线波动、中期周期和长期趋势清晰地分离开,而全程只使用实盘中真实可得的历史信息,从根本上杜绝了未来函数。
第二,CEMD–EMA 策略最稳定、最可靠的贡献是「风险可控的市场参与」,而不是普适的超额收益。在多个资产上,即便它没有跑赢买入持有的绝对收益,也显著降低了波动率、最大回撤、尾部风险和市场暴露时间。QQQ、GLD、BTC 表现最强,SPY 和 IWM 则更偏向风控。
第三,不同市场对同一分解尺度的反应差异很大,这提醒我们把 CEMD 当作一个需要因地制宜的自适应框架,而非一条放之四海皆准的交易铁律。
最后,务必牢记它的局限:滚动窗口计算开销大、对窗口长度和 IMF 组合敏感、存在边界效应与模态混叠、IMF 顺序不保证,而且——它不是预测工具,不要把它的输出当成方向性的涨跌判断。对于学习 Python 的量化爱好者来说,CEMD 更适合作为你工具箱里的一把「结构分析与风控」的手术刀,而不是点石成金的魔法棒。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐