在 pandas 中做投资组合分析:相关系数矩阵与协方差矩阵、加权投资组合波动率和再平衡模拟
根据收益率计算年化协方差矩阵和相关系数矩阵,用 NumPy 推导投资组合波动率和风险贡献,并模拟权重漂移与日历再平衡的差异。
你将学到什么
- 如何把多个价格序列对齐到同一个收益率 DataFrame 中,并正确地年化计算
.cov()和.corr()。 - 投资组合波动率的矩阵公式
w.T @ Σ @ w,以及如何解读每项资产对风险的贡献。 - 如何模拟一个权重自然漂移的买入并持有组合,以及一个按日历再平衡的组合。
- 哪些数值陷阱(日期未对齐、混用货币、协方差窗口中的前视偏差)会导致错误答案。
讲解
相关性与投资组合总风险敞口(课程《风险与交易管理》,第 6 课)、组合两个系统(课程《系统化交易与回测》,第 10 课)以及再平衡规则(课程《ETF、基金与长期投资》,第 10 课)已在其他地方讲过;本课用代码把它们实现为适用于任意数量资产的形式。
从价格到收益率矩阵
把每个品种的复权收盘价(第 4 课)加载到同一个 DataFrame 中,每个代码一列:prices = pd.concat(series_dict, axis=1)。不同市场的假期不同,会产生 NaN 行;请明确地在 prices.dropna()(只保留共同交易日)和 prices.ffill()(把最后价格向前延续)之间做出选择。然后计算 rets = prices.pct_change().dropna()。先把所有数据换算成同一种货币;欧元投资者持有的美国 ETF 必须用 EUR/USD 序列进行换算,否则它的波动率和相关性描述的就是错误的对象。
协方差、相关系数与年化
rets.cov() 是日收益率的样本协方差矩阵;乘以 252 即可年化(方差随时间缩放)。rets.corr() 没有单位,无需缩放。对角线上是每项资产的方差,因此 np.sqrt(np.diag(cov_a)) 给出年化波动率。关系式 Σ_ij = ρ_ij × σ_i × σ_j 让你可以根据波动率和相关系数构建一个矩阵,用来测试诸如“危机中相关系数升至 0.8”之类的假设情景。
投资组合波动率与风险贡献
w = np.array([0.5, 0.3, 0.2]) # 权重,总和为 1cov_a = rets.cov().values * 252port_var = w @ cov_a @ wport_vol = np.sqrt(port_var)marginal = cov_a @ w # d(var)/d(w)risk_contrib = w * marginal / port_var # 各份额之和为 1
@ 是矩阵乘法。risk_contrib 告诉你每项资产承担了投资组合方差的多大比例;它通常与资本权重相差甚远,而这一差距正是风险预算的起点(课程《深入投资组合构建:风险预算、市场状态与压力测试》,第 1 课)。
再平衡模拟
买入并持有会让权重漂移:表现好的资产会占据更大的份额。日历再平衡则在固定日期把权重重置为目标值。在 pandas 中,把收益率重采样到再平衡频率,然后对各个期间进行循环:
period_rets = (1 + rets).resample('ME').prod() - 1 # 月度收益率target = np.array([0.6, 0.4]); hold = 10_000 * target; path = []for _, r in period_rets.iterrows():hold = hold * (1 + r.values) # 各部分分别增长path.append(hold.sum())hold = hold.sum() * target # 再平衡至目标权重
对于权重漂移的版本,删除最后一行即可。季度使用 'QE',年度使用 'YE'(pandas 2.2 及以后版本;更早的版本使用 'M'、'Q'、'A')。一次再平衡的换手为 abs(new_hold - old_hold).sum();乘以一个成本费率(例如 0.1%)后扣除。在德国的应税账户中,再平衡还会实现收益;请向 Steuerberater(税务顾问)核实税务影响。
实例演算
假设示例,输入数据均为编造;不计手续费、点差和税费。
第 1 部分:三项资产的波动率。年化波动率为 20%、10%、15%;相关系数 ρ₁₂ = 0.2,ρ₁₃ = 0.6,ρ₂₃ = 0.1;权重为 50 / 30 / 20%。
- 协方差:Σ₁₂ = 0.2 × 0.20 × 0.10 = 0.0040;Σ₁₃ = 0.6 × 0.20 × 0.15 = 0.0180;Σ₂₃ = 0.1 × 0.10 × 0.15 = 0.0015。对角线:0.0400、0.0100、0.0225。
- 投资组合方差 = Σ wᵢ wⱼ Σᵢⱼ = 0.25 × 0.04 + 0.09 × 0.01 + 0.04 × 0.0225 + 2 × (0.5 × 0.3 × 0.004 + 0.5 × 0.2 × 0.018 + 0.3 × 0.2 × 0.0015) = 0.0100 + 0.0009 + 0.0009 + 2 × (0.0006 + 0.0018 + 0.00009) = 0.01678。
- 投资组合波动率 = √0.01678 = 12.95%,而各波动率的加权平均为 16.0%;两者之差就是分散化带来的好处。
- 风险贡献:资产 1 以 50% 的资本承担了 73.9% 的方差;资产 2 以 30% 的资本承担了 9.5%;资产 3 以 20% 的资本承担了 16.6%。
第 2 部分:权重漂移与再平衡。€10,000 按 60 / 40 分配给资产 A 和资产 B。年收益率:A 为 +20%、−10%、+15%;B 为 +2%、+4%、+1%。
- 权重漂移:A:6,000 × 1.20 × 0.90 × 1.15 = €7,452;B:4,000 × 1.02 × 1.04 × 1.01 = €4,285.63。合计 €11,737.63;A 的权重已漂移至 63.5%。
- 每年再平衡:第 1 年合计 = 7,200 + 4,080 = €11,280 → 重置为 6,768 / 4,512;第 2 年 = 6,091.20 + 4,692.48 = €10,783.68 → 重置;第 3 年 = 7,440.74 + 4,356.61 = €11,797.35。
- 再平衡最终多出 €59.72,因为它在 A 上涨 20% 后卖出,并在下跌 10% 后买回。如果 A 持续单边上涨,权重漂移反而会胜出;再平衡控制的是风险,并不保证收益。
常见错误
rets.cov()、再用结果设定 2015 年权重的回测,使用的是来自未来的信息。请在滚动窗口或扩展窗口上估计协方差(rets.rolling(252).cov() 或 rets.loc[:date].cov()),并只将其应用于随后的期间。危机中相关性也会上升,因此用平静时期估计的矩阵会在最关键的时刻低估风险。练习活动
在纸上完成:两项资产的年化波动率分别为 18% 和 6%,相关系数 0.3,权重 70 / 30%。(a) 构建 2 × 2 协方差矩阵。(b) 计算投资组合的方差和波动率。(c) 计算每项资产的风险贡献在方差中所占的份额。(d) 在相关系数为 0.9 时重新计算波动率,并用一句话说明这为什么重要。
显示答案
(a) Σ₁₁ = 0.0324;Σ₂₂ = 0.0036;Σ₁₂ = 0.3 × 0.18 × 0.06 = 0.00324。
(b) 方差 = 0.49 × 0.0324 + 0.09 × 0.0036 + 2 × 0.7 × 0.3 × 0.00324 = 0.015876 + 0.000324 + 0.0013608 = 0.0175608;波动率 = 13.25%。
(c) 边际贡献:资产 1:0.7 × 0.0324 + 0.3 × 0.00324 = 0.023652;资产 2:0.7 × 0.00324 + 0.3 × 0.0036 = 0.003348。贡献:0.7 × 0.023652 / 0.0175608 = 94.3%;0.3 × 0.003348 / 0.0175608 = 5.7%。
(d) ρ = 0.9 时:Σ₁₂ = 0.00972;方差 = 0.015876 + 0.000324 + 0.0040824 = 0.0202824;波动率 = 14.24%。当相关性上升时,这个“分散化资产”几乎起不到作用,而这恰恰是抛售行情中常见的情形;评判一个投资组合应当依据压力情景下的相关性,而不仅仅是历史平均值。
局限性:用几年日度数据估计的样本协方差噪声很大;窗口的微小变化就可能让相关系数变动 0.1 甚至更多。
要点回顾
- 在计算收益率之前先对齐日期和货币;明确决定如何处理缺失的交易日。
- 把协方差乘以每年期数即可年化;相关系数无需缩放。
- 投资组合波动率为 √(wᵀ Σ w);风险贡献 w × (Σw) / 方差 显示风险真正集中在哪里。
- 通过重采样收益率并重置持仓来模拟再平衡;只用当时可得的数据估计协方差。
下一步
第 12 课把日常流程自动化:一个定时运行的脚本,负责获取数据、计算信号并生成报告,同时妥善处理 API 密钥和速率限制。
自测
回答问题后核对答案。你可以随意重试多次。
供您自行复习的自动自测。您的答案在浏览器中核对,我们不会查看或评估。这不是考试,也不是资格证书。
您的进度
“已完成”仅表示您将本课标记为已学完,并不是对您理解程度的测试。 学习进度和笔记仅保存在此设备的此浏览器中,而非账户中。