用代码计算收益率:简单收益率与对数收益率、累计收益率、年化收益率与波动率,以及滚动窗口
在 pandas 中计算简单收益率和对数收益率,正确地进行复利计算,将收益率和波动率年化,并构建滚动统计量。
你将学到什么
- 如何从收盘价序列计算简单收益率和对数收益率,以及各自在什么情况下才是合适的工具。
- 为什么简单收益率必须用连乘来复利,而对数收益率只需简单相加。
- 如何将日均收益率和日标准差年化,以及“时间平方根”规则隐含了哪些假设。
- 滚动窗口如何生成随时间变化的统计量(例如 20 日波动率),以及为什么最前面的几行是空的。
讲解
简单收益率是价格的百分比变化,rt = (Pt − Pt−1) ÷ Pt−1,在 pandas 中写作 r = df["close"].pct_change()。对数收益率是价格比值的自然对数,ℓt = ln(Pt ÷ Pt−1),在代码中写作 np.log(df["close"]).diff()。两者的第一个值都是 NaN。对于小幅变动,两者几乎相同;对于大幅变动,两者会出现分歧(−50% 的简单收益率对应 −69.3% 的对数收益率)。
复利。跨越多个期间时,简单收益率是相乘的:总收益率 = (1 + r1)(1 + r2)…(1 + rn) − 1。对数收益率是相加的:总对数收益率 = ℓ1 + ℓ2 + … + ℓn,再转换回来得到总简单收益率 = exp(总和) − 1。把简单收益率相加是错误的,而且误差会随波动率增大:+10% 之后再 −10%,相加为 0%,实际结果却是 −1%。累计收益率序列为 (1 + r).cumprod() − 1;从 €10,000 起步的资金曲线为 10_000 * (1 + r).cumprod();使用对数收益率时则为 np.exp(lr.cumsum())。
何时用哪一种。在跨时间汇总和做统计时使用对数收益率;凡是涉及资金往来或组合多项资产时,使用简单收益率,因为投资组合收益率是各项简单收益率的加权和。向人汇报时请使用简单百分比。
年化。惯例上假设股票每年有 252 个交易日(实际天数因交易所和年份而略有不同;请注明你采用的惯例)。日均收益率 × 252 是算术年化收益率,而 CAGR(compound annual growth rate,复合年增长率,已在课程《系统化交易与回测》中定义)为 (期末值 ÷ 期初值)1 ÷ 年数 − 1,这才是回测应当报告的指标。波动率是收益率的标准差;它随时间的平方根缩放,因此年化波动率 ≈ 日标准差 × √252 ≈ 日标准差 × 15.87。月度数据使用 12 和 √12,周度数据使用 52 和 √52。平方根规则假设收益率相互独立,而有趋势的序列违反了这一点;请把它视为一种惯例。
滚动窗口。r.rolling(20).std() * np.sqrt(252) 给出最近 20 天的年化波动率,并在每一行重新计算。前 19 行是 NaN,因为窗口还没有填满(min_periods 可以放宽这一限制)。expanding() 使用截至每一行的全部数据(第 9 课计算回撤时用到的资金运行最大值)。两者都只向后看,因此从构造上就不存在前视偏差。
实例演算
假设示例:一只股票的四个收盘价为 €100.00、€105.00、€100.80、€105.84;不计成本、股息和税费。
简单收益率:(105 − 100) ÷ 100 = +5.00%;(100.80 − 105) ÷ 105 = −4.00%;(105.84 − 100.80) ÷ 100.80 = +5.00%。
对数收益率:ln(1.05) = 0.04879;ln(0.96) = −0.04082;ln(1.05) = 0.04879。总和 = 0.05676。exp(0.05676) − 1 = 0.0584,即 +5.84%。
验证:1.05 × 0.96 × 1.05 = 1.0584 → +5.84%,且 105.84 ÷ 100 − 1 = 5.84%。把简单收益率相加会得到 6.00%,三天就高估了 0.16 个百分点:€10,000 变成 €10,584,而不是 €10,600。
代码:r = c.pct_change(); total = (1 + r.dropna()).prod() - 1 返回 0.0584。r.rolling(3).mean() 在最后一行给出 (0.05 − 0.04 + 0.05) ÷ 3 = 2.00%,之前各行为 NaN。
对一个假设的更长样本进行年化:如果日收益率均值为 0.04%、标准差为 1.2%,则算术年化收益率为 0.0004 × 252 = 10.08%,年化波动率为 0.012 × 15.87 = 19.05%。
常见错误
r.sum() 作为区间收益率。这忽略了复利效应,在波动较大的序列中会高估结果,对于杠杆产品更是严重高估。对简单收益率请使用 (1 + r).prod() − 1,或者把对数收益率相加后再取指数。练习活动
在纸上完成:收盘价为 €50.00、€48.00、€50.40、€49.392(假设数据)。(a) 计算三个简单收益率。(b) 利用 ln(0.96) = −0.04082、ln(1.05) = 0.04879 和 ln(0.98) = −0.02020 计算三个对数收益率。(c) 分别通过复利和对数求和给出总收益率,并与简单收益率的直接相加结果进行比较。(d) 如果另一个序列的日标准差为 1.5%,按 252 天给出其年化波动率,并解释为什么乘以 252 是错误的。
显示答案
(a) 48 ÷ 50 − 1 = −4.00%;50.40 ÷ 48 − 1 = +5.00%;49.392 ÷ 50.40 − 1 = −2.00%。
(b) −0.04082、+0.04879、−0.02020;总和 = −0.01223。
(c) 复利:0.96 × 1.05 × 0.98 = 0.98784 → −1.216%。由对数计算:exp(−0.01223) − 1 = −1.216%,而 49.392 ÷ 50 − 1 = −1.216% 证实了这一点。直接相加得到 −4 + 5 − 2 = −1.00%,低估了亏损。
(d) 年化波动率 = 1.5% × √252 = 1.5% × 15.87 = 23.8%。乘以 252 会得到 378%,这混淆了均值的缩放规则(与时间成线性关系)和标准差的缩放规则(与时间的平方根成正比)。
局限性:√252 规则假设日收益率相互独立;存在自相关的真实序列,其真实年化波动率会有所不同。
要点回顾
pct_change()给出简单收益率,np.log(c).diff()给出对数收益率;两者都会在第一行留下NaN。- 用连乘对简单收益率复利;把对数收益率相加后再取指数;永远不要把简单收益率相加。
- 均值年化用 × 252,标准差年化用 × √252,注明所用惯例,并为回测报告 CAGR。
- 滚动窗口和扩展窗口都只向后看,在窗口填满之前输出
NaN,并默认使用样本标准差。
下一步
第 6 课把标准指标(SMA、EMA、RSI、ATR 和布林带)写成向量化函数,并演示如何对照图表平台进行验证。
自测
回答问题后核对答案。你可以随意重试多次。
供您自行复习的自动自测。您的答案在浏览器中核对,我们不会查看或评估。这不是考试,也不是资格证书。
您的进度
“已完成”仅表示您将本课标记为已学完,并不是对您理解程度的测试。 学习进度和笔记仅保存在此设备的此浏览器中,而非账户中。