打开任何一只股票或者指数的K线图,你都会注意到一个非常明显的事实:暴跌的日子里,紧随其后的往往是更大的波动;而风平浪静的时候,市场甚至可以连续好几周都波澜不惊。这个现象在金融计量里有个专门的名字,叫“波动率聚集”。很多人看见了它,却很少有人能把它量化、预测,甚至用来计算风险。我入行做量化研究之前,也一直觉得波动率是个虚无缥缈的东西——直到我真正用上了GARCH模型,才意识到原来市场波动的“聚集密码”是可以被建模、被估算、被预测的。
这篇文章,我就以GARCH模型为核心,从现象讲到数学构造,再从数学构造讲到完整的Python实现。我不打算只给你一堆公式和代码,而是把每一步“为什么这样做”讲清楚。你会看到怎么判断数据里是不是真的存在波动聚集,怎么建立均值方程,怎么检验ARCH效应,怎么读懂模型输出,最后怎么把GARCH的波动率预测用到VaR计算里。适合的人群很明确:对量化风险感兴趣但没系统学过GARCH的金融从业者、统计背景想转金融的在校学生,以及所有想从“看图说话”升级到“建模说话”的交易员。
1. 先看现象:K线图上那场安静的“聚集”
1.1 波动聚集不是错觉,是数据里的硬规律
我第一次意识到波动聚集的存在,是在一次复盘行情时。那段时间大盘连续阴跌,日内振幅一度超过5%,身边的同事都在互相问“怎么了”;而几个月前市场平稳的时候,我甚至能隔一个小时再看一眼行情,价格几乎没什么变化。后来我明白,市场的脾气从来不是均匀分布的——它更像一个间歇性发作的病人,发作期剧烈颤抖,平稳期安安静静。
波动聚集在时间序列里表现为一种非常明显的“扎堆”模式:大波动之后跟着大波动,小波动之后跟着小波动。这个特征不是靠肉眼脑补出来的,而是有严格的统计证据。你把某只股票每日收益率的平方(或者绝对值)画出来,会看到明显的序列自相关。换句话说,今天的波动水平与昨天的波动水平显著相关,这种相关在滞后很多个交易日之后依然存在。
这就带来一个很实际的矛盾。经典金融模型(比如最早期的资本资产定价模型和期权定价用的几何布朗运动框架)假定收益率序列在不同时间点上是独立同分布的,方差恒定。但真实数据却明确告诉你:方差不是恒定的,它自己也在随时间演化。你要是还拿恒定的波动率去做风险预测,算出来的VaR在平静期可能过高,让资金闲置;在市场突变期又可能严重偏低,让风险敞口失控。这也是为什么我在和不少朋友交流时一直强调查方差的现实意义——它不属于“学术洁癖”,而是直接影响真金白银的决策质量。
1.2 从“波动聚集”到“条件异方差”
“条件异方差”这个词听起来吓人,拆开就很简单。先说“方差”,它衡量的是波动幅度;再说“异”,意思是这个方差不是常数,而是会随着时间变化的;最后加个“条件”——意思是今天这个方差的大小,取决于我们能观测到的过去信息。
我习惯拿天气来打比方。一个地区某个月的平均气温是相对固定的,这叫“无条件均值”;但你不能用同一个均方差去描述雨季和旱季的天气变化幅度,因为不同季节、不同天气系统下,气温的波动特征完全不同。“条件”就是把这些可观测的状态考虑进去。金融市场也一样:同样是下跌1%,发生在长期平静之后与发生在刚刚经历了一轮暴跌之后,对明天波动率的含义完全不一样。
更关键的是,这种“异方差”并不是随机的、无规律的,它具备自相关性。GARCH模型的核心思路,就是把条件方差本身也当作一个动态过程来建模——毕竟如果方差只是随机乱跳,那就没有预测意义了;但我在实际数据里看到的,是波动率的惯性极强:高波动期往往持续数周甚至数月,低波动期也常常维持很久。这种惯性,就是可以被模型捕捉、被预测的“聚集密码”。
2. 为什么老办法抓不住聚集?GARCH的两步破局
2.1 经典金融模型的“平静假设”
要想理解GARCH的贡献,得先知道它之前的模型为什么不行。传统时间序列模型(比如ARMA)处理的是均值方程,它假设序列可以由自身的滞后值和白噪声来解释。在这个框架里,白噪声ε_t的方差是常数σ²,模型预测的只是条件均值。但真实市场的残差远不是同方差的——我在做OLS回归时经常发现,模型的残差图在时间轴上呈现明显的“喇叭口”形态,回归诊断测试一跑,异方差问题几乎必然存在。
异方差的存在不只是违背了经典线性模型的假设,它直接导致标准误估计失真,进而让所有基于标准误的显著性检验、置信区间都变得不可靠。更让人头疼的是,异方差里往往藏着信息:我对比过不少金融产品的残差,高波动期的残差不仅更大,而且自相关更强。如果模型只把这些当作“白噪声的方差变大了”,那等于主动放弃了预测波动率的机会。
2.2 ARCH:给波动率插上自回归的天线
1982年,Robert Engle提出了ARCH模型——自回归条件异方差模型。它的思路很直接:今天的条件方差,可以用过去若干期的残差平方来预测。数学上写成:
σ²_t = ω + α₁ε²_t₋₁ + α₂ε²_t₋₂ + … + α_ε²_t₋m
用大白话说,如果过去的残差很大,那么今天的方差就会跟着变大;过去的残差小,今天波动的基准也就低。这就把“大波动之后跟着大波动”的直觉直接翻译成了模型语言。ARCH模型解决了条件异方差建模的“从无到有”问题,但它有一个明显的短板:为了充分捕捉波动率的长期自相关,需要引入很多阶滞后项。滞后项一多,参数估计的难度和不确定性直线上涨,而且容易出现多重共线性的问题。在我自己早期的实验中,用ARCH(12)甚至更高阶去拟合真实股票数据,拟合效果并不好,系数估计的稳定性也差。
2.3 GARCH(1,1):让旧波动继续“黏”下去
1986年,Bollerslev在ARCH基础上做了一次关键升级,提出了GARCH模型——广义自回归条件异方差模型。GARCH(1,1)只有一个滞后残差平方项和一个滞后条件方差项,形式极其简洁:
σ²_t = ω + α·ε²_t₋₁ + β·σ²_t₋₁
这个式子的绝妙之处在于,“昨天的条件方差”σ²_t₋₁这一项浓缩了之前所有历史波动信息。我们不必像ARCH那样手动加入几十个滞后项,因为σ²_t₋₁本身已经包含了σ²_t₋₂、ε²_t₋₂等信息,信息一层一层往上传导,一条“记忆链条”就自然形成了。这也解释了为什么GARCH在实战中几乎总是以低阶形式出现——我试过GARCH(1,1)、GARCH(2,1)、GARCH(1,2)甚至GARCH(2,2),多数情况下GARCH(1,1)的信息准则已经最优,再叠加滞后项往往是模型拟合过度,而不是真正的精度提升。
2.4 三个参数各自的“人设”
整个GARCH(1,1)只有三个参数,但它们各有明确的语义:
| 参数 | 取值范围 | 作用与含义 |
|---|---|---|
| ω | ω > 0 | 长期基准波动水平,可以理解为无条件方差的基线 |
| α | 0 ≤ α < 1 | 残差平方的系数,衡量“新信息”对波动的即时冲击强度 |
| β | 0 ≤ β < 1 | 滞后条件方差的系数,衡量波动的“惯性”或“黏性” |
实操中我最关心的是α和β之和。α+β代表波动冲击的持续性,也就是一个冲击发生后,波动率以多快的速度衰减。如果α+β接近1,比如0.97甚至0.99,说明波动率的均值回归速度极慢,一次冲击的影响可以持续很久——这正是高波动聚集持续不退的量化表达。统计上要求α+β<1以保证模型平稳,而我见过的大多数真实金融数据里,这个和通常在0.9以上。
为了让你更直观地感受参数对模型行为的影响,我用模拟数据做过一次对比实验。设定ω=0.05、β=0.85保持不变,把α从0.05调大到0.15,结果很明显:α更大时,单次冲击能让波动率跳得更高,峰值的尖刺更突出;而如果把β从0.8提升到0.95,高波动的衰减速度肉眼可见地变慢,波动率在陡峭爬升后迟迟不肯回归长期基准。这也让我在实际建模时养成了一个习惯——看到估计结果中α+β超过0.98,我会意识到自己面对的可能是一段“波动记忆超长”的行情,在预测时要对持续高波动期保持警惕。
3. 亲手建一个GARCH模型:从行情数据到参数估计
3.1 环境准备:两个必要的Python包
实际操作层面,建议使用 Python 生态,环境里至少需要 pandas、numpy、scipy、statsmodels 和 arch 这几个库。其中 arch 库是 Kevin Sheppard 维护的专门做波动率建模的包,API 对 GARCH 系列的支持很完善。安装命令很简单:
pip install arch statsmodels pandas numpy yfinanceyfinance 用来拉行情数据,但它依赖网络且偶尔不太稳定。万一你拉数据失败,别急着放弃,我在 3.2 节末尾给了一段模拟数据生成代码,同样可以把整条建模链路跑通。
3.2 数据获取:真实数据怎么拉,拉不到就用模拟数据
先用 yfinance 拉一段真实行情。下面这个例子我取的是苹果公司股票的日线收盘价,时间范围选在 2020 年到 2024 年,这个区间正好包含了波动极其剧烈的一段行情,非常适合演示波动聚集。
import yfinance as yf ticker = "AAPL" data = yf.download(ticker, start="2020-01-01", end="2024-12-31", auto_adjust=True) close = data["Close"].dropna() close.head()如果你第一次运行就遇到网络超时或者 OHLC 数据为空的报错,不必纠结——yfinance 的接口确实存在偶发失效的情况。一个稳妥的备选方案是直接用模拟数据。下面这段代码用 GARCH(1,1) 的真实生成过程来生成序列,它本身就带有波动聚集特征,足够让后续建模流程完整走一遍:
import numpy as np import pandas as pd np.random.seed(2024) T = 2000 omega, alpha, beta = 0.05, 0.10, 0.85 sigma2 = np.zeros(T) eps = np.zeros(T) for t in range(1, T): sigma2[t] = omega + alpha * eps[t-1]**2 + beta * sigma2[t-1] eps[t] = np.random.normal(0, np.sqrt(sigma2[t])) returns = pd.Series(eps * 100, index=pd.date_range("2016-01-01", periods=T, freq="B"))我把收益率放大了100倍,也就是用百分比单位。这样做的原因很实际:收益率如果是以小数点形式存在,数值太小,在优化过程中容易触发数值精度问题;换算成百分比之后,很多估计器的收敛稳定性会好不少。
3.3 从价格到收益率:这一步别嫌麻烦
拿到收盘价之后,下一步是把价格序列转成收益率序列。我在日常工作中使用对数收益率居多,方便跨资产比较,也便于做多期收益率的加法复合。不过在GARCH建模这一环节,简单百分比收益率和对数收益率的差别并不大,你用 pct_change() 就够了。
returns = close.pct_change().dropna() * 100 returns.plot(title="AAPL Daily Return (%)")画完图之后你会看到,收益率序列确实存在明显的“簇状”波动形态。这个可视化是建模前最重要的体检之一,我一直建议把这个图和K线图对照着看——价格上的阶段性急涨急跌,在收益率图上会表现为先密集后稀疏的波动簇。
3.4 平稳性检验:先确认前提再动手
GARCH模型是建立在平稳序列基础上的。对于收益率序列来说,均值平稳几乎不成问题,但为了严谨,还是要验一下。ADF检验是首选工具:
from statsmodels.tsa.stattools import adfuller adf_stat, p_value, _, _, _, _ = adfuller(returns.dropna()) print(f"ADF statistic: {adf_stat:.4f}, p-value: {p_value:.4f}")一般只要p_value小于0.05,就表明序列不存在单位根,可以用均值方程加GARCH误差结构继续建模。如果序列不平稳,那第一步不是建GARCH,而是回到数据清洗或者做差分——这一点我没有捷径,纯粹是踩过坑后养成的习惯。
3.5 ARCH效应检验:没有“聚集”就别建GARCH
这是油管上很多教程容易跳过的一步,但我认为这恰恰是判断模型是否适用的关键前提。ARCH效应的检验思路是:如果条件异方差存在,那么均值方程残差的平方序列应该存在自相关。常用方法有两个——Ljung-Box检验(对残差平方)和Engle’s ARCH LM检验。我在实际中一般两个都看一眼,重点看后者。
首先对收益率序列拟合一个最简单的均值模型。默认情况下均值方程可以设成常数均值,但如果你发现收益率的ACF和PACF在滞后几阶有明显截尾,可以先拟合一个AR(1)甚至ARMA(1,1)来提取均值动态,再拿残差做ARCH检验。
import statsmodels.api as sm from statsmodels.stats.diagnostic import acorr_ljungbox, het_arch # 拟合常数均值模型 model_mean = sm.OLS(returns, sm.add_constant(np.ones_like(returns))).fit() resid = model_mean.resid # Ljung-Box on squared residuals lb_test = acorr_ljungbox(resid**2, lags=[10], return_df=True) print(lb_test) # Engle's ARCH LM test arch_test = het_arch(resid, nlags=10) lm_stat, lm_pvalue, fvalue, f_pvalue = arch_test print(f"ARCH LM p-value: {lm_pvalue:.4f}")如果p值小于0.05,说明残差中还存在显著的波动聚集效应,这时候用GARCH才有意义。如果p值很大,那说明当前数据几乎没有ARCH效应,建GARCH模型纯属画蛇添足。我在处理日频股票收益率时,几乎没遇到过检验不显著的情况;但如果是月频数据,或者经过了极端值清洗后的低波动产品数据,ARCH效应弱化就很常见。
3.6 定阶与估计:跑出第一份模型报告
确认存在ARCH效应之后,终于可以正式估计GARCH模型了。arch库的接口非常贴近论文里的标准写法。以GARCH(1,1)为例,配一个t分布假设:
from arch import arch_model model = arch_model(returns, mean='Constant', vol='GARCH', p=1, q=1, dist='t') result = model.fit(disp='off') print(result.summary())fit() 方法内部会使用最大似然估计进行参数求解,disp='off' 只是关闭了迭代过程打印。如果你想观察优化过程,也可以改成 disp='on'。这里我给一个提醒:如果你看到类似“Optimization terminated successfully”的提示,说明收敛正常;如果看到收敛失败或异常警告,优先检查数据里是否有极端异常值。
4. 模型结果怎么读:系数、残差和模型体检
4.1 看懂summary表里的每一行
模型拟合完成后,summary表是目前最直接的报告。我拿一次实际拟合的输出来拆解常见字段:
| 字段 | 含义 | 典型观察 |
|---|---|---|
| mu | 均值方程的常数项 | 代表收益率的平均漂移水平 |
| omega | GARCH常数项 | 长期波动基准,通常很小 |
| alpha[1] | 残差平方系数 | 代表新信息冲击的强度 |
| beta[1] | 滞后条件方差系数 | 代表波动的持续性 |
| nu | t分布自由度 | 自由度越小,尾部越厚 |
我在看结果时,第一眼永远是alpha和beta的大小,然后是alpha+beta。如果你算出来alpha+beta离1非常近(比如0.99),说明这个市场的波动惯性极强。另一个非常实用的隐藏指标是无条件方差,计算公式是 ω/(1-α-β)。这个值代表长期平均方差水平,把它开根号后你会发现,它基本等于这段时间收益率的样本标准差,这是一个很好的“模型是否合理”的交叉验证。
下面是一份GARCH(1,1)估计结果的解读代码示例:
omega = result.params['omega'] alpha = result.params['alpha[1]'] beta = result.params['beta[1]'] long_run_var = omega / (1 - alpha - beta) long_run_vol = np.sqrt(long_run_var) print(f"长期年化波动率约: {long_run_vol * np.sqrt(252):.2f}%")4.2 残差体检:模型抓干净了吗
GARCH模型把条件方差建模出来了,但模型是否充分捕捉了波动聚集,不能只看系数,还要考察标准化残差。标准化残差的定义是:
std_resid = ε_t / σ_t
如果模型正确,那标准化残差应该是近似独立同分布的序列,不再残留明显的自相关和ARCH效应。我在实际工作里会做两件事:一是对标准化残差本身做Ljung-Box检验,看均值方程是否充分;二是对标准化残差的平方做Ljung-Box检验或ARCH LM检验,看方差方程是否充分。
std_resid = result.resid / result.conditional_volatility lb_resid = acorr_ljungbox(std_resid, lags=[10], return_df=True) print("标准化残差Ljung-Box:", lb_resid) lb_resid_sq = acorr_ljungbox(std_resid**2, lags=[10], return_df=True) print("标准化残差平方Ljung-Box:", lb_resid_sq)这个环节的潜在反转在于:不少新手看到第一次GARCH(1,1)结果不错,就急着拿去用,结果一查残差发现还有自相关。这意味着模型的动态结构没有完全捕捉数据特征,可能均值方程需要升级,或者方差方程需要增加滞后项。但也有一种很常见的情况:标准化残差平方的Q统计量在边缘显著,但不是结构性缺失,而是几个极端异常值在捣乱——这时候我一般会把样本期内的极端事件标注出来,再看看检验结果是否稳定。
4.3 分布选择与信息准则:对付“肥尾”的三板斧
另一个必须讨论的是残差分布的设定。默认情况下很多人直接用正态分布,但我强烈建议你至少试一下t分布和偏斜t分布。金融收益率的典型特征是“肥尾”,也就是极端收益出现的概率远高于正态分布模型预测的水平。在99%置信度下,正态假设下的分位数大约只有2.33个标准差,而真实数据里超过4个标准差的情形都时有发生。
实现上很简单,把dist参数从'normal'换成't'或者'skewt'即可:
model_normal = arch_model(returns, vol='GARCH', p=1, q=1, dist='normal') result_normal = model_normal.fit(disp='off') model_t = arch_model(returns, vol='GARCH', p=1, q=1, dist='t') result_t = model_t.fit(disp='off')然后用AIC或BIC做比较,选数值小的模型。我自己的经验是:股票日收益率数据用t分布几乎是标配,估计出的自由度常常在4到8之间,这是一个很明显的厚尾信号。如果你用正态分布估计出的VaR经常在事后被击穿,那大概率不是GARCH模型的问题,而是尾部假设偏乐观了。
另外,信息准则也可以用来自动定阶。GARCH(2,1)、GARCH(1,2)这些更高阶的模型看起来更灵活,但在多数情况下,它们带来的AIC下降微乎其微,而参数估计的不确定性却明显增加。我现在的习惯是:默认从GARCH(1,1)开始,只有在对标准化残差平方做检验仍然发现显著自相关时,才会考虑提高p或q。
5. 从“建出来”到“用起来”:波动率预测、VaR计算与实战边界
5.1 预测未来5天的波动率
模型建好了,最终目的是预测。arch库的forecast方法可以直接给出未来多个时间点的条件方差预测。我用的是GARCH(1,1),多步预测的逻辑并不复杂:每往未来推一步,条件方差会以γ = α+β的比例向长期无条件方差回归。也就是说,未来第5天的预测方差已经比第1天更接近 ω/(1-α-β) 了。这个特性在风险管理中意义重大——它不仅告诉你未来波动会怎样,还告诉你波动的回归速度。
forecasts = result.forecast(horizon=5) variance_forecast = forecasts.variance.iloc[-1] vol_forecast = np.sqrt(variance_forecast) print(vol_forecast)你会看到,如果当前条件波动率很高,那么预测曲线是向长期均值下降的;如果当前波动率很低,预测曲线则是向上的。这个“向均值回归”的过程,是金融波动率最重要的动态特征之一。
5.2 把GARCH方差变成VaR数字
做风险的人最常用的一个落地场景,就是用GARCH预测方差来计算VaR。假设你持有市值为V的股票多头头寸,要计算未来1天、99%置信度下的最大预期损失。在t分布假设下,VaR的公式是:
VaR = V × (μ + t_α⁻¹ × σ_t)
其中t_α⁻¹是t分布在α分位数上的值,σ_t是GARCH预测的条件波动率。下面这段代码展示了全流程:
from scipy.stats import t as t_dist nu = result.params['nu'] mu = result.params['mu'] forecast_vol = vol_forecast.iloc[0] z = t_dist.ppf(0.01, df=nu) portfolio_value = 1_000_000 var_99_1d = portfolio_value * (mu + z * forecast_vol) / 100 print(f"99% 1日VaR: {abs(var_99_1d):,.2f} 元")注意收益率之前乘以100了,所以要把模型算出的值再除以100,才能回到百分比口径。我见过不止一个同事在这里栽跟头,最终算出来的VaR比正确结果大了100倍——当时直接吓出一身冷汗。如果你用的是条件波动率的预测序列,而不是历史样本标准差,那么这已经比传统历史模拟法更贴近当前的波动环境了,尤其是市场刚刚经历剧变时,GARCH的VaR能更快地反映风险上升。
5.3 实务中最容易翻车的三件事
第一,汇率的波动聚集和股票不同。股票在暴跌时波动率飙升存在明显的“杠杆效应”——下跌对波动的冲击比上涨更大,而汇率类资产往往没有这么强的非对称性。GARCH(1,1)的对称性假设,让它无法区分好消息和坏消息对波动的不同影响。如果你面对的是股票指数,强烈建议尝试GJR-GARCH或EGARCH这类非对称模型,它们会多出一个描述不对称冲击的系数。
第二,样本期间的极端事件可能会破坏模型估计的稳定性。如果样本里包含了2008年金融危机或者2020年3月的极端行情,那一次巨大冲击会大幅拉高无条件方差估计,导致预测出的波动率在平静期也偏高。解决方式有联网,一个是做滚动窗口估计,另一个是引入虚拟变量或者结构性突变点。我以前处理A股数据时,经常会把2015年、2016年的极端行情单独标记出来,效果比直接全部纳入样本要好很多。
第三,GARCH预测的是条件波动率,不是你交易的方向。这个模型再厉害,也没有信息告诉你未来股价会涨还是跌。如果你把GARCH预测高波动直接翻译成“看空”,那是误用。在实务中,GARCH的作用更多是调节仓位:波动率上升时收缩仓位,波动率下降时放大仓位,而不是做方向的择时信号。
5.4 GARCH家族的扩展方向:GJR、EGARCH与混合分布
作为结尾,我想把话题稍微往前推一步。你在读完这篇文章后,如果想把GARCH用到更复杂的实战场景,下面这几个扩展方向值得关注。
GJR-GARCH(也叫Threshold GARCH)通过引入一个指示函数,允许负面冲击比正面冲击对波动的影响更大,这个特征在股票市场里非常普遍。EGARCH则更进一步,它建模的是对数方差,好处是不需要对参数加非负约束,也能自然处理非对称效应。如果你的目标是期权定价或者风险对冲,另一个常用的做法是把GARCH和跳跃扩散过程结合,在均值方程里加入泊松跳跃项,用来捕捉突发信息带来的跳空。
还有一个更现代的方向是混合频率GARCH,典型代表是GARCH-MIDAS。它把短期波动率与低频的宏观经济状态变量结合起来,让模型既保留日频的波动聚集特征,又能吸收季度级别的宏观基本面信息。我在处理国债收益率和宏观风险指标的关系时用过这种模型,虽然估计过程比GARCH(1,1)复杂很多,但解释力提升明显。
说句实在话,GARCH不是万能的,它无法预测每一次市场突变,也无法告诉你波动率什么时候会突然拐头。但它依然是金融风险建模工具箱里最可靠、最易上手的一把“捕手”——它的价值在于,把“波动会聚集”这个模糊的市场直觉,变成了一套可以估计、可以检验、可以预测的严谨框架。对我个人而言,每次跑完GARCH模型,看到标准化残差变得干净,看到预测的波动率在现实里兑现,都还是会有点兴奋。希望这篇文章,能帮你迈出捕获波动率的第一步。