第三章:资产相关性矩阵——协方差矩阵的计算、相关系数矩阵与估计方法
各位好,欢迎来到第三章。
上一章我们聊了单个资产的风险度量,也就是波动率。但做组合管理,最核心的挑战从来不是单个资产怎么走,而是资产之间怎么联动。说白了,你辛辛苦苦配了十个低风险资产,结果市场一跌,它们全往一个方向跑,那你的分散化就是个笑话。
我早年刚入行时,就吃过这个亏。当时配了一篮子所谓的“低相关”股票,回测漂亮得很。结果2008年一來,相关系数全部飙到0.8以上,组合回撤比单押一只还惨。嗯,从那以后,我对相关性矩阵的敬畏心就刻在骨子里了。
核心观点:协方差矩阵是组合风险管理的“骨架”。它决定了你的分散化效果到底有多强,也决定了风险预算能否真正落地。
3.1 协方差矩阵:组合风险的数学骨架
先讲基础。协方差矩阵长什么样?
假设我们有n个资产,协方差矩阵就是一个n×n的对称矩阵。对角线元素是每个资产的方差,非对角线元素是两两资产之间的协方差。
# 一个简单的例子:3只股票的协方差矩阵
import numpy as np
# 假设我们有3只股票,100个交易日的数据
np.random.seed(42)
returns = np.random.randn(100, 3) * 0.02 # 模拟日收益率
# 计算协方差矩阵
cov_matrix = np.cov(returns, rowvar=False)
print("协方差矩阵:")
print(cov_matrix)
# 输出类似:
# [[0.000412 0.000123 0.000089]
# [0.000123 0.000387 0.000156]
# [0.000089 0.000156 0.000421]]
你注意看,对角线上的数字(0.000412、0.000387、0.000421)就是各资产的方差。非对角线上的数字(0.000123、0.000089、0.000156)就是协方差。正数表示同向波动,负数表示反向。
我个人习惯,拿到数据第一件事就是看协方差矩阵的“条件数”。条件数太大,说明矩阵接近奇异,后续优化容易出问题。这个坑我踩过,后面会细说。
3.2 相关系数矩阵:把协方差标准化
协方差有个问题——它受量纲影响。比如一只股价100块的股票和一只股价10块的股票,它们的协方差天然就大。这不公平。
所以我们需要相关系数矩阵。它就是把协方差除以各自的标准差,结果落在[-1, 1]之间。
# 从协方差矩阵计算相关系数矩阵
std_devs = np.sqrt(np.diag(cov_matrix))
corr_matrix = cov_matrix / np.outer(std_devs, std_devs)
print("相关系数矩阵:")
print(corr_matrix)
# 输出类似:
# [[1.0 0.308 0.213]
# [0.308 1.0 0.387]
# [0.213 0.387 1.0 ]]
你看,0.308、0.213、0.387——这些数字才是真正有意义的。它们告诉你资产之间的联动强度,不受价格水平影响。
实战技巧:我建议你每次构建组合前,先画一个相关系数热力图。一眼就能看出哪些资产“抱团”,哪些是真正的分散化工具。如果发现超过一半的相关系数大于0.6,嗯,你的组合可能是个“伪分散”。
3.3 协方差矩阵的三种估计方法
好,理论讲完了。现在说实操——协方差矩阵到底怎么算?
你可能会说:“直接用历史数据算不就行了?” 嗯,没那么简单。历史数据有噪音,而且市场结构会变。我见过太多人直接用样本协方差,结果组合优化出来的权重一塌糊涂。
下面我介绍三种主流方法,也是我在实际项目中反复对比过的。
3.3.1 历史协方差(样本协方差)
这是最朴素的方法。直接用过去N天的收益率数据,计算样本协方差。
def historical_cov(returns, window=252):
"""
历史协方差矩阵
returns: DataFrame, 每列一个资产
window: 回看窗口,默认252个交易日
"""
return returns.iloc[-window:].cov()
优点:简单、透明、可复现。
缺点:对极端值敏感,而且假设过去能代表未来。说实话,这在金融里很少成立。
避坑指南:我曾经用252天的历史协方差做风险预算,结果遇到一个“黑天鹅”事件——某天某只股票因为财报造假跌了30%。这个极端值直接拉高了整个协方差矩阵,导致后续一个月的风险预算全部失真。所以,用历史协方差前,一定要做异常值处理。
3.3.2 指数加权协方差(EWMA)
这个方法更聪明。它给近期的数据更高的权重,远期的数据权重指数衰减。说白了,就是“最近发生的事更重要”。
def ewma_cov(returns, lambda_=0.94):
"""
指数加权协方差矩阵
lambda_: 衰减因子,通常取0.94(日数据)或0.97(周数据)
"""
n = len(returns)
weights = np.array([(1 - lambda_) * lambda_ ** (n - 1 - i) for i in range(n)])
weights /= weights.sum() # 归一化
mean_returns = returns.mean(axis=0)
centered = returns - mean_returns
cov = np.dot((centered * weights[:, np.newaxis]).T, centered)
return cov
你可能会问:lambda取多少合适?
RiskMetrics(摩根大通那套体系)推荐日数据用0.94,周数据用0.97。我个人经验是,如果你做高频交易,可以取0.92左右;如果是月频调仓,0.97更合适。这个参数直接影响你对“近期波动”的敏感度。
我的经验:EWMA在捕捉“波动率聚集”效应上表现很好。市场剧烈波动时,它能快速调整协方差估计,让你的风险预算更贴近现实。但要注意——它假设所有资产的衰减速度一样,这在某些情况下不一定合理。
3.3.3 收缩估计(Shrinkage Estimation)
这是我最喜欢的方法,也是学术界公认的“最优”估计之一。
它的思路很简单:样本协方差噪音太大,我们把它往一个“更稳定”的目标矩阵拉一拉。就像做菜时盐放多了,加点水稀释一下。
def shrinkage_cov(returns, shrinkage_target='constant_correlation'):
"""
收缩估计协方差矩阵
shrinkage_target: 'constant_correlation' 或 'identity'
"""
# 样本协方差
sample_cov = returns.cov().values
# 目标矩阵:常相关系数矩阵
if shrinkage_target == 'constant_correlation':
# 计算平均相关系数
corr = returns.corr().values
avg_corr = (corr.sum() - len(corr)) / (len(corr) * (len(corr) - 1))
# 构建目标矩阵
std_devs = np.sqrt(np.diag(sample_cov))
target = np.outer(std_devs, std_devs) * avg_corr
np.fill_diagonal(target, np.diag(sample_cov))
# 计算最优收缩强度(Ledoit-Wolf方法)
# 这里简化处理,实际可用sklearn.covariance.LedoitWolf
shrinkage_intensity = 0.2 # 实际项目中需要优化
# 收缩
shrunk_cov = (1 - shrinkage_intensity) * sample_cov + shrinkage_intensity * target
return shrunk_cov
你想想看,为什么收缩估计效果好?
因为样本协方差矩阵在资产数量多、样本量少的时候,噪音特别大。收缩估计通过引入一个“先验”结构(比如假设所有资产的相关性都等于平均相关性),有效降低了估计误差。
实用建议:如果你管理的资产数量超过样本天数的一半(比如50个资产,只有100天数据),我强烈建议你用收缩估计。Ledoit和Wolf在2004年的论文里证明了,这种情况下收缩估计的预测误差比纯样本协方差低30%以上。
3.4 三种方法的对比与选择
说了这么多,到底该用哪个?
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 历史协方差 | 数据干净、市场稳定 | 简单、可解释 | 噪音大、对异常值敏感 |
| EWMA | 市场波动剧烈、需要快速响应 | 捕捉近期变化 | 参数选择主观、所有资产衰减速度相同 |
| 收缩估计 | 资产多、样本少、需要稳健性 | 估计误差小、稳健 | 计算稍复杂、需要选择目标矩阵 |
我个人习惯是:日常监控用EWMA(lambda=0.94),季度调仓用收缩估计。历史协方差?嗯,我只在回测基准时用,或者做对比实验。
3.5 本章知识体系
下面这张图,是我自己梳理的本章核心逻辑。你可以把它当作一个“思维导图”来用。
这张图把本章的核心脉络串起来了。从协方差矩阵出发,到相关系数矩阵的标准化,再到三种估计方法的对比。你记住一句话就行:在风险管理中,稳健的估计比精确的估计更重要。
公众号:蓝海资料掘金营,微信deep3321