halo 的技术博客

返回

先说结论:一个 99% VaR 模型在 1000 个交易日里”应该”被击穿 10 次左右——击穿太多说明模型低估风险,击穿太少说明模型过度保守占用资本。Kupiec 失败比例检验(Proportion of Failures, POF)用一个似然比统计量把”多少次算正常”变成了精确的接受域:T=1000、p=1% 时,突破 5~16 次都在 5% 显著性下可接受,出了这个区间模型就该下线检修。本文的模拟实验里,无条件正态 VaR 突破 11 次(p=0.75,通过),GARCH-正态突破 20 次(p=0.005,拒绝),GARCH-t 突破 13 次(p=0.36,通过)——一个反直觉的结果藏在后面:动态模型配上错误的尾部分布,比静态模型死得更快

VaR 回测的本质:数硬币#

VaR 模型每天输出一个数:“明天的损失有 99% 的概率不超过 X”。事后验证它的方式出奇地简单:数一数实际损失超过 X 的天数。定义突破指示变量:

It={1,rt<VaRt0,否则I_t = \begin{cases} 1, & r_t < -VaR_t \\ 0, & \text{否则} \end{cases}

如果模型正确,ItI_t 应该是参数 p=0.01 的伯努利序列——每天以 1% 的概率掷出”突破”。T 天里的突破总数 x=Itx = \sum I_t 服从二项分布 B(T,p)B(T, p)。VaR 回测就此变成一个统计学入门问题:观测到的突破频率 π^=x/T\hat{\pi} = x/T 与名义水平 p 是否相容

Kupiec (1995) 用似然比构造检验统计量:

LRPOF=2ln(1p)Txpx(1π^)Txπ^xLR_{POF} = -2 \ln \frac{(1-p)^{T-x} \, p^x}{(1-\hat{\pi})^{T-x} \, \hat{\pi}^x}

分子是”模型正确”(突破率恰为 p)下的似然,分母是让数据自己说话(突破率取样本频率 π^\hat{\pi})的似然。模型越离谱,两个似然差距越大,LR 越大。在 H0 下 LR 渐近服从自由度 1 的卡方分布,95% 临界值 3.84。

import numpy as np
from scipy import stats

def kupiec_pof(returns, var_series, p=0.01):
    """returns: 实际日收益;var_series: 每日 VaR(负数,表示损失阈值)"""
    exceptions = returns < var_series
    x, T = exceptions.sum(), len(returns)
    pi_hat = x / T
    if x == 0:
        lr = -2 * T * np.log(1 - p)
    else:
        lr = -2 * (np.log((1-p)**(T-x) * p**x)
                   - np.log((1-pi_hat)**(T-x) * pi_hat**x))
    pval = 1 - stats.chi2.cdf(lr, df=1)
    return x, pi_hat, lr, pval
python

接受域长什么样#

把 T=1000、p=1% 时的 LR 统计量对突破次数画出来:

LR 统计量与接受域:5~16 次突破可接受

曲线在期望值 x=10 附近触底(LR≈0),向两侧陡峭上升。与 3.84 临界线的交点给出接受域 [5, 16]。两个值得注意的细节:

  • 检验是双侧的。突破 3 次和突破 20 次一样会被拒绝——前者说明 VaR 定得太高,资本被无谓锁死;后者说明风险被低估。多数人只盯着”突破太多”,但监管资本占用的角度看”突破太少”同样是模型缺陷。
  • 接受域不对称。左边界离期望值 5 步,右边界离 6 步——似然比在低突破侧上升更快,因为二项分布在小 p 下右偏。

实验:三个 VaR 模型进体检室#

模拟 1000 天带波动聚集的收益序列(GARCH(1,1),创新项为 t(6) 分布),给三个常见的 99% VaR 模型做体检:

  1. 无条件正态:全样本波动率 × 2.33,一条水平线;
  2. GARCH-正态:条件波动率 × 2.33,随波动起伏;
  3. GARCH-t:条件波动率 × t(6) 分位数,动态且厚尾。

静态 VaR 在高波动期被密集击穿

体检结果:

三个模型的突破次数与 POF 检验 p 值

模型突破次数突破率LR 统计量p 值结论
无条件正态111.1%0.100.754通过
GARCH-正态202.0%7.830.005拒绝
GARCH-t131.3%0.830.362通过

最有信息量的是 GARCH-正态的溃败。它正确捕捉了波动的时变性,却用正态分位数 2.33 去覆盖一个 t(6) 的尾巴——每一天都系统性地低估尾部,日积月累突破翻倍。而无条件正态反而”通过”了:静态高波动估计在平静期过度保守、在动荡期严重不足,两种错误在总次数上恰好抵消。这暴露了 POF 检验最著名的盲区。

POF 的盲区:只数次数,不看位置#

无条件正态模型的 11 次突破不是均匀散落的——看第一张图,它们密集扎堆在高波动区间。突破的聚集意味着:当第一次击穿发生时,接下来几天再次击穿的条件概率远高于 1%。对风险管理来说这是灾难性的性质——损失不是独立的意外,而是连环撞击。

POF 检验对此完全失明,因为它把 {It}\{I_t\} 压缩成了一个总和 x,扔掉了全部时序信息。补上这一课需要 Christoffersen 独立性检验(检验突破序列的一阶马尔可夫结构)或动态分位数 DQ 检验(把突破对滞后信息集回归)。工程实践中的标准组合是 POF + 独立性的联合检验(条件覆盖检验 CC)LRCC=LRPOF+LRindLR_{CC} = LR_{POF} + LR_{ind},服从自由度 2 的卡方分布。只跑 POF 相当于体检只量了体重。

功效问题:1000 天其实很短#

检验的另一个软肋是功效。模拟真实突破率从 0.2% 到 4% 的场景,看 POF 在 T=1000 时能以多大概率识破错误模型:

POF 检验功效曲线

真实突破率 1.5%(模型低估风险 50%)时,检验只有约 25% 的概率发出拒绝信号——四次里有三次,一个明显低估风险的模型会拿到体检合格证。要达到 80% 的功效,真实突破率得超过 2.2%,即模型错到两倍以上。原因很朴素:p=1% 意味着 1000 天只有 10 个期望突破,信息量就这么点。这是所有基于突破计数的检验的共同宿命,也是巴塞尔交通灯机制直接用突破次数分区(绿/黄/红)而不纠结 p 值的现实考量——监管者早就接受了这个检验的粗糙。

缓解方式有三条:拉长窗口(250 天的监管窗口功效低得可怜,内部风控应该用 2~4 年);提高置信水平检验的层级(同时回测 95% 和 99% VaR,95% 层期望突破多 5 倍,功效显著更高);或者换用对整条尾部敏感的 ES 回测

工程清单#

把 POF 检验落进风控流水线时的实操要点:

  • 滚动窗口而非一次性:每季度对过去 1000 天滚动跑一次,连续两个季度落入拒绝域才触发模型下线——避免单次噪声引发的模型震荡;
  • 双侧都要报警:突破不足的模型在浪费资本,一个长期突破 2~3 次/千日的 99% VaR,其隐含置信水平其实是 99.7%+,资本占用可能高出 30%;
  • 分层回测:同一模型在 90%/95%/99% 三个水平各跑一次 POF,只有 99% 层通过而 95% 层拒绝,通常说明尾部形状(而非尺度)错了——该换分布,不是调波动率;
  • x=0 的陷阱:零突破时 π^=0\hat{\pi}=0,似然公式里出现 000^0,代码里要走单独分支 LR=2Tln(1p)LR = -2T\ln(1-p)。T=1000、p=1% 时这个值是 20.1,零突破本身就足以拒绝模型——这经常让人意外,但 1000 天一次都不破的 99% VaR 确实不是好模型。

POF 检验是 VaR 验证体系的地基而非全部:它便宜、透明、监管认可,能在十行代码里告诉你模型的突破频率是否离谱。但它对突破聚集失明、小样本下牙齿不利。正确的用法是把它当第一道闸门,后面串上独立性检验和 ES 回测——就像体检先量体重,但没人只靠体重判断健康。

Kupiec POF 检验:用失败次数的似然比给 VaR 模型做体检
https://blog.halo26812.eu.org/blog/kupiec-pof-test
Author halo
Published at 2026年7月27日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨