第1章:VaR与CVaR——参数法、历史模拟法、蒙特卡洛模拟法的代码实现与对比
做量化投资的朋友,对VaR肯定不陌生。我入行那会儿,第一次被老板问「今天组合的VaR是多少」,我愣是翻了半天报告才找到。后来自己动手写代码实现,才真正搞明白这玩意儿到底在算什么。
说白了,VaR就是回答一个问题:在给定的置信水平下,我的组合最多可能亏多少钱?比如95%的VaR是100万,意思就是只有5%的概率,亏损会超过100万。
但VaR有个硬伤——它只告诉你「最多亏多少」,却不告诉你「真亏起来会多惨」。于是CVaR(条件VaR)就登场了,它计算的是超过VaR那部分损失的平均值。嗯,这才是真正的「尾部风险」。
今天咱们就把三种主流方法——参数法、历史模拟法、蒙特卡洛模拟法——的代码撸一遍,再比比它们的优劣。
核心知识点速览
- VaR:给定置信水平下的最大可能损失
- CVaR:超过VaR的尾部损失均值
- 三种方法:参数法(正态假设)、历史模拟法(无假设)、蒙特卡洛法(随机模拟)
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不是算出来就完事了。你得理解每种方法的假设和局限。参数法快但不准,历史法直观但依赖数据,蒙特卡洛灵活但慢。实际工作中,我建议三种方法都跑一遍,看结果是否一致。如果差异很大——嗯,那说明你的组合风险可能比想象中复杂。