主题切换
16.3 压力测试与情景分析
名词解释:压力测试(Stress Testing)
压力测试是评估投资组合在极端但合理(Severe but Plausible)的市场条件下表现的技术。与 VaR 关注"正常市场条件下的最大损失"不同,压力测试关注"当一切同时出问题时会发生什么"。监管机构(如美联储的 CCAR/DFAST)要求大型金融机构定期进行系统性压力测试。
一、历史情景 vs 假设情景
1.1 两类情景的比较
| 维度 | 历史情景 | 假设情景 |
|---|---|---|
| 构建方式 | 回放历史上发生过的事件 | 基于经济逻辑和判断构建的"未发生过"的事件 |
| 优点 | 客观、可验证、市场联动真实 | 灵活、可覆盖未知风险、可针对特定脆弱性 |
| 缺点 | 不一定重复、忽略结构性变化 | 主观性强、难以验证合理性 |
| 典型例子 | 2008金融危机、2020新冠崩盘、1997亚洲危机 | 台海冲突、欧元解体、美国债务违约、大流行病变种 |
1.2 历史情景的因子映射
此处有展示代码展开 ▼
python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 关键历史危机情景的定义
HISTORICAL_SCENARIOS = {
'2008_Financial_Crisis': {
'name': '2008 全球金融危机',
'period': ('2008-09-01', '2009-03-09'),
'description': '雷曼兄弟破产引发的系统性危机'
},
'2010_Flash_Crash': {
'name': '2010 闪崩',
'period': ('2010-05-06', '2010-05-06'),
'description': '算法交易引发的盘中闪崩'
},
'2011_EU_Debt_Crisis': {
'name': '2011 欧债危机',
'period': ('2011-07-01', '2011-10-04'),
'description': '希腊债务危机蔓延,意大利西班牙国债收益率飙升'
},
'2015_China_Crash': {
'name': '2015 中国股灾',
'period': ('2015-06-12', '2015-08-24'),
'description': 'A股去杠杆引发的暴跌'
},
'2020_COVID': {
'name': '2020 新冠崩盘',
'period': ('2020-02-19', '2020-03-23'),
'description': '新冠疫情全球蔓延引发的流动性危机'
},
'2022_Rate_Hike': {
'name': '2022 加息冲击',
'period': ('2022-01-01', '2022-06-30'),
'description': '美联储激进加息,股债双杀'
}
}
def historical_scenario_analysis(portfolio_holdings: dict,
market_data: pd.DataFrame,
scenarios: list = None) -> pd.DataFrame:
"""
将当前持仓映射到历史危机情景中,计算如果历史重演的组合损失。
参数:
portfolio_holdings: {资产名称: 持仓市值}
market_data: 历史市场数据(价格序列)
scenarios: 要测试的情景列表
返回:
各情景下的组合损失
"""
if scenarios is None:
scenarios = list(HISTORICAL_SCENARIOS.keys())
results = []
for scenario_key in scenarios:
scenario = HISTORICAL_SCENARIOS[scenario_key]
start, end = scenario['period']
# 提取该期间的资产收益
scenario_returns = market_data.loc[start:end]
if len(scenario_returns) < 2:
continue
# 累计收益
cumulative_returns = (scenario_returns.iloc[-1] /
scenario_returns.iloc[0] - 1)
# 计算组合损失
portfolio_loss = 0
asset_contributions = {}
for asset, position in portfolio_holdings.items():
if asset in cumulative_returns.index:
asset_loss = position * cumulative_returns[asset]
portfolio_loss += asset_loss
asset_contributions[asset] = asset_loss
total_portfolio_value = sum(portfolio_holdings.values())
results.append({
'Scenario': scenario['name'],
'Period': f"{start} to {end}",
'Portfolio_Loss': portfolio_loss,
'Loss_Pct': portfolio_loss / total_portfolio_value * 100,
'Worst_Asset': max(asset_contributions,
key=lambda x: abs(asset_contributions[x])),
'Asset_Contributions': asset_contributions
})
return pd.DataFrame(results).sort_values('Loss_Pct')
# ===== 演示:用合成的历史价格序列跑一遍情景映射 =====
# 真实使用时把 market_data 换成 Wind/Tushare/yfinance 拉下来的真实收盘价面板
np.random.seed(42)
dates = pd.date_range('2008-01-01', '2022-12-31', freq='B')
assets = ['A_Share_Equity', 'US_Equity', 'Gold', 'Treasury_Bond', 'Credit_Bond']
# 每个资产的正常期漂移与波动(年化)
params = {
'A_Share_Equity': (0.06, 0.25),
'US_Equity': (0.08, 0.18),
'Gold': (0.04, 0.15),
'Treasury_Bond': (0.03, 0.05),
'Credit_Bond': (0.04, 0.08),
}
# 危机期的日度额外冲击(体现"股债金"在不同危机中的分化)
crisis_shock = {
('2008-09-01', '2009-03-09'): {'A_Share_Equity': -0.0035, 'US_Equity': -0.0050,
'Gold': 0.0004, 'Treasury_Bond': 0.0006, 'Credit_Bond': -0.0018},
('2015-06-12', '2015-08-24'): {'A_Share_Equity': -0.0090, 'US_Equity': -0.0012,
'Gold': 0.0002, 'Treasury_Bond': 0.0004, 'Credit_Bond': -0.0006},
('2020-02-19', '2020-03-23'): {'A_Share_Equity': -0.0040, 'US_Equity': -0.0090,
'Gold': -0.0010, 'Treasury_Bond': 0.0010, 'Credit_Bond': -0.0030},
('2022-01-01', '2022-06-30'): {'A_Share_Equity': -0.0012, 'US_Equity': -0.0018,
'Gold': -0.0002, 'Treasury_Bond': -0.0008, 'Credit_Bond': -0.0010},
}
price_data = {}
for asset in assets:
mu, vol = params[asset]
daily = np.random.normal(mu / 252, vol / np.sqrt(252), len(dates))
ret = pd.Series(daily, index=dates)
for (s, e), shocks in crisis_shock.items():
mask = (ret.index >= s) & (ret.index <= e)
ret[mask] += shocks[asset]
price_data[asset] = 100 * (1 + ret).cumprod()
market_data = pd.DataFrame(price_data)
holdings = {
'A_Share_Equity': 40_000_000,
'US_Equity': 25_000_000,
'Gold': 10_000_000,
'Treasury_Bond': 15_000_000,
'Credit_Bond': 10_000_000,
}
stress = historical_scenario_analysis(holdings, market_data)
print(f"组合总市值: {sum(holdings.values()):,.0f} 元\n")
print("历史情景重演下的组合损益(按损失从大到小):")
print("-" * 76)
print(f"{'情景':<18}{'区间':<26}{'损益(万元)':>14}{'损益%':>9}{'最大贡献资产':>16}")
print("-" * 76)
for _, row in stress.iterrows():
print(f"{row['Scenario']:<18}{row['Period']:<26}"
f"{row['Portfolio_Loss'] / 1e4:>13,.0f}{row['Loss_Pct']:>9.2f}{row['Worst_Asset']:>18}")
worst = stress.iloc[0]
print(f"\n最坏情景: {worst['Scenario']},损失 {abs(worst['Portfolio_Loss']) / 1e4:,.0f} 万元"
f"({worst['Loss_Pct']:.2f}%)")
print("各资产贡献拆解:")
for asset, contrib in sorted(worst['Asset_Contributions'].items(), key=lambda kv: kv[1]):
print(f" {asset:<18}{contrib / 1e4:>12,.0f} 万元")
print("\n注:2010 闪崩只有单日数据(len<2),函数按设计自动跳过。")点击展开可浏览运行结果
组合总市值: 100,000,000 元 历史情景重演下的组合损益(按损失从大到小): ---------------------------------------------------------------------------- 情景 区间 损益(万元) 损益% 最大贡献资产 ---------------------------------------------------------------------------- 2008 全球金融危机 2008-09-01 to 2009-03-09 -2,560 -25.60 US_Equity 2015 中国股灾 2015-06-12 to 2015-08-24 -1,498 -14.98 A_Share_Equity 2022 加息冲击 2022-01-01 to 2022-06-30 -1,373 -13.73 US_Equity 2020 新冠崩盘 2020-02-19 to 2020-03-23 -943 -9.43 US_Equity 2011 欧债危机 2011-07-01 to 2011-10-04 4 0.04 A_Share_Equity 最坏情景: 2008 全球金融危机,损失 2,560 万元(-25.60%) 各资产贡献拆解: US_Equity -1,401 万元 A_Share_Equity -1,124 万元 Credit_Bond -129 万元 Gold -70 万元 Treasury_Bond 163 万元 注:2010 闪崩只有单日数据(len<2),函数按设计自动跳过。
二、系统性压力测试框架
2.1 CCAR/DFAST 框架
美联储的综合资本分析与审查(CCAR)和多德-弗兰克法案压力测试(DFAST)是全球最严格和系统的压力测试框架。其核心流程包括:
- 情景设计:美联储提供基准、不利和严重不利三种宏观经济情景
- 损失估计:9个季度前瞻性的贷款损失、交易损失、对手方损失
- 收入预测:PPNR(拨备前净收入)的预测
- 资本充足率:CET1、Tier1、总资本充足率在压力下的演变
2.2 简化版交易账户压力测试
此处有展示代码展开 ▼
python
def trading_book_stress_test(positions: dict,
risk_factors: dict,
stress_shocks: pd.DataFrame,
pricing_functions: dict = None) -> dict:
"""
交易账户的系统性压力测试。
参数:
positions: {头寸ID: {risk_factors: sensitivity}}
sensitivity是头寸对各风险因子的敏感性(PV01, delta, vega等)
risk_factors: {因子名: 当前值}
stress_shocks: (N_scenarios, M_factors) 各风险因子在各情景下的冲击
pricing_functions: {头寸类型: 定价函数} 可选,用于非线性头寸
返回:
各情景下的P&L
"""
results = []
for scenario_name, shocks in stress_shocks.iterrows():
scenario_pnl = 0
position_pnls = {}
for position_id, sensitivities in positions.items():
position_pnl = 0
for factor, sensitivity in sensitivities.items():
if factor in shocks:
shock = shocks[factor]
# 线性近似
linear_pnl = sensitivity * shock * risk_factors.get(
factor, 1.0
)
# 如果有定价函数,加入非线性项(gamma等)
if pricing_functions and position_id in pricing_functions:
nonlinear_pnl = pricing_functions[position_id](
risk_factors, shocks
)
position_pnl += nonlinear_pnl
else:
position_pnl += linear_pnl
scenario_pnl += position_pnl
position_pnls[position_id] = position_pnl
results.append({
'Scenario': scenario_name,
'Total_PnL': scenario_pnl,
'Position_PnLs': position_pnls
})
return results点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:函数 `trading_book_stress_test`(交易账户的系统性压力测试)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。
三、反事实情景生成
3.1 反事实情景的定义
反事实情景(Counterfactual Scenarios)回答"如果...会怎样"的问题。与历史情景不同,反事实情景是可能发生但尚未发生的事件组合。
生成反事实情景的方法:
- 专家判断法:由投资委员会、风险委员会定义特定的极端事件组合
- 统计生成法:基于历史数据的极端分位数组合(如所有资产的 99% VaR 同时触发)
- 逆向压力测试:先设定一个不可接受的损失水平,然后找出导致这种损失的情景组合
3.2 逆向压力测试
此处有展示代码展开 ▼
python
def reverse_stress_test(positions_sensitivities: np.ndarray,
max_acceptable_loss: float,
factor_bounds: list,
n_search: int = 10000) -> dict:
"""
逆向压力测试:找出导致给定损失的最小因子冲击组合。
优化问题:min ||Δf||_2, s.t. P&L(Δf) ≤ -max_acceptable_loss
参数:
positions_sensitivities: (M,) 对各风险因子的敏感度向量
max_acceptable_loss: 最大可接受损失(正值)
factor_bounds: [(min, max), ...] 各因子的合理变动范围
n_search: 搜索次数
返回:
最小范数冲击组合及其 P&L
"""
M = len(positions_sensitivities)
best_shock = None
best_norm = np.inf
for _ in range(n_search):
# 随机生成因子冲击(在各因子的边界内均匀采样)
shocks = np.array([
np.random.uniform(low, high) for low, high in factor_bounds
])
# 计算 P&L
pnl = np.dot(positions_sensitivities, shocks)
if pnl <= -max_acceptable_loss:
norm = np.linalg.norm(shocks, 2)
if norm < best_norm:
best_norm = norm
best_shock = shocks.copy()
if best_shock is None:
return {'found': False, 'message': '在搜索范围内未找到符合条件的冲击组合'}
return {
'found': True,
'shocks': best_shock,
'shock_norm': best_norm,
'resulting_pnl': np.dot(positions_sensitivities, best_shock),
'normalized_shocks': best_shock / np.linalg.norm(best_shock)
}点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:函数 `reverse_stress_test`(逆向压力测试:找出导致给定损失的最小因子冲击组合)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。
四、多资产联动压力测试
4.1 相关性破裂的情景设计
在极端压力下,资产间的相关性往往趋向于 1 或 -1(相关性破裂)。多资产压力测试需要模拟这种"一切同时崩坏"的情景:
此处有展示代码展开 ▼
python
def correlation_breakdown_stress(correlation_matrix: np.ndarray,
volatilities: np.ndarray,
crisis_multiplier: float = 3.0) -> np.ndarray:
"""
生成相关性破裂的极端压力协方差矩阵。
在极端压力下,所有相关性向 ±1 收缩,
同时波动率放大。
参数:
correlation_matrix: 正常时期的相关系数矩阵
volatilities: 正常时期的波动率
crisis_multiplier: 波动率放大倍数
返回:
极端压力下的协方差矩阵
"""
n = len(volatilities)
# 相关性破裂:向 +1 收缩
crisis_corr = np.ones((n, n))
for i in range(n):
for j in range(n):
if i != j:
# 相关性向 1 收缩 80%
crisis_corr[i, j] = correlation_matrix[i, j] * 0.2 + 0.8
# 波动率放大
crisis_vol = volatilities * crisis_multiplier
# 协方差矩阵
D = np.diag(crisis_vol)
crisis_cov = D @ crisis_corr @ D
return crisis_cov
def multi_asset_stress_pnl(positions: np.ndarray,
normal_cov: np.ndarray,
confidence: float = 0.999,
n_simulations: int = 10000) -> dict:
"""
多资产联动压力测试:模拟极端市场条件下的P&L分布。
使用 Copula 或 t-Copula 捕捉极端事件间的尾部依赖。
参数:
positions: 各头寸的市场价值
normal_cov: 正常时期的协方差矩阵
confidence: 极端情景的置信水平
n_simulations: 蒙特卡洛模拟次数
返回:
压力测试结果
"""
n_assets = len(positions)
# 使用 t-Copula 生成尾部相依的随机冲击
df_t = 3 # 自由度越低,尾部相依性越强
vol = np.sqrt(np.diag(normal_cov))
corr = normal_cov / np.outer(vol, vol)
# t-Copula 模拟
np.random.seed(42)
z = np.random.randn(n_simulations, n_assets)
chi = np.random.chisquare(df_t, n_simulations) / df_t
t_samples = z / np.sqrt(chi[:, np.newaxis]) # 多元 t 分布
# 映射到收益率
L = np.linalg.cholesky(corr)
correlated_t = t_samples @ L.T
# 极端情景:取 t 分布的极端分位数
extreme_returns = np.percentile(correlated_t, (1 - confidence) * 100, axis=0)
# P&L
pnl_vector = positions * extreme_returns * vol
total_pnl = np.sum(pnl_vector)
return {
'total_pnl_impact': total_pnl,
'asset_level_impacts': dict(zip(
[f'Asset_{i}' for i in range(n_assets)], pnl_vector
)),
'pct_of_portfolio': total_pnl / np.sum(np.abs(positions)) * 100
}点击展开可浏览运行结果
📘 本段代码定义了 2 个函数/类:函数 `correlation_breakdown_stress`(生成相关性破裂的极端压力协方差矩阵)、函数 `multi_asset_stress_pnl`(多资产联动压力测试:模拟极端市场条件下的P&L分布)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。
压力测试是量化风险管理中连接模型与现实的关键桥梁。它不仅帮助我们量化极端事件的冲击,也为满足监管资本要求(巴塞尔协议)提供了方法论基础。