12 ETF 蒙特卡洛对照:六模型 ES 波动比 0.671–0.768,但 Gaussian 下行共振 0.3180 低于训练参照 0.4211。
图:同一训练历史下,情景翻倍降低抽样波动,却未自动修复 Gaussian 的下行共振差距;底部 OOS 是另外三种单期 VaR 方法。

做蒙特卡洛风险模拟,结果一抖,最顺手的办法往往是再多生成一倍情景。但多跑出来的这 756 条,到底补进了什么风险信息?我想把这件事算清楚。

这次我把这个问题拆开,用 12 只 ETF 的真实历史价格、6 种经典生成方法和 50 个配对随机种子做对照。先说结果:从 756 条增加到 1,512 条,部分指标的随机波动确实下降了;但 Gaussian 生成的下行共振率仍只有约 0.3180,固定训练样本的参照是 0.4211。 简单说,这里的“下行共振”就是:SPY 大跌时,其他 ETF 有多少也跟着大跌? “大跌”的门槛提前固定为各只 ETF 在训练期最差 5% 的收益,生成情景时不重新划线。

如果你正在评估一套风险情景生成器,我觉得值得带着三个问题看结果:重复生成是否更稳定?同日共跌和连续多日风险是否保留下来?样本外风险阈值能否校准?

为什么选这 12 只 ETF 和 756 日训练窗口

数据来自 Boris Marjanovic 的 Huge Stock Market Dataset,来源标明 CC0,价格已按其说明调整分红和拆股。我选的 12 只 ETF 跨越股票、债券、信用、黄金、商品、美元和房地产敞口;单看 SPY 自己的涨跌,很难研究跨资产共跌。完整标的代码放在文末。

我把 12 个 ETF 原始文件中的调整后 Close 按日期取交集,缺的日子不补。受这份数据里 HYG 首个记录日的限制,共同样本从 2007-04-11 到 2017-11-10,一共 2,668 个价格日。随后转成日简单收益,每天把 12 只 ETF 等权平均;从头到尾都用这个组合口径。

情景生成统一使用最初 756 个日收益,大约三个交易年,包含 2008 年的市场冲击。固定这段训练历史,是为了让不同方法面对同一个参照,而不是让每个方法拿最合适自己的窗口。Student-t 的自由度直接固定为 5,也不根据后面的样本外表现调整。

六种方法的差别可以压缩成一张小表:

方法情景怎么生成
Gaussian用训练期均值和协方差生成多元正态收益
Student-t df=5固定自由度 5 的多元 t 分布,协方差与训练期匹配
IID Bootstrap独立、有放回地抽取训练期的整行 ETF 收益
Moving-block 5 / 20 / 60每次抽连续 5、20 或 60 日的收益块

这里先防一个很容易把结论带歪的坑:两次模拟如果换了随机数,差异可能只是运气。 所以我把种子固定在 1000–1049,一共 50 组;每组先生成 1,512 条,再从同一条序列里截前 756 条作配对比较。说白了,就是拿这 756 天的老底子反复抽,模拟条数多了,真实行情可不会凭空长出来。

Student-t 还有个坑:套上“肥尾”分布,如果忘了匹配方差,就把波动率一起放大了。 我的实现先用训练协方差的 Cholesky 因子得到相关正态向量,再乘 √(3/χ²₅)(χ²₅ 是自由度为 5 的卡方变量)。等价地,t 分布的尺度矩阵用训练协方差的 3/5;省掉这步,生成协方差会虚胖到 5/3 倍。

情景翻倍:Monte Carlo 误差小了多少

先看一日 ES(5%),也就是收益落在最差 5% 时的平均值。我没有只盯着某次模拟的结果:每个种子各算一次 ES,再拿 50 次结果的标准差作比较,看看从 756 条加到 1,512 条究竟省下多少抽样噪音。

生成方法ES 跨种子标准差比:1,512 / 756
Gaussian0.718
Student-t df=50.671
IID Bootstrap0.746
Moving-block 50.768
Moving-block 200.681
Moving-block 600.734

六组比值落在 0.671–0.768,都小于 1:这次 50 个种子之间的 ES 估计更集中。样本翻倍时常用 1/√2≈0.707 作抽样波动的直觉参照,但 ES 是尾部统计量,不必整齐地按同一速度收敛。

共跌率的统计过程是:只选 SPY 跌过训练期 5% 阈值的日子,逐日计算其余 11 只 ETF 也跌过各自训练期阈值的比例,再对这些日子平均。Gaussian 相对训练参照的平均误差,从 N=756 的约 -0.1036 到 N=1,512 的约 -0.1031,差距几乎没动。

抽样更多让模型输出更稳,却没让它自动生成更多符合训练历史的共同下跌场景。

IID Bootstrap 保留了共跌,却丢了波动聚集

单日共跌接近,不代表坏行情连续出现的方式也接近。下表的指标都算在 12 只 ETF 的等权组合日收益上:ACF(1) 衡量大幅波动是否挨着发生;60 日 ES 和回撤则观察更长的路径。

这里还得先防一个计算坑:连续亏几天,不能把日收益简单相加就当成最终跌幅。 我沿生成序列每天向前滑一格,取连续 60 日,按 ∏(1+rₜ)-1 连乘出窗口收益;最差 5% 窗口的平均收益就是 60 日 ES。回撤则先在每个窗口的复利净值路径上单独算,再取所有窗口回撤的 5% 分位数。

下面全部是 N=1,512、50 个随机种子的生成指标均值,与同一训练窗口的经验参照比较:

情景来源下行共振率绝对收益 ACF(1)60 日 ES(5%)60 日最大回撤 5% 分位
训练样本参照0.42110.3000-21.01%-25.45%
Gaussian0.3180-0.0011-15.77%-16.53%
Student-t df=50.3549-0.0005-15.97%-17.00%
IID Bootstrap0.42650.0059-15.15%-16.26%
Block 50.42090.2408-14.45%-15.75%
Block 200.42270.2842-16.54%-18.56%
Block 600.41130.2684-17.49%-20.48%

不用逐格读。先看 IID Bootstrap:它抽的是同一交易日的整行收益,因此下行共振 0.4265 与训练参照 0.4211 很接近;但它把不同日期独立抽出,ACF(1) 只剩 0.0059,而历史参照是 0.3000。

换句话说,它还记得哪些资产可能同一天跌,却几乎忘了大波动会不会连续发生。

Block 20 的 ACF(1) 达到 0.2842,已经接近固定训练参照。Block 60 的 60 日 ES 则从 IID 的 -15.15% 移到 -17.49%,但距离历史参照 -21.01% 仍有差距。块长也不是越长越好:例如 Block 5 的 60 日 ES 比 IID 还浅。

我为什么非要用重叠窗口?如果硬改成完全不重叠,756 日的训练历史只能切出约 12 段,1,512 日的生成路径也不过约 25 段。拿十来段独立路径估最差 5%,我不太信这个数能稳到哪里去。于是保留逐日滑动:训练侧有 697 个窗口,生成侧有 1,453 个,但它们互相重叠,当然不能当成那么多次独立风险事件。再加上训练期包含 2008 年冲击,-21.01% 是我这段历史里的参照,不是必须精确命中的靶心。

Moving-block Bootstrap:块接缝与样本边缘是两回事

看到 Block 的 ACF 上来,我最初也想顺手归因:它把连续日期保留下来了嘛。可我回头检查抽样器,才发现自己把两件事混在了一起。

第一种是块接缝:一块里面是连续的真实日期,下一块却可能跳到另一个历史位置。块长为 L 时,相邻两天跨过接缝的比例大约是 1/L;在理想化的独立拼块近似下,一阶自相关会按 1−1/L 的量级衰减。训练参照 ACF 为 0.3000,L=5/20 的粗估分别是 0.240/0.285,与实测 0.2408/0.2842 很接近。不过 L=60 的结果并不符合这么简单的缩放,这只能当理解机制的直觉,不能代替实验。

第二种是历史样本首尾的抽样权重。普通非循环 Moving-block 的合法块起点为 0 到 756−L,最边缘的日期比中间日期更少进入抽样块,位置权重最多相差 L 倍。为了看它在这份数据里究竟影响多大,我又用均匀起点、首尾循环接续的 Circular Block,按原来的 50 个种子补了对照:

块长ACF(1):普通 / 循环60 日 ES:普通 / 循环
50.2408 / 0.2411-14.45% / -14.35%
200.2842 / 0.2884-16.54% / -16.37%
600.2684 / 0.2792-17.49% / -17.23%

结果有点不给这个“边界问题”面子:Block 20 的 ACF 只从 0.2842 走到 0.2884,60 日 ES 从 -16.54% 到 -16.37%。端点确实有权重差异,但在这份样本里,它解释不了 Block 和 IID 的主要差别。而且 Circular 会把历史末尾和开头硬接起来,我也不会因为名字里有个“修正”就宣布它更好。

这次追加对照的意义,是把“块接缝打断相关性”和“原始历史首尾样本降权”分开,不再把 Block 的变化都归给同一个原因。Circular Block Resampling 的方法背景可进一步追溯。

一日 VaR 回测:三个单期方法在整段样本外未通过校准

前面我问的是生成情景像不像那段历史。现在换个问题:真实的下一天来了,风险阈值到底准不准? 这张表不拿六种生成器争冠军,只测历史经验分位数、Gaussian 和 Student-t 三个单期 VaR 方法。每到一个目标日,我都用它前面最近 756 日的真实等权组合收益重算阈值,而不是一直抱着前面那段固定训练窗。

Student-t 仍固定自由度 5,5% 分位按训练方差匹配;这里说的 VaR “突破”,就是次日实际组合收益跌过模型给出的左尾 5% 阈值。

这项滚动检验一共得到 1,911 个不重叠的日度目标:

一日 95% VaR 方法突破次数突破率Kupiec p 值Christoffersen 独立性 p 值
历史经验522.72%6.16e-70.0145
Gaussian512.67%3.16e-70.0122
Student-t df=5623.24%1.75e-40.0171

按名义 5% 计算,1,911 天预期约有 95.55 次突破,实际三组只有 51–62 次;突破更少不等于校准通过。Kupiec 检验看频率是否对齐名义水平,Christoffersen 检验看突破是否独立,表中两类 p 值都较小。

我先把结论停在整个 1,911 日区间:滚动训练窗开始时还含着 2008 年,随后它逐渐退出。分开市场阶段再测,也许能看出不同的覆盖情况,但我这次没有做这个拆分。三法相邻连续两次突破分别只有 5 / 5 / 6 次,独立性检验值得连着这些实际计数一起看。

至于 Block 60 的 60 日 ES 样本外回测,我这次干脆没硬凑。60 日预测目标一旦按重叠窗口滑动,相邻观测就缠在一起;要给它设计一套站得住脚的回测口径,我还没有完成。一日 VaR 的 Kupiec/Christoffersen 检验不能直接搬过去,所以这里老老实实只报告单日 VaR。

还有一个实用细节:Student-t 经常被概括成“肥尾,所以 VaR 更保守”。这里自由度固定为 5、方差匹配时,其标准化左侧 5% 分位约为 -1.56085,反而比正态的 -1.64485 更靠近零。只看分布名称,不能推断每个置信水平的阈值方向。

下一次该把算力花在哪里

下次有人建议“结果不够稳,那就把情景再翻倍”,我不会马上加算力。我会先问三件事:现在抖的是抽样噪音吗?我们关心的共跌和连续亏损,生成器到底保留了多少?真正对应这个风险期限的样本外检验做过没有?多抽一倍,主要只能解决第一问。

如果必须在 Block 20、Block 60 和 Student-t 中挑一个线上风控兜底,我不会用这份训练期比较硬选冠军。Block 20 的 ACF 接近这段历史,不等于它未来覆盖率最好;Student-t 有肥尾,也不保证 95% VaR 更保守。先定风险期限,再做相应的 OOS 检验,比凭感觉选一行表格靠谱。

这次最值得留下的,是 Gaussian 的共跌差距没有因情景翻倍而消失,以及补做 Circular Block 后不得不收窄的归因。以后看到“百万条风险情景”,我会先问:这些情景重复了哪段真实信息,又保留了哪些风险结构?

如果想从更小的案例亲手检查尾部指标,我还写过 vectorbt 中 NaN 怎样改变 CVaR 的样本分母:有可跟着计算的 Python 合成例子。先看清一个缺失值如何改变尾部平均,再回来理解这里的重叠窗口和风险统计,会容易不少。


数据来源与固定参数:Kaggle Huge Stock Market Dataset v3;标的 SPY、IWM、EFA、EEM、TLT、IEF、LQD、HYG、GLD、DBC、UUP、VNQ,调整收盘价日期交集为 2007-04-11 至 2017-11-10,共 2,668 行,未补齐缺失值。固定 CSV SHA-256:0a20c095f0d9400fbf7d30e17a0c664716f147c460b03bf5439cd980ef8bf38a。情景实验用 756 日训练收益与 50 个配对随机种子,另有 1,911 日滚动单期 VaR 对照。本文只讨论历史量化风险建模方法,不构成投资建议。