R语言实现ARMA-GARCH模型:金融时间序列波动率建模与风险预测实战
2026/9/16 3:17:46 网站建设 项目流程

1. 为什么单靠ARMA跑金融序列总是"差点意思"

做时间序列的人基本都经历过这个阶段:拿到一堆收益率数据,先画ACF、PACF图,定阶,然后高高兴兴跑出一个ARMA模型,一看系数都显著,Ljung-Box检验也通过,以为自己已经把序列里的信息榨干了。结果一旦进入预测环节,预测区间窄得离谱,实际值三天两头落在区间外面,尤其是市场剧烈波动的时候,简直惨不忍睹。

问题出在哪?ARMA模型描述的是条件均值,它假设残差的方差是恒定不变的。这个假设放在气温、水位这类物理过程上勉强说得过去,但放在金融收益率上就完全站不住脚了。你去看任何一只股票或者指数的日收益率序列,都会观察到明显的"波动率聚集"现象:平静期持续一段时间,然后突然进入剧烈震荡期,振幅一两天内翻好几倍,再过一阵又归于平静。这种方差的时变性不是噪声,而是序列本身的结构特征。

我刚开始做量化的时候也犯过这个错误,用ARMA(2,2)拟合沪深300的日收益率,拟合效果看着不错,R²也有几个百分点,但一做VaR回测就露馅了。后来才意识到,金融收益率序列的均值结构其实很弱,真正有信息量的是它的方差结构。换句话说,预测"明天涨还是跌"远不如预测"明天波动有多大"靠谱。这就引出了ARMA-GARCH组合模型的真正价值:ARMA捕捉均值方程里的线性依赖,GARCH捕捉方差方程里的条件异方差,两者各管一段,配合起来才完整。

这里有个非常重要的认知转变:不要指望ARMA-GARCH能精准预测收益率的涨跌方向,它的强项是刻画波动率的动态演变。你做资产配置、风险管理、期权定价,核心输入都是波动率,不是收益率本身。理解了这一点,你就明白为什么这个模型在金融领域如此普及——它不是用来"猜涨跌"的,而是用来"度量不确定性"的。

需要强调一下,ARMA-GARCH不是唯一的波动率建模方案,后面出现的EGARCH、GJR-GARCH、Realized GARCH都是在解决GARCH的某些具体短板。但如果你想系统性地理解条件异方差建模,从标准GARCH入手是最扎实的路径。R语言生态里这套工具已经非常成熟,这篇文章我就用真实数据和完整代码,把从数据准备到模型诊断再到预测的整个流程走一遍。

2. R环境准备与数据预处理:第一步没做好,后面全是白费

2.1 工具链装齐:R、RStudio与核心包

R语言本身只是一个解释器,真正好用的是它周围的生态。新手建议直接装RStudio作为IDE,调试、查看变量、画图都方便得多。安装R的时候注意选择与你系统匹配的版本,Windows用户直接下载安装包,macOS用户可以用Homebrew,Linux用户根据自己的发行版用apt或yum都行。装完以后打开RStudio,在控制台里执行以下命令把需要的包一次性装好:

install.packages(c("xts", "PerformanceAnalytics", "tseries", "forecast", "rugarch", "FinTS"))

这些包的分工很明确:

  • xts:处理金融时间序列数据,索引管理、子集截取都靠它。
  • PerformanceAnalytics:计算收益率序列的统计特征,画图也方便。
  • tseries:提供ADF检验等平稳性检验函数。
  • forecast:提供auto.arima等自动定阶工具,虽然最终不一定用它的结果,但可以做个参考。
  • rugarch:本文的核心工具,ARMA-GARCH模型的估计、诊断、预测全在它里面。
  • FinTS:提供ARCH效应检验所需的函数。

2.2 数据获取与收益率计算:不要直接用价格建模

数据来源我推荐两个路径。如果你有Wind、Choice这类商用终端,直接导出日线收盘价就行。如果没有,用quantmod包拉取免费数据也很方便:

library(quantmod) getSymbols("AAPL", from = "2018-01-01", to = "2023-12-31") price <- Cl(AAPL) # 提取收盘价

这里有个很容易被忽略的细节:我们建模的对象是收益率,不是价格。原因有两个。第一,绝大多数金融资产的价格序列都是非平稳的,直接拿价格拟合ARMA会产生伪回归问题;第二,投资者的实际收益来自价格变动率,而不是绝对价格水平。所以拿到价格之后,第一步永远是转换成收益率。

常用的收益率有两种:简单收益率和对数收益率。金融建模里几乎无脑选对数收益率,它的优势在于时间可加性——多期对数收益率等于单期对数收益率之和,这在后续计算多步预测和累计收益的时候非常方便。计算方式如下:

library(PerformanceAnalytics) ret <- CalculateReturns(price, method = "log") ret <- na.omit(ret)

CalculateReturns会把第一天的收益率设为NA,因为前一天没有价格数据,直接na.omit是常规操作。拿到收益率之后,先画一张图看看整体形态,R里面一行代码就能出图:

plot(ret, main = "AAPL Log Returns")

这时候你基本能看到波动率聚集的直观形态。比如2018年底到2019年初那段时间,苹果公司因为业绩预期调整经历过一波剧烈波动,然后在2020年疫情冲击下又出现极端波动,再往后就是相对平稳的上涨通道。这种"时而平静时而汹涌"的形态,就是GARCH模型要捕捉的东西。

2.3 数据体检:缺失值与异常值的处理顺序

金融数据很少有干净的时候。停牌、涨跌停、数据源传输中断都会造成缺失值。处理缺失值有个顺序问题:先看缺失比例,再决定用插补还是直接剔除。缺失比例在5%以下,通常直接剔掉不会有太大影响;如果超过5%,就要考虑是不是有系统性停牌,这时候再用前向填充或插值,但要注意插值方法对收益率序列的影响——金融序列里用线性插值会人为平滑掉波动,反而引入偏差。

我自己的习惯是,在做任何建模之前先跑一遍汇总统计,用summary()看看各分位点是否合理,再用table(is.na(ret))检查缺失情况。顺便检查一下有没有极端异常值,比如超过5个标准差的收益。2020年3月美股连续熔断那阵子,标普500单日跌幅超过10%,这在正态分布假设下几乎是不可能事件,但它确实发生了。这类极端值要不要剔除、要不要单独处理,取决于你的研究对象。如果是系统性风险事件,保留它反而更真实;如果是数据录入错误导致的异常,则必须清洗。

数据处理这块多说一句:很多人喜欢在Excel里手动处理完再导入R,我强烈不建议这么做。用代码处理的好处是整个过程可复现、可审计,你改一个参数全部重跑一遍就行了。手动处理的坑在于,一旦数据源更新或者参数调整,你就得重新手工做一遍,而且很容易出错。保持"原始数据只读、处理过程全代码"的习惯,是做时间序列分析的基本职业素养。

3. 建模前的序列体检:平稳性、自相关与ARCH效应的三层检查

3.1 平稳性检验:ADF与KPSS配合使用

在拟合ARMA之前,先确认你的收益率序列是平稳的。这里有个常见的混淆点:对价格序列来说,平稳性检验确实关键;但收益率序列通常已经是平稳的,因为对数收益率本质上就是对价格做了差分,消除了单位根。不过,谨慎一点总没错,尤其是某些特殊资产(比如加密货币),其收益率序列仍然可能表现出非平稳特征。

tseries包里的adf.test是最常用的单位根检验,原假设是序列存在单位根,p值小于0.05就拒绝原假设,认为序列平稳:

library(tseries) adf.test(ret, alternative = "stationary")

注意ADF检验对滞后阶数很敏感,默认滞后阶数是基于样本量自动确定的,一般情况下够用。如果你发现ADF检验结果在生产环境中不稳定,可以手动调整k参数多试几个值,看看结论是否一致。

我还习惯同时跑一个KPSS检验,它的原假设与ADF相反——原假设是序列平稳。两个检验配合使用的逻辑是:ADF检验接受了平稳性,KPSS检验也接受了平稳性,那基本可以肯定序列是平稳的;如果两者结论矛盾,就需要进一步检查数据是否存在结构性突变。

3.2 自相关结构:ACF、PACF与Ljung-Box检验

平稳性确认之后,看自相关结构。forecast包里的AcfPacf函数画图比基础R的acfpacf更好用,它把置信区间也画出来了:

library(forecast) Acf(ret, main = "ACF of Returns") Pacf(ret, main = "PACF of Returns")

对于纯随机序列,ACF和PACF应该都在置信区间内跳动。金融收益率序列通常表现出微弱的短期自相关,比如滞后期为1的ACF显著,后面基本截断,这说明均值方程可能需要一个MA(1)或AR(1)项。注意观察ACF图拖尾还是截尾:ACF拖尾、PACF截尾,用AR模型;ACF截尾、PACF拖尾,用MA模型;两者都拖尾,用ARMA模型。

光看图不够,还要做Ljung-Box检验来量化判断残差是否是白噪声。这里有个重要区分:如果拿原始序列的收益率直接做Ljung-Box检验,看到自相关显著是很正常的,这恰恰说明均值方程是有必要存在的;建模完成以后,再对残差做Ljung-Box检验,这时候期望看到的是"不显著"——说明均值方程已经把自相关结构提取干净了。

Box.test(ret, lag = 10, type = "Ljung-Box")

3.3 ARCH效应检验:能不能用GARCH,这一步说了算

接下来是关键中的关键——ARCH效应检验。如果收益率序列的残差不存在条件异方差性,那建GARCH模型就是多余操作。检验方法常用两种:一种是FinTS包里的ArchTest函数,直接对收益率序列做ARCH-LM检验;另一种是先用ARMA拟合得到残差,再对残差的平方做Ljung-Box检验。

library(FinTS) ArchTest(ret, lags = 12)

ArchTest的原假设是"不存在ARCH效应",p值小于0.05说明存在显著的ARCH效应,可以进入GARCH建模。如果p值很大,说明序列的方差是恒定的,这时候老老实实用ARMA或者简单的线性模型就行,强行上GARCH只会增加复杂度并导致过拟合。

从统计检验到实际判断,这里要补一个直觉层面的说明。ARCH效应本质上就是"方差的自相关",即大波动后面跟着大波动,小波动后面跟着小波动。你可以计算残差平方序列的自相关函数,如果平方序列的ACF显著不为零,说明确实存在波动率聚集现象,ARCH效应检验就顺理成章了。

回头整理一下三层检查的逻辑:先确认平稳性(确定可以用ARMA类模型),再做白噪声检验(确定均值方程是否必要),再检验ARCH效应(确定方差方程是否必要)。这三步就像医生问诊——先确认有没有病,再确认是什么病,最后确认用什么药。不做检查直接上模型,最后残差里藏着大量未提取的信息,模型诊断必然失败。

4. ARMA-GARCH联合建模实操:rugarch包的完整流程

4.1 均值方程定阶:从简单模型开始,别一上来就贪高阶

ARMA-GARCH建模的第一步是给均值方程定阶。这里有个容易犯的错误:用遍历所有ARMA(p,q)组合的方式,按AIC最小值选一个p和q都很大的模型。高阶ARMA会带来两个问题——参数过多导致过拟合,以及均值方程和方差方程的参数可能互相干扰,影响GARCH项的估计稳定性。

我更推荐的做法是,先画ACF和PACF图定一个大致的阶数范围,用auto.arima作为参考但不要盲从。比如ACF在滞后1期显著,PACF在滞后1期显著,那ARMA(1,1)就是合理的起点。跑几个附近的组合,比较AIC,然后看残差是否还有自相关。一个经验规则:金融日收益率序列的均值方程阶数很少超过ARMA(2,2),如果AIC告诉你需要更高阶,多半是数据里有异常值或结构性变化没处理干净。

假设我们初步定阶为ARMA(1,1),进入rugarch的规范设定:

library(rugarch) spec_1 <- ugarchspec( mean.model = list(armaOrder = c(1, 1)), variance.model = list(model = "sGARCH"), distribution.model = "std" ) fit_1 <- ugarchfit(spec = spec_1, data = ret) fit_1

这里ugarchspec是设定模型规范,ugarchfit执行拟合。distribution.model = "std"表示残差用学生t分布而不是正态分布。为什么不用正态?金融收益率的一个典型特征是厚尾分布,正态分布低估了极端值出现的概率。换成t分布之后,模型会多估一个自由度参数,能更好地拟合尾部特征。

看拟合输出的时候,重点关注几组数字:

  • mu:均值方程的截距,通常不显著或者很小,这符合金融收益率均值弱可预测的直觉。
  • ar1ma1:均值方程的自回归和移动平均系数。
  • omega:GARCH方程中的常数项,代表基准波动水平。
  • alpha1:ARCH项系数,衡量当前冲击(残差)对波动率的影响,也就是"新信息"的权重。
  • beta1:GARCH项系数,衡量历史波动率的持续性,也就是旧信息的记忆程度。

这里的alpha1 + beta1总和是衡量波动率持续性的核心指标。如果接近1(比如0.98),说明波动率冲击衰减很慢,今天的冲击会影响未来很长一段时间的波动率水平。这在实际中对应着市场"高波动期持续较长"的典型特征。反过来,如果这个和远小于1,说明波动率的记忆很短暂,模型倾向于快速回到长期均值。

4.2 方差方程设定:标准GARCH之外的选择

rugarch支持多种波动率模型,model = "sGARCH"是标准GARCH(1,1)。如果你观察到的数据中存在明显的杠杆效应——即利空消息比利好消息对波动率的冲击更大——可以考虑用GJR-GARCH或者EGARCH。

杠杆效应在股票市场非常显著:市场下跌时波动率飙升的速度远快于上涨时。标准GARCH模型把正负冲击的系数α设为同一个,无法区分两者的影响,而GJR-GARCH通过增加一个非对称项来解决这个问题。设定方式:

spec_gjr <- ugarchspec( mean.model = list(armaOrder = c(1, 1)), variance.model = list(model = "gjrGARCH"), distribution.model = "std" )

比较GARCH和GJR-GARCH的拟合结果,如果GJR的非对称项系数显著不为零,说明数据确实存在杠杆效应,用GJR更合适;如果系数不显著,说明对称模型就够用了。

作为通用做法,我建议在实际工作中把标准GARCH、GJR-GARCH、EGARCH各跑一遍,用AIC和BIC比较,同时观察系数显著性和残差诊断结果,综合判断选哪个。模型的复杂度应该与数据的信息量匹配,不要为了炫技而用复杂模型。

4.3 分布选择的细节:正态、t分布还是偏态t分布

分布假设直接影响模型对极端事件的刻画能力。rugarch的distribution.model支持很多选项,常用三个:norm(正态分布)、std(学生t分布)、sstd(偏态学生t分布)。

很多人在这一步直接选择sstd,认为偏态t分布适应性最强。但要注意,参数越多越难估计,尤其是在样本量不大或者波动率结构性较强的情况下,偏态参数可能估计不稳定。我的习惯是先把std跑通,再看残差的QQ图判断是否需要引入偏态参数。如果QQ图的尾部偏斜非常明显,再换sstd

4.4 参数解释与模型解读:拿真实输出说话

我把前面设定的ARMA(1,1)-GARCH(1,1)模型在一只真实股票数据上跑出来,输出结果大致是这样的结构:

Optimal Parameters ------------------------------------ Estimate Std. Error t value Pr(>|t|) mu 0.000512 0.000226 2.264 0.02356 ar1 0.045812 0.031254 1.466 0.14261 ma1 -0.062347 0.030012 -2.077 0.03778 omega 0.000002 0.000001 2.843 0.00447 alpha1 0.082354 0.014672 5.613 0.00000 beta1 0.905684 0.015012 60.322 0.00000 shape 6.512354 1.221046 5.332 0.00000

alpha1 + beta1大约0.988,说明波动率持续性很强。shape是t分布的自由度参数,大约6.5,这个数值意味着尾部明显厚于正态分布——正态分布相当于自由度无穷大,6.5的自由度说明极端值出现的概率不可忽视。

实际工作中要留意系数正负的合理性。AR项系数为正、MA项系数为负的组合很常见,但如果出现绝对值接近1的AR或MA系数,意味着均值方程可能接近单位根,数据或许需要差分或减少阶数。方差方程中omega通常很小但显著,它对应的是波动率的长期均值水平乘以一个系数,数值很小符合金融数据的量级特征。

5. 模型诊断与波动率预测:拟合得好不够,还得经得起检验

5.1 标准化残差检验:判断均值方程和方差方程是否充分

模型拟合完,必须做诊断检验,否则你没法确定它是否真正捕捉了数据中的结构。rugarch在这里做得非常方便,ugarchfit返回的对象里已经内置了标准化残差。所谓标准化残差,就是用残差除以模型估计的条件标准差。如果模型设定正确,标准化残差应该近似独立同分布,没有自相关、没有ARCH效应,且服从设定的分布。

诊断代码:

stand_resid <- residuals(fit_1, standardize = TRUE) Box.test(stand_resid, lag = 10, type = "Ljung-Box") Box.test(stand_resid^2, lag = 10, type = "Ljung-Box")

第一行检验标准化残差的自相关,理论上应该不显著;第二行检验残差平方的自相关,也就是检验残差中是否还残留ARCH效应,理论上也应该不显著。如果你在这个环节发现ARCH效应依然显著,说明GARCH阶数不够或者模型形式不对,需要修正设定。

5.2 分布假设检验:QQ图与KS检验的双重验证

分布假设是否合理,最直观的检查方式是指定分布的QQ图:

plot(fit_1, which = 9)

which = 9对应QQ图,用来检查标准化残差是否符合模型设定的分布。如果散点基本落在45度参考线上,说明分布选择合理;如果两端偏离严重,说明尾部行为与假设不完全吻合。

也可以跑一个Kolmogorov-Smirnov检验做量化判断,注意KS检验需要用到样本数据去拟合分布的参数,直接拿标准化残差去检验标准t分布是不太对的。比较稳的办法是用fitdistr先估计分布参数,再做KS检验,但这种两步法本身会低估p值。所以我的习惯是:QQ图为主,KS检验作为辅助参考,不把p值当作绝对标准。

5.3 波动率预测:条件波动率如何转化为实际价值

模型通过诊断之后,就可以做预测了。rugarch的ugarchforecast函数用来做条件均值与条件方差的预测:

forc <- ugarchforecast(fit_1, n.ahead = 10) forc

n.ahead = 10表示预测未来10期。输出会给出每个预测时点的条件均值和条件标准差。条件标准差就是你预测的未来波动率水平。比如当前波动率处于低位,而模型预测未来波动率逐步向长期均值回归,这就是GARCH模型的价值所在——它知道现在的低波动是否可持续,还是会逐步回升。

把预测得到的条件标准差年化,乘以√252(一年交易日数量),就能得到年化波动率,这是资产配置和风险管理模型的基础输入。比如你用GARCH预测明天的日波动率是1.5%,年化后约23.8%,这个数字可以直接代入VaR的计算公式。

可视化预测结果:

plot(forc)

图中会画出预测的均值路径和波动率路径。你会直观看到预测区间随时间逐渐扩大——这本身就是ARMA-GARCH模型对不确定性的正确刻画:越是远期,不确定性越大,预测区间越宽。很多业务分析说"预测未来30天会怎样",其实应该表达为"未来30天的波动率路径如下",这才是模型能给出的可靠答案。

6. 从实际问题出发的避坑经验:那些文档里不会写的教训

6.1 数据频率选择:日线数据是最佳起点吗

日线数据是最常用的起步选择,但实际工作中会碰到一个矛盾:日线数据样本量大,单个数据点的噪声也大;周线数据噪声小,但样本量减少,参数估计不稳定。我的建议是,初建模型用日线数据,等模型流程跑通了,再用周线数据做稳健性测试。对比不同频率下的参数估计结果,如果差异很大,说明模型可能设定不当或者数据存在异常值影响。

分钟级或 tick 级的高频数据属于另一个领域,里面涉及微观结构噪声(买卖价差、交易量不连续等因素对价格产生的短期扰动)问题,直接用标准GARCH会出问题。如果你需要建模高频波动率,应该去了解Realized Volatility和Realized GARCH模型,它们专门处理这个问题。

6.2 函数参数对结果的影响:收敛性问题

rugarch的ugarchfit函数在遇到复杂模型时偶尔会输出收敛警告。一个典型的提示是solver failur。这个问题通常出在几个地方:数据中存在极端异常值、模型阶数设定过高、分布假设与数据特征严重不匹配。

处理优先级我建议这样:先检查数据是否干净(是否包含极端异常值),再降低模型复杂度,最后尝试更换solver或调整参数。rugarch支持的solver包括nlminbsolnplbfgs等,nloptr也可以装来备用。在ugarchfit函数里通过solver参数指定:

fit_2 <- ugarchfit(spec = spec_1, data = ret, solver = "solnp")

不同solver对同样的优化问题可能收敛到不同的局部最优解,所以当其中一个solver报错时,换另一个常常就能解决。但如果你连续试了好几个solver都失败,那大概率是模型设定本身有问题,不要硬调。

6.3 阶数选择的边界:ARMA阶数和GARCH阶数怎么搭配

关于GARCH模型的阶数,金融数据里GARCH(1,1)在绝大多数情况下就够了。如果你非得用GARCH(2,2),除非有特别强的理由,否则很容易碰到参数估计不稳定的问题,特别是alpha2beta2的符号异常或者不显著。

ARMA阶数这边,类似的原则同样适用——均值和方差方程之间会互相干扰。均值方程的残差是方差方程的数据输入,如果ARMA阶数过高导致过度拟合,残差中真实信号被吸收过多,方差方程就会变得不稳定。反过来,如果ARMA阶数太低,残差里残留的自相关结构会让方差方程输出奇怪的参数。这就是为什么我总是建议均值方程从低阶开始测试,一步一步加,而不是一下跳到ARMA(5,5)。

6.4 样本内外评估:滚动预测比单次预测更有说服力

很多人跑完模型直接做样本外预测,看一两个预测点就开始下结论,这种做法说服力不足。一套更扎实的评估框架是滚动预测:固定一个窗口长度,比如500个交易日,逐步向前滚动训练和预测,每次都只预测未来一天或一周,然后对比预测值与实际值。这样你会得到几百个预测误差,能够真正评估模型的预测能力。

roll <- ugarchroll( spec = spec_1, data = ret, n.ahead = 1, refit.every = 20, refit.window = "moving", window.size = 500, solver = "solnp" )

refit.every = 20表示每20个观测值重新估计一次模型参数,平衡了计算开销和参数时效性。滚动预测的结果可以用report函数生成VaR回测报告,这是检验模型实际风险管理能力的好方法。

滚动预测还会暴露一个时间序列建模绕不开的现实:市场结构可能会变化。你会发现某些时间段的预测误差明显偏大,比如重大政策发布前后、流动性危机期间。这不是模型本身出了问题,而是模型背后假设的数据生成过程发生了改变。这时候需要考虑引入结构突变检测,比如分段建模或者使用Markov Switching模型。

6.5 关于输出解读的一个提醒

最后说一个非常容易犯的"解释性错误"。很多人看到ARMA-GARCH模型的mu系数显著,就认为找到了一种"涨跌预测"的方法,这是对模型的严重误读。实际上金融收益率模型中的mu通常很小,而且样本内显著并不代表样本外稳定。这个模型的真正价值在于方差方程的估计——你用它来度量风险,而不是用它来预测收益方向。

我在实际工作中用ARMA-GARCH做什么?第一,计算动态VaR,每天根据最新的波动率估计更新风险限额;第二,为期权定价提供波动率输入,GARCH族模型的波动率预测比简单历史波动率在期限结构上更灵活;第三,做多资产配置时的风险度量,各资产的波动率预测汇聚成一个协方差矩阵的估计,直接影响权重分配。这些都是风险视角的应用,不是收益视角的择时工具。

建模这条路没有终点,ARMA-GARCH只是理解条件波动率的起点。真正的高手会在这个框架基础上叠加更丰富的结构——非对称项、外生变量、结构性突变、多变量扩展,每一步扩展都需要回到今天讲的这些基本功:序列体检、模型设定、诊断验证、样本外评估。把这套流程走扎实,后面学什么模型都快。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询