第 2 章 · 02 指数加权 EMA 本节摘要:本节讲 的第一个改进版—— 。它保留「外推过去」的直觉,但把等权平均换成指数加权移动平均(EWMA):近期收益权重大、远期收益权重按指数衰减。这更符合「一只股票最近三个月的表现,比十年前的表现更能预示未来」的常识。本节拆解它在 里的实现: 参数如何控制衰减速度、几何与算术两种年化模式、与等权均值的对比,以及作者对「该用大 span 还是小 span」的实务建议——默认 500,如果调仓更频繁可适当调小,但极限趋向等权均值。 内容来源:原项目源码 (函数 )、文档 ,汉化并套用体系化模板。 学习目标 阅读完本节,你应当能够: 说清 指数加权 为何比等权平均更贴合近期。 解释 span 参数的数学含义与衰减系数 α 的关系。
本节摘要:本节讲
mean_historical_return的第一个改进版——ema_historical_return。它保留「外推过去」的直觉,但把等权平均换成指数加权移动平均(EWMA):近期收益权重大、远期收益权重按指数衰减。这更符合「一只股票最近三个月的表现,比十年前的表现更能预示未来」的常识。本节拆解它在expected_returns.py里的实现:span参数如何控制衰减速度、几何与算术两种年化模式、与等权均值的对比,以及作者对「该用大 span 还是小 span」的实务建议——默认 500,如果调仓更频繁可适当调小,但极限趋向等权均值。
内容来源:原项目源码
pypfopt/expected_returns.py(函数ema_historical_return)、文档docs/ExpectedReturns.rst,汉化并套用体系化模板。
阅读完本节,你应当能够:
ema_historical_return 计算年化期望收益。上一节的 mean_historical_return 把所有历史数据等权对待——2010 年的某天收益和昨天的收益权重一样。这与金融常识相悖:公司的业务、行业格局、宏观环境都在变,近期数据比远期数据更代表「现在这只资产」。
指数加权移动平均(EWMA)给每期收益一个随时间指数衰减的权重:
r̂_t = α r_t + (1 - α) r̂_{t-1}
其中 α 是平滑系数(0 < α ≤ 1)。展开看,每期收益的权重是 (1-α)^k(k 是距今的期数)——距今越远,权重越小,且按指数衰减。
ema_historical_return 用 pandas 的 ewm(span=...) 实现,它把平滑系数 α 与更直观的 span 关联起来:
α = 2 / (span + 1)
span 越大,α 越小,衰减越慢,平均窗口越长——趋向等权均值。span 越小,α 越大,衰减越快,只看最近几期——响应更快但更噪。| span | α | 大致等效窗口 | 行为 |
|---|---|---|---|
| 500(默认) | 0.004 | 最近约 2 年 | 平滑,接近等权均值 |
| 180 | 0.011 | 最近约 9 个月 | 中等响应 |
| 30 | 0.065 | 最近 1~2 个月 | 快速响应,较噪 |
💡 pandas 的 span 语义:在 pandas 里,
ewm(span=N).mean()给出的是「N 期加权移动平均」,其中最近一期的权重最大、向前指数衰减。这与「N 日简单移动平均」不同——SMA 是硬截断窗口,EWMA 是无限长但权重衰减的窗口。
打开 pypfopt/expected_returns.py,定位 ema_historical_return(源码 174-223 行):
def ema_historical_return( prices, returns_data=False, compounding=True, span=500, frequency=252, log_returns=False, ): if not isinstance(prices, pd.DataFrame): warnings.warn("prices are not in a dataframe", RuntimeWarning) prices = pd.DataFrame(prices) if returns_data: returns = prices else: returns = returns_from_prices(prices, log_returns) _check_returns(returns) if compounding: return (1 + returns.ewm(span=span).mean().iloc[-1]) ** frequency - 1 else: return returns.ewm(span=span).mean().iloc[-1] * frequency
关键点:
mean_historical_return 完全一致,保证两个函数接口统一。returns.ewm(span=span).mean():对每只资产的日度收益序列算 EWMA,返回一个与原 DataFrame 同形状的对象。.iloc[-1]:只取最后一期的 EWMA 值——因为 EWMA 是递推公式,最后一期已经包含了所有历史的指数加权信息。compounding=True,默认):(1 + ewma_T)^frequency - 1,即把最后一期 EWMA 当成「日度等价复合收益率」,年化成 CAGR。compounding=False):ewma_T × frequency,直接乘 252。⚠️ 几何模式的语义:这里
(1 + ewma_T)^frequency - 1是把「最近一期的指数加权日度收益」当成日度复利基础,再年化。它不是真正的 CAGR(那要对所有历史收益做几何平均),而是「以当前 EWMA 为代表的年化收益」。数值上对近期趋势更敏感。
同一份数据,等权均值与 EMA 的差异:
from pypfopt import expected_returns mu_mean = expected_returns.mean_historical_return(df) mu_ema = expected_returns.ema_historical_return(df, span=500) mu_ema_fast = expected_returns.ema_historical_return(df, span=60)
经验上:
span=60 的 EMA 偏离均值最远,span=500 次之,等权均值最稳。💡 选 span 的经验:作者在文档里说,默认 500 偏保守;若你调仓更频繁(比如月度或周度),有理由调小 span 以捕捉近期趋势。但 span 太小会让 μ 在市场短期波动里剧烈跳变,反而增加换手与噪声。一般 span ∈ [60, 500] 是合理区间,先用 180 试。
默认调用(span=500):
mu = expected_returns.ema_historical_return(df)
快速响应版本(span=60):
mu = expected_returns.ema_historical_return(df, span=60)
直接喂优化器:
from pypfopt import EfficientFrontier, risk_models mu = expected_returns.ema_historical_return(df, span=180) S = risk_models.sample_cov(df) ef = EfficientFrontier(mu, S) ef.max_sharpe()
通过统一入口:
expected_returns.return_model(df, method="ema_historical_return", span=180)
EMA 改进了「对近期不敏感」的问题,但没有解决「外推过去」的根本局限:
docs/ExpectedReturns.rst 给出的官方建议:
指数移动平均是对均值历史收益的简单改进;它给近期收益更高权重,以提高估计的时效性。一般我会倾向于更高的 span——在极限下它趋向均值历史收益。然而,如果你打算更频繁地调仓,有理由降低 span 以捕捉近期趋势。
💡 何时用 EMA:如果你坚持用历史收益(而非 Black-Litterman 或因子模型),且数据足够长(至少几年),EMA(span ∈ [180, 500])通常比等权均值更合理——它至少承认「最近比久远更重要」。但请把它与一个稳健的风险模型(如 Ledoit-Wolf 收缩)搭配,以减少 μ 错误的放大效应。
α = 2/(span+1);span 越大越平滑(趋向等权),越小越响应(但更噪)。compounding=True 几何((1+ewma_T)^freq - 1,默认),False 算术(ewma_T × freq)。.iloc[-1]:EWMA 是递推式,最后一期已包含所有历史的指数加权信息。下一节,我们看 CAPM——一个理论根基最稳、依赖市场 beta 与风险溢价的收益模型。