第九章 压力测试框架:历史情景模拟、极端市场情景、蒙特卡洛模拟、压力测试报告
做市商的风控体系里,压力测试是我个人认为最「反人性」的一环。为什么这么说?因为平时我们都在追求盈利,而压力测试偏偏要你盯着最惨的情况看。我见过太多团队,平时回测跑得飞起,一到黑天鹅事件就爆仓。说白了,压力测试不是用来证明你的策略有多牛,而是用来告诉你——你可能会怎么死。
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
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 核心原理
说白了,蒙特卡洛模拟就是「掷骰子掷一万次,看看结果长什么样」。具体来说:
- 假设资产价格服从某种随机过程(通常是几何布朗运动)
- 生成N条可能的价格路径(N通常取10000~100000)
- 对每条路径计算组合损益
- 统计损益分布,找出极端情况
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: 在极端条件下的在险价值
9.5 压力测试报告
测试做完了,数据也跑出来了,但如果没有一份好的报告,前面所有工作都白费。压力测试报告不是给技术团队看的,而是给管理层和风控委员会看的。所以,报告要清晰、直观、有结论。
9.5.1 报告结构
我个人习惯的报告模板是这样的:
- 执行摘要: 一句话说清楚测试结果(例如:「在2008年情景下,组合预计亏损15%,超过风险限额」)
- 测试方法: 简要说明用了哪些情景、参数设置
- 测试结果: 用表格和图表展示各情景下的损益
- 风险暴露分析: 哪些资产或策略是主要亏损来源
- 应对措施: 针对识别出的风险,提出具体的调整方案
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倍以下」或「增加对冲头寸」
9.6 压力测试框架总览
最后,我用一张图来总结整个压力测试框架的核心逻辑:
这张图展示了我做压力测试的标准流程:从输入数据开始,经过三种方法的交叉验证,最终输出报告并指导风控决策。记住,三种方法各有优劣,我建议至少使用两种方法进行交叉验证,这样得出的结论才可靠。
无相订单流研究社 微信Lucian808555