波动率建模实战:历史波动率、隐含波动率、GARCH模型在Python中的实现

波动率,说白了就是市场的心跳。

做量化投资这么多年,我越来越觉得,不懂波动率,就像开船不看天气预报。你可能顺风顺水一阵子,但一个浪打过来,连人带船都得翻。今天咱们就来聊聊波动率建模这件事,我会把我在实战中踩过的坑、总结的经验,都揉进这章内容里。

一、波动率到底是什么?

先别急着上代码,咱们得先搞清楚一个事儿:波动率到底在衡量什么?

很多人以为波动率就是「涨跌幅度」,其实不准确。波动率衡量的是资产价格的不确定性,或者说,是价格变动的剧烈程度。你想想看,一只股票每天涨跌0.5%,和一只股票每天涨跌5%,哪个更「波动」?显然是后者。

我个人习惯把波动率分成三类:

  • 历史波动率:基于过去数据算出来的,反映「已经发生」的波动。
  • 隐含波动率:从期权价格反推出来的,反映「市场预期」的波动。
  • 模型波动率:用GARCH这类模型预测出来的,反映「未来可能」的波动。

这三者各有各的用处,也各有各的坑。咱们一个一个来拆解。

二、历史波动率:最简单,也最容易犯错

历史波动率的计算,其实就三步:

  1. 取对数收益率
  2. 计算标准差
  3. 年化处理

听起来很简单对吧?但我在项目中遇到过一个问题:很多人直接用收盘价算收益率,结果发现波动率曲线跟实际走势对不上。为什么?因为收盘价有跳空,尤其是隔夜跳空,会严重扭曲波动率的计算。

我的建议:用日内高频数据算波动率,比用日收盘价靠谱得多。如果只有日线数据,至少要用「调整收盘价」而不是「收盘价」。

来看代码实现:

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模型对输入数据很敏感。我建议先用ADF检验确保收益率序列是平稳的,否则模型会给出荒谬的结果。

这里有个小技巧:GARCH模型预测的波动率,往往比实际波动率偏小。为什么?因为模型假设波动率是均值回归的,但现实中极端事件发生的频率比模型假设的要高。所以,我一般会在GARCH预测值上乘以一个1.2到1.5的系数,作为安全边际。

五、三种波动率的对比与选择

说了这么多,到底该用哪种?

波动率类型 优点 缺点 适用场景
历史波动率 计算简单,数据易得 滞后性,无法反映突变 长期趋势判断
隐含波动率 前瞻性,包含市场情绪 依赖期权市场,可能有偏 期权定价、事件驱动策略
GARCH波动率 可预测,动态调整 模型风险,参数敏感 风险度量、组合优化

我个人习惯是:做风控时用GARCH,做交易时看隐含波动率,做回测时用历史波动率。三者结合,才能看到全貌。

六、知识体系总览

下面这张图,是我自己整理的知识框架。你可以把它当成这章的地图:

波动率建模 历史波动率 隐含波动率 GARCH模型 对数收益率 滚动标准差 BS公式反推 波动率微笑 参数估计 波动率预测 三者结合:风控用GARCH,交易看隐含,回测用历史

这张图把三种波动率的关系和各自的核心要点都串起来了。你可以把它当成一个速查表,做策略时对照着看。

七、实战中的几个坑

最后,分享几个我踩过的坑,希望能帮你省点时间:

  • 数据频率不匹配:用日线数据算波动率,却用分钟线做交易,结果对不上。记住,波动率的时间尺度必须和你的交易频率一致。
  • 忽略跳跃风险:GARCH模型假设波动率是连续的,但现实中经常出现跳空。我建议在模型中加入跳跃项,或者至少做一下跳跃检测。
  • 过度依赖单一模型:没有哪个模型是万能的。我一般会同时跑3-4个模型,取它们的加权平均作为最终结果。

嗯,这章的内容就到这里。波动率建模是个大话题,一节课肯定讲不完。但掌握了这三种方法,你已经能应对大部分实战场景了。


公众号:蓝海资料掘金营,微信deep3321