
打开任何一只股票或者指数的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, start2020-01-01, end2024-12-31, auto_adjustTrue) 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, indexpd.date_range(2016-01-01, periodsT, freqB))我把收益率放大了100倍也就是用百分比单位。这样做的原因很实际收益率如果是以小数点形式存在数值太小在优化过程中容易触发数值精度问题换算成百分比之后很多估计器的收敛稳定性会好不少。3.3 从价格到收益率这一步别嫌麻烦拿到收盘价之后下一步是把价格序列转成收益率序列。我在日常工作中使用对数收益率居多方便跨资产比较也便于做多期收益率的加法复合。不过在GARCH建模这一环节简单百分比收益率和对数收益率的差别并不大你用 pct_change() 就够了。returns close.pct_change().dropna() * 100 returns.plot(titleAAPL Daily Return (%))画完图之后你会看到收益率序列确实存在明显的“簇状”波动形态。这个可视化是建模前最重要的体检之一我一直建议把这个图和K线图对照着看——价格上的阶段性急涨急跌在收益率图上会表现为先密集后稀疏的波动簇。3.4 平稳性检验先确认前提再动手GARCH模型是建立在平稳序列基础上的。对于收益率序列来说均值平稳几乎不成问题但为了严谨还是要验一下。ADF检验是首选工具from statsmodels.tsa.stattools import adfuller adf_stat, p_value, _, _, _, _ adfuller(returns.dropna()) print(fADF 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_dfTrue) print(lb_test) # Engles ARCH LM test arch_test het_arch(resid, nlags10) lm_stat, lm_pvalue, fvalue, f_pvalue arch_test print(fARCH 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, meanConstant, volGARCH, p1, q1, distt) result model.fit(dispoff) print(result.summary())fit() 方法内部会使用最大似然估计进行参数求解dispoff 只是关闭了迭代过程打印。如果你想观察优化过程也可以改成 dispon。这里我给一个提醒如果你看到类似“Optimization terminated successfully”的提示说明收敛正常如果看到收敛失败或异常警告优先检查数据里是否有极端异常值。4. 模型结果怎么读系数、残差和模型体检4.1 看懂summary表里的每一行模型拟合完成后summary表是目前最直接的报告。我拿一次实际拟合的输出来拆解常见字段字段含义典型观察mu均值方程的常数项代表收益率的平均漂移水平omegaGARCH常数项长期波动基准通常很小alpha[1]残差平方系数代表新信息冲击的强度beta[1]滞后条件方差系数代表波动的持续性nut分布自由度自由度越小尾部越厚我在看结果时第一眼永远是alpha和beta的大小然后是alphabeta。如果你算出来alphabeta离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_dfTrue) print(标准化残差Ljung-Box:, lb_resid) lb_resid_sq acorr_ljungbox(std_resid**2, lags[10], return_dfTrue) 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, volGARCH, p1, q1, distnormal) result_normal model_normal.fit(dispoff) model_t arch_model(returns, volGARCH, p1, q1, distt) result_t model_t.fit(dispoff)然后用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(horizon5) 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, dfnu) portfolio_value 1_000_000 var_99_1d portfolio_value * (mu z * forecast_vol) / 100 print(f99% 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模型看到标准化残差变得干净看到预测的波动率在现实里兑现都还是会有点兴奋。希望这篇文章能帮你迈出捕获波动率的第一步。