风险模型与因子归因

风险模型与因子归因的系统拆解:Barra 类多因子风险模型的结构、因子暴露与因子收益的横截面估计、特质风险的贝叶斯收缩、组合风险的因子分解与欧拉分解、边际风险贡献与风险预算、Brinson 式归因与压力测试情景设计,以及风险模型与约束优化的衔接。

风险模型回答一个核心问题:一个组合的风险到底来自哪里。表面上看,你持有了 500 只股票,风险似乎被分散了;但如果这 500 只都是小盘成长股,那么它们会同时下跌——真正的风险是「小盘成长」这一个因子,而不是 500 只个股。

工程上真正难的地方在于:风险模型要同时做到「稳定」和「敏感」。太敏感,协方差矩阵噪声大、优化器会追逐噪声;太稳定,又无法及时反映市场结构的变化。Barra 式的结构化模型之所以成为行业标准,就是因为它在这两者之间找到了一个可调的结构。

本文聚焦风险模型本身:怎么估计、怎么分解、怎么归因、怎么压力测试。从风险模型到最优权重的优化过程,在 组合优化与风险模型 中讨论,本文提供的是那一步的输入与解释框架。

目录

  1. 风险模型的定位与结构
  2. 因子暴露的估计
  3. 因子收益与因子协方差
  4. 特质风险的估计
  5. 组合风险的分解
  6. 风险归因:因子与选股
  7. 边际风险贡献与风险预算
  8. 压力测试与情景分析
  9. 与约束优化的衔接

1. 风险模型的定位与结构

风险模型的基本形式是把个股收益拆成「因子部分 + 特质部分」:

r = X·f + u

r:N 只股票的收益向量
X:N×K 的因子暴露矩阵
f:K 维因子收益
u:特质收益(假设互不相关)

在这个结构下,组合协方差变成:

Σ = X·Σf·Xᵀ + Δ

Σf:K×K 的因子协方差(K 通常 30~60)
Δ:N×N 的对角矩阵(特质风险)

这个结构的核心价值是降维。N = 5000 只股票,直接估计协方差需要 1250 万个参数;用 50 个因子,只需要 1275 个因子协方差参数 + 5000 个特质方差。参数的减少直接带来估计的稳定。

风险模型按因子来源分三类:

类型因子来源优点缺点
基本面行业 + 风格(Barra)可解释、稳定需要维护暴露数据
统计PCA / 因子分析无需外部数据因子含义不明确
宏观利率、汇率、商品与宏观联动解释力弱、频率低

基本面模型是主流,因为它的因子有明确的经济含义,能支持归因和约束。统计模型(PCA)在纯预测场景有优势,但无法回答「我为什么承担了这个风险」。

2. 因子暴露的估计

因子暴露矩阵 X 的构造分两类:

行业因子用哑变量,一只股票属于一个行业,对应位置为 1,其余为 0。A 股通常用申万一级(31 个)或中信一级(30 个)行业。

风格因子用标准化后的指标。Barra 的经典风格因子:

风格因子主要指标
Size对数总市值
Beta对市场的历史回归斜率
Momentum过去 12 个月(剔除最近 1 月)收益
Value账面市值比、盈利收益率
Volatility历史波动率、日收益标准差
Liquidity换手率
Growth营收/盈利增速

标准化必须用市值加权,而不是等权。因为组合的收益是按市值加权的,暴露的基准也应该一致:

def standardize_exposure(raw, mktcap):
    # 市值加权均值与标准差
    w = mktcap / mktcap.sum()
    mean = (raw * w).sum()
    std = np.sqrt((w * (raw - mean) ** 2).sum())
    return (raw - mean) / std          # 市值加权的 z-score

暴露的时效性很关键:市值每天变、财报每季度更新。基本面数据必须用公告日对齐,且要处理「报告期与公告日之间」的时间差,否则就是前视偏差。暴露数据的更新与存储可以参考 数据库 里时序数据的建模方式。

3. 因子收益与因子协方差

因子收益 f 无法直接观测,需要用横截面回归从个股收益反推。每个调仓期做一次回归:

import statsmodels.api as sm

def estimate_factor_returns(returns, X, weights=None):
    # 每期横截面回归 r = X·f + u,加权最小二乘
    Xw = sm.add_constant(X)                  # 加截距(可吸收市场均值)
    model = sm.WLS(returns, Xw, weights=weights).fit()
    return model.params, model.resid         # 因子收益与特质收益

加权最小二乘(WLS) 比普通最小二乘更合适:用市值平方根或特异度倒数加权,降低小盘股噪声的影响。A 股的横截面回归尤其要注意极端值——涨跌停、停牌、次新股都会污染回归。

因子协方差 Σf 的估计有几个关键处理:

处理目的方法
指数加权近期数据权重更高EWMA,半衰期 90~180 天
自相关调整修正序列相关Newey-West
波动率偏误调整修正高波动期低估波动率状态调整
特征值调整修正系统性低估特征值去偏

波动率偏误调整是 Barra 模型的一个精妙之处:波动率有均值回归特性,高波动期之后波动率往往下降,低波动期之后往往上升。如果直接用历史数据,会高估当前高波动期的未来波动、低估低波动期的未来波动。调整方法是把预测波动率向长期均值拉回一部分。

def volatility_regime_adjust(cov, current_vol, long_run_vol, beta=0.5):
    # 把当前协方差按波动率偏离长期均值的程度做修正
    scale = (long_run_vol / current_vol) ** beta
    return cov * scale

4. 特质风险的估计

特质风险 Δ 是残差收益的方差。它看似简单(对残差求方差),但直接估计会系统性低估,因为:

  1. 样本期内恰好没有大波动的股票,方差被低估。
  2. 停牌、次新股的样本量不足。
  3. 残差的厚尾使样本方差低估尾部风险。

贝叶斯收缩是标准解法:把每只股票的特质方差向「同组股票的先验」收缩:

def shrink_specific_var(sample_var, prior_var, n_obs, prior_weight=50):
    # 向先验收缩,样本量越少收缩越强
    lam = prior_weight / (prior_weight + n_obs)
    return lam * prior_var + (1 - lam) * sample_var
先验可以按「行业 × 市值组」计算:
小盘股的特质风险天然高于大盘股,先验应该分组而非全局

特质风险也要做自相关调整。残差收益往往有正的序列相关(动量、流动性),这会让「T 天的方差」比「单日方差的 T 倍」更大。Newey-West 调整能修正这个偏差。

5. 组合风险的分解

有了 Σ = XΣfXᵀ + Δ,组合风险可以精确分解成因子部分与特质部分:

def decompose_risk(w, X, Sigma_f, Delta):
    total_var = w @ (X @ Sigma_f @ X.T + Delta) @ w
    factor_var = w @ X @ Sigma_f @ X.T @ w       # 因子贡献
    specific_var = w @ Delta @ w                 # 特质贡献
    return {
        'total_vol': np.sqrt(total_var) * np.sqrt(252),
        'factor_vol': np.sqrt(factor_var) * np.sqrt(252),
        'specific_vol': np.sqrt(specific_var) * np.sqrt(252),
        'factor_ratio': factor_var / total_var,   # 因子风险占比
    }

「因子风险占比」是最有诊断价值的指标。一个分散良好的主动组合,因子风险占比通常在 80%~95%——因为分散化消灭了特质风险,剩下的主要是因子暴露。如果因子占比低于 60%,说明组合的特质风险过高,可能是持仓太集中(个股集中)而不是因子暴露问题。

进一步的分解是按单个因子:

def factor_risk_contribution(w, X, Sigma_f):
    expo = X.T @ w                       # 组合的因子暴露(K 维)
    factor_var = expo @ Sigma_f @ expo
    contrib = expo * (Sigma_f @ expo) / factor_var   # 各因子的风险贡献,和为 1
    return dict(zip(FACTOR_NAMES, contrib))

这个分解告诉你:组合的风险里,多少来自行业暴露、多少来自市值暴露、多少来自动量暴露。如果风险集中在某一个因子上,这个因子反转时组合就会巨亏。

6. 风险归因:因子与选股

风险分解是「事前」的(基于模型预测),归因是「事后」的(基于实际收益)。两者要分开看。

Brinson 归因把主动收益拆成三部分:

主动收益 = 配置效应 + 选股效应 + 交互效应

配置效应:行业权重的偏离 × 行业基准收益
选股效应:行业内的选股 × 行业权重
交互效应:配置与选股的交叉项
def brinson_attribution(w_p, w_b, r_p, r_b, sectors):
    alloc = ((w_p - w_b) * r_b).sum()        # 配置效应
    select = (w_b * (r_p - r_b)).sum()       # 选股效应
    interact = ((w_p - w_b) * (r_p - r_b)).sum()   # 交互效应
    return {'allocation': alloc, 'selection': select, 'interaction': interact}

因子归因是更现代的视角:把收益拆成「因子暴露 × 因子收益 + 特质 alpha」:

def factor_attribution(portfolio_returns, exposures, factor_returns):
    explained = (exposures * factor_returns).sum(axis=1)   # 因子解释的收益
    alpha = portfolio_returns - explained                  # 特质 alpha
    return {
        'factor_return': explained.sum(),
        'alpha': alpha.sum(),
        'alpha_ir': alpha.mean() / alpha.std() * np.sqrt(252),   # alpha 的信息比率
    }

如果 alpha 项接近 0 而因子项很高,说明你的策略只是在承担因子风险。2021 年初「抱团股」崩盘时大量量化产品巨亏,根因就是它们的收益主要来自「大盘成长」这一个因子暴露,而非真正的选股能力。

7. 边际风险贡献与风险预算

边际风险贡献(Marginal Risk Contribution, MRC) 是组合波动对某个持仓权重变化的敏感度:

MRC_i = ∂σ_p / ∂w_i = (Σw)_i / σ_p

风险贡献(RC)= w_i · MRC_i
组合波动 = Σ RC_i     (欧拉分解,贡献之和等于总风险)
def risk_contributions(w, cov):
    port_var = w @ cov @ w
    port_vol = np.sqrt(port_var)
    mrc = (cov @ w) / port_vol          # 边际贡献
    rc = w * mrc                        # 总贡献
    return rc, rc / port_vol            # 绝对贡献与占比

欧拉分解的关键性质是「贡献之和等于总风险」,这让「谁的贡献最大」成为一个有明确定义的问题。这个性质只在「波动率」这个风险度量下成立——用 VaR 或 CVaR 时,分解不再可加(除非假设正态)。

风险预算(risk budgeting) 就是给每个资产/因子分配一个风险贡献目标,然后求解满足这个目标的权重。它比「分配资金」更符合直觉:给低波动资产更多资金、高波动资产更少资金,让每份风险贡献相等。

from scipy.optimize import minimize

def risk_budget(cov, budget):
    # budget: 各资产的目标风险贡献占比,和为 1
    n = len(budget)
    def objective(w):
        rc = w * (cov @ w)
        rc_frac = rc / rc.sum()
        return ((rc_frac - budget) ** 2).sum()
    cons = [{'type': 'eq', 'fun': lambda w: w.sum() - 1}]
    res = minimize(objective, np.ones(n) / n, bounds=[(1e-6, 1)] * n, constraints=cons)
    return res.x

风险预算与组合优化里的风险平价是同一族方法:风险平价是「预算相等」的特例,风险预算是它的推广。

8. 压力测试与情景分析

风险模型基于历史协方差,无法预测从未发生过的极端场景。压力测试弥补这个盲区。

方法做法优点缺点
历史情景用历史极端日的实际收益真实、含相关性只有发生过的
假设情景手工设定因子冲击灵活、可极端相关性靠假设
因子冲击对因子施加冲击后传导结构化依赖模型
反向压力求解「什么情景会让我爆仓」揭示脆弱点计算复杂

假设情景是最实用的:直接对因子施加冲击,看组合损失:

def scenario_loss(w, X, factor_shock, specific_shock=0.0):
    # factor_shock: K 维的因子冲击(如「小盘 -10%、成长 -8%」)
    factor_pnl = (X @ factor_shock) @ w
    specific_pnl = specific_shock * (w ** 2).sum() ** 0.5
    return factor_pnl + specific_pnl

反向压力测试(reverse stress testing) 最有启发性:不问「这个情景下我会亏多少」,而问「什么情景会让我亏损超过 20%」。它把问题反过来,揭示组合最脆弱的暴露方向。

def reverse_stress(w, X, max_loss):
    # 找到让组合损失达到 max_loss 的最小因子冲击方向
    exposure = X.T @ w                       # 组合的因子暴露
    # 最脆弱的方向 = 与暴露方向相反的冲击
    direction = -exposure / np.linalg.norm(exposure)
    magnitude = max_loss / np.linalg.norm(exposure)
    return direction, magnitude

压力测试的结果应该定期审查并转化为约束。如果压力测试显示组合在「小盘股下跌 15%」时会亏损 12%,而你的风控上限是 10%,那么这个暴露就应该被限制。风控侧的限额设计在 交易风控与实时限额 中有详细讨论。

9. 与约束优化的衔接

风险模型是组合优化的输入,它同时提供目标函数(最小化 wᵀΣw)和约束(限制因子暴露):

import cvxpy as cp

def optimize_with_risk_model(mu, w, X, Sigma_f, Delta, max_factor_expo=0.05):
    n, k = X.shape
    cov = X @ Sigma_f @ X.T + Delta          # 由风险模型构造协方差
    objective = cp.Maximize(mu @ w - 5.0 / 2 * cp.quad_form(w, cov))
    constraints = [
        cp.sum(w) == 1,
        w <= 0.03,
        cp.abs(X.T @ w) <= max_factor_expo,  # 限制因子暴露
    ]
    prob = cp.Problem(objective, constraints)
    prob.solve(solver=cp.OSQP)
    return w.value

用结构化风险模型而非样本协方差做优化,本身就是一种正则化。样本协方差含大量噪声,优化器会利用这些噪声(在「假低相关」的股票对上下重注);因子模型把协方差约束在低维结构上,天然抑制了这种噪声利用。

风险模型还支持因子中性约束:强制 Xᵀw = 0(或接近 0),使组合对某些因子免疫。这是「指数增强」产品的标准做法——不做行业与风格暴露,只赚选股 alpha。

优化之外,风险模型还要服务于实时监控:组合上线后,需要持续计算实时的因子暴露与风险分解,与限额对比。这类监控指标的实时计算与告警,与 实盘运维与策略监控 中的健康度监控是同一套体系。

权衡取舍

维度样本协方差因子模型收缩估计
参数数量N(N+1)/2K(K+1)/2 + NN(N+1)/2
稳定性差(N 大时)好中
可解释性无强无
支持归因否是否
适用规模小组合大组合中等

股票数越多,越应该用因子模型:N < 100 时样本协方差够用;N > 500 时因子模型是唯一可行的选择;中间区间可以用收缩估计过渡。这个选择不是偏好问题,而是参数估计的统计必要性。

因子数量的选择也有取舍:因子太少,模型解释力不足、特质风险过高;因子太多,因子协方差本身的估计噪声上升,且因子之间多重共线。30~60 个因子是实践中的合理区间,再多就需要更强的正则化。

常见坑清单

  1. 用等权标准化暴露:暴露基准应与组合加权一致,用市值加权。
  2. 基本面数据用报告期对齐:必须用公告日,否则前视偏差。
  3. 因子收益用 OLS 估计:小盘股噪声大,应使用 WLS。
  4. 特质方差直接估计:系统性低估,需贝叶斯收缩 + 自相关调整。
  5. 忽略波动率均值回归:高波动期高估未来风险,需向长期均值调整。
  6. 只看总风险不看分解:不知道风险来自哪个因子,无法针对性控制。
  7. 归因时 alpha 与因子不分开:把因子暴露的收益当成选股能力。
  8. 风险贡献用 VaR 分解:欧拉分解只对波动率可加,VaR 不可加。
  9. 只做历史压力测试:无法覆盖未发生过的极端场景。
  10. 压力测试结果不转化为约束:发现问题却不限制暴露,等于没测。

小结

风险模型的核心是用低维结构刻画高维风险。它把「5000 只股票的协方差」压缩成「50 个因子的协方差 + 特质风险」,既解决了参数估计的统计问题,又让风险变得可分解、可归因、可约束。Barra 式模型之所以流行几十年,就是因为这个结构在稳定性、可解释性与灵活性之间取得了很好的平衡。

判断一个风险模型是否可用,看三个问题:它的风险分解是否可加(因子 + 特质 = 总风险)?它的归因能否把 alpha 与因子收益分开?它的压力测试结果有没有转化为实际约束? 三个都做到,风险模型才不只是报表,而是真正的决策工具。

下一步建议阅读组合优化与风险模型,看风险模型如何与收益预测一起生成最优权重;如果你关心实盘中的风险监控,可以回顾交易风控与实时限额。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「量化交易」更多文章

  1. 回测偏差与过拟合防范
  2. 市场微结构与流动性
  3. 衍生品定价与希腊字母