引言:波动率预测在投资决策中的核心地位
波动率作为衡量资产价格变动幅度和不确定性的关键指标,在现代投资组合管理、风险控制和衍生品定价中扮演着至关重要的角色。对于CFA持证人而言,掌握波动率预测方法不仅是通过考试的必备技能,更是实际工作中进行资产配置、风险管理和投资决策的核心能力。波动率预测的准确性直接影响到投资组合的VaR(风险价值)计算、期权定价、对冲策略效果以及资本配置效率。
波动率预测的复杂性在于它并非一个静态概念,而是随时间、市场条件和信息流动态变化的随机过程。市场波动率通常表现出明显的聚集性(volatility clustering)、均值回归(mean reversion)和杠杆效应(leverage effect)等统计特征。理解并利用这些特征是构建有效预测模型的基础。
本文将系统性地介绍CFA框架下波动率预测的主要方法,从基础的历史波动率计算到复杂的隐含波动率模型,并通过具体案例和代码示例展示如何在实际应用中实现这些方法。我们将重点讨论各种方法的优缺点、适用场景以及如何结合多种方法进行综合判断,从而更精准地预判市场风险。
一、历史波动率(Historical Volatility):基于过去数据的统计基础
历史波动率是最直观、最常用的波动率度量方法,它通过计算过去一段时间内资产收益率的标准差来估计未来的波动率水平。这种方法基于一个基本假设:过去的价格变动模式会在未来一定程度上重复。
1.1 简单历史波动率的计算方法
简单历史波动率通常采用以下步骤计算:
- 选择观测窗口(如20天、60天或252天)
- 计算每日对数收益率:\(r_t = \ln(P_t/P_{t-1})\)
- 计算该窗口内收益率的平均值:\(\bar{r} = \frac{1}{n}\sum_{t=1}^{n}r_t\)
- 计算标准差:\(\sigma = \sqrt{\frac{1}{n-1}\sum_{t=1}^{n}(r_t - \bar{r})^2}\)
- 年化波动率:\(\sigma_{annual} = \sigma \times \sqrt{252}\)(假设252个交易日)
Python代码实现:
import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
def calculate_historical_volatility(prices, window=20, annualization_factor=252):
"""
计算简单历史波动率
参数:
prices: 收盘价序列 (pandas Series)
window: 观察窗口天数
annualization_factor: 年化因子(通常为252个交易日)
返回:
历史波动率序列
"""
# 计算对数收益率
log_returns = np.log(prices / prices.shift(1))
# 计算滚动标准差
rolling_std = log_returns.rolling(window=window).std()
# 年化
historical_vol = rolling_std * np.sqrt(annualization_factor)
return historical_vol
# 获取标普500指数数据
spy = yf.download('SPY', start='2020-01-01', end='2023-12-31')
spy_vol = calculate_historical_volatility(spy['Adj Close'], window=20)
# 可视化
plt.figure(figsize=(12, 6))
plt.plot(spy_vol, label='20-day Historical Volatility')
plt.title('SPY Historical Volatility (20-day window)')
plt.xlabel('Date')
plt.ylabel('Annualized Volatility')
plt.legend()
plt.grid(True)
plt.show()
1.2 历史波动率的优缺点分析
优点:
- 计算简单,易于理解和实现
- 完全基于客观市场数据,无主观参数
- 在市场相对平稳时期表现良好
- 计算结果具有可重复性
缺点:
- 对近期数据和远期数据赋予相同权重,无法反映最新市场信息
- 对异常值(如市场崩盘)敏感,可能导致波动率高估
- 存在滞后性,无法预测突变
- 在市场结构变化时表现不佳
1.3 指数加权历史波动率(EWMA)
为了解决简单历史波动率对所有数据一视同仁的问题,CFA教材推荐使用指数加权移动平均(EWMA)模型,给予近期数据更高权重:
\(\sigma_t^2 = \lambda \sigma_{t-1}^2 + (1-\lambda)r_{t-1}^2\)
其中\(\lambda\)是衰减因子,通常取0.94(J.P. Morgan RiskMetrics标准)。
EWMA代码实现:
def calculate_ewma_volatility(prices, lambda_factor=0.94, annualization_factor=252):
"""
计算EWMA波动率
参数:
prices: 收盘价序列
lambda_factor: 衰减因子
annualization_factor: 年化因子
返回:
EWMA波动率序列
"""
log_returns = np.log(prices / prices.shift(1))
# 初始化方差序列
variance = pd.Series(index=prices.index, dtype=float)
variance.iloc[0] = log_returns.var() # 使用初始方差
# 递归计算EWMA方差
for i in range(1, len(prices)):
variance.iloc[i] = (lambda_factor * variance.iloc[i-1] +
(1 - lambda_factor) * log_returns.iloc[i]**2)
# 年化
ewma_vol = np.sqrt(variance) * np.sqrt(annualization_factor)
return ewma_vol
# 计算EWMA波动率
spy_ewma_vol = calculate_ewma_volatility(spy['Adj Close'])
# 对比简单历史波动率与EWMA
plt.figure(figsize=(12, 6))
plt.plot(spy_vol, label='20-day Historical Volatility', alpha=0.7)
plt.plot(spy_ewma_vol, label='EWMA Volatility (λ=0.94)', color='red')
plt.title('Comparison: Simple Historical vs EWMA Volatility')
plt.xlabel('Date')
plt.ylabel('Annualized Volatility')
plt.legend()
plt.grid(True)
plt.show()
EWMA的特点:
- 对近期数据赋予更高权重,反应更灵敏
- 适用于高频数据和短期预测
- 缺点是需要选择合适的λ值,且对极端事件的反应可能过度
1.4 历史波动率的应用场景与局限性
历史波动率最适合用于:
- 长期投资组合的风险评估
- 作为其他复杂模型的基准
- 在市场相对平稳时期的短期预测
但需注意,历史波动率无法预测未来结构性变化,例如:
- 货币政策突然转向
- 地缘政治危机爆发
- 金融危机期间的流动性枯竭
在这些情况下,历史波动率会严重低估实际风险。因此,CFA强调必须结合其他方法进行综合判断。
2. 隐含波动率(Implied Volatility):市场预期的前瞻性指标
隐含波动率是从期权市场价格反推出来的波动率,反映了市场参与者对未来波动率的预期。它是CFA衍生品定价和风险管理的核心内容。
2.1 隐含波动率的计算原理
隐含波动率无法直接观察,需要通过期权定价模型反解。最常用的是Black-Scholes模型:
\(C = S_0 N(d_1) - K e^{-rT} N(d_2)\)
其中:
- \(d_1 = \frac{\ln(S_0/K) + (r + \sigma^2/2)T}{\sigma\sqrt{T}}\)
- \(d_2 = d_1 - \sigma\sqrt{T}\)
通过迭代算法(如牛顿法或二分法)求解σ,使得模型价格等于市场价格。
Python代码实现:
from scipy.stats import norm
import numpy as np
def black_scholes_call(S, K, T, r, sigma):
"""Black-Scholes看涨期权定价"""
d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
d2 = d1 - sigma*np.sqrt(T)
call_price = S*norm.cdf(d1) - K*np.exp(-r*T)*norm.cdf(d2)
return call_price
def black_scholes_put(S, K, T, r, sigma):
"""Black-Scholes看跌期权定价"""
d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
d2 = d1 - sigma*np.sqrt(T)
put_price = K*np.exp(-r*T)*norm.cdf(-d2) - S*norm.cdf(-d1)
return put_price
def calculate_implied_volatility(market_price, S, K, T, r, option_type='call',
initial_guess=0.2, tolerance=1e-6, max_iter=100):
"""
使用牛顿法计算隐含波动率
参数:
market_price: 期权市场价格
S: 标的资产现价
K: 行权价
T: 到期时间(年)
r: 无风险利率
option_type: 'call' 或 'put'
initial_guess: 初始猜测值
tolerance: 收敛容忍度
max_iter: 最大迭代次数
返回:
隐含波动率
"""
sigma = initial_guess
for i in range(max_iter):
if option_type == 'call':
model_price = black_scholes_call(S, K, T, r, sigma)
# Vega: 期权价格对波动率的导数
d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
vega = S * norm.pdf(d1) * np.sqrt(T)
else:
model_price = black_scholes_put(S, K, T, r, sigma)
d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
vega = S * norm.pdf(d1) * np.sqrt(T)
# 牛顿法更新
diff = model_price - market_price
if abs(diff) < tolerance:
return sigma
if vega == 0:
return sigma
sigma = sigma - diff / vega
# 防止sigma为负
if sigma <= 0:
sigma = tolerance
return sigma
# 示例:计算某期权的隐含波动率
S = 100 # 标的资产现价
K = 105 # 行权价
T = 0.25 # 3个月到期
r = 0.02 # 无风险利率
market_price = 4.5 # 市场价格
iv = calculate_implied_volatility(market_price, S, K, T, r, option_type='call')
print(f"隐含波动率: {iv:.4f} ({iv*100:.2f}%)")
# 验证:用计算出的IV重新定价
reprice = black_scholes_call(S, K, T, r, iv)
print(f"模型价格: {reprice:.4f}, 市场价格: {market_price}")
2.2 VIX指数:市场恐慌指标
VIX(Volatility Index)是由CBOE编制的30天隐含波动率指数,基于SPX期权价格计算,被称为”恐慌指数”。VIX的计算涉及多个行权价的期权,采用特定权重方法。
VIX计算简化示例:
def calculate_vix_component(near_term_options, next_term_options, T1, T2, r):
"""
计算VIX指数的组成部分
参数:
near_term_options: 近月期权数据(行权价、买价、卖价)
next_term_options: 远月期权数据
T1, T2: 到期时间
r: 无风险利率
返回:
VIX值
"""
def calculate_component(options, T, r):
# 计算每个行权价的贡献
contributions = []
for i, row in options.iterrows():
K = row['strike']
bid = row['bid']
ask = row['ask']
mid_price = (bid + ask) / 2
# 选择最接近的期权(买价或卖价)
if i == 0:
# 最低行权价
if bid > 0:
F = K + np.exp(r*T) * bid
else:
continue
else:
# 计算方差贡献
if bid > 0 and ask > 0:
sigma2 = (2/T) * (np.exp(r*T) * mid_price - K/F - 1)**2
contributions.append(sigma2)
# 简化的VIX计算(实际计算更复杂)
if len(contributions) > 0:
return np.mean(contributions)
return 0
# 计算近月和远月成分
sigma1 = calculate_component(near_term_options, T1, r)
sigma2 = calculate_component(next_term_options, T2, r)
# 插值计算30天VIX
if sigma1 > 0 and sigma2 > 0:
T1_minutes = T1 * 365 * 24 * 60
T2_minutes = T2 * 365 * 24 * 60
N30 = 30 * 24 * 60
vix = 100 * np.sqrt((T1_minutes * sigma1 * ((T2_minutes - N30) / (T2_minutes - T1_minutes)) +
T2_minutes * sigma2 * ((N30 - T1_minutes) / (T2_minutes - T1_minutes))) *
(525600 / N30))
return vix
return None
# 示例数据(简化)
near_term = pd.DataFrame({
'strike': [95, 100, 105],
'bid': [5.2, 2.5, 0.8],
'ask': [5.4, 2.7, 1.0]
})
next_term = pd.DataFrame({
'strike': [95, 100, 105],
'bid': [6.5, 4.2, 2.5],
'ask': [6.7, 4.4, 2.7]
})
T1 = 0.1 # 近月到期时间
T2 = 0.2 # 远月到期时间
r = 0.02
# 计算VIX(简化版)
vix_value = calculate_vix_component(near_term, next_term, T1, T2, r)
if vix_value:
print(f"计算的VIX值: {vix_value:.2f}")
else:
print("VIX计算需要更完整的期权数据")
实际应用中:
- VIX > 30:市场恐慌,预期高波动
- VIX < 15:市场平静,预期低波动
- VIX快速上升:市场不确定性增加
2.3 隐含波动率的优缺点
优点:
- 前瞻性:反映市场对未来波动率的预期
- 包含市场情绪:隐含波动率上升往往伴随恐慌情绪
- 对事件敏感:能快速反映即将发生的重大事件
缺点:
- 受供需关系影响:期权市场流动性不足时可能失真
- 模型依赖:依赖Black-Scholes等模型的假设(如波动率恒定)
- 市场操纵可能:大机构可能通过期权交易影响VIX
- 数据获取成本高:需要实时期权数据
2.4 隐含波动率曲面(Volatility Surface)
实际市场中,隐含波动率随行权价和到期时间变化,形成波动率曲面。CFA重点分析两个关键特征:
波动率微笑(Volatility Smile):
- 深度实值和深度虚值期权的隐含波动率高于平值期权
- 反映市场对极端价格变动的担忧
波动率期限结构:
- 不同到期日的隐含波动率不同
- 通常短期波动率高于长期(市场预期波动会回归均值)
Python绘制波动率曲面:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def plot_volatility_surface():
"""
绘制隐含波动率曲面示例
"""
# 生成行权价和到期时间网格
strikes = np.linspace(80, 120, 20) # 行权价80-120
maturities = np.linspace(0.1, 1.0, 10) # 到期时间0.1-1年
# 创建波动率曲面(模拟数据)
# 实际中需要从市场获取真实数据
K, T = np.meshgrid(strikes, maturities)
# 模拟波动率曲面:包含微笑和期限结构特征
# 基础波动率
base_vol = 0.20
# 行权价影响(微笑效应)
strike_effect = 0.05 * np.exp(-((K - 100) / 15)**2)
# 到期时间影响(期限结构)
maturity_effect = 0.03 * np.exp(-T * 2)
# 随机噪声
noise = 0.01 * np.random.randn(*K.shape)
implied_vol = base_vol + strike_effect + maturity_effect + noise
# 绘制3D曲面
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(K, T, implied_vol, cmap='viridis', alpha=0.8)
ax.set_xlabel('Strike Price')
ax.set_ylabel('Maturity (Years)')
ax.set_zlabel('Implied Volatility')
ax.set_title('Implied Volatility Surface\n(Showing Smile and Term Structure)')
# 添加颜色条
fig.colorbar(surf, ax=ax, shrink=0.5, aspect=5)
plt.show()
# 2D等高线图
plt.figure(figsize=(10, 6))
contour = plt.contour(K, T, implied_vol, levels=10, cmap='viridis')
plt.clabel(contour, inline=True, fontsize=10)
plt.xlabel('Strike Price')
plt.ylabel('Maturity (Years)')
plt.title('Implied Volatility Contour Plot')
plt.colorbar(label='Implied Volatility')
plt.show()
plot_volatility_surface()
3. GARCH模型族:时间序列分析的精髓
GARCH(Generalized Autoregressive Conditional Heteroskedasticity)模型是CFA二级和三级考试的重点,也是实际应用中预测波动率最强大的工具之一。它能够捕捉波动率聚集性、均值回归等关键特征。
3.1 GARCH(1,1)模型原理
GARCH(1,1)是最基础且最常用的模型,其方差方程为:
\(\sigma_t^2 = \omega + \alpha r_{t-1}^2 + \beta \sigma_{t-1}^2\)
其中:
- \(\omega\):长期平均方差水平
- \(\alpha\):ARCH项系数,反映新信息对波动率的冲击
- \(\beta\):GARCH项系数,反映波动率的持续性
- 约束条件:\(\omega > 0, \alpha \geq 0, \beta \geq 0, \alpha + \beta < 1\)
GARCH(1,1)的经济学含义:
- \(\alpha\)越大,市场对新信息反应越剧烈
- \(\beta\)越大,波动率持续性越强,”波动率聚集”越明显
- 长期均衡波动率:\(\sigma^2 = \omega / (1 - \alpha - \beta)\)
3.2 GARCH模型的Python实现
import pandas as pd
import numpy as np
from scipy.optimize import minimize
import yfinance as yf
import matplotlib.pyplot as plt
class GARCH11:
"""
GARCH(1,1)模型实现
"""
def __init__(self, returns):
"""
参数:
returns: 收益率序列(百分比或小数形式)
"""
self.returns = returns
self.params = None
self.fitted = False
def _negative_log_likelihood(self, params):
"""
计算负对数似然函数
"""
omega, alpha, beta = params
# 约束条件检查
if omega <= 0 or alpha < 0 or beta < 0 or alpha + beta >= 1:
return 1e10
# 初始化条件方差
T = len(self.returns)
h = np.zeros(T)
h[0] = np.var(self.returns) # 初始方差
# 递归计算条件方差
for t in range(1, T):
h[t] = omega + alpha * self.returns[t-1]**2 + beta * h[t-1]
# 计算对数似然(假设正态分布)
log_likelihood = -0.5 * np.sum(np.log(2 * np.pi * h) + (self.returns**2) / h)
return -log_likelihood # 负值
def fit(self, initial_guess=(0.01, 0.1, 0.85)):
"""
拟合GARCH(1,1)模型
"""
# 使用最小化求解
result = minimize(
self._negative_log_likelihood,
x0=initial_guess,
method='L-BFGS-B',
bounds=[(1e-6, None), (0, 1), (0, 1)]
)
if result.success:
self.params = result.x
self.fitted = True
return self.params
else:
print("优化失败:", result.message)
return None
def conditional_volatility(self):
"""
计算条件波动率序列
"""
if not self.fitted:
raise ValueError("需要先拟合模型")
omega, alpha, beta = self.params
T = len(self.returns)
h = np.zeros(T)
h[0] = np.var(self.returns)
for t in range(1, T):
h[t] = omega + alpha * self.returns[t-1]**2 + beta * h[t-1]
return np.sqrt(h)
def forecast(self, steps=1):
"""
预测未来波动率
"""
if not self.fitted:
raise ValueError("需要先拟合模型")
omega, alpha, beta = self.params
last_return = self.returns[-1]
last_variance = self.conditional_volatility()[-1]**2
forecasts = []
current_variance = last_variance
for i in range(steps):
# GARCH方差预测
current_variance = omega + alpha * last_return**2 + beta * current_variance
forecasts.append(np.sqrt(current_variance))
# 对于多步预测,假设收益率为0(或使用均值预测)
last_return = 0
return forecasts
# 获取数据并拟合GARCH模型
def garch_example():
# 获取标普500收益率数据
spy = yf.download('SPY', start='2020-01-01', end='2023-12-31')
spy_returns = np.log(spy['Adj Close'] / spy['Adj Close'].shift(1)).dropna() * 100 # 转换为百分比
# 创建并拟合GARCH模型
garch = GARCH11(spy_returns.values)
params = garch.fit()
if params is not None:
print("GARCH(1,1)参数估计:")
print(f"ω (omega): {params[0]:.6f}")
print(f"α (alpha): {params[1]:.6f}")
print(f"β (beta): {params[2]:.6f}")
print(f"α + β: {params[1] + params[2]:.6f}")
# 长期均衡波动率
long_run_vol = np.sqrt(params[0] / (1 - params[1] - params[2]))
print(f"长期均衡波动率: {long_run_vol:.2f}%")
# 获取条件波动率
cond_vol = garch.conditional_volatility()
# 预测未来5天波动率
forecasts = garch.forecast(steps=5)
print("\n未来5天波动率预测:")
for i, vol in enumerate(forecasts, 1):
print(f"Day {i}: {vol:.2f}%")
# 可视化
plt.figure(figsize=(12, 8))
plt.subplot(2, 1, 1)
plt.plot(spy_returns.index, spy_returns, label='SPY Daily Returns', alpha=0.7)
plt.title('SPY Daily Returns')
plt.ylabel('Return (%)')
plt.legend()
plt.grid(True)
plt.subplot(2, 1, 2)
plt.plot(spy_returns.index, cond_vol, label='GARCH Conditional Volatility', color='red')
plt.title('GARCH(1,1) Conditional Volatility')
plt.ylabel('Volatility (%)')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
garch_example()
3.3 GARCH模型的扩展与变体
1. EGARCH(指数GARCH)
- 解决GARCH对正负冲击的对称反应问题
- 方程:\(\ln(\sigma_t^2) = \omega + \alpha \frac{|r_{t-1}|}{\sigma_{t-1}} + \gamma \frac{r_{t-1}}{\sigma_{t-1}} + \beta \ln(\sigma_{t-1}^2)\)
- 优点:允许杠杆效应(负收益对波动率影响更大)
2. GJR-GARCH
- 引入虚拟变量区分正负冲击
- 方程:\(\sigma_t^2 = \omega + \alpha r_{t-1}^2 + \gamma I_{r_{t-1}<0} r_{t-1}^2 + \beta \sigma_{t-1}^2\)
- 优点:明确建模杠杆效应
3. 多元GARCH(DCC-GARCH)
- 用于资产组合的协方差预测
- 适合CFA三级资产组合管理中的风险相关性分析
3.4 GARCH模型的CFA考试要点
在CFA考试中,GARCH模型的重点包括:
- 理解参数含义:α+β越接近1,波动率持续性越强
- 长期均衡波动率的计算
- 模型假设:正态分布、平稳性
- 模型选择:AIC/BIC准则
- 模型诊断:残差检验、ARCH-LM检验
4. 波动率预测的综合应用与风险预判
4.1 多方法综合框架
CFA强调在实际应用中应结合多种方法:
综合预测模型:
最终预测 = w1 × 历史波动率 + w2 × 隐含波动率 + w3 × GARCH预测 + w4 × 宏观因素调整
权重分配原则:
- 短期预测(个月):隐含波动率权重更高
- 长期预测(>3个月):历史波动率和GARCH权重更高
- 极端市场:增加隐含波动率权重,减少历史数据权重
4.2 实际案例:2020年3月市场崩盘分析
案例背景: 2020年3月,COVID-19引发全球市场恐慌,标普500指数在一个月内下跌约30%,VIX指数从15飙升至80以上。
不同方法的表现对比:
def analyze_crisis_period():
"""
分析2020年3月危机期间各种波动率预测方法的表现
"""
# 获取数据
spy = yf.download('SPY', start='2020-01-01', end='2020-06-30')
spy_returns = np.log(spy['Adj Close'] / spy['Adj Close'].shift(1)).dropna()
# 1. 历史波动率(20天)
hist_vol_20 = spy_returns.rolling(20).std() * np.sqrt(252) * 100
# 2. EWMA波动率
def ewma_vol(returns, lambda_factor=0.94):
variance = pd.Series(index=returns.index, dtype=float)
variance.iloc[0] = returns.var()
for i in range(1, len(returns)):
variance.iloc[i] = (lambda_factor * variance.iloc[i-1] +
(1 - lambda_factor) * returns.iloc[i]**2)
return np.sqrt(variance) * np.sqrt(252) * 100
ewma_vol_20 = ewma_vol(spy_returns)
# 3. GARCH预测(简化:使用滚动窗口)
garch_forecasts = []
window = 60 # 使用60天数据拟合
for i in range(window, len(spy_returns)):
subset_returns = spy_returns.iloc[i-window:i].values
garch = GARCH11(subset_returns)
try:
params = garch.fit()
if params is not None:
forecast = garch.forecast(steps=1)[0]
garch_forecasts.append(forecast)
else:
garch_forecasts.append(np.nan)
except:
garch_forecasts.append(np.nan)
garch_vol = pd.Series(garch_forecasts, index=spy_returns.index[window:])
# 4. 获取VIX数据(隐含波动率)
vix = yf.download('^VIX', start='2020-01-01', end='2020-06-30')['Adj Close']
# 可视化对比
plt.figure(figsize=(14, 8))
# 价格走势
plt.subplot(2, 1, 1)
plt.plot(spy['Adj Close'], label='SPY Price', color='black')
plt.title('SPY Price - 2020 Crisis Period')
plt.ylabel('Price ($)')
plt.legend()
plt.grid(True)
# 波动率对比
plt.subplot(2, 1, 2)
plt.plot(hist_vol_20, label='Historical Vol (20d)', alpha=0.7)
plt.plot(ewma_vol_20, label='EWMA Volatility', alpha=0.7)
plt.plot(garch_vol, label='GARCH Forecast', alpha=0.7)
plt.plot(vix, label='VIX (Implied)', color='red', linewidth=2)
# 标记危机爆发点
crisis_start = pd.Timestamp('2020-02-20')
plt.axvline(crisis_start, color='gray', linestyle='--', alpha=0.5, label='Crisis Start')
plt.title('Volatility Forecasting Methods Comparison - 2020 Crisis')
plt.ylabel('Volatility (%)')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
# 计算各方法在危机期间的预测误差(假设真实波动率为后续20天实际波动率)
print("\n=== 2020年3月危机期间各方法表现分析 ===")
# 选择危机高峰期(2020年3月)
crisis_period = spy_returns['2020-03-01':'2020-03-31']
if len(crisis_period) > 0:
# 计算实际后续波动率(作为基准)
actual_future_vol = crisis_period.std() * np.sqrt(252) * 100
print(f"危机期间实际波动率: {actual_future_vol:.2f}%")
print("\n各方法在危机前的预测值(2020-02-28):")
# 获取各方法在2月28日的值
feb_28 = pd.Timestamp('2020-02-28')
if feb_28 in hist_vol_20.index:
print(f"历史波动率: {hist_vol_20.loc[feb_28]:.2f}%")
if feb_28 in ewma_vol_20.index:
print(f"EWMA波动率: {ewma_vol_20.loc[feb_28]:.2f}%")
if feb_28 in garch_vol.index:
print(f"GARCH预测: {garch_vol.loc[feb_28]:.2f}%")
if feb_28 in vix.index:
print(f"VIX隐含波动率: {vix.loc[feb_28]:.2f}%")
print("\n分析结论:")
print("- 历史波动率和EWMA严重低估了实际风险(滞后性)")
print("- VIX在危机前已升至30%以上,提供了预警信号")
print("- GARCH模型在危机初期快速反应,但可能过度波动")
print("- 综合模型应给予隐含波动率更高权重")
analyze_crisis_period()
案例分析结论:
- 历史波动率:严重滞后,在危机爆发前仍维持在15%左右,完全未能预警
- EWMA:反应稍快,但仍低估风险,无法预测突变
- VIX隐含波动率:在2月底已升至25-30%,提供了前瞻性预警信号
- GARCH模型:在危机初期快速反应,但参数估计不稳定
CFA实践建议:
- 在市场平稳期,可依赖历史波动率和GARCH模型
- 当VIX快速上升或处于高位时,必须提高风险权重
- 建立多指标预警体系,单一方法不可靠
- 定期回测和调整模型参数
4.3 波动率预测在投资组合管理中的应用
1. 风险预算与资本配置
- 使用预测波动率计算组合VaR
- 动态调整杠杆:低波动时增加风险暴露,高波动时降低暴露
2. 期权交易策略
- 当预测波动率 < 隐含波动率:卖出期权(做空波动率)
- 当预测波动率 > 隐含波动率:买入期权(做多波动率)
3. 对冲策略优化
- 使用预测波动率动态调整对冲比率
- 在波动率预测准确时,可降低对冲成本
4. 资产配置
- 波动率预测用于风险平价策略
- 在高波动预期下,增加防御性资产配置
4.4 模型验证与回测框架
def backtest_volatility_forecast(returns, forecast_method, window=252, test_periods=20):
"""
波动率预测回测框架
参数:
returns: 收益率序列
forecast_method: 预测函数
window: 训练窗口
test_periods: 预测后多少天的波动率作为真实值
"""
errors = []
predictions = []
actuals = []
for i in range(window, len(returns) - test_periods):
# 训练数据
train_returns = returns[i-window:i]
# 测试数据(真实波动率)
test_returns = returns[i:i+test_periods]
# 预测
pred_vol = forecast_method(train_returns)
# 真实波动率
actual_vol = test_returns.std() * np.sqrt(252)
predictions.append(pred_vol)
actuals.append(actual_vol)
errors.append((pred_vol - actual_vol)**2)
# 计算评估指标
mse = np.mean(errors)
mae = np.mean([abs(e) for e in errors])
# 相关系数
corr = np.corrcoef(predictions, actuals)[0, 1]
return {
'MSE': mse,
'MAE': mae,
'Correlation': corr,
'Predictions': predictions,
'Actuals': actuals
}
# 定义预测方法
def historical_forecast(returns):
return returns.std() * np.sqrt(252)
def ewma_forecast(returns, lambda_factor=0.94):
variance = returns.var() # 简化:使用最近值
for ret in returns[::-1]: # 反向计算
variance = lambda_factor * variance + (1 - lambda_factor) * ret**2
return np.sqrt(variance) * np.sqrt(252)
# 获取数据
spy = yf.download('SPY', start='2018-01-01', end='2023-12-31')
spy_returns = np.log(spy['Adj Close'] / spy['Adj Close'].shift(1)).dropna()
# 回测
print("=== 波动率预测回测结果 ===")
for name, method in [('Historical', historical_forecast), ('EWMA', ewma_forecast)]:
result = backtest_volatility_forecast(spy_returns.values, method)
print(f"\n{name}方法:")
print(f" MSE: {result['MSE']:.6f}")
print(f" MAE: {result['MAE']:.4f}")
print(f" 相关系数: {result['Correlation']:.4f}")
5. 高级主题:随机波动率模型与机器学习
5.1 随机波动率模型(Stochastic Volatility)
随机波动率模型假设波动率本身是一个随机过程,最著名的是Heston模型:
\(dS_t = \mu S_t dt + \sqrt{v_t} S_t dW_t^S\) \(d v_t = \kappa(\theta - v_t) dt + \xi \sqrt{v_t} dW_t^v\) \(dW_t^S dW_t^v = \rho dt\)
优点: 更符合实际,波动率是随机的 缺点: 参数估计复杂,计算成本高
5.2 机器学习方法
现代波动率预测越来越多地采用机器学习:
1. LSTM神经网络
- 捕捉长期依赖关系
- 适合处理时间序列数据
2. 随机森林/梯度提升
- 处理非线性关系
- 可以整合多种特征(技术指标、宏观变量)
3. 注意力机制
- 自动学习不同时间点的重要性
- 适合高频数据
机器学习代码示例(简化版):
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
def create_features(returns, lags=5):
"""
创建机器学习特征
"""
df = pd.DataFrame({'returns': returns})
# 滞后特征
for i in range(1, lags+1):
df[f'return_lag_{i}'] = df['returns'].shift(i)
# 波动率特征
df['vol_5'] = df['returns'].rolling(5).std()
df['vol_20'] = df['returns'].rolling(20).std()
# 目标变量:未来波动率
df['target_vol'] = df['returns'].rolling(20).std().shift(-20)
return df.dropna()
def ml_volatility_forecast():
"""
机器学习预测波动率示例
"""
# 获取数据
spy = yf.download('SPY', start='2015-01-01', end='2023-12-31')
returns = np.log(spy['Adj Close'] / spy['Adj Close'].shift(1)).dropna().values
# 创建特征
df = create_features(returns)
X = df.drop('target_vol', axis=1).values
y = df['target_vol'].values
# 分割数据
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, shuffle=False)
# 训练随机森林
rf = RandomForestRegressor(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
# 预测
y_pred = rf.predict(X_test)
# 评估
mse = mean_squared_error(y_test, y_pred)
corr = np.corrcoef(y_pred, y_test)[0, 1]
print(f"机器学习预测结果:")
print(f"MSE: {mse:.6f}")
print(f"相关系数: {corr:.4f}")
# 特征重要性
importances = rf.feature_importances_
feature_names = df.drop('target_vol', axis=1).columns
print("\n特征重要性:")
for name, imp in zip(feature_names, importances):
print(f" {name}: {imp:.4f}")
# 注意:此代码需要安装scikit-learn
# ml_volatility_forecast()
6. 波动率预测的实践建议与CFA考试要点
6.1 实际应用中的关键原则
1. 多方法验证
- 永远不要依赖单一方法
- 建立方法间的交叉验证机制
- 当不同方法结果差异较大时,提高警惕
2. 动态调整权重
- 根据市场状态调整各方法权重
- 危机期间增加隐含波动率权重
- 平稳期增加历史数据权重
3. 模型回测与更新
- 定期回测预测准确性
- 监控参数稳定性
- 及时更新模型
4. 理解局限性
- 所有模型都有假设前提
- 没有模型能预测黑天鹅事件
- 波动率预测本质是概率性判断
6.2 CFA考试高频考点
一级:
- 波动率的基本概念和计算
- 历史波动率 vs 隐含波动率
- VIX指数的含义
二级:
- GARCH模型原理和参数解释
- 波动率微笑和期限结构
- 隐含波动率计算(Black-Scholes反解)
三级:
- 波动率预测在资产配置中的应用
- 动态对冲策略
- 风险预算与波动率预测
6.3 常见错误与注意事项
1. 混淆已实现波动率和预测波动率
- 已实现波动率是历史数据
- 预测波动率是未来估计
2. 忽略波动率的均值回归特性
- 高波动率会下降,低波动率会上升
- 长期均衡水平是关键
3. 过度拟合模型
- 复杂模型不一定更好
- 注意样本外表现
4. 忽视市场结构变化
- 监管变化、市场微观结构改变会影响波动率特征
- 需要定期重新评估模型
7. 结论:构建稳健的波动率预测体系
波动率预测是CFA知识体系中的核心技能,也是实际投资管理的关键环节。通过本文的系统介绍,我们了解到:
核心方法总结:
- 历史波动率:基础但滞后,适合作为基准
- 隐含波动率:前瞻性好,但受市场情绪影响
- GARCH模型:能捕捉动态特征,是量化预测的利器
- 机器学习:处理复杂非线性关系,但需要大量数据
成功的关键在于:
- 综合运用:没有单一最优方法,必须结合多种手段
- 动态调整:根据市场状态和预测表现调整权重
- 严格验证:通过回测和样本外测试确保稳健性
- 理解局限:承认预测的不确定性,做好风险管理
对于CFA持证人而言,波动率预测不仅是技术问题,更是风险管理哲学的体现。精准预判市场风险的目标不是消除不确定性,而是在不确定性中做出更明智的决策,为投资组合构建更稳健的防御体系。
最终,最好的波动率预测模型是那个在特定市场环境下、特定时间范围内、特定资产类别上表现最稳健的模型。持续学习、实践验证和动态优化,是提升波动率预测能力的不二法门。
