# 蒙特卡洛情景翻倍，尾部风险就更真实吗？12 ETF 实测

> Canonical: https://vxi.me/posts/monte-carlo-scenarios-tail-risk-12-etf/
> Locale: zh-cn
> Published: 2026-10-23
> Updated: 2026-10-23
> Markdown: https://vxi.me/posts/monte-carlo-scenarios-tail-risk-12-etf.md

<figure class="quant-scenario-hero not-prose" lang="zh-cn">
<picture><source media="(prefers-reduced-motion: reduce) and (max-width: 640px)" srcset="/images/posts/monte-carlo-scenarios-tail-risk-12-etf/scenario-fidelity.mobile.still.zh-cn.svg" type="image/svg+xml"><source media="(prefers-reduced-motion: reduce)" srcset="/images/posts/monte-carlo-scenarios-tail-risk-12-etf/scenario-fidelity.still.zh-cn.svg" type="image/svg+xml"><source media="(max-width: 640px)" srcset="/images/posts/monte-carlo-scenarios-tail-risk-12-etf/scenario-fidelity.mobile.zh-cn.svg" type="image/svg+xml"><img src="/images/posts/monte-carlo-scenarios-tail-risk-12-etf/scenario-fidelity.zh-cn.svg" alt="12 ETF 蒙特卡洛对照：六模型 ES 波动比 0.671–0.768，但 Gaussian 下行共振 0.3180 低于训练参照 0.4211。" width="1200" height="700" loading="eager" fetchpriority="high" decoding="async" style="display:block;width:100%;height:auto"></picture>
<figcaption>图：同一训练历史下，情景翻倍降低抽样波动，却未自动修复 Gaussian 的下行共振差距；底部 OOS 是另外三种单期 VaR 方法。</figcaption>
</figure>

做蒙特卡洛风险模拟，结果一抖，最顺手的办法往往是再多生成一倍情景。但多跑出来的这 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](https://www.kaggle.com/datasets/borismarjanovic/price-volume-data-for-all-us-stocks-etfs)，来源标明 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 |
| --- | ---: |
| Gaussian | 0.718 |
| Student-t df=5 | 0.671 |
| IID Bootstrap | 0.746 |
| Moving-block 5 | 0.768 |
| Moving-block 20 | 0.681 |
| Moving-block 60 | 0.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.4211 | 0.3000 | -21.01% | -25.45% |
| Gaussian | 0.3180 | -0.0011 | -15.77% | -16.53% |
| Student-t df=5 | 0.3549 | -0.0005 | -15.97% | -17.00% |
| IID Bootstrap | 0.4265 | 0.0059 | -15.15% | -16.26% |
| Block 5 | 0.4209 | 0.2408 | -14.45% | -15.75% |
| Block 20 | 0.4227 | 0.2842 | -16.54% | -18.56% |
| Block 60 | 0.4113 | 0.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：普通 / 循环 |
| --- | --- | --- |
| 5 | 0.2408 / 0.2411 | -14.45% / -14.35% |
| 20 | 0.2842 / 0.2884 | -16.54% / -16.37% |
| 60 | 0.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 的方法背景](https://statistics.stanford.edu/technical-reports/circular-block-resampling-procedure-stationary-data)可进一步追溯。

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

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

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

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

| 一日 95% VaR 方法 | 突破次数 | 突破率 | Kupiec p 值 | Christoffersen 独立性 p 值 |
| --- | ---: | ---: | ---: | ---: |
| 历史经验 | 52 | 2.72% | 6.16e-7 | 0.0145 |
| Gaussian | 51 | 2.67% | 3.16e-7 | 0.0122 |
| Student-t df=5 | 62 | 3.24% | 1.75e-4 | 0.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 的样本分母](/posts/vectorbt-nan-cvar-tail-sample-test-assertions/)：有可跟着计算的 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 对照。本文只讨论历史量化风险建模方法，不构成投资建议。*
