第2章:风险度量基础——波动率、协方差矩阵、VaR与CVaR
做量化投资,说白了就是跟风险打交道。我刚开始入行那会儿,带我的老交易员跟我说过一句话,我一直记到现在——「你赚的钱,其实都是你承担风险的补偿」。所以,怎么度量风险,就成了咱们这行的基本功。
这一章,咱们就把几个最核心的风险度量指标掰开揉碎了讲清楚。波动率、协方差矩阵、VaR、CVaR,这些概念你肯定听过,但真正在实战中怎么算、怎么用,里面有不少坑。
2.1 波动率:风险的「体温计」
波动率,英文叫Volatility,是衡量资产价格变动幅度的指标。你想想看,一个股票一天涨跌0.5%,另一个一天能涨跌5%,哪个风险大?显然是后者。波动率就是量化这个「蹦跶程度」的。
我个人习惯用对数收益率来计算波动率,而不是简单收益率。为什么?因为对数收益率在时间上可加,而且更符合正态分布的假设。嗯,这里要注意——实际收益率并不完全服从正态分布,尾部更厚,但这是后话了。
日波动率 σ = std(ln(Pt / Pt-1))
年化波动率 σ年 = σ日 × √252
252是A股一年的实际交易天数。美股用252,港股用247左右。我在项目中遇到过有人直接用365,结果年化波动率算出来虚高不少,回测绩效看着很差,其实是被这个细节坑了。
import numpy as np
import pandas as pd
def calc_volatility(price_series, annual_factor=252):
"""
计算年化波动率
price_series: 价格序列,pandas Series
"""
log_returns = np.log(price_series / price_series.shift(1))
# 去掉NaN
log_returns = log_returns.dropna()
daily_vol = np.std(log_returns)
annual_vol = daily_vol * np.sqrt(annual_factor)
return annual_vol
# 举个实际例子
prices = pd.Series([100, 102, 101, 105, 103, 107, 106])
vol = calc_volatility(prices)
print(f"年化波动率: {vol:.4f}")
2.2 协方差矩阵:多资产之间的「关系网」
单看一个资产的波动率,其实不够。你想想看,如果你同时持有茅台和五粮液,它们俩的走势往往是同向的——白酒板块一涨都涨。这时候组合的风险就不是简单相加了。
协方差矩阵,就是用来描述多个资产之间联动关系的工具。矩阵的对角线是各资产的方差(波动率的平方),非对角线是资产两两之间的协方差。
| Var(资产1) | Cov(1,2) | Cov(1,3) |
| Cov(2,1) | Var(资产2) | Cov(2,3) |
| Cov(3,1) | Cov(3,2) | Var(资产3) |
协方差的值受量纲影响很大,所以实战中我更常用相关系数矩阵。相关系数其实就是标准化后的协方差,取值在-1到1之间,直观多了。
def calc_cov_matrix(returns_df):
"""
计算协方差矩阵
returns_df: 各资产收益率DataFrame,每列一个资产
"""
cov_matrix = returns_df.cov()
# 年化处理
annual_cov = cov_matrix * 252
return annual_cov
# 假设我们有3只股票的日收益率数据
import pandas as pd
data = {
'茅台': [0.01, -0.005, 0.02, -0.01, 0.015],
'五粮液': [0.008, -0.003, 0.018, -0.008, 0.012],
'招商银行': [0.005, 0.002, -0.003, 0.006, -0.002]
}
returns_df = pd.DataFrame(data)
cov_mat = calc_cov_matrix(returns_df)
print(cov_mat)
2.3 VaR:在险价值——「最坏情况下亏多少」
VaR,全称Value at Risk。这个概念在90年代由J.P. Morgan推广开来,现在已经是金融风控的标配了。说白了就是:在给定的置信水平和持有期内,你的投资组合最多可能亏多少钱。
举个例子:95%置信水平下,日VaR为100万。意思是——在100天里,大概有5天亏损会超过100万。注意,它没说超过多少,只说「至少亏100万」。
- 参数法(方差-协方差法): 假设收益率服从正态分布,直接用均值和标准差算。
- 历史模拟法: 直接用过去N天的收益率排序,取第(1-置信水平)分位数。
- 蒙特卡洛模拟法: 假设一个随机过程,模拟出大量路径,再取分位数。
我个人最常用的是历史模拟法。为什么?因为它不需要假设分布形态,简单粗暴,而且监管也认可。参数法虽然计算快,但遇到极端行情(比如2020年3月)就完全失效了——正态分布假设在尾部根本不成立。
def historical_var(returns, confidence_level=0.95):
"""
历史模拟法计算VaR
returns: 收益率序列
confidence_level: 置信水平,默认95%
"""
sorted_returns = np.sort(returns)
index = int((1 - confidence_level) * len(sorted_returns))
var = -sorted_returns[index] # 取正值表示损失
return var
# 示例
np.random.seed(42)
returns = np.random.normal(0.001, 0.02, 1000) # 模拟1000个日收益率
var_95 = historical_var(returns, 0.95)
var_99 = historical_var(returns, 0.99)
print(f"95% VaR: {var_95:.4f}")
print(f"99% VaR: {var_99:.4f}")
2.4 CVaR:条件在险价值——「亏钱时的平均亏损」
VaR有个明显的缺陷:它只告诉你「会亏超过某个值」,但没告诉你「一旦亏了,平均亏多少」。举个例子,95% VaR是100万,那剩下的5%极端情况,可能是亏101万,也可能是亏1000万。这两种情况的风险完全不一样,但VaR给出的数字是一样的。
CVaR(Conditional VaR),也叫Expected Shortfall,就是来解决这个问题的。它计算的是「超过VaR的那些损失的平均值」。说白了就是:在那些最倒霉的日子里,你平均亏多少钱。
CVaRα = E[ -R | -R > VaRα ]
其中R是收益率,α是置信水平。
def historical_cvar(returns, confidence_level=0.95):
"""
历史模拟法计算CVaR
"""
sorted_returns = np.sort(returns)
index = int((1 - confidence_level) * len(sorted_returns))
# 取所有超过VaR的损失
tail_losses = -sorted_returns[:index]
cvar = np.mean(tail_losses)
return cvar
# 接上面的例子
cvar_95 = historical_cvar(returns, 0.95)
print(f"95% CVaR: {cvar_95:.4f}")
2.5 四个指标怎么选?我的实战建议
| 指标 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 波动率 | 日常风控、仓位管理 | 计算简单,直观 | 只反映波动幅度,不区分方向 |
| 协方差矩阵 | 多资产组合、风险分解 | 刻画资产间关系 | 高维时不稳定 |
| VaR | 监管报告、止损线设定 | 监管认可,易于理解 | 忽略尾部风险 |
| CVaR | 极端风险控制、优化目标 | 考虑尾部风险,数学性质好 | 计算量稍大 |
我个人习惯的组合是:日常监控用波动率,做组合优化用协方差矩阵+CVaR,给老板汇报用VaR。你想想看,老板只想知道「最多亏多少」,VaR就够了。但真正做策略时,CVaR才能帮你躲过那些「黑天鹅」。
公众号:蓝海资料掘金营,微信deep3321