第 3 章 · 03 指数协方差 本节摘要:本节讲 的指数加权协方差—— 。它的动机与第 2 章的 EMA 收益完全一致:近期数据比远期更能代表当前的资产联动关系。样本协方差把 5 年前和昨天的数据等权对待,但两家公司十年前的业务关系可能早已瓦解。 给近期收益对更高的权重、远期权重指数衰减,从而对「当前真实的联动结构」更敏感。本节拆解它的源码实现:逐对算 (用 )、为何要双重循环(协方差没有现成的 pandas 向量化接口)、span 参数(默认 180)与第 2 章 EMA 的 span(默认 500)的差异,以及作者论文背景。 内容来源:原项目源码 (函数 、 )、文档 ,汉化并套用体系化模板。 学习目标 阅读完本节,你应当能够: 解释指数协方差对近期联动更敏感的动机。
本节摘要:本节讲
risk_models.py的指数加权协方差——exp_cov。它的动机与第 2 章的 EMA 收益完全一致:近期数据比远期更能代表当前的资产联动关系。样本协方差把 5 年前和昨天的数据等权对待,但两家公司十年前的业务关系可能早已瓦解。exp_cov给近期收益对更高的权重、远期权重指数衰减,从而对「当前真实的联动结构」更敏感。本节拆解它的源码实现:逐对算_pair_exp_cov(用ewm(span).mean())、为何要双重循环(协方差没有现成的 pandas 向量化接口)、span 参数(默认 180)与第 2 章 EMA 的 span(默认 500)的差异,以及作者论文背景。
内容来源:原项目源码
pypfopt/risk_models.py(函数exp_cov、_pair_exp_cov)、文档docs/RiskModels.rst,汉化并套用体系化模板。
阅读完本节,你应当能够:
_pair_exp_cov 的算法:协变积的 EWMA。exp_cov 算指数加权协方差矩阵。exp_cov 的 span(默认 180)与 ema_historical_return 的 span(默认 500)。资产之间的相关性不是恒定的。最典型的例子:
样本协方差把所有历史数据等权,无法捕捉这种「联动结构随时间变化」。指数加权给近期更高权重,更聚焦「现在的联动」。
打开 pypfopt/risk_models.py,定位 exp_cov 与 _pair_exp_cov(源码 256-327 行)。
_pair_exp_covdef _pair_exp_cov(X, Y, span=180): covariation = (X - X.mean()) * (Y - Y.mean()) if span < 10: warnings.warn("it is recommended to use a higher span, e.g 30 days") return covariation.ewm(span=span).mean().iloc[-1]
算法分三步:
(X - μ_X) × (Y - μ_Y)——两个序列的中心化乘积,逐期算。正值表示同期同向偏离(正相关),负值表示反向(负相关)。ewm(span).mean(),近期协变积权重更大。.iloc[-1]——EWMA 是递推式,最后一期已包含全部历史的指数加权信息,就是当前的「指数协方差」估计。⚠️ span < 10 警告:太小的 span 会让 EWMA 只看最近几期,协方差估计噪声爆炸。源码主动发警告推荐至少 30 天。
exp_covdef exp_cov(prices, returns_data=False, span=180, frequency=252, log_returns=False, **kwargs): ... N = len(assets) S = np.zeros((N, N)) for i in range(N): for j in range(i, N): S[i, j] = S[j, i] = _pair_exp_cov( returns.iloc[:, i], returns.iloc[:, j], span ) cov = pd.DataFrame(S * frequency, columns=assets, index=assets) return fix_nonpositive_semidefinite(cov, kwargs.get("fix_method", "spectral"))
要点:
returns.cov()),必须逐对算。利用对称性 S[i,j] = S[j,i],只算上三角,N(N+1)/2 次。* frequency,与 sample_cov 一致。fix_nonpositive_semidefinite 修复。💡 为什么不能用 returns.ewm().cov():pandas 确实有
returns.ewm(span=...).cov()的接口,但它返回的是逐期的协方差序列(每个时点一个矩阵),不是「整体」的指数加权协方差矩阵。PyPortfolioOpt 想要的是「用最后一期 EWMA 代表当前协方差」,所以手动逐对算。这也是 N 大时(比如 100 资产,需 5050 次循环)该方法较慢的原因。
exp_cov 的 span 默认 180,而 ema_historical_return 的 span 默认 500。两者都是「指数加权」,为何默认值不同?
| 方法 | span 默认 | 直觉 |
|---|---|---|
ema_historical_return |
500 | 收益的长期均值相对稳定,用更长窗口 |
exp_cov |
180 | 联动结构变化更快,用更短窗口捕捉近期 |
这反映了作者的实务判断:资产间的相关性比单只资产的均值更容易且更快地变化,所以协方差用更小的 span。当然,这只是默认值,实务中可根据你的调仓频率调整。
| 参数 | 默认 | 含义 | 备注 |
|---|---|---|---|
prices |
— | 价格 DataFrame | 必填 |
returns_data |
False |
True 表示输入已是收益 | |
span |
180 |
指数加权窗口 | 小于 10 会警告 |
frequency |
252 |
年化系数 | 月度改 12 |
log_returns |
False |
是否用对数收益 | |
fix_method |
"spectral" |
PSD 修正方法 |
默认调用:
from pypfopt import risk_models S_exp = risk_models.exp_cov(df)
调短 span 捕捉近期联动:
S_exp = risk_models.exp_cov(df, span=60)
通过统一入口:
risk_models.risk_matrix(df, method="exp_cov", span=90)
直接喂优化器:
from pypfopt import EfficientFrontier, expected_returns mu = expected_returns.capm_return(df) S = risk_models.exp_cov(df, span=90) ef = EfficientFrontier(mu, S) ef.max_sharpe()
同一份数据,两种协方差矩阵的热力图差异明显:
作者在博客 Exponential Covariance 里写到,这个估计的灵感来自 EMA 价格相对 SMA 价格的优势——给近期更高权重,通常更贴近「当前市场状态」。
💡 何时用 exp_cov:如果你相信近期联动结构比长期平均更能预示未来(例如交易频率较高、市场处于状态切换期),
exp_cov比sample_cov更合理。但要注意 span 调节——太小噪声大,太大退化为样本协方差。
docs/RiskModels.rst 把 exp_cov 描述为 PyPortfolioOpt 的「实验性替代方案」之一(与 semicovariance 并列):
指数协方差矩阵是一种新颖的方法,在计算协方差时给近期数据更高权重,就像指数移动平均价格常优于简单平均价格一样。完整解释请参考作者的博客文章。
它的局限:
⚠️ 生产首选仍是收缩:虽然 exp_cov 直觉合理,但作者在多个场合强调,Ledoit-Wolf 收缩是更稳健的生产默认(偏差-方差权衡更优)。exp_cov 适合作为「快速捕捉近期」的备选,或与收缩估计做对比验证。
_pair_exp_cov 对协变积 (r_i-μ_i)(r_j-μ_j) 做 ewm(span).mean(),取最后一期。fix_nonpositive_semidefinite 修复。下一节是本章重头戏——收缩估计(Ledoit-Wolf 与 OAS),这是 PyPortfolioOpt 文档反复强调的生产默认风险模型。