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 历史情景模拟的核心流程图

下面这张图,是我自己画的历史情景模拟流程。你看一遍就懂了。

历史情景模拟核心流程 历史数据提取 2008/2020冲击因子 冲击因子映射 收益率/波动率/相关性 组合损失计算 加权冲击+路径依赖 风险指标输出 VaR/最大回撤/损失 反馈调整 压力测试报告

3.5 实战中的避坑指南

我做了这么多年压力测试,踩过的坑不少。说几个最典型的:

  1. 不要直接用历史收益率。2008年标普跌了57%,但你的组合里可能有一半是债券。直接乘会高估损失。正确做法是按资产类别分别映射。
  2. 注意时间窗口对齐。2020年疫情冲击只持续了2个月,但2008年持续了6个月。如果你用同样的时间窗口,结果会失真。
  3. 流动性冲击要单独算。历史情景里,很多资产是有价无市。我建议在模拟结果上再加一个「流动性折扣因子」,比如10%-20%。

我的小技巧:做完历史情景模拟后,把结果和实际历史回撤对比一下。如果偏差超过20%,说明你的映射逻辑有问题。我一般会调一下「路径衰减因子」和「相关性冲击系数」。

3.6 结果解读:什么算「通过」?

历史情景模拟的结果,不是看「亏了多少」,而是看「能不能扛住」。

我个人习惯用三个标准:

  • 最大回撤不超过30%:超过这个数,客户基本要赎回
  • VaR(95%)不超过组合净值的5%:这是监管红线
  • 流动性覆盖率不低于80%:确保能应对赎回

如果三个标准都过了,那你的组合在历史极端事件下基本安全。如果没过,嗯,那就得调整仓位了。

说白了,历史情景模拟就是给组合做一次「压力体检」。体检不过,别硬撑,该减仓减仓,该对冲对冲。