波动率建模实战:历史波动率、隐含波动率、GARCH模型在Python中的实现
波动率,说白了就是市场的心跳。
做量化投资这么多年,我越来越觉得,不懂波动率,就像开船不看天气预报。你可能顺风顺水一阵子,但一个浪打过来,连人带船都得翻。今天咱们就来聊聊波动率建模这件事,我会把我在实战中踩过的坑、总结的经验,都揉进这章内容里。
一、波动率到底是什么?
先别急着上代码,咱们得先搞清楚一个事儿:波动率到底在衡量什么?
很多人以为波动率就是「涨跌幅度」,其实不准确。波动率衡量的是资产价格的不确定性,或者说,是价格变动的剧烈程度。你想想看,一只股票每天涨跌0.5%,和一只股票每天涨跌5%,哪个更「波动」?显然是后者。
我个人习惯把波动率分成三类:
- 历史波动率:基于过去数据算出来的,反映「已经发生」的波动。
- 隐含波动率:从期权价格反推出来的,反映「市场预期」的波动。
- 模型波动率:用GARCH这类模型预测出来的,反映「未来可能」的波动。
这三者各有各的用处,也各有各的坑。咱们一个一个来拆解。
二、历史波动率:最简单,也最容易犯错
历史波动率的计算,其实就三步:
- 取对数收益率
- 计算标准差
- 年化处理
听起来很简单对吧?但我在项目中遇到过一个问题:很多人直接用收盘价算收益率,结果发现波动率曲线跟实际走势对不上。为什么?因为收盘价有跳空,尤其是隔夜跳空,会严重扭曲波动率的计算。
来看代码实现:
import numpy as np
import pandas as pd
def historical_volatility(prices, window=20, trading_days=252):
"""
计算历史波动率
prices: 价格序列
window: 滚动窗口
trading_days: 年化交易日数
"""
log_returns = np.log(prices / prices.shift(1))
rolling_std = log_returns.rolling(window=window).std()
annualized_vol = rolling_std * np.sqrt(trading_days)
return annualized_vol
# 示例用法
# df['vol'] = historical_volatility(df['close'])
嗯,这里要注意:窗口大小的选择很关键。20个交易日大概对应一个月,60个交易日对应一个季度。我个人习惯用20天做短期波动率,60天做中期波动率。别问我为什么不用30天——试过,效果不如20天好。
三、隐含波动率:市场的「情绪温度计」
隐含波动率这个东西,很有意思。它不是算出来的,是「反推」出来的。
你想想看,期权的价格里已经包含了市场对未来波动率的预期。我们只需要把期权定价公式(比如Black-Scholes)倒过来解,就能得到隐含波动率。
我曾经犯过一个错误:直接用BS公式算隐含波动率,结果发现有些期权算出来是负数。当时我还以为是数据问题,查了半天才发现——原来是期权价格出现了套利机会,导致隐含波动率「扭曲」了。
Python实现如下:
from scipy.optimize import brentq
from scipy.stats import norm
def bs_price(S, K, T, r, sigma, option_type='call'):
"""Black-Scholes定价公式"""
d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
d2 = d1 - sigma*np.sqrt(T)
if option_type == 'call':
price = S*norm.cdf(d1) - K*np.exp(-r*T)*norm.cdf(d2)
else:
price = K*np.exp(-r*T)*norm.cdf(-d2) - S*norm.cdf(-d1)
return price
def implied_volatility(market_price, S, K, T, r, option_type='call'):
"""用二分法反推隐含波动率"""
objective = lambda sigma: bs_price(S, K, T, r, sigma, option_type) - market_price
try:
iv = brentq(objective, 0.01, 5.0)
except ValueError:
iv = np.nan
return iv
# 示例
# iv = implied_volatility(2.5, 100, 100, 30/365, 0.03)
你看,代码其实不复杂。但实际应用中,隐含波动率会形成「波动率微笑」或者「波动率偏斜」——不同行权价的期权,隐含波动率不一样。这个现象在2018年我做过的一个期权策略里特别明显,当时我利用偏斜做套利,年化收益做到了15%以上。
四、GARCH模型:预测未来的「水晶球」
历史波动率看过去,隐含波动率看现在,那GARCH模型呢?它试图预测未来。
GARCH模型的核心思想是:波动率不是常数,它会聚集。大涨之后往往跟着大跌,平静之后往往还是平静。这就是所谓的「波动率聚集效应」。
我记得第一次用GARCH模型时,调参调了整整一周。后来发现,其实GARCH(1,1)在大多数情况下就够用了。别一上来就搞GARCH(5,5),过拟合会让你哭的。
from arch import arch_model
def fit_garch(returns, p=1, q=1):
"""
拟合GARCH(p,q)模型
returns: 收益率序列
"""
model = arch_model(returns * 100, vol='Garch', p=p, q=q)
result = model.fit(disp='off')
return result
# 示例
# result = fit_garch(df['return'])
# forecast = result.forecast(horizon=5)
# predicted_vol = np.sqrt(forecast.variance.values[-1] / 100)
这里有个小技巧:GARCH模型预测的波动率,往往比实际波动率偏小。为什么?因为模型假设波动率是均值回归的,但现实中极端事件发生的频率比模型假设的要高。所以,我一般会在GARCH预测值上乘以一个1.2到1.5的系数,作为安全边际。
五、三种波动率的对比与选择
说了这么多,到底该用哪种?
| 波动率类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 历史波动率 | 计算简单,数据易得 | 滞后性,无法反映突变 | 长期趋势判断 |
| 隐含波动率 | 前瞻性,包含市场情绪 | 依赖期权市场,可能有偏 | 期权定价、事件驱动策略 |
| GARCH波动率 | 可预测,动态调整 | 模型风险,参数敏感 | 风险度量、组合优化 |
我个人习惯是:做风控时用GARCH,做交易时看隐含波动率,做回测时用历史波动率。三者结合,才能看到全貌。
六、知识体系总览
下面这张图,是我自己整理的知识框架。你可以把它当成这章的地图:
这张图把三种波动率的关系和各自的核心要点都串起来了。你可以把它当成一个速查表,做策略时对照着看。
七、实战中的几个坑
最后,分享几个我踩过的坑,希望能帮你省点时间:
- 数据频率不匹配:用日线数据算波动率,却用分钟线做交易,结果对不上。记住,波动率的时间尺度必须和你的交易频率一致。
- 忽略跳跃风险:GARCH模型假设波动率是连续的,但现实中经常出现跳空。我建议在模型中加入跳跃项,或者至少做一下跳跃检测。
- 过度依赖单一模型:没有哪个模型是万能的。我一般会同时跑3-4个模型,取它们的加权平均作为最终结果。
嗯,这章的内容就到这里。波动率建模是个大话题,一节课肯定讲不完。但掌握了这三种方法,你已经能应对大部分实战场景了。
公众号:蓝海资料掘金营,微信deep3321