为什么风险分解至关重要?#
构建多因子投资组合时,研究者往往聚焦于收益预测——哪个因子未来表现更好、权重如何分配。但一个同样重要却常被忽视的问题是:
我的投资组合风险,究竟来自哪里?
一个看似分散化的多因子组合,可能 60% 的风险来自市场因子(β),剩下 40% 分散在其它因子上。当市场转向时,这个组合的表现本质上仍是一个「披着多因子外衣的大盘跟踪器」。
风险分解(Risk Decomposition)让你看清每一单位风险中,各因子的贡献比例,从而做出更有依据的仓位调整决策。
多因子模型基础回顾#
Fama-French 框架及其扩展#
经典多因子模型的出发点是对资产收益的分解:
其中:
- :资产 i 的超额收益
- :市场因子(市场超额收益)
- (Small Minus Big):规模因子,小市值股票相对大市值的超额收益
- (High Minus Low):价值因子,高账面市值比相对低账面市值比的超额收益
在 A 股实证中,常用的因子还包括:
| 因子代码 | 名称 | 经济学逻辑 |
|---|---|---|
| MKT | 市场因子 | 承担系统性风险的补偿 |
| SMB | 规模因子 | 小市值公司的流动性溢价与风险溢价 |
| HML | 价值因子 | 价值陷阱后的均值回归 |
| MOM | 动量因子 | 趋势延续的行为金融学基础 |
| QMJ | 质量因子 | 高质量公司的稳健经营溢价 |
| BAB | 低波动因子 | 彩票偏好导致高波动股票被高估 |
投资组合层面的因子暴露#
对于包含 N 个资产的投资组合 ,其因子暴露为各资产因子载荷的加权平均:
但因子暴露不等于风险贡献。当因子之间存在相关性时,风险的计算必须考虑协方差矩阵。这正是风险分解的用武之地。
风险分解的数学原理#
投资组合方差的矩阵形式#
设因子收益率向量为 ,其协方差矩阵为 。投资组合的因子暴露矩阵为 ,则投资组合收益方差为:
其中 为特质风险(idiosyncratic risk)的对角矩阵。为聚焦因子层面,忽略特质风险(或在 A 股中假设其可被因子解释),则:
Euler 定理与边际风险贡献#
风险分解的核心数学工具是 Euler 定理。对于齐次函数 ,有:
投资组合风险 是权重向量 的一阶齐次函数。因此:
其中 称为边际风险贡献(Marginal Risk Contribution, MRC)。每一项的 即为资产 i 对总风险的百分比贡献。
因子层面的风险分解#
将 Euler 定理应用到因子层面。定义因子 k 的边际风险贡献:
更直观地,因子 k 的风险贡献百分比为:
其中 为组合在因子 k 上的总暴露。
Python 实现:完整的风险分解框架#
下面用一个完整的 Python 类来实现多因子风险分解,数据使用模拟的 A 股因子收益率。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
class MultiFactorRiskDecomposer:
"""
多因子模型风险分解器
核心功能:
1. 估计资产在因子上的暴露(Beta)
2. 计算因子协方差矩阵
3. 分解投资组合风险至各因子
4. 可视化风险贡献
"""
def __init__(self, factor_names=None):
self.factor_names = factor_names or ['MKT', 'SMB', 'HML', 'MOM', 'QMJ', 'BAB']
self.n_factors = len(self.factor_names)
self.factor_returns = None
self.asset_returns = None
self.beta_matrix = None
def fit_beta(self, asset_returns, factor_returns, method='ols', ri_skuas_penalty=0.001):
"""
估计资产对因子的暴露矩阵 Beta
参数:
- asset_returns: DataFrame, shape (T, N), 资产超额收益
- factor_returns: DataFrame, shape (T, K), 因子收益
- method: 'ols' 或 'ri_skuas' (RI-Skuas 正则化)
"""
self.factor_returns = factor_returns
self.asset_returns = asset_returns
T, N = asset_returns.shape
K = factor_returns.shape[1]
# 添加截距项(Alpha)
X = np.column_stack([np.ones(T), factor_returns.values])
if method == 'ols':
# 普通最小二乘:Beta = (X'X)^(-1) X'Y
beta_with_alpha = np.linalg.solve(X.T @ X, X.T @ asset_returns.values)
self.beta_matrix = beta_with_alpha[1:, :] # 去掉 Alpha 行
self.alpha = beta_with_alpha[0, :]
elif method == 'ri_skuas':
# RI-Skuas 惩罚:对 Beta 施加 L2 正则,防止过拟合
# Beta_penalized = (X'X + lambda*I)^(-1) X'Y
reg_matrix = np.eye(K + 1) * ri_skuas_penalty
reg_matrix[0, 0] = 0 # 不对截距惩罚
beta_with_alpha = np.linalg.solve(X.T @ X + reg_matrix, X.T @ asset_returns.values)
self.beta_matrix = beta_with_alpha[1:, :]
self.alpha = beta_with_alpha[0, :]
self.beta_df = pd.DataFrame(
self.beta_matrix.T,
index=asset_returns.columns,
columns=self.factor_names
)
return self.beta_df
def compute_factor_covariance(self, method='simple', half_life=63):
"""
计算因子收益率协方差矩阵
参数:
- method: 'simple'(等权)或 'ewm'(指数加权,更关注近期)
- half_life: EWM 的半衰期(交易日数)
"""
if method == 'simple':
self.Sigma_f = self.factor_returns.cov().values
elif method == 'ewm':
# 指数加权协方差:近期波动更有信息量
ewm = self.factor_returns.ewm(halflife=half_life, adjust=False)
self.Sigma_f = ewm.cov().values[-self.n_factors:, -self.n_factors:]
self.Sigma_f_df = pd.DataFrame(
self.Sigma_f,
index=self.factor_names,
columns=self.factor_names
)
return self.Sigma_f_df
def decompose_risk(self, portfolio_weights):
"""
将投资组合总风险分解至各因子
参数:
- portfolio_weights: array-like, shape (N,), 投资组合权重
返回:
- risk_contrib: DataFrame,各因子的风险贡献(绝对值与百分比)
"""
w = np.array(portfolio_weights).reshape(-1, 1)
# 组合因子暴露:beta_p = sum(w_i * beta_i)
beta_p = (self.beta_matrix @ w).flatten() # shape (K,)
# 组合方差:sigma_p^2 = beta_p' * Sigma_f * beta_p
sigma_p2 = beta_p @ self.Sigma_f @ beta_p
sigma_p = np.sqrt(sigma_p2)
# 边际风险贡献:MRC_k = (Sigma_f * beta_p)_k / sigma_p
MRC = (self.Sigma_f @ beta_p) / sigma_p # shape (K,)
# 风险贡献百分比:RC_k = beta_p_k * MRC_k / sigma_p^2
RC_pct = beta_p * MRC / sigma_p2 # shape (K,)
# 绝对风险贡献(年化)
annualize_factor = np.sqrt(252)
RC_abs = np.abs(RC_pct) * sigma_p * annualize_factor
self.risk_decomp = pd.DataFrame({
'因子暴露': beta_p,
'边际风险贡献': MRC,
'风险贡献(%)': RC_pct * 100,
'绝对风险(年化)': RC_abs,
}, index=self.factor_names)
self.portfolio_vol = sigma_p * annualize_factor
self.beta_p = beta_p
return self.risk_decomp
def plot_risk_contribution(self, figsize=(10, 5)):
"""绘制风险贡献条形图"""
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
# 左图:风险贡献百分比
colors = plt.cm.Set3(np.linspace(0, 1, self.n_factors))
bars1 = ax1.bar(self.factor_names, self.risk_decomp['风险贡献(%)'],
color=colors, edgecolor='black')
ax1.set_ylabel('风险贡献 (%)')
ax1.set_title(f'因子风险贡献分解 (总风险: {self.portfolio_vol:.2%})')
ax1.tick_params(axis='x', rotation=45)
ax1.grid(axis='y', alpha=0.3)
for bar, val in zip(bars1, self.risk_decomp['风险贡献(%)']):
ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
f'{val:.1f}%', ha='center', fontweight='bold')
# 右图:因子暴露与风险贡献的关系
ax2.scatter(self.risk_decomp['因子暴露'], self.risk_decomp['风险贡献(%)'],
s=100, c=colors, edgecolor='black', alpha=0.7)
ax2.set_xlabel('因子暴露 (Beta)')
ax2.set_ylabel('风险贡献 (%)')
ax2.set_title('因子暴露 vs 风险贡献')
ax2.grid(alpha=0.3)
for fname in self.factor_names:
ax2.annotate(fname,
(self.risk_decomp.loc[fname, '因子暴露'],
self.risk_decomp.loc[fname, '风险贡献(%)']),
xytext=(5, 5), textcoords='offset points', fontsize=8)
plt.tight_layout()
return figpython
图1:多因子模型各因子的风险贡献百分比(MKT因子占据最大风险敞口)
图2:模拟的六大因子累计净值曲线(252个交易日),可观察到不同因子的收益特征差异
使用示例:A股六因子模型风险分解#
下面用模拟数据演示完整流程(实战中可对接 tushare / westock-data 获取真实因子收益率):
# ========== 1. 生成模拟因子收益率 ==========
np.random.seed(20240621)
T = 504 # 2年数据
# 构建有相关性的因子收益率
factor_cov = np.array([
[0.0016, 0.0002, -0.0001, 0.0003, 0.0001, 0.0000], # MKT
[0.0002, 0.0009, -0.0002, 0.0001, 0.0001, 0.0001], # SMB
[-0.0001, -0.0002, 0.0008, 0.0000, 0.0001, 0.0002], # HML
[0.0003, 0.0001, 0.0000, 0.0012, 0.0002, 0.0001], # MOM
[0.0001, 0.0001, 0.0001, 0.0002, 0.0006, 0.0003], # QMJ
[0.0000, 0.0001, 0.0002, 0.0001, 0.0003, 0.0007], # BAB
])
# 用 Cholesky 分解生成相关随机数
L = np.linalg.cholesky(factor_cov)
factor_returns_arr = (L @ np.random.randn(6, T)).T
factor_returns = pd.DataFrame(factor_returns_arr, columns=['MKT', 'SMB', 'HML', 'MOM', 'QMJ', 'BAB'])
# ========== 2. 生成模拟资产收益 ==========
N = 50 # 50只股票
true_beta = np.random.randn(N, 6) * 0.3 + np.array([1.0, 0.2, 0.15, 0.1, 0.1, 0.05])
asset_returns_arr = factor_returns.values @ true_beta.T + np.random.randn(T, N) * 0.02
asset_returns = pd.DataFrame(asset_returns_arr, columns=[f'STOCK_{i}' for i in range(N)])
# ========== 3. 拟合 Beta 并分解风险 ==========
decomposer = MultiFactorRiskDecomposer()
beta_df = decomposer.fit_beta(asset_returns, factor_returns, method='ols')
Sigma_f = decomposer.compute_factor_covariance(method='ewm', half_life=63)
# 构建一个等权组合(可替换为任何权重方案)
portfolio_weights = np.ones(N) / N
risk_decomp = decomposer.decompose_risk(portfolio_weights)
print("=== 多因子风险分解结果 ===")
print(f"投资组合年化波动率: {decomposer.portfolio_vol:.2%}")
print()
print(risk_decomp[['因子暴露', '风险贡献(%)', '绝对风险(年化)']].round(4))
print()
print(f"风险贡献合计: {risk_decomp['风险贡献(%)'].sum():.2f}%")
# 可视化
fig = decomposer.plot_risk_contribution()
plt.savefig('risk_decomp_result.png', dpi=150, bbox_inches='tight')python关键输出解读:
假设输出显示 MKT 的风险贡献为 58.3%,这意味着该等权组合虽然持有了 50 只股票、覆盖了 6 个因子,但过半的波动风险仍来自市场整体走势。如果这是你刻意构建的「市场中性」策略,那这个结果是失败的——你并没有真正中性。
风险预算(Risk Budgeting)配置#
风险分解的自然延伸是风险预算配置:不直接指定资产权重,而是指定各因子(或资产)允许承担的风险贡献比例,然后反解权重。
风险平价在因子层面的应用#
经典风险平价要求每个资产对总风险的贡献相等。在因子层面,我们可以要求:
反解权重是一个凸优化问题:
from scipy.optimize import minimize
def risk_budget_optimize(decomposer, target_risk_budget, w0):
"""
风险预算优化:找到使各因子风险贡献等于目标的权重
参数:
- target_risk_budget: array, 目标风险贡献比例,和为1
"""
K = decomposer.n_factors
def objective(w):
# 当前风险分解
rc = decomposer.decompose_risk(w)['风险贡献(%)'].values / 100
# 目标:||RC - target||^2
return np.sum((rc - target_risk_budget) ** 2)
# 约束:权重和为1,多头(可选)
constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
bounds = [(0, 1) for _ in range(len(w0))] # 多头约束
result = minimize(objective, w0, method='SLSQP',
constraints=constraints, bounds=bounds)
return result.x
# 等风险预算:每个因子贡献 1/6 ≈ 16.67%
target = np.ones(6) / 6
w_opt = risk_budget_optimize(decomposer, target, np.ones(N)/N)
risk_decomp_opt = decomposer.decompose_risk(w_opt)
print("\n=== 风险预算优化后 ===")
print(risk_decomp_opt[['风险贡献(%)']].round(2))python
图3:因子收益率相关系数矩阵热图(A股因子间相关性往往高于美股,需特别关注)
A 股实证中的特殊考量#
1. 因子相关性在 A 股更高#
A 股市场的因子相关性往往高于美股,原因包括:
- 散户占比高,情绪驱动导致因子同向波动
- 政策市特征使价值和动量在某些时期同时失效
- 小市值效应与价值因子高度重叠(小盘股多为价值股)
这导致风险分解结果对协方差矩阵估计更敏感。建议使用 Ledoit-Wolf 压缩估计 替代样本协方差:
from sklearn.covariance import LedoitWolf
def ledoit_wolf_covariance(factor_returns):
"""Ledoit-Wolf 压缩估计,处理高维低样本问题"""
lw = LedoitWolf()
lw.fit(factor_returns)
return lw.covariance_, lw.shrinkage_
Sigma_lw, shrinkage = ledoit_wolf_covariance(factor_returns)
print(f"Ledoit-Wolf 压缩系数: {shrinkage:.4f}") # 0 = 样本协方差,1 = 单位矩阵python2. 特质风险的不可忽视性#
A 股个股的特质波动率显著高于美股(散户交易、信息不对称)。忽略 矩阵(特质风险)会低估总风险,但对因子风险贡献的比例影响有限——因为特质风险在各资产间不相关,其边际贡献对因子分解影响较小。
3. 因子暴露的非平稳性#
A 股因子的 Beta 存在明显的时间变化(regime switch)。建议:
- 用 滚动窗口 估计 Beta(如 126 个交易日)
- 或用 Kalman 滤波 动态估计时变 Beta
- 风险分解结果也应报告置信区间(bootstrap 方法)
常见陷阱#
陷阱 1:用因子收益率相关性替代协方差#
有些研究者直接用因子相关性矩阵做风险分解,这等价于假设所有因子波动率为 1。在 A 股,MKT 的波动率往往是 QMJ 的 2 倍以上,这种做法会严重低估市场因子的风险贡献。
陷阱 2:忽略因子暴露的符号#
如果某因子暴露为负(例如做空 MOM 因子),其风险贡献可能为负(对冲效果)。在报告时,建议用 绝对值 展示各因子的「风险规模」,同时用带符号的版本展示对冲效果。
陷阱 3:把风险贡献当收益贡献#
风险贡献高的因子,不一定收益高。低波动因子(BAB)通常风险贡献较低,但可能在特定时期提供稳定正收益。风险分解是风险管理工具,不是因子排序工具。
总结#
多因子模型的风险分解是连接「因子暴露」与「实际风险」的桥梁。核心要点:
- Euler 定理 保证了风险可以精确分解到每个因子,无残差
- 边际风险贡献(MRC) 告诉你:如果组合在因子 k 上的暴露增加 1 单位,总风险会变化多少
- 风险预算配置 是比传统市值加权更科学的方法,尤其适合多因子策略
- A 股的特殊性(高因子相关性、时变 Beta)要求使用更稳健的协方差估计方法
风险分解不会告诉你哪个因子未来会涨,但它会告诉你:如果市场再次大跌,你的组合会怎样亏钱——以及为什么。
这才是风控的真正价值。
延伸阅读:
- Meucci, A. (2009). Risk and Asset Allocation. Springer. (Euler 分解的经典教材)
- Qian, E. (2006). On the Financial Interpretation of Risk Contribution. Journal of Investment Management. (风险贡献的金融直觉解释)
- Bruder, B. & Roncalli, T. (2012). Managing Risk Exposures Using the Risk Budgeting Approach. Lyxor Research.