3、历史情景模拟:基于历史极端事件构建压力情景
历史情景模拟,说白了就是「让历史重演一次」。
我们拿2008年金融危机和2020年疫情这两个极端事件,把当时的市场冲击数据提取出来,然后施加到当前的投资组合上。看看如果历史再来一次,你的仓位扛不扛得住。
我个人习惯,做压力测试时,历史情景是最先做的。为什么?因为它真实。不像那些纯数学构造的情景,历史数据里藏着真实的人性恐慌和流动性枯竭。
3.1 为什么要选2008和2020?
这两个事件,是近二十年里最极端的两个「黑天鹅」。
- 2008年金融危机:流动性瞬间冻结,相关性全部趋近于1。你以为是分散风险的资产,结果一起跌。
- 2020年疫情冲击:速度极快,两周内美股熔断四次。波动率指数VIX飙到82,历史极值。
我建议,做历史情景模拟时,不要只挑一个事件。至少选两个,一个偏流动性危机,一个偏波动率冲击。这样覆盖面才够。
核心逻辑:历史情景模拟的本质,是「把过去的价格路径,映射到当前持仓上」。不是简单看涨跌百分比,而是看整个路径中的最大回撤、最大亏损、流动性冲击。
3.2 数据准备:提取历史冲击因子
我们需要从历史数据中提取三类因子:
| 因子类型 | 2008年数据 | 2020年数据 |
|---|---|---|
| 收益率冲击 | 标普500跌57%,新兴市场跌65% | 标普500跌34%,原油跌55% |
| 波动率冲击 | VIX从20飙到80 | VIX从12飙到82 |
| 相关性冲击 | 股债相关性从-0.3变为+0.6 | 股债相关性从-0.4变为+0.3 |
嗯,这里要注意。相关性冲击往往被忽略,但它才是真正的杀手。2008年的时候,很多对冲基金死就死在「以为分散了风险,结果全仓一起崩」。
我的经验:提取历史冲击因子时,不要只取峰值。取整个「冲击窗口」——比如2008年9月到2009年3月,2020年2月到4月。因为压力测试要看的是「持续失血」的能力,不是一锤子买卖。
3.3 代码实现:历史情景模拟引擎
下面这段代码,是我在项目中实际用过的简化版。它把历史冲击因子映射到当前组合上。
import pandas as pd
import numpy as np
class HistoricalScenarioEngine:
def __init__(self, portfolio_weights, current_prices):
self.weights = portfolio_weights
self.prices = current_prices
def load_scenario(self, scenario_name):
"""加载历史情景的冲击因子"""
scenarios = {
'2008_crisis': {
'equity_shock': -0.57,
'bond_shock': 0.05,
'vol_shock': 4.0, # VIX倍数
'corr_shock': 0.6 # 股债相关性
},
'2020_pandemic': {
'equity_shock': -0.34,
'bond_shock': 0.08,
'vol_shock': 6.8,
'corr_shock': 0.3
}
}
return scenarios.get(scenario_name, {})
def run_scenario(self, scenario_name):
"""运行历史情景模拟"""
shock = self.load_scenario(scenario_name)
# 计算组合冲击
equity_exposure = self.weights.get('equity', 0)
bond_exposure = self.weights.get('bond', 0)
portfolio_shock = (
equity_exposure * shock['equity_shock'] +
bond_exposure * shock['bond_shock']
)
# 计算波动率冲击后的VaR
base_vol = 0.15 # 基准年化波动率
shocked_vol = base_vol * shock['vol_shock']
var_95 = 1.65 * shocked_vol * np.sqrt(1/252)
return {
'portfolio_loss': portfolio_shock,
'shocked_vol': shocked_vol,
'var_95': var_95,
'max_drawdown': portfolio_shock * 1.5 # 经验调整
}
# 使用示例
portfolio = {
'equity': 0.6,
'bond': 0.3,
'commodity': 0.1
}
engine = HistoricalScenarioEngine(portfolio, current_prices=None)
result_2008 = engine.run_scenario('2008_crisis')
print(f"2008情景模拟损失: {result_2008['portfolio_loss']:.2%}")
我曾经踩过的坑:直接用历史收益率乘以当前权重,忽略了「路径依赖」。比如2008年,市场不是一天跌完的,中间有反弹。如果你用简单乘法,会高估或低估实际损失。我后来加了一个「路径衰减因子」,模拟逐日冲击。
3.4 历史情景模拟的核心流程图
下面这张图,是我自己画的历史情景模拟流程。你看一遍就懂了。
3.5 实战中的避坑指南
我做了这么多年压力测试,踩过的坑不少。说几个最典型的:
- 不要直接用历史收益率。2008年标普跌了57%,但你的组合里可能有一半是债券。直接乘会高估损失。正确做法是按资产类别分别映射。
- 注意时间窗口对齐。2020年疫情冲击只持续了2个月,但2008年持续了6个月。如果你用同样的时间窗口,结果会失真。
- 流动性冲击要单独算。历史情景里,很多资产是有价无市。我建议在模拟结果上再加一个「流动性折扣因子」,比如10%-20%。
我的小技巧:做完历史情景模拟后,把结果和实际历史回撤对比一下。如果偏差超过20%,说明你的映射逻辑有问题。我一般会调一下「路径衰减因子」和「相关性冲击系数」。
3.6 结果解读:什么算「通过」?
历史情景模拟的结果,不是看「亏了多少」,而是看「能不能扛住」。
我个人习惯用三个标准:
- 最大回撤不超过30%:超过这个数,客户基本要赎回
- VaR(95%)不超过组合净值的5%:这是监管红线
- 流动性覆盖率不低于80%:确保能应对赎回
如果三个标准都过了,那你的组合在历史极端事件下基本安全。如果没过,嗯,那就得调整仓位了。
说白了,历史情景模拟就是给组合做一次「压力体检」。体检不过,别硬撑,该减仓减仓,该对冲对冲。