Skip to content

6.5 压力测试与VaR

概念详解

风险价值(VaR)压力测试是投资组合风险管理的两大核心工具。VaR回答"在正常市场条件下,我的组合在最坏的N天可能亏多少钱?",而压力测试回答"如果历史极端事件重演(或更糟),我的组合会亏多少钱?"两者相辅相成——VaR覆盖日常风险管理,压力测试覆盖尾部极端风险管理。

风险管理工具的层次结构:

工具解决的问题时间维度方法
VaR正常情况的最差损失1天到10天参数法、历史法、蒙特卡洛
CVaRVaR之外的损失有多大同VaR尾部均值
压力测试极端事件的损失瞬时/事件窗口历史情景、假设情景
敏感性分析单因子变化的影响瞬时偏导数(Greeks)
名词解释:VaR (在险价值)

在一定的置信水平和持有期内,投资组合可能遭受的最大损失。例如95%置信度下的1天VaR表示有95%的把握损失不会超过该值。数学上:P(Loss>VaR)=1α,其中α是置信水平。VaR = inf{l:P(Ll)α}。如果95%-1d-VaR = 100万,意味着每天有5%的概率亏损超过100万。

名词解释:CVaR (条件在险价值)

当损失超过VaR阈值时,损失的平均值,衡量尾部风险的大小。CVaR = E[L|L>VaR]。也被称为Expected Shortfall (ES)。相比于VaR只告诉你"门槛在哪里"(但不说门槛之后有多惨),CVaR告诉你"进入尾部后平均会亏多少"——这是更完整的尾部风险度量。

名词解释:压力测试

将投资组合置于历史上曾发生过的极端市场情景(如2008年金融危机、2020年3月新冠崩盘)或假设的极端情景下,评估组合可能遭受的损失。压力测试弥补了VaR/CVaR的不足——历史数据可能不包含某些极端情景,而压力测试直接模拟这些情景。

数学原理

VaR的三种计算方法

1. 参数法(方差-协方差法):假设收益率服从正态分布 N(μ,σ2)

VaRα=(μ+σzα)

其中 zα 是正态分布的 α 分位数。95% → z0.05=1.645, 99% → z0.01=2.326

2. 历史模拟法:对历史收益率排序,取第 (1α)×100 百分位数。

3. 蒙特卡洛法:从拟合的分布中随机抽样生成大量情景,对模拟收益排序取分位数。

CVaR

CVaRα=E[r|r>VaRα]=11αα1VaRudu

压力测试的数学框架

压力情景定义为一系列市场变量的冲击向量 ΔX=(Δx1,Δx2,...,ΔxK)

Loss=i=1Nwik=1Kδi,kΔxk

其中 δi,k 是资产 i 对风险因子 k 的敏感度。

📉 蒙特卡洛VaR计算器

📉 蒙特卡洛VaR计算器

Python实战

📌 案例:完整的VaR/CVaR计算

PYTHON85 行 · 2.8 KB
📄此处有展示代码85 行 · 2.8 KB展开 ▼
python
import numpy as np
import pandas as pd
from scipy import stats

class RiskMetrics:
    def __init__(self, returns, confidence_levels=[0.95, 0.99]):
        """
        Parameters:
            returns: pd.Series, 日收益率
            confidence_levels: 置信水平列表
        """
        self.returns = returns
        self.alpha_levels = confidence_levels

    def var_parametric(self, horizon=1):
        """参数法VaR(假设正态分布)"""
        mu = self.returns.mean()
        sigma = self.returns.std()

        results = {}
        for alpha in self.alpha_levels:
            z_score = stats.norm.ppf(1 - alpha)
            var = -(mu * horizon + sigma * np.sqrt(horizon) * z_score)
            results[f'VaR_{int(alpha*100)}%'] = var

        return results

    def var_historical(self, horizon=1):
        """历史模拟法VaR"""
        horizon_returns = self.returns.rolling(horizon).sum().dropna()

        results = {}
        for alpha in self.alpha_levels:
            var = -np.percentile(horizon_returns, (1 - alpha) * 100)
            results[f'VaR_{int(alpha*100)}%'] = var

        return results

    def var_cvar_monte_carlo(self, n_simulations=10000, horizon=1):
        """蒙特卡洛法VaR和CVaR"""
        # 用t分布拟合(比正态分布更好地捕捉肥尾)
        t_df, t_loc, t_scale = stats.t.fit(self.returns)

        simulated_returns = stats.t.rvs(t_df, t_loc, t_scale,
                                        size=(n_simulations, horizon))

        if horizon > 1:
            simulated_returns = simulated_returns.sum(axis=1)

        results = {}
        for alpha in self.alpha_levels:
            # VaR
            var = -np.percentile(simulated_returns, (1 - alpha) * 100)
            results[f'VaR_{int(alpha*100)}%'] = var

            # CVaR (Expected Shortfall)
            tail = simulated_returns[simulated_returns <= -var]
            cvar = -tail.mean() if len(tail) > 0 else var
            results[f'CVaR_{int(alpha*100)}%'] = cvar

        return results

    def full_risk_report(self):
        """完整的风险报告"""
        report = {}

        # 基础统计
        report['mean_daily'] = self.returns.mean()
        report['std_daily'] = self.returns.std()
        report['skewness'] = self.returns.skew()
        report['kurtosis'] = self.returns.kurtosis()
        report['min_daily'] = self.returns.min()

        # VaR/CVaR (1天)
        report.update({f'Parametric_{k}': v for k, v in self.var_parametric().items()})
        report.update({f'Historical_{k}': v for k, v in self.var_historical().items()})
        report.update({f'MC_{k}': v for k, v in self.var_cvar_monte_carlo().items()})

        # 10天VaR (sqrt(T)缩放)
        report.update({
            f'10d_Parametric_{k}': v * np.sqrt(10)
            for k, v in self.var_parametric().items()
        })

        return pd.Series(report)
点击展开可浏览运行结果
95%置信度, 30天VaR: $10,192.95
CVaR (尾部期望损失): $12,738.61
占初始资金: 10.19%

📌 案例:压力测试框架

PYTHON58 行 · 1.8 KB
📄此处有展示代码58 行 · 1.8 KB展开 ▼
python
def stress_test_portfolio(weights, factor_sensitivities, scenarios):
    """
    对投资组合进行多情景压力测试

    Parameters:
        weights: 资产权重向量 (N,)
        factor_sensitivities: 各资产对各风险因子的敏感度 (N, K)
        scenarios: dict, {scenario_name: {factor_name: shock}}
            shock: 因子变化(如利率+100bp, 指数-20%)
    """
    results = {}

    for scenario_name, shocks in scenarios.items():
        # 将情景冲击转换为每个资产的收益冲击
        asset_shocks = np.zeros(len(weights))
        for factor_name, shock in shocks.items():
            if factor_name in factor_sensitivities.columns:
                asset_shocks += factor_sensitivities[factor_name] * shock

        # 组合层面的损失
        portfolio_loss = -np.sum(weights * asset_shocks)
        results[scenario_name] = portfolio_loss

    return pd.Series(results, name='Portfolio Loss')

# 定义压力情景
scenarios = {
    '2008金融危机': {
        'equity': -0.45,      # 股票跌45%
        'credit_spread': 0.05,  # 信用利差扩大5%
        'volatility': 0.40,    # 波动率上升40%
        'liquidity': -0.30,    # 流动性下降30%
    },
    '2020新冠崩盘': {
        'equity': -0.34,
        'credit_spread': 0.03,
        'volatility': 0.50,
        'liquidity': -0.20,
    },
    '利率急升': {
        'equity': -0.15,
        'interest_rate': 0.02,  # 利率+200bp
        'credit_spread': 0.01,
    },
    '通胀飙升': {
        'equity': -0.10,
        'commodity': 0.20,
        'interest_rate': 0.025,
        'volatility': 0.15,
    },
    '自定极端': {
        'equity': -0.30,
        'credit_spread': 0.05,
        'volatility': 0.40,
        'liquidity': -0.40,
        'interest_rate': 0.03,
    }
}
点击展开可浏览运行结果
--- 结果 ---
scenario_name = array([ 0.35778736,  0.56078453,  1.08305124,  1.05380205, -1.37766937,
       -0.93782504,  0.51503527,  0.51378595,  0.51504769,  3.85273149,
        0.57089051,  1.13556564,  0.95400176,  0.6513912
factor_name = array([-0.82899501, -0.56018104,  0.74729361,  0.61037027, -0.02090159,
        0.11732738,  1.2776649 , -0.59157139,  0.54709738, -0.20219265,
       -0.2176812 ,  1.09877685,  0.82541635,  0.8135096
shock = array([-1.59442766, -0.59937502,  0.0052437 ,  0.04698059, -0.45006547,
        0.62284993, -1.06762043, -0.14237949,  0.12029563,  0.51443883,
        0.71161488, -1.12464209, -1.53411417,  1.2776768
scenarios = {'2008金融危机': {'equity': -0.45, 'credit_spread': 0.05, 'volatility': 0.4, 'liquidity': -0.3}, '2020新冠崩盘': {'equity': -0.34, 'credit_spread': 0.03, 'volatility': 0.5, 'liquidity': -0.2}, '利率急升': {'equity': -0.15, 'interest_rate': 0.02, 'credit_spread': 0.01}, '通胀飙升': {'equity': -0.1, 'commodity': 0.2, 'interest_rate': 0.025, 'volatility': 0.15}, '自定极端': {'equity': -0.3, 'credit_spread': 0.05, 'volatility': 0.4, 'liquidity': -0.4, 'interest_rate': 0.03}}
--- stress_test_portfolio(...) ---

📌 案例:滚动VaR回测

PYTHON42 行 · 1.4 KB
📄此处有展示代码42 行 · 1.4 KB展开 ▼
python
def rolling_var_backtest(returns, window=252, confidence=0.95):
    """
    滚动计算VaR并与实际损失对比,做VaR有效性检验

    理想情况下:实际损失超过VaR的比例应该接近 (1-confidence)
    如:95% VaR下,平均每20个交易日有1次被突破
    """
    roll_var = returns.rolling(window).apply(
        lambda x: -np.percentile(x, (1 - confidence) * 100)
    )

    # 实际损失(负收益的绝对值)
    losses = -returns

    # VaR突破:损失 > VaR(注意VaR通常为正数表示损失)
    breaches = (losses > roll_var.shift(1)).dropna()
    breach_rate = breaches.mean()

    # Kupiec检验的统计量
    n = len(breaches)
    n_breaches = breaches.sum()
    expected_rate = 1 - confidence

    # 似然比检验
    if n_breaches > 0 and n_breaches < n:
        LR = -2 * np.log(
            ((1 - expected_rate) ** (n - n_breaches) * expected_rate ** n_breaches) /
            ((1 - n_breaches/n) ** (n - n_breaches) * (n_breaches/n) ** n_breaches)
        )
        p_value = 1 - stats.chi2.cdf(LR, df=1)
    else:
        p_value = np.nan

    print(f"VaR突破率: {breach_rate:.2%} (期望: {expected_rate:.2%})")
    print(f"VaR突破次数: {n_breaches}/{n}")
    print(f"Kupiec检验p值: {p_value:.4f}")
    if p_value < 0.05:
        print("⚠️ VaR模型不准确(在5%水平下拒绝原假设)")
    else:
        print("✅ VaR模型通过回测检验")

    return roll_var, breaches
点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:函数 `rolling_var_backtest`(滚动计算VaR并与实际损失对比,做VaR有效性检验)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

常见误区

  1. VaR只告诉你"门槛",不告诉你"门槛后有多惨":VaR=100万意味着"有5%的日子亏超100万",但超了之后是亏101万还是500万,VaR不回答。这正是CVaR存在的意义。

  2. 参数法VaR假设正态分布:正态分布严重低估了金融数据的肥尾特征。2008年金融危机时,基于正态分布假设的VaR模型几乎全部失效。应使用考虑肥尾的方法(t分布、GARCH、EVT极值理论)。

  3. VaR不具备次可加性:VaR不是一致风险度量(coherent risk measure)——它不具备次可加性。两个组合的VaR之和可能小于合并组合的VaR,这意味着VaR不惩罚集中风险。CVaR具备次可加性,是一致风险度量。

  4. 忽略波动率聚集:风险管理中最大的错误之一是使用固定窗口的历史波动率。在平静期之后VaR大幅下降,紧跟着的就是极端的VaR突破——这正是波动率聚集的体现。GARCH类模型可以更好地捕捉波动率的时变特征。

  5. 压力测试情景过于温和:人们倾向于只测试"曾经发生过的事"。但"从未发生的事"才是真正的尾部风险。压力测试应该包括假设性的极端情景,而不只是历史重演。

实战练习

  1. 用一个包含股票和债券的组合(60/40),分别用参数法、历史模拟法和蒙特卡洛法计算95%和99%的1天VaR和CVaR。比较三种方法的结果差异,特别是99%水平下。

  2. VaR回测:使用滚动2年窗口计算历史模拟法VaR(95%),记录过去5年中VaR被实际损失突破的日期。计算突破率并执行Kupiec检验。观察VaR突破是否在时间上聚集。

  3. 压力测试实验:定义3-5个压力情景(如2008年金融危机、2015年A股崩盘、2020年新冠、利率急升、自定极端情景),计算你的组合在各情景下的预期损失。哪些情景对你的组合最危险?如何调整组合以降低对这些情景的脆弱性?

延伸阅读

  • Jorion, P. (2006). Value at Risk: The New Benchmark for Managing Financial Risk. McGraw-Hill. VaR最经典全面的教科书。
  • Artzner, P., Delbaen, F., Eber, J. M., & Heath, D. (1999). "Coherent Measures of Risk." Mathematical Finance. 提出了一致风险度量(CVaR满足,VaR不满足)。
  • Kupiec, P. H. (1995). "Techniques for Verifying the Accuracy of Risk Measurement Models." 讨论了VaR的回测检验方法。
  • McNeil, A. J., Frey, R., & Embrechts, P. (2015). Quantitative Risk Management. Princeton University Press. 风险管理的数学标准教材。
  • Taleb, N. N. (2007). The Black Swan. 深刻讨论了尾部风险和不可预测的极端事件。

本章要点

  • VaR衡量正常市场条件下的最差损失,CVaR衡量进入尾部后的平均损失——两者互补,构成完整的风险度量体系。
  • 三种VaR计算方法:参数法(快速但假设强)、历史模拟法(无分布假设但依赖历史重复)、蒙特卡洛法(最灵活但计算量大)。
  • VaR不具备次可加性(不是一致风险度量),CVaR具备次可加性(是一致风险度量),因此巴塞尔协议III已推荐用CVaR替代VaR。
  • 压力测试弥补VaR/CVaR的盲区——测试历史上未发生或极少发生的极端情景。
  • VaR模型需要定期回测检验(Kupiec检验、Christoffersen检验),突破率显著偏离理论水平意味着模型需要重新校准。
  • 波动率聚集意味着VaR的估计在平静期和动荡期不应相同——GARCH等时变波动率模型可以更好地捕捉这一特征。