第九章 压力测试框架:历史情景模拟、极端市场情景、蒙特卡洛模拟、压力测试报告

做市商的风控体系里,压力测试是我个人认为最「反人性」的一环。为什么这么说?因为平时我们都在追求盈利,而压力测试偏偏要你盯着最惨的情况看。我见过太多团队,平时回测跑得飞起,一到黑天鹅事件就爆仓。说白了,压力测试不是用来证明你的策略有多牛,而是用来告诉你——你可能会怎么死。

9.1 为什么需要压力测试?

常规的VaR(在险价值)模型有个致命缺陷:它假设市场是正态分布的。但你想想看,真实市场哪有那么乖?2020年3月原油暴跌、2021年5月加密货币闪崩、2022年英镑危机……这些极端事件在正态分布模型里几乎不可能发生,但它们就是发生了。

我曾在某家做市商团队里见过一个案例:他们的VaR模型显示99%置信度下日最大亏损不超过50万,结果某天因为一个流动性枯竭事件,实际亏损超过了800万。嗯,从那以后,我再也不敢只依赖VaR了。

核心观点: 压力测试不是为了预测未来,而是为了理解你的策略在极端情况下的脆弱性。

9.2 历史情景模拟

历史情景模拟是最直观的方法。说白了,就是把过去发生过的极端行情,直接套用到你当前的持仓上,看看会亏多少。

9.2.1 经典历史情景

我个人习惯维护一个「黑天鹅事件库」,至少包含以下事件:

  • 2008年全球金融危机: 股市暴跌、信用利差飙升、流动性枯竭
  • 2010年闪电崩盘: 道琼斯指数几分钟内暴跌近1000点
  • 2015年瑞郎黑天鹅: 瑞士央行突然取消欧元兑瑞郎汇率下限,单日波动超过30%
  • 2020年3月新冠危机: 全球资产同步暴跌,连黄金和国债都被抛售
  • 2022年Luna崩盘: 加密货币市场系统性崩溃,稳定币脱锚

9.2.2 实现方法

代码实现其实不复杂,核心逻辑就是「回放历史」:

def historical_scenario_simulation(portfolio, historical_data, event_date):
    """
    历史情景模拟
    :param portfolio: 当前持仓
    :param historical_data: 历史价格数据
    :param event_date: 事件日期
    """
    # 获取事件前后的价格变化
    event_prices = historical_data.loc[event_date]
    prev_prices = historical_data.loc[event_date - timedelta(days=1)]
    
    # 计算收益率变化
    returns = (event_prices - prev_prices) / prev_prices
    
    # 应用到当前持仓
    pnl = 0
    for asset, position in portfolio.items():
        if asset in returns.index:
            pnl += position * returns[asset] * current_price[asset]
    
    return pnl
经验之谈: 不要只模拟单一事件。我建议把多个历史事件组合起来,比如「2008年金融危机 + 2020年流动性危机」的叠加场景,这种组合往往能暴露出更深层次的风险。

9.3 极端市场情景

历史情景模拟有个问题:它只能覆盖已经发生过的事件。但真正的黑天鹅,往往是前所未见的。所以我们需要自己构造极端情景。

9.3.1 常见极端情景类型

情景类型 描述 典型参数
价格冲击 单一资产或板块瞬间暴跌 -20% ~ -50%
流动性枯竭 买卖价差扩大,成交困难 价差扩大10~100倍
相关性突变 原本负相关的资产变成正相关 相关系数从-0.5变为+0.8
波动率爆发 隐含波动率瞬间飙升 波动率上升300%~500%
多资产联动 股、债、汇、商品同步暴跌 所有资产-10%以上

9.3.2 构造方法

我一般用「参数化情景生成器」来做这件事:

def extreme_scenario_generator(portfolio, scenario_params):
    """
    极端情景生成器
    :param scenario_params: 包含各资产的冲击幅度
    """
    scenario_pnl = {}
    
    for asset, params in scenario_params.items():
        # 价格冲击
        price_shock = params.get('price_shock', 0)
        # 流动性冲击(影响滑点)
        liquidity_shock = params.get('liquidity_shock', 1)
        # 波动率冲击(影响期权定价)
        vol_shock = params.get('vol_shock', 1)
        
        # 计算该资产在极端情景下的损益
        pnl = calculate_pnl(asset, price_shock, liquidity_shock, vol_shock)
        scenario_pnl[asset] = pnl
    
    return scenario_pnl
避坑指南: 我曾经犯过一个错误——只考虑价格冲击,忽略了流动性冲击。结果在模拟中看起来亏损不大,但实际交易时因为滑点太大,亏损直接翻了三倍。记住:极端行情下,流动性比价格更重要。

9.4 蒙特卡洛模拟

蒙特卡洛模拟是我个人最喜欢的压力测试方法。它不像历史情景那样依赖过去,也不像极端情景那样靠人拍脑袋。它通过随机生成大量可能的未来路径,让你看到策略在各种情况下的表现分布。

9.4.1 核心原理

说白了,蒙特卡洛模拟就是「掷骰子掷一万次,看看结果长什么样」。具体来说:

  1. 假设资产价格服从某种随机过程(通常是几何布朗运动)
  2. 生成N条可能的价格路径(N通常取10000~100000)
  3. 对每条路径计算组合损益
  4. 统计损益分布,找出极端情况

9.4.2 代码实现

import numpy as np

def monte_carlo_simulation(portfolio, mu, sigma, days=252, n_simulations=10000):
    """
    蒙特卡洛模拟
    :param mu: 预期收益率
    :param sigma: 波动率
    :param days: 模拟天数
    :param n_simulations: 模拟次数
    """
    dt = 1/252
    n_assets = len(portfolio)
    
    # 生成随机路径
    random_matrix = np.random.normal(0, 1, (n_simulations, days, n_assets))
    
    # 计算价格路径
    prices = np.zeros((n_simulations, days, n_assets))
    prices[:, 0, :] = current_prices
    
    for t in range(1, days):
        drift = (mu - 0.5 * sigma**2) * dt
        diffusion = sigma * np.sqrt(dt) * random_matrix[:, t, :]
        prices[:, t, :] = prices[:, t-1, :] * np.exp(drift + diffusion)
    
    # 计算每条路径的损益
    pnl_simulations = []
    for sim in range(n_simulations):
        final_prices = prices[sim, -1, :]
        pnl = sum(portfolio[asset] * (final_prices[i] - current_prices[i]) 
                  for i, asset in enumerate(portfolio.keys()))
        pnl_simulations.append(pnl)
    
    return pnl_simulations

9.4.3 结果分析

模拟完成后,我们需要关注几个关键指标:

  • 最大回撤: 模拟路径中最差的亏损幅度
  • 尾部风险: 95%或99%分位数的亏损值
  • 破产概率: 亏损超过资本金的比例
  • 压力VaR: 在极端条件下的在险价值
关键点: 蒙特卡洛模拟的准确性取决于你对参数(mu、sigma)的估计。我建议使用「滚动窗口估计法」,而不是简单用历史均值。因为市场参数是时变的,用过去5年的数据估计今天的波动率,结果会偏差很大。

9.5 压力测试报告

测试做完了,数据也跑出来了,但如果没有一份好的报告,前面所有工作都白费。压力测试报告不是给技术团队看的,而是给管理层和风控委员会看的。所以,报告要清晰、直观、有结论。

9.5.1 报告结构

我个人习惯的报告模板是这样的:

  1. 执行摘要: 一句话说清楚测试结果(例如:「在2008年情景下,组合预计亏损15%,超过风险限额」)
  2. 测试方法: 简要说明用了哪些情景、参数设置
  3. 测试结果: 用表格和图表展示各情景下的损益
  4. 风险暴露分析: 哪些资产或策略是主要亏损来源
  5. 应对措施: 针对识别出的风险,提出具体的调整方案

9.5.2 关键指标表格

情景 预期亏损 最大回撤 恢复时间 是否触发限额
2008年金融危机 -15.3% -22.1% 45天
2020年新冠危机 -8.7% -12.4% 18天
流动性枯竭 -5.2% -7.8% 30天
蒙特卡洛95%分位 -11.8% -18.5% 35天

9.5.3 报告撰写要点

  • 不要堆数据: 管理层没时间看几百行的数字,用图表说话
  • 给出明确结论: 「风险可控」或「需要调整」,不要模棱两可
  • 附带行动建议: 比如「建议降低杠杆至2倍以下」或「增加对冲头寸」
经验之谈: 我曾经见过一份压力测试报告,写了50页,各种复杂模型和公式,但最后管理层只问了三个问题:「亏多少?」「概率多大?」「怎么办?」。从那以后,我的报告第一页永远是这三个问题的答案。

9.6 压力测试框架总览

最后,我用一张图来总结整个压力测试框架的核心逻辑:

做市商压力测试框架 输入:持仓数据 + 市场数据 历史情景模拟 回放过去极端事件 极端市场情景 构造假设性危机 蒙特卡洛模拟 随机生成大量路径 结果汇总与交叉验证 压力测试报告 风控决策:调整限额 / 降低杠杆 / 增加对冲

这张图展示了我做压力测试的标准流程:从输入数据开始,经过三种方法的交叉验证,最终输出报告并指导风控决策。记住,三种方法各有优劣,我建议至少使用两种方法进行交叉验证,这样得出的结论才可靠。

最后提醒: 压力测试不是一次性工作。市场在变,策略在变,持仓也在变。我建议至少每周跑一次压力测试,遇到重大市场事件时更要立即跑。别等到爆仓了才想起压力测试,那时候就晚了。

无相订单流研究社 微信Lucian808555