第1章:VaR与CVaR——参数法、历史模拟法、蒙特卡洛模拟法的代码实现与对比

做量化投资的朋友,对VaR肯定不陌生。我入行那会儿,第一次被老板问「今天组合的VaR是多少」,我愣是翻了半天报告才找到。后来自己动手写代码实现,才真正搞明白这玩意儿到底在算什么。

说白了,VaR就是回答一个问题:在给定的置信水平下,我的组合最多可能亏多少钱?比如95%的VaR是100万,意思就是只有5%的概率,亏损会超过100万。

但VaR有个硬伤——它只告诉你「最多亏多少」,却不告诉你「真亏起来会多惨」。于是CVaR(条件VaR)就登场了,它计算的是超过VaR那部分损失的平均值。嗯,这才是真正的「尾部风险」。

今天咱们就把三种主流方法——参数法、历史模拟法、蒙特卡洛模拟法——的代码撸一遍,再比比它们的优劣。

核心知识点速览

  • VaR:给定置信水平下的最大可能损失
  • CVaR:超过VaR的尾部损失均值
  • 三种方法:参数法(正态假设)、历史模拟法(无假设)、蒙特卡洛法(随机模拟)
VaR与CVaR 参数法 历史模拟法 蒙特卡洛法 假设收益率服从正态分布 计算简单,速度快 ⚠ 厚尾分布下失效 基于历史数据分位数 无需分布假设 ⚠ 依赖历史数据长度 随机生成大量路径 可处理复杂非线性 ⚠ 计算量大,慢 实际工作中:参数法做快速估算,历史法做基准,蒙特卡洛做压力测试

1. 参数法:又快又简单,但别太当真

参数法假设收益率服从正态分布。你想想看,如果数据真的正态,那VaR就是均值减去几个标准差的事。我刚开始做风控时,觉得这方法太方便了,一行代码搞定。

但问题来了——金融数据哪有那么乖?真实收益率往往有厚尾,极端行情比正态分布预测的多得多。我曾经用参数法算一个CTA策略的VaR,结果2015年股灾直接被打脸,实际亏损是VaR预测的3倍多。

import numpy as np
import pandas as pd

def parametric_var_cvar(returns, confidence_level=0.95):
    """
    参数法计算VaR和CVaR
    假设收益率服从正态分布
    """
    mu = np.mean(returns)
    sigma = np.std(returns, ddof=1)
    
    # 正态分布分位数
    z = stats.norm.ppf(1 - confidence_level)
    
    VaR = -(mu + z * sigma)
    
    # CVaR:正态分布下尾部期望
    # 公式:CVaR = -mu + sigma * phi(z) / (1 - confidence_level)
    phi_z = stats.norm.pdf(z)
    CVaR = -mu + sigma * phi_z / (1 - confidence_level)
    
    return VaR, CVaR

# 示例
np.random.seed(42)
returns = np.random.normal(0.001, 0.02, 1000)
VaR, CVaR = parametric_var_cvar(returns, 0.95)
print(f"参数法 VaR(95%): {VaR:.4f}")
print(f"参数法 CVaR(95%): {CVaR:.4f}")

避坑指南

我曾经用参数法算一个高杠杆产品的VaR,结果发现算出来的值偏小。后来排查发现,杠杆放大了尾部风险,但正态假设完全没捕捉到。所以——参数法只适合做快速估算,千万别用它做最终风控决策。

2. 历史模拟法:不假设,直接看历史

历史模拟法就简单粗暴了——把历史收益率排个序,取第5%的分位数就是95%的VaR。不需要任何分布假设,数据长什么样就是什么样。

我个人习惯用历史模拟法做基准。为什么?因为它最直观,你跟老板解释「历史上最差的5%情况平均亏这么多」,老板一听就懂。

def historical_var_cvar(returns, confidence_level=0.95):
    """
    历史模拟法计算VaR和CVaR
    """
    sorted_returns = np.sort(returns)
    n = len(sorted_returns)
    
    # VaR:第 (1-confidence_level) 分位数
    index = int(np.floor((1 - confidence_level) * n))
    VaR = -sorted_returns[index]
    
    # CVaR:尾部损失的均值
    tail_returns = sorted_returns[:index+1]
    CVaR = -np.mean(tail_returns)
    
    return VaR, CVaR

# 示例
VaR_h, CVaR_h = historical_var_cvar(returns, 0.95)
print(f"历史模拟法 VaR(95%): {VaR_h:.4f}")
print(f"历史模拟法 CVaR(95%): {CVaR_h:.4f}")

注意

历史模拟法有个致命弱点——它假设历史会重演。但市场结构会变,2015年的波动率跟2020年能一样吗?我建议至少用3-5年的数据,太短了样本不够,太长了市场结构可能已经变了。

3. 蒙特卡洛模拟法:最灵活,也最慢

蒙特卡洛法就是自己造数据。先估计收益率的分布参数,然后随机生成成千上万条路径,最后从这些模拟数据里算VaR和CVaR。

这方法的好处是——你可以加入各种复杂假设。比如波动率聚类、跳跃扩散、甚至随机波动率。我在做期权组合风控时,就不得不用蒙特卡洛,因为期权的非线性收益,参数法和历史法都搞不定。

def monte_carlo_var_cvar(mu, sigma, n_simulations=100000, 
                         confidence_level=0.95, initial_price=1.0):
    """
    蒙特卡洛模拟法计算VaR和CVaR
    """
    # 生成随机收益率
    np.random.seed(42)
    simulated_returns = np.random.normal(mu, sigma, n_simulations)
    
    # 计算模拟价格
    simulated_prices = initial_price * np.exp(simulated_returns)
    
    # 计算收益率(相对于初始价格)
    returns = (simulated_prices - initial_price) / initial_price
    
    # 排序后取分位数
    sorted_returns = np.sort(returns)
    index = int(np.floor((1 - confidence_level) * n_simulations))
    
    VaR = -sorted_returns[index]
    tail_returns = sorted_returns[:index+1]
    CVaR = -np.mean(tail_returns)
    
    return VaR, CVaR

# 示例
VaR_mc, CVaR_mc = monte_carlo_var_cvar(0.001, 0.02, 100000, 0.95)
print(f"蒙特卡洛法 VaR(95%): {VaR_mc:.4f}")
print(f"蒙特卡洛法 CVaR(95%): {CVaR_mc:.4f}")

实战技巧

蒙特卡洛的模拟次数很关键。太少不稳定,太多又慢。我一般先用1万次试跑,看结果稳定了再提到10万次。另外,记得设随机种子,不然每次结果不一样,跟老板汇报时很尴尬。

4. 三种方法对比:谁更靠谱?

维度 参数法 历史模拟法 蒙特卡洛法
计算速度 极快
分布假设 正态分布 无假设 可自定义
尾部风险捕捉 差(厚尾失效) 中等(依赖数据) 好(可建模厚尾)
非线性资产 不适用 不适用 适用
实现难度 中高
实际应用场景 快速估算、日报 基准风控、监管报告 压力测试、复杂组合

你看,没有哪个方法是完美的。我个人的工作流是这样的:

  • 每天跑参数法,快速看个大概
  • 每周用历史模拟法做一次基准校验
  • 遇到市场异动或新产品上线,上蒙特卡洛做压力测试

核心结论

VaR和CVaR不是算出来就完事了。你得理解每种方法的假设和局限。参数法快但不准,历史法直观但依赖数据,蒙特卡洛灵活但慢。实际工作中,我建议三种方法都跑一遍,看结果是否一致。如果差异很大——嗯,那说明你的组合风险可能比想象中复杂。

专注资料整理