风险度量基础:波动率、VaR、CVaR、最大回撤、跟踪误差
做量化对冲,说白了就是跟风险打交道。你赚的每一分钱,本质上都是对风险的补偿。我见过太多人,策略回测曲线漂亮得不行,一上实盘就崩了。为什么?因为他对风险的理解太肤浅了。
今天咱们就把风险度量这件事彻底聊透。我个人习惯把风险度量分成两类:一类是波动性度量,告诉你资产有多「颠」;另一类是尾部风险度量,告诉你最坏情况下会亏多少。这两类缺一不可。
1. 波动率:最基础的风险刻度
波动率就是收益率的标准差。你想想看,如果一个资产每天涨跌幅度很大,那它的波动率自然就高。我习惯用年化波动率来比较不同资产——把日波动率乘以根号252,就得到年化值。
但这里有个坑。我记得刚入行时,直接用历史波动率做风险预算,结果市场一换挡,模型全废了。为什么?因为历史波动率是向后看的,而市场是向前走的。
import numpy as np
def ewma_volatility(returns, lambda_=0.94):
"""指数加权移动平均波动率"""
weights = np.array([(1-lambda_) * lambda_**i for i in range(len(returns))])
weights = weights[::-1] # 最近的数据权重最高
weights /= weights.sum()
mean = np.average(returns, weights=weights)
variance = np.average((returns - mean)**2, weights=weights)
return np.sqrt(variance * 252) # 年化
# 示例
daily_returns = np.random.randn(500) * 0.01
vol = ewma_volatility(daily_returns)
print(f"EWMA年化波动率: {vol:.2%}")
2. VaR:在险价值,一个让人又爱又恨的指标
VaR回答的问题是:「在95%的置信水平下,我一天最多亏多少钱?」说白了,就是找收益率分布的第5百分位数。
但VaR有个致命缺陷——它只告诉你「最多亏到这儿」,却不告诉你「亏到这儿之后会怎样」。我在2015年股灾时就吃过这个亏。当时组合的95% VaR显示日亏损不超过3%,结果有一天直接亏了8%。为什么?因为VaR根本不关心尾部那5%的分布形状。
计算VaR有三种常用方法:
- 参数法: 假设收益率服从正态分布,直接用均值和标准差算。快,但不准——金融数据哪有正态的?
- 历史模拟法: 直接用过去N天的收益率排序,取第5百分位。简单粗暴,但假设历史会重演。
- 蒙特卡洛模拟: 假设一个随机过程,模拟出成千上万条路径。最灵活,也最慢。
def historical_var(returns, confidence=0.95):
"""历史模拟法计算VaR"""
sorted_returns = np.sort(returns)
index = int((1 - confidence) * len(sorted_returns))
return sorted_returns[index]
def parametric_var(returns, confidence=0.95, horizon=1):
"""参数法计算VaR(正态假设)"""
from scipy.stats import norm
mu = returns.mean() * horizon
sigma = returns.std() * np.sqrt(horizon)
z = norm.ppf(1 - confidence)
return mu + z * sigma
# 对比两种方法
np.random.seed(42)
data = np.random.randn(1000) * 0.02
print(f"历史VaR(95%): {historical_var(data):.2%}")
print(f"参数VaR(95%): {parametric_var(data):.2%}")
3. CVaR:比VaR更诚实的尾部风险度量
CVaR,也叫期望损失(Expected Shortfall)。它计算的是「当亏损超过VaR时,平均会亏多少」。说白了,就是看尾部那5%的「平均深度」。
我个人更偏爱CVaR。为什么?因为它满足次可加性,而且对尾部风险更敏感。在风险预算模型中,用CVaR做约束条件,组合会更稳健。
def historical_cvar(returns, confidence=0.95):
"""历史模拟法计算CVaR"""
sorted_returns = np.sort(returns)
index = int((1 - confidence) * len(sorted_returns))
tail_returns = sorted_returns[:index]
return tail_returns.mean()
# 对比VaR和CVaR
var_95 = historical_var(data, 0.95)
cvar_95 = historical_cvar(data, 0.95)
print(f"VaR(95%): {var_95:.2%}")
print(f"CVaR(95%): {cvar_95:.2%}") # CVaR通常比VaR更负
4. 最大回撤:投资者心理的底线
最大回撤(Max Drawdown)衡量的是从历史高点跌到最低点的幅度。这玩意儿跟投资者的心理承受能力直接挂钩。我记得有个客户,策略年化收益30%,但最大回撤15%,他晚上就睡不着觉了。
做风险预算时,我通常会把最大回撤作为一个硬约束。比如:「组合的最大回撤不能超过10%」。然后通过调整资产权重,让回撤控制在这个范围内。
def max_drawdown(equity_curve):
"""计算最大回撤"""
peak = np.maximum.accumulate(equity_curve)
drawdown = (equity_curve - peak) / peak
return drawdown.min()
# 模拟一条净值曲线
np.random.seed(42)
returns = np.random.randn(500) * 0.01 + 0.0005
equity = 100 * np.exp(np.cumsum(returns))
mdd = max_drawdown(equity)
print(f"最大回撤: {mdd:.2%}")
5. 跟踪误差:对冲基金的「紧箍咒」
跟踪误差(Tracking Error)是组合收益率与基准收益率之差的标准差。说白了,就是你的组合跟基准「走偏」了多少。
做对冲基金,跟踪误差是个双刃剑。太小了,说明你只是个指数基金;太大了,说明你偏离基准太远,可能是在赌博。我一般把跟踪误差控制在2%-5%之间,具体看策略类型。
def tracking_error(portfolio_returns, benchmark_returns):
"""计算跟踪误差"""
diff = portfolio_returns - benchmark_returns
return np.std(diff, ddof=1) * np.sqrt(252)
# 示例
port_ret = np.random.randn(252) * 0.012
bench_ret = np.random.randn(252) * 0.01
te = tracking_error(port_ret, bench_ret)
print(f"年化跟踪误差: {te:.2%}")
把这些指标串起来:风险预算的输入
好了,现在我们有五个风险度量指标。在风险预算模型中,它们各自扮演什么角色?
| 指标 | 在风险预算中的角色 | 我的使用频率 |
|---|---|---|
| 波动率 | 基础风险分配单位 | ⭐⭐⭐⭐⭐ |
| VaR | 日常风险限额 | ⭐⭐⭐ |
| CVaR | 尾部风险约束 | ⭐⭐⭐⭐ |
| 最大回撤 | 硬性止损线 | ⭐⭐⭐⭐⭐ |
| 跟踪误差 | 偏离基准的容忍度 | ⭐⭐⭐⭐ |
我个人习惯的做法是:先用波动率做初步的风险预算分配,然后用CVaR和最大回撤做尾部风险校验,最后用跟踪误差约束整体偏离度。这样一套组合拳下来,组合的风险特征就比较清晰了。
嗯,风险度量这块就聊到这儿。这些指标是后续所有风险预算模型的基础。你想想看,如果连风险都量不准,那还谈什么管理?