土法炼钢 · 系统与基础设施

【量化交易】风险模型:Barra 多因子、风险归因、压力测试

文章导航

分类入口
quant
标签入口
#risk-model#barra-use4#barra-cne5#covariance-shrinkage#ledoit-wolf#random-matrix-theory#risk-attribution#expected-shortfall#var-backtesting#stress-test#frtb

量化交易系列导航

按系列顺序继续阅读,而不是停在单篇。

系列目录上一篇:【量化交易】组合构建:均值方差、风险平价、Black-Litterman下一篇:【量化交易】头寸管理:Kelly、波动率目标、风险预算

源码下载

本文相关源码已整理,共 2 个文件。

打开下载目录 →

目录

风险模型要回答的是一个具体问题:给定今天的持仓,下一个调仓周期里组合可能波动多大、风险集中在哪些因子上、极端情况下最多亏多少。常见的错误有三种:拿组合历史净值算波动率(刚换过仓的组合,历史净值反映的是旧持仓);把 np.cov 得到的样本协方差直接喂给优化器(股票数接近样本天数时,优化器会专门挑”看起来最安全”的噪声方向下注);以及只看 VaR、不做压力测试。本文的结论是:用 Barra 式多因子结构把 \(N \times N\) 协方差压到 \(K \times K\) 因子协方差加对角特异方差;用收缩估计或 eigenfactor 调整修正优化组合的风险低估;用欧拉分解做事前归因;尾部风险靠 ES、VaR 回测和压力测试补足。第二节的模拟里,样本协方差对最小方差组合的风险预测只有真实值的 0.20 倍,与随机矩阵理论给出的 \(1-N/T\) 吻合。

速查表:风险问题 → 工具 → 关键假设

风险问题 对应工具 关键假设(不成立时会出什么问题) 见
当前持仓下个月波动多大 因子模型协方差 \(X\Sigma_f X^\top+\Delta\),EWMA 加 Newey-West 特异收益互不相关;因子结构在半衰期内大致稳定 第二、三节
优化器给出的风险是否被低估 Ledoit-Wolf 收缩、eigenfactor 风险调整 样本误差是主要偏差来源;收益近似平稳 第二节、第九节
风险集中在哪个因子或行业 事前风险归因(MCR / CCR,欧拉分解) 风险度量对权重一次齐次 第五节
收益从哪里来 事后多因子收益归因、Brinson 暴露取 \(t-1\) 值;归因必须闭合 第五节
单日或 10 日尾部损失多大 历史模拟 / 参数法 / Hull-White VaR 与 ES 历史窗口能代表未来;参数法还假设正态 第六节
VaR 模型是否失准 Kupiec 检验、Christoffersen 检验、巴塞尔红绿灯 例外事件独立同分布 第六节
历史上没出现过的极端情景 历史 / 假设 / 反向压力测试 冲击可以通过因子暴露线性传导;尾部相关性需要单独上调 第七节
方法从哪里来、哪里还有争论 谱系表、三个争论、开放问题 — 第九节

本文所有代码都在同目录的 risk_model_demo.py 和 plot_risk_figures.py 里,数据全部是固定随机种子生成的模拟数据。运行环境:Windows 11、Intel Core i9-12900K(16 核 24 线程)、64 GB 内存、Python 3.12.10、NumPy 2.5.3(OpenBLAS)、pandas 3.0.6、SciPy 1.18.1、scikit-learn 1.9.1、matplotlib 3.11.2。正文中的输出均摘自这两个脚本的实际运行结果。

风险提示与适用范围:本文不构成任何投资建议。所有代码、数据和示例数值都只用于说明算法,没有经过生产审计,直接套用造成的损失由使用者自负。文中的风险模型骨架参照 MSCI 公开的 USE4 / CNE5 方法论文档实现,与商业产品的实际公式和参数存在差异。VaR、ES 与压力测试的监管报送(巴塞尔市场风险框架 MAR 章节、SEC Rule 18f-4、各国银行资本监管规则等)有专门的口径要求,本文不替代任何监管文档。


一、风险模型的目的

估计组合波动只是风险模型的副产品。如果只需要一个波动率数字,GARCH、EWMA、已实现波动率都能给出来,用不着国家、行业、风格三层结构。风险模型存在的理由有三条。

事前预测(ex ante forecasting)

在开盘前回答:按当前持仓 \(w_t\) 拿到下一个调仓日,组合可能损失多大、损失主要来自哪些因子、有没有触碰硬约束。这件事只能建立在对未来协方差 \(\Sigma_{t+1}\) 的估计上:

\[ \sigma_p^2 = w_t^\top \Sigma_{t+1} w_t \]

难点在维度。\(N\) 只股票的协方差矩阵有 \(N(N+1)/2\) 个自由参数。因子分解把它压到 \(K(K+1)/2 + N\):以 MSCI 的 A 股模型 CNE5 为例,它有 1 个国家因子、32 个行业因子、10 个风格因子(Orr, Mashtaler & Nagy 2012),即 \(K=43\)。

事后归因(ex post attribution)

把已实现收益拆成因子贡献和特异贡献:

\[ r_p = \sum_{k=1}^{K} b_{p,k} f_k + \sum_i w_i u_i, \qquad b_{p,k} = \sum_i w_i X_{ik} \]

\(b_{p,k}\) 是组合在第 \(k\) 个因子上的暴露,\(f_k\) 是当期因子收益,最后一项是因子解释不了的选股收益。如果一个月的收益大部分来自市值因子,这个组合本质上是在押注小盘风格,而不是在选股。

组合优化输入(optimizer input)

均值方差、风险平价、Black-Litterman 的核心都是把协方差矩阵放进二次规划(见 组合构建)。协方差矩阵不可信时,优化器会放大估计误差。风险模型还给优化器提供一组结构化约束:因子暴露上下界、行业偏离上下界、跟踪误差目标、特异风险占比下界。这些约束都需要逐股票的暴露 \(X\) 和特异方差 \(\delta_i^2\) 才能写出来。下游的 头寸管理 用 \(\sigma_p\) 做波动率目标和风险预算,同样依赖这一步的输出。

因此,风险模型是同时给优化器、风控系统、归因报告供数的统一基础设施。估计频率、刷新延迟、覆盖范围、版本切换这些工程决策(见第八节)都要按这个定位来取舍。

下图是全文的骨架:Barra 式模型的因子分类和每日估计流程。

Barra 式风险模型的因子分类与每日估计流程

二、协方差矩阵估计

样本协方差为什么不稳定

设 \(N\) 只股票、\(T\) 天的收益矩阵 \(R \in \mathbb{R}^{T \times N}\),样本协方差为:

\[ \hat\Sigma = \frac{1}{T-1}(R - \bar R)^\top (R - \bar R) \]

第一,秩不够。\(\hat\Sigma\) 的秩不超过 \(\min(N, T-1)\)。\(N=4000\)、\(T=252\) 时,至少 3749 个特征值严格为零,矩阵不可逆。

第二,特征值谱失真。即使 \(T > N\),只要 \(q = N/T\) 不可忽略,样本特征值也会系统性地向两端散开。Marčenko & Pastur(1967)给出了纯噪声情形下的极限谱:当真实协方差为 \(\sigma^2 I\) 时,样本特征值落在区间

\[ \lambda_\pm = \sigma^2 (1 \pm \sqrt{q})^2 \]

之内。Laloux, Cizeau, Bouchaud & Potters(1999,Physical Review Letters)把这个结论用到 S&P 500 成分股的相关矩阵上,发现经验特征值密度的主体与随机矩阵预测高度吻合,因此对风险管理中直接使用经验相关矩阵提出了”严重怀疑”。

第三,优化会专门放大误差。优化器偏爱样本方差小的方向,而这些方向的小特征值恰恰是被低估得最厉害的。Michaud(1989,Financial Analysts Journal)把均值方差优化称为”误差最大化”(error maximization)。Bouchaud & Potters(2009,arXiv:0910.1205,综述,未经同行评审)给出了定量结论:在收益独立同分布、\(N, T \to \infty\) 且 \(q\) 固定的假设下,用样本协方差求出的最小方差组合满足

\[ R_{\text{in}} = R_{\text{true}} \sqrt{1-q} = R_{\text{out}} (1-q) \]

其中 \(R_{\text{in}}\) 是样本内预测风险,\(R_{\text{true}}\) 是真实协方差已知时可达到的最小风险,\(R_{\text{out}}\) 是这个组合的真实风险。也就是说,预测风险与实际风险之比约为 \(1-q\)。\(N=200\)、\(T=250\) 时这个比值只有 0.2。

第四,时变性。真实的协方差结构本身在变,危机中相关性上升(见第七节)。等权平均过去 252 天的估计,在最需要它的时候最不准。

收缩估计:把样本协方差拉向结构化目标

Ledoit & Wolf 的线性收缩把样本协方差和一个结构化目标 \(F\) 加权平均:

\[ \hat\Sigma_{\text{shrink}} = \delta F + (1 - \delta) \hat\Sigma, \quad \delta \in [0, 1] \]

收缩强度 \(\delta\) 按最小化 \(\mathbb{E}\|\hat\Sigma_{\text{shrink}} - \Sigma\|_F^2\) 的渐近最优公式估计。三篇论文用了三种目标:

  1. 单指数模型(Ledoit & Wolf 2003,Journal of Empirical Finance):\(F = \sigma_m^2 \beta \beta^\top + D\)。
  2. 常数相关阵(Ledoit & Wolf 2004,Journal of Portfolio Management):\(F_{ii} = \hat\sigma_i^2\),\(F_{ij} = \bar\rho\, \hat\sigma_i \hat\sigma_j\)。
  3. 单位阵的倍数(Ledoit & Wolf 2004,Journal of Multivariate Analysis):\(F = \mu I\),\(\mu = \operatorname{tr}(\hat\Sigma)/N\)。

sklearn.covariance.LedoitWolf 实现的是第 3 种。代码里常见”sklearn 的 LedoitWolf 用常数相关目标”的说法是错的。

import numpy as np
import pandas as pd
from sklearn.covariance import LedoitWolf


def sample_cov(returns: pd.DataFrame) -> np.ndarray:
    """样本协方差,作为基准"""
    return returns.cov().values


def ledoit_wolf_cov(returns: pd.DataFrame) -> tuple[np.ndarray, float]:
    """sklearn 的 Ledoit-Wolf 线性收缩,目标为 mu * I(Ledoit & Wolf 2004, JMVA)"""
    lw = LedoitWolf().fit(returns.values)
    return lw.covariance_, lw.shrinkage_

在一组 \(T=250\)、\(N=200\) 的三因子模拟收益上运行(risk_model_demo.py 第 [1] 段):

shrinkage intensity = 0.0781
cond(sample) = 3.72e+03, cond(LW) = 339

收缩强度不到 8%,条件数就降了一个数量级。线性收缩的代价是对所有特征值一视同仁地往均值拉,并且它优化的是 Frobenius 损失,而不是优化组合的风险偏差。Ledoit & Wolf 后来提出的非线性收缩(2017,Review of Financial Studies;2020,Annals of Statistics)对每个特征值单独收缩,目标直接对准组合选择。

因子模型:把维度从 N 降到 K

把每只股票的收益拆成因子部分和特异部分:

\[ r_i = \sum_{k=1}^{K} X_{ik} f_k + u_i \]

如果特异收益 \(u_i\) 互不相关且与因子无关,协方差矩阵为:

\[ \Sigma = X \Sigma_f X^\top + \Delta, \qquad \Delta = \operatorname{diag}(\delta_1^2, \dots, \delta_N^2) \]

需要估计的只有 \(K \times K\) 的 \(\Sigma_f\) 和 \(N\) 个特异方差。\(N=4000\)、\(K=43\) 时,参数从约 800 万降到 4946 个。更重要的是,样本误差决定的偏差比例从 \(N/T\) 变成了 \(K/T\):优化器能”钻空子”的方向只剩 \(K\) 个因子方向,加上对角的特异部分。

这个压缩依赖”特异收益互不相关”的假设。同一概念板块、同一产业链的股票,特异收益常常同向变动,行业因子只能吸收其中一部分。MSCI 的做法是把行业切细:USE4 有 60 个行业因子(Menchero, Orr & Wang 2011,Figure 3.2 图注),CNE5 有 32 个。剩余的跨股票相关会在第四节的残差检查中暴露出来。

模拟:样本、收缩、统计因子、基本面因子、eigenfactor 调整

下图用一个已知真实协方差的模拟市场比较五种估计量:真实模型为 5 因子加异质特异波动,\(N=200\)、\(T=250\)(\(q=0.8\)),高斯收益,重复 100 次;每次用估计出的协方差求全额投资的最小方差组合,比较预测波动与真实波动之比。

五种协方差估计的特征值谱与最小方差组合风险偏差
估计量 预测/真实波动(中位数) 四分位区间
样本协方差 0.199 [0.178, 0.212]
Ledoit-Wolf(\(\mu I\) 目标) 0.589 [0.571, 0.609]
统计因子(PCA 前 5 个主成分 + 对角残差) 0.845 [0.821, 0.863]
基本面因子(真实暴露 \(X\) 已知,逐日 OLS 估计 \(f_t\)) 0.992 [0.986, 0.997]
样本 + eigenfactor 调整(USE4 附录 B 式 B7,30 次蒙特卡洛) 0.888 [0.858, 0.921]

几点解读:

估计细节:半衰期、Newey-West、两套权重

半衰期分开设。USE4 的做法是用较长的半衰期估相关、较短的半衰期估波动,再按 \(F_{ij} = \rho_{ij}\sigma_i\sigma_j\) 合成(Menchero, Orr & Wang 2011,式 4.1)。原因写在方法论文档里:对角元的采样误差对矩阵条件数影响很小,非对角元影响大,所以相关的半衰期必须足够长,保证有效样本数远大于因子数 \(K\)。USE4 表 4.1 的参数(单位:交易日):

模型 波动半衰期 波动 NW 滞后 相关半衰期 相关 NW 滞后 VRA 半衰期
USE4S 84 5 504 2 42
USE4L 252 5 504 2 168

Newey-West 修正。USE4 的预测期是一个月,因子收益却是日频估计。日频收益若有序列相关,直接按 \(\sqrt{21}\) 放大会有偏差。USE4 用 Newey & West(1987,Econometrica)的滞后加权修正:

\[ \hat\Sigma_f^{NW} = \hat\Gamma(0) + \sum_{l=1}^{L} \left(1 - \frac{l}{L+1}\right) \left(\hat\Gamma(l) + \hat\Gamma(l)^\top\right), \qquad \hat\Gamma(l) = \sum_t w_t\, c_t c_{t-l}^\top \]

Bartlett 权重 \(1 - l/(L+1)\) 保证结果半正定。Newey-West 在因子检验中的另一种用法(Fama-MacBeth 斜率的 \(t\) 统计量)见 因子动物园 的单因子检验流水线。

相关与波动必须各用各的权重。常见的写法是”用相关权重算协方差、再除以波动权重算出来的标准差”。这样得到的矩阵对角线不是 1,非对角元可能超过 1,合成后的 \(\Sigma_f\) 也不保证半正定。正确的顺序是:先用相关权重得到完整的协方差并归一化成相关阵,再用波动权重得到的标准差缩放。

def ewma_weights(T: int, halflife: float) -> np.ndarray:
    w = 0.5 ** (np.arange(T)[::-1] / halflife)
    return w / w.sum()


def newey_west_cov(x: np.ndarray, weights: np.ndarray, lags: int) -> np.ndarray:
    """
    指数加权 + Newey-West 自相关修正:
        Gamma(l) = sum_t w_t c_t c_{t-l}^T
        Sigma = Gamma(0) + sum_{l=1..L} (1 - l/(L+1)) (Gamma(l) + Gamma(l)^T)
    """
    mean = weights @ x
    c = x - mean
    cov = (c * weights[:, None]).T @ c
    for lag in range(1, lags + 1):
        w = weights[lag:] / weights[lag:].sum()
        gamma = (c[lag:] * w[:, None]).T @ c[:-lag]
        cov += (1 - lag / (lags + 1)) * (gamma + gamma.T)
    return cov


def factor_model_cov(
    exposures: np.ndarray,
    factor_returns: pd.DataFrame,
    specific_var: np.ndarray,
    halflife_vol: int = 84,
    halflife_cor: int = 504,
    nw_lags_vol: int = 5,
    nw_lags_cor: int = 2,
) -> tuple[np.ndarray, np.ndarray]:
    """
    Barra 风格因子模型协方差:Sigma = X Sigma_f X^T + Delta
    - 相关性与波动率分别用不同半衰期与 Newey-West 滞后估计(参数取 USE4S 表 4.1)
    - 返回 (资产协方差, 因子协方差),单位与输入收益相同(日频)
    """
    fr = factor_returns.values
    T, _ = fr.shape

    cov_c = newey_west_cov(fr, ewma_weights(T, halflife_cor), nw_lags_cor)
    sd_c = np.sqrt(np.diag(cov_c))
    corr = cov_c / np.outer(sd_c, sd_c)

    cov_v = newey_west_cov(fr, ewma_weights(T, halflife_vol), nw_lags_vol)
    vol = np.sqrt(np.diag(cov_v))

    sigma_f = corr * np.outer(vol, vol)
    sigma = exposures @ sigma_f @ exposures.T + np.diag(specific_var)
    return (sigma + sigma.T) / 2, sigma_f

这段骨架没有包含 EM 算法补缺失值、eigenfactor 调整和波动率状态调整,后两者见第三节末尾。


三、Barra 多因子结构

从 Rosenberg 和 Fama-MacBeth 说起

Rosenberg(1974,Journal of Financial and Quantitative Analysis)给出了这套结构的起点:证券收益之间存在高度显著的”市场外协方差成分”(extra-market components of covariance),而且个股在这些因子上的载荷可以由可观测的公司特征决定,包括损益表和资产负债表数据、行业归属、历史收益行为。“暴露由公司特征给定,因子收益由横截面回归估计”,就是今天 Barra 式基本面因子模型的定义。

横截面回归的统计原型更早:Fama & MacBeth(1973,Journal of Political Economy)每期做一次横截面回归,再对斜率的时间序列做检验。两者的区别在于用途。Fama-MacBeth 关心斜率的均值是否显著(定价问题),风险模型关心斜率的协方差(风险问题)。同一个回归,前者产出因子溢价,后者产出 \(\Sigma_f\)。这也是 因子动物园 里的 alpha 因子和本文风险因子的分界:风险因子要求解释协方差,不要求有正溢价。

MSCI 公开的方法论文档记录了 Barra 模型的演进:初代全球模型 GEM 用月度横截面回归,GEM2 改为更高频观测并引入 World 因子,USE4 在 2011 年引入 eigenfactor 风险调整和波动率状态调整(Menchero, Orr & Wang 2011,第 1.2 节)。另一家主流厂商 Axioma 的美股模型 AXUS4 同时提供基本面版本(市场因子 + 风格因子 + 68 个 GICS 行业,平方根市值加权、带行业约束的 Huber 稳健回归)和统计版本(15 个统计因子,两步渐近主成分法)(Axioma AXUS4 factsheet)。两家公开文档的共同点是:国家或市场因子、行业因子、风格因子、特异收益四层结构,加上每日横截面回归。

国家因子

单一市场模型里,每只股票的国家暴露恒为 1。行业哑变量之和也恒为 1,与国家列完全共线,所以回归必须加一个约束(见下文)。USE4 选的约束让国家因子近似等于估计域的市值加权组合:方法论文档报告,1995 到 2011 年间 USE4 国家因子与估计域超额收益的时间序列相关约为 99.9%(Menchero, Orr & Wang 2011,Figure 3.1)。引入国家因子的另一个好处是行业间相关性的响应更快:两个行业通过共同的国家暴露联动,而国家因子的波动用的是较短的波动半衰期。

行业因子

行业暴露通常是 0/1 哑变量。USE4 和 CNE5 都以 GICS 为底,再按本地市场合并或拆分,选择标准是经济直觉、统计显著性、对解释力的增量,以及避免股票太少的”薄行业”(Menchero, Orr & Wang 2011,第 2.4 节)。

多元化经营的公司怎么处理?USE4 支持多行业暴露:在每个行业内用市值对资产(Assets)和销售额(Sales)做回归,据此把一家公司的暴露拆到多个行业(同上,第 2.5 节)。

行业的工程坑在行业切换。公司重组改行后,暴露会在某一天跳变。回测和归因必须用当时的行业分类,不能用今天的分类回填历史,这和 特征存储 讲的 point-in-time 原则是一回事。

风格因子

下表是 CNE5 的 10 个风格因子及其描述子权重,摘自 CNE5 Empirical Notes 附录 A(Orr, Mashtaler & Nagy 2012):

风格因子 描述子与权重 要点
Size 1.0 · LNCAP 总市值的自然对数
Beta 1.0 · BETA 对估计域市值加权超额收益的时序回归斜率,252 日窗口,半衰期 63 日
Momentum 1.0 · RSTR 超额对数收益之和,504 日窗口,跳过最近 21 日,半衰期 126 日
Residual Volatility 0.74 · DASTD + 0.16 · CMRA + 0.10 · HSIGMA 对 Beta 和 Size 正交化
Non-linear Size 1.0 · NLSIZE 标准化 Size 的立方,对 Size 正交化后再缩尾、标准化
Book-to-Price 1.0 · BTOP 最近一期普通股账面价值 / 当前市值
Liquidity 0.35 · STOM + 0.35 · STOQ + 0.30 · STOA 1、3、12 个月换手率的对数;对 Size 正交化
Earnings Yield 0.68 · EPFWD + 0.21 · CETOP + 0.11 · ETOP 分析师预测 12 个月远期 EP 权重最大
Growth 0.18 · EGRLF + 0.11 · EGRSF + 0.24 · EGRO + 0.47 · SGRO 分析师长短期盈利增速预测 + 过去五年盈利和营收增长
Leverage 0.38 · MLEV + 0.35 · DTOA + 0.27 · BLEV MLEV \(=(ME+PE+LD)/ME\),BLEV \(=(BE+PE+LD)/BE\),DTOA = 总负债 / 总资产

注意几点:USE4 的风格集合与 CNE5 不同,不要把这张表当成”Barra 通用十因子”;描述子权重是 MSCI 用优化算法”最大化模型解释力”得到的(USE4 第 2.3 节),自建模型需要在自己的样本上重新确定。

标准化约定。USE4 和 CNE5 都把描述子和风格暴露标准化为市值加权均值为 0、等权标准差为 1(USE4 式 2.4,CNE5 第 3.3 节)。用市值加权均值,是为了让市值加权的估计域对所有风格的暴露都约为 0;用等权标准差,是为了不让大盘股主导暴露的尺度。

共线性。Size、Beta、Residual Volatility、Liquidity 之间相关性高,所以 CNE5 对 Residual Volatility 做了 Beta 和 Size 正交化,对 Liquidity 做了 Size 正交化,Non-linear Size 对 Size 正交化。USE4 文档用方差膨胀因子(VIF)和因子稳定系数来筛查共线与不稳定的因子(第 2.1 节)。

横截面回归

每个交易日 \(t\) 在估计域内做一次回归:

\[ r_{n,t} = f_{c,t} + \sum_{j} X_{nj} f_{j,t} + \sum_{s} X_{ns} f_{s,t} + u_{n,t} \]

暴露 \(X\) 必须用 \(t-1\) 日收盘后可得的数据。用当日市值或当日动量解释当日收益,是典型的前视偏差(见 数据陷阱)。

回归权重。USE4 用加权最小二乘,假设特异收益的方差与总市值的平方根成反比(Menchero, Orr & Wang 2011,第 3.1 节)。按方差倒数加权,回归权重就是 \(v_n = \sqrt{\text{cap}_n}\)。CNE5 文档同样使用平方根市值加权。

约束。行业哑变量之和等于国家列,设计矩阵不满秩。USE4 的约束是行业因子收益的市值加权和为零:

\[ \sum_j w_j f_j = 0 \]

\(w_j\) 是估计域在行业 \(j\) 上的市值权重。方法论文档强调,约束的选择不影响拟合优度,只影响因子的解释:在这个约束下,国家因子近似为市值加权大盘,行业因子是”多该行业、空国家因子”的美元中性组合,风格因子是对该风格暴露为 1、对其他所有因子暴露为 0 的美元中性组合(USE4 第 3.1 节)。这里的”纯因子组合”解释与 Heston & Rouwenhorst(1994,Journal of Financial Economics)的做法同源:他们在分离国家效应和行业效应时,同样对哑变量系数施加了加权和为零的约束。

带约束的 WLS 可以写成 KKT 线性方程组:

\[ \begin{pmatrix} X^\top V X & R \\ R^\top & 0 \end{pmatrix} \begin{pmatrix} f \\ \lambda \end{pmatrix} = \begin{pmatrix} X^\top V r \\ 0 \end{pmatrix}, \qquad V = \operatorname{diag}(v_n) \]

\(R\) 在行业因子的位置上取行业市值权重,在其他位置取 0。

STYLE_FACTORS = [
    "size", "beta", "momentum", "residual_volatility", "non_linear_size",
    "book_to_price", "liquidity", "earnings_yield", "growth", "leverage",
]


def winsorize(x: pd.Series, n_std: float = 3.0) -> pd.Series:
    mu, sd = x.mean(), x.std()
    return x.clip(lower=mu - n_std * sd, upper=mu + n_std * sd)


def standardize(x: pd.Series, market_cap: pd.Series) -> pd.Series:
    """USE4 约定:市值加权均值为 0,等权标准差为 1"""
    w = market_cap / market_cap.sum()
    mu = (x * w).sum()
    return (x - mu) / (x.std(ddof=0) + 1e-12)


def build_exposure_matrix(
    descriptors: pd.DataFrame,
    industry: pd.Series,
    market_cap: pd.Series,
) -> tuple[pd.DataFrame, list[str], list[str]]:
    """一日的暴露矩阵 X = [country | industry dummies | standardized styles]"""
    style_block = pd.DataFrame(index=descriptors.index)
    for col in STYLE_FACTORS:
        x = winsorize(descriptors[col].astype(float))
        style_block[col] = standardize(x, market_cap)

    industry_block = pd.get_dummies(industry, prefix="ind").astype(float)
    country_block = pd.DataFrame({"country": 1.0}, index=descriptors.index)

    X = pd.concat([country_block, industry_block, style_block], axis=1)
    return X, list(industry_block.columns), STYLE_FACTORS


def cross_section_regression(
    returns: pd.Series,
    X: pd.DataFrame,
    market_cap: pd.Series,
    industry_cols: list[str],
) -> tuple[pd.Series, pd.Series]:
    """
    带约束的 WLS(回归权重 v_n = sqrt(cap_n)):
        min sum_n v_n (r_n - X_n f)^2
        s.t. sum_j w_j^{ind} f_j^{ind} = 0   (w^{ind} 为行业市值权重)
    用 KKT 线性方程组求解。
    """
    v = np.sqrt(market_cap.values)
    Xv = X.values
    rv = returns.values

    XtV = Xv.T * v
    AtA = XtV @ Xv
    Atb = XtV @ rv

    K = Xv.shape[1]
    ind_idx = [X.columns.get_loc(c) for c in industry_cols]
    ind_cap = X[industry_cols].T.values @ market_cap.values
    R = np.zeros(K)
    R[ind_idx] = ind_cap / ind_cap.sum()

    KKT = np.block([[AtA, R[:, None]], [R[None, :], np.zeros((1, 1))]])
    sol = np.linalg.solve(KKT, np.concatenate([Atb, [0.0]]))
    f = sol[:K]

    factor_returns = pd.Series(f, index=X.columns)
    residuals = pd.Series(rv - Xv @ f, index=returns.index)
    return factor_returns, residuals

两处实现细节值得对照检查。其一,回归权重是 \(v_n\) 本身,构造加权矩阵时不要再开一次方或平方:如果写成 A = diag(sqrt(cap)) @ X 再求 A.T @ A,实际权重就变成了 \(\text{cap}_n\),大盘股的影响被放大。其二,\(N=4000\) 时不要构造 \(N \times N\) 的对角矩阵,用广播 Xv.T * v 即可。

在 1000 只股票、10 个行业、10 个风格的模拟市场上跑一天(真实因子收益已按同一约束生成,risk_model_demo.py 第 [2] 段):

constraint sum_j w_j f_j = 1.98e-19
corr(f_hat, f_true) = 0.9717, max |f_hat - f_true| = 0.00425
cross-sectional R^2 = 0.255
             true      est
country  -0.01605 -0.01495
ind_00   -0.01008 -0.00921
size      0.00161  0.00147
momentum  0.00184  0.00222

约束在机器精度内成立,估计的因子收益与真值相关 0.97。单日的估计误差(这里最大 0.4%)来自特异收益噪声,这也是因子协方差要用几百天数据做指数加权的原因。

因子协方差的两项 USE4 修正

eigenfactor 风险调整。USE4 方法论文档第 4.2 节和附录 B 描述了流程:

  1. 对样本因子协方差 \(F_0\) 做特征分解 \(D_0 = U_0^\top F_0 U_0\),特征向量就是互不相关的”特征因子”组合。
  2. 假设 \(F_0\) 是真实协方差,模拟 \(M\) 组同样长度的因子收益,每组重新估计协方差 \(F_m\) 并分解得到 \(U_m, D_m\)。
  3. 计算模拟特征因子的真实方差 \(\tilde D_m = U_m^\top F_0 U_m\) 的对角元,得到偏差 \[v(k) = \sqrt{\frac{1}{M}\sum_m \frac{\tilde D_m(k)}{D_m(k)}}\]
  4. 把样本特征方差乘以 \(v^2(k)\) 再转回原坐标:\(\tilde F_0 = U_0 \operatorname{diag}(v^2 D_0) U_0^\top\)。

文档承认模拟假设正态和平稳,低估了真实偏差,于是在研究版本里对 \(v(k)\) 做抛物线拟合并按 \(v_S(k) = a\,[v_P(k) - 1] + 1\)、\(a = 1.4\) 放大;但这样会让单个纯因子出现小偏差,所以生产中的 USE4 采用较温和的未缩放版本(式 B7)。第二节模拟里的”样本 + eigenfactor 调整”用的就是式 B7 的做法。Menchero, Wang & Orr(2012,Financial Analysts Journal)给出了这一方法的完整分析。

波动率状态调整(Volatility Regime Adjustment,VRA)。用横截面信息判断整体风险预测是否偏低或偏高。定义每日的横截面偏差统计量

\[ B_t^F = \sqrt{\frac{1}{K}\sum_k \left(\frac{f_{kt}}{\sigma_{kt}}\right)^2} \]

如果预测准确,标准化收益的横截面标准差应接近 1。对 \((B_t^F)^2\) 做指数加权平均得到乘数 \(\lambda_F^2\),整个因子协方差矩阵乘以 \(\lambda_F^2\)(USE4 式 4.3 至 4.5)。它只改变波动的水平,不改变相关。文档报告,没有 VRA 时出现了”进入危机时低估风险、危机后高估风险”的典型形态,加入 VRA 后大部分样本期的平均偏差统计量更接近 1(第 4.3 节,Figure 4.7)。

因子收益怎么读

因子收益序列本身是市场结构的记录:哪段时间小盘占优、哪段时间价值风格轮动、低波动是否持续有溢价。A 股上各风格的方向与 regime 切换,因子动物园 第十二节有专门讨论,那里也说明了量级必须在自有 point-in-time 样本上复现。这里只强调一条:风险因子是为了解释协方差而设计的,它的收益方向不稳定是常态,不能直接拿来当选股信号。


四、特异风险与残差波动

横截面回归的残差 \(u_{n,t}\) 是特异收益,它的方差 \(\delta_n^2\) 是特异风险。分散化组合里特异风险占比小,但集中持仓、小盘股、跟踪误差预算里它往往是主角。

朴素估计的问题

最朴素的做法是对每只股票用过去 \(T\) 天残差的样本方差。问题有三个:

第一,停牌、新股、长期缺失让有效样本变短。连续停牌期内残差记为 0 会低估方差,正确的做法是剔除停牌日,并把复牌日的”补涨补跌”单独处理,因为它累积了停牌期间的市场变动,不是当日的特异收益。

第二,样本方差的抽样误差会被优化器放大。方差估计的标准误约为 \(\delta^2\sqrt{2/(T-1)}\),\(T=252\) 时相对误差约 9%。优化器偏爱特异风险看起来低的股票,而这些股票恰恰是被低估的那一批。USE4 用分位数检验确认了这种偏差:不做收缩时,模型倾向于低估低特异波动股票的风险、高估高特异波动股票的风险(Menchero, Orr & Wang 2011,Figure 5.1)。

第三,特异波动有共同的时变成分。市场整体进入高波动状态时,单只股票的时间序列看不出这个共同冲击。

USE4 的特异风险流程

USE4 第 5 节把特异风险估计拆成四步,CNE5 沿用同一方法:

1. 时间序列估计。用指数加权和 Newey-West 自相关修正估计每只股票的特异波动 \(\sigma_n^{TS}\)。USE4S 的参数是:特异波动半衰期 84 日,Newey-West 滞后 5 日,自相关半衰期 252 日(表 5.1)。

2. 结构化模型混合。对样本不足(缺失收益多)或尾部过厚的股票,时间序列估计不可靠。USE4 定义混合系数 \(\gamma_n \in [0, 1]\):缺失少且尾部不过厚时取 1,严重违反时取 0。用 \(\gamma_n = 1\) 的股票做横截面回归,把特异波动的对数回归到模型自身的因子暴露上:

\[ \ln \sigma_n^{TS} = \sum_k X_{nk} b_k + \epsilon_n, \qquad \sigma_n^{STR} = E_0 \exp\Big(\sum_k X_{nk} b_k\Big) \]

\(E_0\) 是略大于 1 的常数,用来修正指数化残差带来的偏差。最终 \(\hat\sigma_n = \gamma_n \sigma_n^{TS} + (1-\gamma_n)\sigma_n^{STR}\)。直觉是:特征相近的股票,特异波动也相近。文档说明大部分 USE4 股票 \(\gamma_n = 1\)。

3. 贝叶斯收缩。按市值十分位分组,把每只股票的估计向所在组的市值加权平均 \(\bar\sigma(s_n)\) 收缩:

\[ \sigma_n^{SH} = v_n \bar\sigma(s_n) + (1 - v_n)\hat\sigma_n, \qquad v_n = \frac{q\,|\hat\sigma_n - \bar\sigma(s_n)|}{\Delta_\sigma(s_n) + q\,|\hat\sigma_n - \bar\sigma(s_n)|} \]

\(\Delta_\sigma(s_n)\) 是组内特异波动的标准差,USE4 取 \(q = 0.1\)。偏离组均值越远,收缩越多。文档报告,收缩后各特异波动十分位的偏差统计量接近 1,且不再随波动水平单调变化(Figure 5.2)。

4. 特异波动率状态调整。与因子 VRA 同理,用特异收益的横截面偏差统计量计算乘数,USE4S 的 VRA 半衰期为 42 日。

时间序列部分也可以用 EWMA 的递推形式写:

\[ \hat\delta_{n,t}^2 = \lambda \hat\delta_{n,t-1}^2 + (1-\lambda) u_{n,t}^2 \]

J.P. Morgan/Reuters 的 RiskMetrics 技术文档(1996)对日频数据取 \(\lambda = 0.94\),对应半衰期 \(\ln 0.5 / \ln 0.94 \approx 11.2\) 天;\(\lambda = 0.97\) 对应约 22.8 天。GARCH(1,1)(Engle 1982;Bollerslev 1986)多了均值回复项,但参数要逐股票估计,对样本短的股票不稳定。

特异收益独立性的检查

“特异收益互不相关”是因子模型最脆弱的假设。残差里如果藏着一个未被模型覆盖的共同驱动(某个题材、某类资金流),所有暴露在它上面的股票残差会同向偏离,组合风险被低估。可执行的检查是对残差矩阵做主成分分析:如果前几个主成分解释的方差明显超出纯噪声下 Marčenko-Pastur 上界给出的水平,就说明残差还有结构。处理方式要么是加入新的行业或风格因子,要么在残差上叠加少量统计因子,这也是第九节”基本面 vs 统计因子”争论的工程落点。统计套利 里的 PCA 残差策略,用的是同一套分解,只是方向相反:它交易残差,风险模型要把残差解释干净。


五、风险归因

事前风险归因:边际贡献与组合贡献

组合预测方差:

\[ \sigma_p^2 = w^\top \Sigma w = b^\top \Sigma_f b + \sum_i w_i^2 \delta_i^2, \qquad b = X^\top w \]

只拆到”因子 vs 特异”还不够,还要知道具体哪个因子贡献最大。边际风险贡献(marginal contribution to risk,MCR)和组合风险贡献(component contribution to risk,CCR)定义为:

\[ \text{MCR}_k = \frac{\partial \sigma_p}{\partial b_k} = \frac{(\Sigma_f b)_k}{\sigma_p}, \qquad \text{CCR}_k = b_k \cdot \text{MCR}_k = \frac{b_k (\Sigma_f b)_k}{\sigma_p} \]

由于 \(\sigma_p\) 对 \((b, w)\) 是一次齐次函数,欧拉定理保证

\[ \sum_k \text{CCR}_k + \frac{\sum_i w_i^2 \delta_i^2}{\sigma_p} = \sigma_p \]

各项贡献加起来恰好等于组合波动。单个 CCR 可以为负,表示该因子在组合里起对冲作用。

def portfolio_risk_attribution(
    weights: pd.Series,
    exposures: pd.DataFrame,
    sigma_f: pd.DataFrame,
    specific_var: pd.Series,
) -> pd.DataFrame:
    """事前风险归因:每个因子与特异部分的方差贡献、波动贡献、占比(欧拉分解)"""
    w = weights.reindex(exposures.index).fillna(0).values
    X = exposures.values
    Sf = sigma_f.values
    delta = specific_var.reindex(exposures.index).fillna(0).values

    b = X.T @ w
    factor_ccr_var = b * (Sf @ b)
    specific_total = float((w ** 2 * delta).sum())
    total_var = float(factor_ccr_var.sum()) + specific_total
    sigma_p = np.sqrt(total_var)

    out = pd.DataFrame({
        "exposure": b,
        "var_contrib": factor_ccr_var,
        "vol_contrib": factor_ccr_var / sigma_p,
        "share": factor_ccr_var / total_var,
    }, index=exposures.columns)
    out.loc["specific"] = [np.nan, specific_total, specific_total / sigma_p,
                           specific_total / total_var]
    out.loc["TOTAL"] = [np.nan, total_var, sigma_p, 1.0]
    return out

risk_model_demo.py 第 [3] 段在第三节的模拟市场上连续跑 500 天横截面回归,用 USE4S 参数估计 \(\Sigma_f\),特异方差取残差平方的 84 日半衰期 EWMA,然后对”市值最小 30% 的股票等权”组合做归因:

min eigenvalue of asset Sigma = 4.162e-05 (PSD check)
country vol: est 0.165 vs true 0.190 (annualized)
small-cap equal-weight portfolio, variance share by group:
country     0.5270
industry   -0.0043
style       0.4634
specific    0.0139
annualized vol = 0.2257, sum of vol_contrib = 0.2257
                 exposure  var_contrib  vol_contrib     share
country          1.000000     0.000106     0.007492  0.527002
size            -2.391970     0.000049     0.003433  0.241482
non_linear_size -2.122424     0.000044     0.003082  0.216772
specific              NaN     0.000003     0.000197  0.013874
TOTAL                 NaN     0.000202     0.014216  1.000000

各项波动贡献之和等于组合波动(0.2257),欧拉分解闭合。这个”小盘等权”组合有 46% 的方差来自风格,几乎全部集中在 Size 和 Non-linear Size 两个因子上:标准化用的是市值加权均值,小盘股的 Size 暴露因此远低于 0(这里是 -2.39)。国家因子波动的估计值 16.5% 低于真值 19.0%,是 84 日半衰期下有效样本有限造成的抽样误差。

下图换一个参数完全指定的模拟风险模型(参数写在图注里),对比四种组合的风险结构:

四种组合的风险分解:国家、行业、风格、特异
组合 国家 行业 风格 特异 年化波动 / 跟踪误差
市值加权基准 95.3% 4.3% 0.0% 0.4% 20.5%
小盘倾斜(市值后 30% 等权) 74.5% 3.4% 20.5% 1.6% 23.2%
两个行业内 30 只股票等权 86.9% 8.6% 0.8% 3.7% 21.5%
主动部分:小盘倾斜 − 基准 0.0% 0.4% 91.5% 8.0% 跟踪误差 11.0%

两条结论。第一,多头组合的总风险里国家因子占绝对多数,所以要看主动部分的分解,主动风险才是基金经理真正做出的选择:这里 91.5% 的跟踪误差方差来自风格押注。第二,基准的风格暴露为 0,是市值加权均值标准化的直接结果,与第三节的约定一致。

事后收益归因:Brinson 与多因子

Brinson & Fachler(1985,Journal of Portfolio Management)把超额收益拆成行业配置和选股两部分:

\[ r_p - r_b = \underbrace{\sum_j (w^p_j - w^b_j)(r^b_j - r^b)}_{\text{配置}} + \underbrace{\sum_j w^p_j (r^p_j - r^b_j)}_{\text{选股(含交互项)}} \]

Brinson, Hood & Beebower(1986,Financial Analysts Journal)给出了配置、选股、交互三项的分解。Brinson 框架只到行业一层;多因子归因把风格也拆出来:

\[ r_p = \underbrace{\sum_k b_{p,k} f_k}_{\text{因子收益}} + \underbrace{\sum_i w_i u_i}_{\text{特异收益}} \]

def portfolio_return_attribution(
    weights: pd.Series,
    exposures: pd.DataFrame,
    factor_returns: pd.Series,
    asset_returns: pd.Series,
) -> pd.DataFrame:
    """事后收益归因:组合收益 = sum_k b_k f_k + sum_n w_n u_n"""
    w = weights.reindex(exposures.index).fillna(0).values
    X = exposures.values
    f = factor_returns.reindex(exposures.columns).fillna(0).values
    r = asset_returns.reindex(exposures.index).fillna(0).values

    b = X.T @ w
    specific = float(w @ (r - X @ f))
    out = pd.DataFrame({"exposure": b, "factor_return": f, "contribution": b * f},
                       index=exposures.columns)
    out.loc["specific"] = [np.nan, np.nan, specific]
    out.loc["TOTAL"] = [np.nan, np.nan, float(b @ f) + specific]
    return out

运行检查归因是否闭合:

return attribution closes: TOTAL -0.01239973 vs w'r -0.01239973

归因的常见误用

把因子收益当 alpha。大部分收益来自 Size、Momentum 这类风格因子的组合,实质是风格暴露,换一个风格周期就会消失。事后绩效指标怎么剔除这部分,见 绩效指标。

归因不闭合。因子贡献加特异贡献必须等于组合收益(不考虑交易成本和现金)。不闭合通常意味着:暴露用错了日期(用了当日而非 \(t-1\))、因子收益与暴露版本不匹配、组合权重没用日初权重。

跨期累加。日度简单收益的贡献不能直接相加成月度贡献,因为简单收益是复利累乘的。Cariño(1999,Journal of Performance Measurement)提出的对数链接系数是常用的修正方法;直接加总在短区间、低收益时误差小,区间越长误差越大。


六、VaR 与 ES

定义

损失 \(L = -r_p\) 在置信水平 \(\alpha\) 下的风险价值(value at risk,VaR)和预期短缺(expected shortfall,ES)为:

\[ \text{VaR}_\alpha = \inf\{\ell : \Pr(L \le \ell) \ge \alpha\}, \qquad \text{ES}_\alpha = \frac{1}{1-\alpha}\int_\alpha^1 \text{VaR}_u \, du \]

对连续分布,\(\text{ES}_\alpha = \mathbb{E}[L \mid L \ge \text{VaR}_\alpha]\)。

三种估计法

参数法。假设组合收益正态、均值近似为 0,直接用第二节的 \(\Sigma\):

\[ \text{VaR}_\alpha(h) = z_\alpha \sigma_p \sqrt{h}, \qquad \text{ES}_\alpha(h) = \sigma_p \sqrt{h}\, \frac{\phi(z_\alpha)}{1-\alpha}, \qquad z_\alpha = \Phi^{-1}(\alpha) \]

\(\alpha = 0.99\) 时 \(z_\alpha \approx 2.326\)。快,能直接接因子模型,也能按因子做 VaR 归因;代价是忽略厚尾。\(\sqrt{h}\) 放大只在收益独立同分布时成立。

历史模拟法。把过去 \(T\) 天的组合收益(按当前持仓重估)当作经验分布,取分位数。不假设分布形式,能保留偏度和厚尾;代价是对 regime 切换反应慢,而且尾部样本很少:1000 个观测在 99% 水平只有 10 个尾部点。

Monte Carlo 法。从拟合的分布(多元 \(t\)、copula 等)采样路径,逐路径重定价。含期权等非线性头寸时基本只能用这种方法,代价是计算量和模型设定误差。

Hull-White 波动率调整。Hull & White(1998,Journal of Risk)把每个历史收益按”当前波动 / 当时波动”缩放后再做历史模拟,让历史分布的形状保留下来、尺度对齐到当前。

from scipy.stats import chi2, norm


def historical_var(returns: pd.Series, alpha: float = 0.99, horizon: int = 1) -> dict:
    """历史模拟 VaR / ES(正数表示损失);horizon>1 时用重叠窗口累加收益"""
    r = returns.dropna()
    if horizon > 1:
        r = r.rolling(horizon).sum().dropna()
    losses = -r.values
    var_q = float(np.quantile(losses, alpha))
    tail = losses[losses >= var_q]
    return {"VaR": var_q, "ES": float(tail.mean()), "n_obs": len(losses),
            "n_tail": len(tail)}


def historical_var_with_volatility_scaling(
    returns: pd.Series, alpha: float = 0.99, lam: float = 0.94
) -> dict:
    """
    Hull-White (1998) 波动率调整历史模拟:
    sigma_t^2 = lam * sigma_{t-1}^2 + (1 - lam) * r_{t-1}^2(只用 t 之前的信息)
    每个历史收益按 sigma_now / sigma_t 缩放后再取分位数
    """
    r = returns.dropna().values
    sig2 = np.empty(len(r) + 1)
    sig2[0] = r[:20].var()
    for t in range(1, len(r) + 1):
        sig2[t] = lam * sig2[t - 1] + (1 - lam) * r[t - 1] ** 2
    sigma_hist, sigma_now = np.sqrt(sig2[:-1]), np.sqrt(sig2[-1])
    losses = -r * (sigma_now / sigma_hist)
    var_q = float(np.quantile(losses, alpha))
    tail = losses[losses >= var_q]
    return {"VaR": var_q, "ES": float(tail.mean()), "sigma_now": float(sigma_now)}


def parametric_var(weights: np.ndarray, sigma: np.ndarray, alpha: float = 0.99,
                   horizon: int = 1) -> dict:
    """正态参数法(均值取 0):VaR = z * sigma_p * sqrt(h),ES = sigma_p * sqrt(h) * phi(z) / (1-alpha)"""
    sigma_p = float(np.sqrt(weights @ sigma @ weights)) * np.sqrt(horizon)
    z = norm.ppf(alpha)
    return {"VaR": z * sigma_p, "ES": sigma_p * norm.pdf(z) / (1 - alpha),
            "sigma_p": sigma_p}

risk_model_demo.py 第 [4] 段构造了一个有 regime 切换的序列:1000 天自由度为 4 的 \(t\) 分布收益(标准化为单位方差),前 750 天日波动 1%,最后 250 天升到 2%。当前 regime 下的真实 99% VaR 和 ES 可以解析算出,用来对照:

alpha=0.99: HS VaR 0.0364 ES 0.0536 | HW VaR 0.0725 ES 0.1091 | n_tail 10
alpha=0.975: HS VaR 0.0276 ES 0.0400 | HW VaR 0.0513 ES 0.0809 | n_tail 25
true current-regime t(4) 99%: VaR 0.0530 ES 0.0738
parametric (last 250d vol) 99%: VaR 0.0474 ES 0.0543
10-day HS VaR 99% (overlapping) = 0.1256

三种方法的误差方向各不相同。历史模拟(HS)把 750 天低波动样本混进来,VaR 0.0364 比真值 0.0530 低了三成。正态参数法用对了最近 250 天的波动,但正态尾部太薄,ES 0.0543 比真值 0.0738 低了 26%。Hull-White 在这一次实现里高估了(0.0725):结尾那段的 EWMA 波动 \(\sigma_{\text{now}}\) 恰好被几个大的 \(t\) 分布收益推高,而 \(\lambda=0.94\) 的有效窗口只有十几天。这个例子说明,没有哪种方法在所有 regime 下都占优,这也是生产中通常让几种方法并行、互相校验的原因。

工程对比

方法 相对速度 分布假设 厚尾 适用范围 主要陷阱
参数法 最快 正态或 \(t\) 弱 线性头寸 尾部低估;\(\sqrt{h}\) 放大依赖独立同分布
历史模拟 快 无 保留历史尾部 线性头寸,或可快速全量重估的组合 regime 切换时滞后;尾部样本少
Hull-White 快 历史分布形状不变 保留 同上 对 \(\lambda\) 和近期大收益敏感
Monte Carlo 最慢 任意可采样分布 取决于模型 含衍生品的组合 模型设定误差;计算成本

几种方法并行时,结果差异超过某个阈值就触发人工复核,阈值按组合类型校准,这是工程做法,不是监管要求。

为什么监管从 VaR 转向 ES

Artzner, Delbaen, Eber & Heath(1999,Mathematical Finance)提出一致性风险测度(coherent risk measure)的四条公理:单调性、次可加性、正齐次性、平移不变性。VaR 一般不满足次可加性:合并两个组合后的 VaR 可能大于各自 VaR 之和,与”分散化不应增加风险”相矛盾。Acerbi & Tasche(2002,Journal of Banking & Finance)证明了 ES 是一致性风险测度。VaR 也不区分超过分位数之后的损失有多大,两个分布在 99% 分位数相同、尾部长度不同,VaR 报同一个数,ES 能区分。

巴塞尔委员会的市场风险框架(BCBS 2016 年 1 月发布 d352,2019 年 1 月发布修订版 d457,现已并入 Basel Framework 的 MAR 章节)把内部模型法的核心度量从 VaR(99%)加压力 VaR 换成了压力期校准的 ES。MAR33.2 规定计算 ES 必须使用 97.5% 单尾置信水平;MAR33.3 至 MAR33.4 要求以 10 天为基准流动性期限,再按风险因子的流动性期限放大。d457 的发布页写明修订版自 2022 年 1 月 1 日生效,各司法辖区的实际落地时间不同。

回测:Kupiec、Christoffersen 与红绿灯

VaR 模型要回测:\(T\) 天里实际损失超过 VaR 的次数 \(x\) 应接近 \(T(1-\alpha)\)。Kupiec(1995,Journal of Derivatives)的失败比例检验(proportion of failures,POF):

\[ \text{LR}_{POF} = -2\ln\left[\frac{(1-\alpha)^x \alpha^{T-x}}{\hat p^{\,x} (1-\hat p)^{T-x}}\right], \qquad \hat p = x/T \]

原假设下渐近服从 \(\chi^2(1)\),5% 显著性水平的临界值为 3.84。Christoffersen(1998,International Economic Review)进一步检验例外是否在时间上独立(不扎堆),并把频率检验和独立性检验合成条件覆盖检验。

巴塞尔的红绿灯规则见 MAR32.9 表 1:对 250 个观测、99% VaR 的例外次数分区,绿区 0 到 4 次(乘数 1.50),黄区 5 到 9 次(乘数 1.70 至 1.92),红区 10 次及以上(乘数 2.00)。MAR32.14 规定,进入红区时监管会自动提高乘数,或者不再允许银行使用该模型。

def kupiec_pof(n_exceptions: int, n_obs: int, alpha: float = 0.99) -> dict:
    """Kupiec (1995) proportion-of-failures 似然比检验"""
    p, x, T = 1 - alpha, n_exceptions, n_obs
    phat = x / T
    ll0 = (T - x) * np.log(1 - p) + x * np.log(p)
    ll1 = (T - x) * np.log(1 - phat) + (x * np.log(phat) if x > 0 else 0.0)
    lr = -2 * (ll0 - ll1)
    return {"LR": float(lr), "p_value": float(1 - chi2.cdf(lr, df=1))}


def basel_traffic_light(n_exceptions: int) -> str:
    """BCBS MAR32.9:250 个观测、99% VaR 的例外次数分区"""
    if n_exceptions <= 4:
        return "green"
    return "amber" if n_exceptions <= 9 else "red"

对上面那个 regime 切换序列的最后 250 天做滚动回测(risk_model_demo.py 第 [5] 段):

HS: exceptions 5/250, Kupiec LR 1.96, p 0.1619, traffic light amber
Hull-White: exceptions 3/250, Kupiec LR 0.09, p 0.7580, traffic light green

滚动 250 日历史模拟出现 5 次例外,落入黄区,但 Kupiec 检验在 5% 水平上并不拒绝原假设(\(p = 0.16\))。原因是 250 个观测在 99% 水平下只期望 2.5 次例外,检验功效很低,这也是红绿灯规则需要用分区而不是单一 \(p\) 值的原因。


七、压力测试与情景分析

VaR 和 ES 都是从某个分布里估计出来的统计量,分布外的事件它们看不到。压力测试直接构造极端情景,按情景重新计算组合损失。两者是互补关系。

压力测试的三类情景与共同冲击层

在因子模型下,情景就是一个因子冲击向量 \(\Delta f\) 加上个股特异冲击 \(\Delta u\),组合损失为:

\[ L_{\text{scen}} = -b^\top \Delta f - w^\top \Delta u \]

历史情景

从自有因子收益库里截取历史窗口的累计因子收益作为 \(\Delta f\),叠加到当前持仓上。下表是 A 股常用的候选窗口。本文不给出各窗口的指数跌幅:冲击向量应当用自己的因子库按窗口重算,口径不同,数字差异很大。

情景 窗口 需要刻画的冲击
全球金融危机 2008 年 9 月至 11 月 国家因子大幅下跌,相关性整体上升,流动性收缩
A 股去杠杆 2015 年 6 月至 9 月 国家因子连续下跌,杠杆资金被动平仓,大面积跌停导致无法成交
熔断周 2016 年 1 月第一周 交易时间被截断,日内无法调仓
疫情首轮冲击 2020 年春节后首个交易日前后 单日跳空,随后快速修复
风格反转 2021 年 2 月起 大市值、高估值风格回撤,Size 因子方向切换
小盘急跌 2024 年 2 月上旬 Size、Liquidity 等风格因子极端收益,小盘股流动性折价

历史情景的优点是真实发生过,缺点是下一次危机的细节不会重复。2016 年熔断这类事件主要冲击的是”能不能交易”,因子收益本身刻画不了,要和第八节的执行约束、运维与合规 里的熔断处置预案一起看。

假设情景

由风控人员手工构造联合冲击,例如:国家因子单日 -10%;动量 -3σ 同时价值 +3σ;单一行业 -25% 其他行业不变;利率 +200bp 或汇率 ±5% 通过行业暴露传导;冲击成本 ×3、可成交量 ×0.3。最后一类不改变因子收益,改变的是平仓成本,需要接 交易成本模型 里的冲击成本函数,把”按当前持仓在 N 天内平掉要付多少成本”加进情景损失。

假设情景的难点是”哪些因子同时动、动多少”。冲击之间不协调(例如国家因子大跌但所有行业不动)会得到一个在现有相关结构下几乎不可能的情景,下面的反向压力测试可以用来校验。

反向压力测试

给定目标损失 \(L^*\),求”最可能”导致该损失的因子冲击,即在因子分布下马氏距离最小的冲击:

\[ \min_{\Delta f}\ \frac{1}{2}\Delta f^\top \Sigma_f^{-1} \Delta f \quad \text{s.t.} \quad -b^\top \Delta f = L^* \]

拉格朗日条件给出闭式解:

\[ \Delta f^* = -\frac{L^*}{b^\top \Sigma_f b}\, \Sigma_f b \]

冲击方向是 \(\Sigma_f b\),即组合的因子风险贡献方向;所需的马氏距离为 \(L^*/\sqrt{b^\top \Sigma_f b}\),也就是”要亏 \(L^*\) 需要几个因子风险标准差”。这个闭式解假设损失对因子线性、因子收益近似正态,含期权的组合不适用。

def reverse_stress(b: np.ndarray, sigma_f: np.ndarray, target_loss: float) -> np.ndarray:
    """
    min 0.5 * df' Sigma_f^{-1} df  s.t.  -b' df = L*
    闭式解:df* = -L* Sigma_f b / (b' Sigma_f b)
    """
    sb = sigma_f @ b
    return -target_loss * sb / float(b @ sb)

对第五节那个小盘等权组合,用月度因子协方差(日协方差 ×21)求 10% 损失的反向情景,并把冲击换算成各因子月度标准差的倍数(risk_model_demo.py 第 [6] 段):

loss check -b'df = 0.1000
largest shocks in monthly sigma units:
country           -1.12
size               0.76
non_linear_size    0.65
ind_03             0.28

结论可以直接读:这个组合亏 10% 最可能的路径是”大盘跌约 1.1 个月度标准差,同时大盘股跑赢小盘股约 0.7 个标准差”。如果风控的假设情景库里没有类似组合,就是一个缺口。

尾部相关性

压力情景还要面对相关性在尾部上升的问题。Longin & Solnik(2001,Journal of Finance)用极值理论研究国际股市,发现相关性在熊市尾部上升、在牛市尾部不上升;Ang & Chen(2002,Journal of Financial Economics)发现美股组合与市场之间的相关性在下跌时明显高于上涨时。正常时期估计的相关矩阵会高估危机中的分散化效果。

一种工程处理是在压力情景中把相关阵向全 1 矩阵 \(J\) 推:

\[ \rho^{\text{stress}} = (1-\kappa)\,\rho + \kappa J, \qquad \kappa \in [0, 1] \]

非对角元即 \(\rho_{ij} + \kappa(1-\rho_{ij})\)。因为 \(\rho\) 和 \(J\) 都半正定,凸组合仍然半正定,不需要额外修复。\(\kappa\) 的取值没有理论答案,应当用自己的历史危机窗口校准;更精细的做法是用 \(t\)-copula 或 Clayton copula 显式建模下尾相关。

监管框架

巴塞尔市场风险框架里,ES 要按压力期校准(d352 发布页称之为”a shift from value-at-risk to an expected shortfall measure of risk under stress”),交易台层面还要通过损益归属测试(P&L attribution test)和回测才能使用内部模型法,否则回退到标准法(MAR32)。资管机构受到的监管压力测试要求比银行少,但内部风控通常也会参照这套”ES + 情景库 + 回测”的结构搭建。


八、工程实现

每日刷新流水线

下面是一个按 A 股收盘时间设计的示例时间表,用来说明依赖顺序,不是某家机构的实际流程:

  1. 收盘后:等待收盘价、复权因子、行业分类、财务数据的 ETL 完成(数据管线见 行情与基本面数据管线)。
  2. 构造 \(t-1\) 期暴露矩阵 \(X\)。
  3. 横截面回归,得到当日因子收益 \(f_t\) 和残差 \(u_t\)。
  4. 更新因子协方差 \(\Sigma_f\)(含 Newey-West、eigenfactor 调整、VRA)和特异波动 \(\delta_i\)。
  5. 合成资产协方差或直接以因子形式落库。
  6. 对当前持仓跑风险归因、VaR / ES、压力测试,生成报告。

每一步都需要依赖管理、失败重试、版本号和回滚。调度通常交给 Airflow、Prefect、Dagster 这一类框架。

计算量:本机实测

用 risk_model_demo.py 第 [8] 段在 A 股量级上计时:\(N=4000\),1 个国家、32 个行业、10 个风格(\(K=43\)),每项运行 5 次取中位数,环境见开头说明。

步骤 规模 中位耗时(ms)
build_exposure_matrix 4000 × 10 个描述子 16.42
cross_section_regression 4000 × 43,含 KKT 求解 1.26
factor_model_cov 1000 天 × 43 因子,含 4000 × 4000 资产协方差合成 420.84
portfolio_risk_attribution 单组合 1.50
historical_var 1000 个观测 0.08

耗时大头是 \(4000 \times 4000\) 资产协方差的合成,单个矩阵就是 128 MB(float64)。实际系统通常只存 \(X\)、\(\Sigma_f\)、\(\delta^2\),需要组合风险时按 \(b = X^\top w\) 在因子空间里算,不物化完整的 \(\Sigma\)。按日存档时,这三样的体积也远小于完整协方差。

模型版本与回测一致性

风险模型会迭代:加因子、调权重、改估计方法。回测和归因必须用当时的模型版本,而不是最新版本:

每个优化任务的输入快照都带哈希存档,“同样输入得到不同输出”就说明数据流或优化器存在非确定性问题:

import hashlib


class RiskModelSnapshot:
    def __init__(self, date: pd.Timestamp, version: str, exposures: pd.DataFrame,
                 factor_cov: pd.DataFrame, specific_var: pd.Series):
        self.date = date
        self.version = version
        self.exposures = exposures          # (N, K)
        self.factor_cov = factor_cov        # (K, K)
        self.specific_var = specific_var    # (N,)

    def asset_cov(self) -> np.ndarray:
        X = self.exposures.values
        S = X @ self.factor_cov.values @ X.T + np.diag(self.specific_var.values)
        return (S + S.T) / 2

    def hash(self) -> str:
        h = hashlib.sha256()
        h.update(self.date.isoformat().encode())
        h.update(self.version.encode())
        for arr in (self.exposures.values, self.factor_cov.values, self.specific_var.values):
            h.update(np.ascontiguousarray(arr, dtype=np.float64).tobytes())
        return h.hexdigest()[:16]
[7] snapshot hash 04622b15fca214da, asset cov symmetric True

np.ascontiguousarray(..., dtype=np.float64) 不能省:pandas 返回的数组可能是非连续视图或不同 dtype,直接 tobytes() 会让同样的数值得到不同的哈希。

与组合优化器对接

在二次规划里显式使用风险模型:

\[ \max_w \quad \mu^\top w - \frac{\lambda}{2} w^\top \Sigma w \]

加上行业偏离、风格偏离、跟踪误差、单股上限等约束。因子形式可以把稠密的 \(N \times N\) 二次项拆开:引入辅助变量 \(y = X^\top w\),

\[ w^\top \Sigma w = y^\top \Sigma_f y + \sum_i \delta_i^2 w_i^2 \]

二次项变成 \(K \times K\) 稠密加对角,cvxpy、MOSEK 这类建模工具都能直接表达。\(\Sigma_f\) 半正定、\(\delta_i^2 > 0\) 时,整个二次型自然正定,不需要额外的 PSD 投影。约束互相矛盾时优化器会返回 infeasible,回测必须有降级策略(沿用上一日权重,或放松部分约束)。回测引擎如何在每个调仓日调用风险模型快照,见 回测引擎设计;同样的嵌入方式也用于 交易成本模型 第六节的成本项。

监控与告警

以下阈值是工程经验值,需要按自己的模型校准:


九、谱系、争论与开放问题

谱系:今天的风险模型从哪里来

年份 工作 贡献 在本文中的位置
1967 Marčenko & Pastur,Math. USSR-Sbornik 随机矩阵特征值的极限谱 第二节:样本谱失真
1973 Fama & MacBeth,JPE 逐期横截面回归 + 斜率时序检验 第三节:横截面回归原型
1974 Rosenberg,JFQA 市场外协方差;载荷由公司特征决定 第三节:基本面因子模型
1986 Connor & Korajczyk,JFE 渐近主成分法 统计因子模型(Axioma 统计版用两步 APC)
1987 Newey & West,Econometrica 半正定的 HAC 协方差 第二节:日频估计月度风险
1989 Michaud,FAJ 均值方差优化是”误差最大化” 第二节
1999 Laloux et al.,PRL 经验相关矩阵的主体是噪声 第二节、争论二
1999 Artzner et al.,Mathematical Finance 一致性风险测度 第六节、争论三
2004 Ledoit & Wolf,JMVA / JPM 线性收缩估计 第二节
2010 El Karoui,Annals of Statistics 高维二次规划的风险低估 争论二
2011–2012 Menchero, Orr & Wang(USE4 notes);Menchero, Wang & Orr,FAJ eigenfactor 风险调整、波动率状态调整 第三、四节
2016 / 2019 BCBS d352 / d457 以 97.5% ES 取代 99% VaR 作为内部模型核心度量 第六节
2017 / 2020 Ledoit & Wolf,RFS / AoS;Bun, Bouchaud & Potters,Physics Reports 非线性收缩、旋转不变估计 争论二、开放问题

演进的主线是:Rosenberg 和 Fama-MacBeth 定义了”暴露 × 因子收益 + 特异”的结构;随机矩阵理论和 Michaud 指出直接用样本协方差会被优化器利用;收缩估计和 eigenfactor 调整分别从统计学和厂商实践两条路去修正;VaR 的一致性缺陷推动了监管转向 ES。

争论一:基本面因子还是统计因子

基本面派。Connor(1995,Financial Analysts Journal)比较了宏观、统计、基本面三类因子模型对美股收益的解释力:统计模型和基本面模型都明显好于宏观模型,基本面模型还略好于统计模型。这有些反直觉,因为统计模型是按最大化解释力估计的;Connor 的解释是基本面模型用了大量外部数据,尤其是行业哑变量。他还发现,在基本面模型之上加入宏观因子,边际解释力为零。基本面模型的另一个优势是可解释:约束、归因、报告都要求因子有名字。

统计派。统计因子从收益本身提取,能捕捉到尚未命名的短期共同驱动(第四节残差检查里发现的那类结构),也不依赖财务数据的发布时滞。它的代价是可识别性:主成分的顺序和符号会随样本变化。Axioma 的 AXUS4 factsheet 专门说明它用一个专有算法”保持因子收益的顺序和符号随时间一致”,这正说明了这个问题在生产中真实存在。

模拟证据与边界。第二节的模拟里,统计 PCA-5 的预测/真实波动比为 0.845,已知真实暴露的基本面模型为 0.992。后者的暴露是真实的,所以这是基本面模型的理想上界;现实中两者的差距取决于基本面暴露的设定误差有多大。厂商的实际做法是两种都提供(Axioma 同时提供两类模型),用户按用途选择或对照。

争论二:高维协方差怎么”清洗”

几条路线的分歧在于优化目标:

理论上,El Karoui(2010,Annals of Statistics)证明了高维下由样本协方差求解的二次规划会系统性低估风险;Bouchaud & Potters 的综述给出了 \(R_{\text{in}} = R_{\text{out}}(1-q)\) 这一具体比例,并指出在多元 \(t\) 分布下低估会更严重。各方对”样本协方差在高维下低估最优组合风险”没有分歧,分歧在于哪种修正在非平稳、厚尾的真实数据上更稳。

争论三:ES 取代 VaR 之后怎么回测

ES 满足一致性(Artzner et al. 1999;Acerbi & Tasche 2002),但 Gneiting(2011,Journal of the American Statistical Association)证明了 ES 不具有可引出性(elicitability),即不存在一个评分函数使 ES 恰好是其期望损失的最小化者,而 VaR(分位数)具有可引出性。这一结果一度被解读为”ES 无法回测”。Acerbi & Székely(2014,Risk,MSCI 研究)反驳说,可引出性关系到模型比较,不是回测的必要条件,并给出了三种不依赖模型的 ES 回测方法;Fissler & Ziegel(2016,Annals of Statistics)证明了 VaR 与 ES 联合可引出。

监管的落地方式反映了这场争论的妥协:资本用 97.5% ES 计算(MAR33.2),回测仍然基于 VaR。银行整体层面按 250 个观测、99% VaR 的红绿灯分区(MAR32.9),交易台层面同时回测 97.5% 和 99% 两个水平的单日 VaR(MAR32.17)。

开放问题

  1. 特异收益相关的建模。行业切细能吸收多少跨股票相关,剩下的概念板块、产业链相关怎么在不引爆参数量的前提下进入模型?残差上叠加统计因子是一种做法,但它会和基本面因子的可解释性冲突。这个问题直接决定集中持仓和小盘组合的风险是否被低估。可读入口:Bun, Bouchaud & Potters(2017)综述中关于相关矩阵清洗的章节。
  2. 非平稳与厚尾下的偏差修正。eigenfactor 调整的模拟偏差假设正态、平稳,USE4 文档自己承认这样估出的偏差不够,需要经验缩放系数 \(a=1.4\);半衰期和 VRA 也都是启发式规则。能否有在 regime 切换下仍然有理论保证的修正,目前没有定论。可读入口:Menchero, Wang & Orr(2012);Ledoit & Wolf(2020)。
  3. 小样本下的 ES 回测功效。97.5% 水平下 1000 个观测只有 25 个尾部点(第六节输出中的 n_tail 25),250 个观测的 VaR 回测在 \(p=0.16\) 时就已进入黄区。ES 回测的统计功效、压力期窗口如何选取,都直接影响资本和限额。可读入口:Acerbi & Székely(2014);Fissler & Ziegel(2016)。

十、小结


参考文献

规范与监管

厂商方法论文档

核心论文

其他论文与书

实验与工具


系列导航

延伸阅读

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。

2026-05-01 · quant

【量化交易】市场微结构:订单簿、价差、流动性、冲击

回测按中价成交,实盘却被价差、冲击和逆向选择吃掉收益。本文从限价订单簿出发拆解价差口径与分解、流动性四维、OFI、Kyle λ、VPIN 与 Hawkes,并用币安 LTCUSDT 真实数据验证:10 秒 OFI 解释约六成中价变化,有效价差三分之二以上表现为价格冲击。

2026-05-01 · quant

【量化交易】高频交易架构:低延迟、内核旁路、FPGA 概览

高频交易(HFT)的工程难点不在策略,而在「让一行代码以纳秒为单位稳定运行」。本文从延迟预算分解开始,依次走过 colocation 与微波链路、内核旁路(DPDK / Solarflare Onload / AF_XDP)、NUMA 与缓存调优、FPGA tick-to-trade、lock-free SPSC 队列、以及 Python 在这种系统中的合理边界。给出可运行的 numba 版 SPSC ring buffer 与 timeit 基准,用于演示热路径的设计思想。


By .