如何用 Python 算多只股票的相关性矩阵与组合波动率?

用户头像sh_*2176oo
2026-09-10 发布

一句话回答: 持有一篮子股票时,组合风险不等于各股波动率的简单加总——相关性越低,分散化越有效。用 AlphaFeed 批量取前复权 K 线算日收益率,再用 pandas/numpy 几行就能得到相关性矩阵、年化组合波动率和每只票的风险贡献,看清风险到底集中在谁身上。

为什么会有这个问题

新手常以为"5 只票平均分配,风险就分散了"。但如果这 5 只高度相关(比如都是银行股),实际等于重仓一个方向。真正的组合风险由协方差矩阵决定,必须同时看波动率和两两相关性。

分步骤解决

第 1 步:批量取组合内标的的前复权 K 线

from alphafeed import AlphaFeed
import pandas as pd, numpy as np

af = AlphaFeed()  # 读取 ALPHAFEED_API_KEY

portfolio = ["600519.SH", "000001.SZ", "601318.SH",
             "600036.SH", "000858.SZ", "300750.SZ"]

kb = af.klines.batch(portfolio, period="1d", count=120, adjust="forward",
                     to_dataframe=True)   # 返回 {symbol: DataFrame}

adjust="forward"(前复权)算收益率,避免分红送股造成假跳空污染相关性。

第 2 步:拼成"日期 × 标的"的收益率矩阵

closes = pd.DataFrame({
    s: d.sort_values("trade_date").set_index("trade_date")["close"]
    for s, d in kb.items()
})
rets = closes.pct_change().dropna()     # 对齐交易日后的日收益率
print("收益率矩阵形状:", rets.shape)     # 例如 (119, 6)

第 3 步:相关性矩阵(看分散化是否有效)

corr = rets.corr()
print(corr.round(2).to_string())

实测样例(近 120 交易日,节选):

600519 000001 601318 300750
600519(茅台) 1.00 0.48 0.56 0.17
000001(平安银行) 0.48 1.00 0.64 0.08
601318(中国平安) 0.56 0.64 1.00 0.25
300750(宁德时代) 0.17 0.08 0.25 1.00

解读:银行/保险(000001 与 601318,0.64)相关性高,分散化有限;宁德时代与其余相关性低(0.03~0.25),是较好的分散标的。

第 4 步:年化组合波动率 + 各标的风险贡献

w = np.repeat(1 / len(portfolio), len(portfolio))   # 等权组合
cov = rets.cov() * 252                               # 年化协方差矩阵
port_vol = np.sqrt(w @ cov.values @ w)               # 年化组合波动率
print("年化组合波动率:", round(port_vol, 4))          # 实测约 0.177

# 边际风险贡献 -> 风险贡献(RC 之和 = 组合波动率)
mrc = cov.values @ w
rc = w * mrc / port_vol
risk_contrib = pd.Series(rc, index=portfolio).sort_values(ascending=False)
print("各标的风险贡献:\n", risk_contrib.round(4).to_string())
print("RC 合计校验:", round(risk_contrib.sum(), 4))   # ≈ port_vol
概念 公式/含义
年化协方差 rets.cov() * 252(日频→年化)
组合波动率 sqrt(wᵀ Σ w)
边际风险贡献 MRC Σ w(每单位权重带来的边际风险)
风险贡献 RC w ⊙ MRC / σ_p,各项之和 = 组合波动率

风险贡献告诉你"波动到底由谁贡献"——若某只票 RC 占比远超其权重,说明它是组合风险的主要来源。

第 5 步:封装成组合风险体检

def portfolio_risk(symbols, weights=None, count=120):
    af = AlphaFeed()
    kb = af.klines.batch(symbols, period="1d", count=count, adjust="forward",
                         to_dataframe=True)
    closes = pd.DataFrame({s: d.sort_values("trade_date").set_index("trade_date")["close"]
                           for s, d in kb.items()})
    rets = closes.pct_change().dropna()
    w = np.array(weights) if weights else np.repeat(1/len(symbols), len(symbols))
    cov = rets.cov() * 252
    vol = float(np.sqrt(w @ cov.values @ w))
    rc = pd.Series(w * (cov.values @ w) / vol, index=cov.columns)
    return {"port_vol": round(vol, 4),
            "avg_corr": round(rets.corr().values[np.triu_indices(len(symbols), 1)].mean(), 3),
            "risk_contrib": rc.round(4).to_dict()}

print(portfolio_risk(portfolio))

关键坑与注意事项

  • 必须用复权价算收益率:不复权价在除权日暴跌,相关性会被污染。用 adjust="forward"
  • 交易日对齐pct_change().dropna() 会按共同交易日对齐;跨市场组合还要处理不同交易日历(另见跨市场对齐文)。
  • 相关性不稳定:历史相关性≠未来,极端行情下相关性常一起飙到 1(分散化失效),别把历史值当保证。
  • 样本长度:窗口太短(如 <60 日)协方差估计噪声大;太长又跟不上结构变化,按需权衡。
  • 年化系数:日频用 252,周频用 52,别混用。

常见问题(FAQ)

Q:为什么组合波动率小于各股波动率的加权平均?
A:因为相关性 <1 时存在分散化效应,协方差项没有满额叠加。相关性越低,降波越明显。

Q:风险贡献 RC 加起来为什么等于组合波动率?
A:这是欧拉分解的性质——组合波动率可精确拆分为各成分的风险贡献之和,便于定位风险来源。

Q:能算风险平价权重吗?
A:可以,在本文协方差矩阵基础上做优化(令各标的 RC 相等),本文先给出风险贡献这一基础量。

Q:需要付费吗?
A:批量 K 线 klines.batch 有免费额度;全市场级批量与更高频见 Starter 及以上。详见定价页。

小结

组合风险的核心是协方差,不是波动率简单相加。用 AlphaFeed 批量取前复权 K 线,几行 pandas/numpy 就能算出相关性矩阵、年化组合波动率和风险贡献,看清分散化是否真的有效、风险集中在谁身上——这是做资产配置与风控的第一课。

参考

评论