ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

金融时间序列建模全流程:从平稳性检验到GARCH波动率预测

金融时间序列建模全流程:从平稳性检验到GARCH波动率预测

1. 从第一节课到第四章:金融时间序列到底在研究什么

如果你正在啃金融相关的课程,大概率会在一门叫《金融时间序列分析》或类似名字的课上卡住。这门课和传统统计学、计量经济学都不一样,它研究的是股票收益率、汇率波动、债券利差这类有“先后顺序”的金融数据,核心目标不是给人解释过去,而是从历史数据里提炼规律,给未来定价和风控提供依据。

我当年学这门课的时候,最大的感受是:教材里全是公式推导,但真到了自己拿数据上手,第一反应是懵的。第四章通常是一个分水岭——前面的章节在铺垫概率论和统计基础,到这一章才开始真正进入“金融时间序列”的核心:如何把收益率序列建模、如何判断一个序列有没有记忆性、如何在随机波动中做出有意义的预测。

这篇文章我会用一份课程笔记的视角,把金融时间序列建模从数据预处理、平稳性检验、ARMA/ARIMA建模到波动率模型这条路完整讲一遍,再附上我实际踩过的一些坑。无论你是刚学完概率论准备入门量化,还是已经在做因子研究但建模部分一直停留在调包,这篇内容应该都能帮你把“为什么这么做”补上。

1.1 金融时间序列和普通统计数据的本质区别

普通统计分析往往假设样本之间是独立的,比如调查问卷里1000个人的身高,理论上彼此不影响。但金融数据完全不同,今天的股价和昨天的股价高度相关,或者说,今天的波动会让明天的波动也变大。这就是所谓的“时序依赖”。

除此之外,金融时间序列还有几个典型特征:第一,收益率序列的波动忽大忽小,大波动后面往往跟着大波动,小波动后面跟着小波动,这叫波动率聚集效应;第二,极端值出现的概率比正态分布预测的要高,也就是“肥尾”;第三,序列通常不是平稳的,价格序列往往有趋势,直接建模容易得到伪回归。

所以,处理金融数据第一件事就是彻底忘掉“样本独立”这个假设,所有模型都要围绕自相关性、条件异方差性来重新组织思路。

1.2 一条完整的金融时间序列建模流水线

拿到任何金融序列,不管是用Python还是R,建模流程基本是固定的。我把第四章的笔记浓缩成一张流程清单,后面每一步都按这个走:

  1. 数据获取与清洗:除权除息处理、缺失值填补、异常值剔除。
  2. 可视化与描述统计:看走势图、自相关图、分布直方图。
  3. 平稳性检验:ADF检验、KPSS检验,判断是否需要进行差分。
  4. 模型识别与定阶:观察ACF和PACF图,结合信息准则定阶。
  5. 参数估计与诊断:拟合模型,检验残差是否为白噪声。
  6. 预测与回测:用滚动窗口评估样本外表现。

很多人拿到数据后直接跳去调包做预测,这是最容易翻车的做法。金融时序模型对数据质量极度敏感,缺一个异常值,可能整个自相关系数的估计就变了。

2. 拿到数据先别急建模,平稳性检验是第一道砍

2.1 数据清洗里的几个隐形陷阱

我最早做课程作业时,直接下载了一段日线收盘价就开始建模,结果跑出来的模型预测结果离谱。后来才发现问题出在数据预处理上。金融数据清洗有三个坑特别容易忽略。

一是复权问题。股票分红、送股之后价格会跳空,如果你直接用原始价格序列,相当于引入了一个“假信号”。做时间序列建模之前,应该使用前复权或后复权数据,让价格序列具有连续性。

二是非交易日缺口。有些数据集把周末和节假日直接留空,有些则会填充前值。如果直接填充,你要确保自己知道填充的逻辑,否则自相关性会被人为抬高。最稳妥的做法是把日期索引转成连续的时间戳,再检查缺失率。

三是极端值处理。比如一笔错误报价把收益率拉到±50%,这种值如果不处理,ACF和PACF图会被严重污染。我的习惯是先画出收益率分布图,把超过5倍标准差的值筛出来人工确认,是真实事件就保留,是数据错误就剔除。

2.2 平稳性到底意味着什么,ADF检验怎么读

平稳性分为严平稳和弱平稳。实际建模中我们只需要关心弱平稳,也就是均值和自协方差不随时间变化。打个比方,扔硬币的结果序列是平稳的,因为每次的概率都一样,过去的任何信息都不能帮你预测下一次是正是反。而股价序列不是平稳的,因为今天的价格包含了既往所有信息,一直在漂移。

ADF检验是最常用的平稳性检验工具。Python里执行一次很简单:

from statsmodels.tsa.stattools import adfuller result = adfuller(returns) print(f'ADF统计量: {result[0]:.4f}') print(f'p值: {result[1]:.4f}') print(f'临界值: {result[4]}')

关键看p值。如果p值小于0.05,拒绝原假设,说明序列是平稳的。对于对数收益率序列,绝大多数情况下ADF检验都能直接通过,因为收益率的均值围绕0附近波动,不像价格序列那样有趋势。如果你发现自己拿到的收益率序列ADF不通过,先回头检查是不是包含了未复权的价格或异常跳变。

提示:ADF检验的原假设是“序列存在单位根,即非平稳”。很多初学者会记反,一定要记住p值小才是平稳。

2.3 差分操作与单整阶数的确定

如果序列不平稳,最常见的处理是差分。一阶差分后的序列平稳,就说原序列是一阶单整,记作I(1)。股价序列通常就是I(1),对数收益率其实就是价格序列的一阶差分,所以它天然是平稳的。

很多教材会把差分简单理解成“让序列变平稳的工具”,但你要知道为什么。差分实际上是在移除序列中的趋势成分。对于带有随机游走性质的资产价格,价格等于昨天的价格加上一个随机扰动,这个随机扰动就是收益率,一阶差分后剩下的正是这个扰动,也就是我们要建模的对象。

我自己的经验是:能不差分就不差分,因为差分会损失信息,而且差分后的模型解释起来更绕。真正需要差分的场景是对数价格趋势项明显、且自相关在低阶衰减很慢的情况。如果你只是为了预测收益率,直接用日收益率序列就够了,不需要额外差分。

3. ARMA与ARIMA建模:收益率序列的精简武器

3.1 从ACF和PACF判断模型类型

把收益率序列稳定下来之后,下一步是看它是否有自相关性。如果收益率之间完全独立,那它的最优预测就是历史均值,没必要建模。但现实中的收益率序列往往存在短期自相关,特别是高频数据或某些特定资产,这就轮到ARMA模型登场。

ARMA(p,q)由自回归部分AR(p)和移动平均部分MA(q)组成。判断p和q最直接的工具就是ACF(自相关函数)和PACF(偏自相关函数)。

  • 如果ACF在q阶之后截尾,PACF拖尾,适合MA(q)模型;
  • 如果PACF在p阶之后截尾,ACF拖尾,适合AR(p)模型;
  • 如果两者都拖尾,考虑ARMA(p,q)。

说人话:截尾就是相关系数在某一阶之后突然变成不显著,拖尾就是相关系数慢慢衰减,像一条逐渐平息的尾巴。

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf plot_acf(returns, lags=40) plot_pacf(returns, lags=40)

但是请注意,实际数据的ACF和PACF很少像教科书那么标准。我曾经看过一个收益率的ACF图,在滞后7阶有个明显的尖峰,其他都基本为零,按规则应该选MA(7),但跑出来的模型怎么看怎么别扭。后来才知道那只是某次数据异常造成的假相关。图形判断只提供线索,最终要靠信息准则来确认。

3.2 信息准则定阶:AIC和BIC的使用心得

定阶不能完全靠肉眼看图,还需要用AIC或BIC做量化比较。这两种准则都在惩罚模型复杂度,BIC的惩罚力度更大,倾向于选择更精简的模型。

实操时我会写一个循环,遍历p和q在0到5之间的所有组合,记录每个模型的AIC和BIC:

import itertools import warnings from statsmodels.tsa.arima.model import ARIMA warnings.filterwarnings('ignore') best_aic = float('inf') best_order = None for p, q in itertools.product(range(6), range(6)): try: model = ARIMA(returns, order=(p, 0, q)) result = model.fit() if result.aic < best_aic: best_aic = result.aic best_order = (p, q) except Exception: continue print(f'最优阶数: p={best_order[0]}, q={best_order[1]}, AIC={best_aic:.4f}')

不过要提醒自己:AIC选出来的模型往往偏复杂,尤其是在样本量大的时候。金融数据动辄几千条,如果AIC选出了ARMA(5,4)这种高阶模型,要格外小心过拟合。我的习惯是:用BIC选一个作为基准模型,再用AIC选一个作为对比模型,如果两者预测效果接近,就选BIC推荐的简化版。

3.3 残差诊断:Ljung-Box检验与QQ图

模型拟合完之后,残差诊断是绝对不能省的一步。如果残差里还有自相关结构,说明模型没有把信息提取干净,需要重新定阶。主流做法是Ljung-Box检验。

from statsmodels.stats.diagnostic import acorr_ljungbox lb_test = acorr_ljungbox(result.resid, lags=[10, 20], return_df=True) print(lb_test)

p值大于0.05说明残差是白噪声,模型拟合可接受。如果p值很小,说明信息残留,需要调整阶数。同时我还会画残差的QQ图,看是否接近正态分布。虽然ARMA模型不要求残差严格正态,但明显的偏态会让后续的置信区间失真。

实操心得:残差诊断要在多个滞后阶数上做,不要只看一个。我遇到过滞后5阶白噪声、滞后20阶却显著的情况,说明模型在高频周期上还有遗漏。用10和20两个滞后是做诊断的起始值。

3.4 滚动窗口预测:样本外检验才是真本事

很多人建模时模型训练集误差很低,一拿去预测真实未来就垮掉。金融时序建模里,唯一有说服力的评估方式是滚动时间窗口。

思路很简单:固定一个窗口长度,比如500个交易日,每次往前预测1步或5步,把预测值和真实值对比,然后滚动到下一期,重复这个过程,最后汇总误差指标。

from sklearn.metrics import mean_absolute_error window = 500 preds = [] actuals = [] for end in range(window, len(returns)): train = returns[end - window:end] model = ARIMA(train, order=(best_order[0], 0, best_order[1])) fit = model.fit() pred = fit.forecast(steps=1)[0] actual = returns[end] preds.append(pred) actuals.append(actual) mae = mean_absolute_error(actuals, preds) print(f'滚动预测MAE: {mae:.6f}')

做完滚动预测,再看实际效果,你会发现收益率预测的误差大得吓人。这很正常,金融时序的预测上限本身就低。ARMA模型对收益率的预测能力非常有限,它的价值更多体现在捕捉短期动量或均值回复效应上,而不是让你靠它稳定盈利。如果你的目的是预测波动率,那就要进入下一章的内容了。

4. 收益率预测价值有限,波动率建模才是真功夫

4.1 波动率聚集与金融序列的“肥尾”现象

金融时间序列教材里,第四章后面几乎都会马上引入ARCH和GARCH,因为仅仅对收益率建模远远不够。收益率预测不了,但收益率的波动率是可以被建模和预测的,而且波动率预测在风险管理、期权定价、投资组合优化里非常关键。

先看一个直观现象:市场大跌之后往往还有大跌,大涨之后波动也不会马上平息。这就是波动率聚集效应,也就是大的变化倾向于跟随大的变化,小的变化倾向于跟随小的变化。如果用ARMA模型处理这种情况,残差不会表现出线性自相关,但残差的平方会表现出显著自相关,这就是ARCH效应的信号。

肥尾和波动率聚集是一体两面的。正态分布预测的极端值概率太低了,而真实收益率数据里极端值出现得比正态预测频繁得多。因为波动率本身在变,当波动率高的时候,收益率就像被“放大”了一样。

4.2 如何检验ARCH效应

对收益率序列判断是否需要用GARCH模型,先做ARCH效应检验。最实用的方法是做残差的平方自相关检验,或者直接使用ARCH检验。

from statsmodels.stats.diagnostic import het_arch arch_test = het_arch(returns, nlags=10) print(f'LM统计量: {arch_test[0]:.4f}') print(f'p值: {arch_test[1]:.4f}')

如果p值很小,说明序列存在显著的ARCH效应,这时候就可以考虑GARCH模型了。另一个直观的方法是看残差平方的ACF图,如果滞后阶上出现明显的相关性,同样是ARCH效应存在的信号。

4.3 GARCH(1,1):波动率建模的基准模型

GARCH(1,1)是使用最广泛的波动率模型,表达式为:

sigma_t^2 = omega + alpha * epsilon_{t-1}^2 + beta * sigma_{t-1}^2

通俗解释:今天的波动率由三部分构成——长期平均波动率、昨天的新息冲击、昨天的波动率水平。alpha衡量的是对新信息的敏感度,beta衡量的是波动率衰减的持续性。

用Python拟合如下:

from arch import arch_model garch = arch_model(returns * 100, vol='GARCH', p=1, q=1, mean='AR', lags=1) garch_fit = garch.fit(disp='off') print(garch_fit.summary())

注意我在这里把收益率乘以100,这是为了数值稳定性。原始收益率可能只有0.001量级,平方之后会非常小,导致优化器收敛困难。乘以100后相当于把波动率调整到百分比量级,模型更容易收敛,参数解读也更直观。

4.4 GARCH族扩展:EGARCH、GJR-GARCH怎么选

GARCH(1,1)有一个著名假设:波动率对好消息和坏消息的响应是对称的。但实际市场存在“杠杆效应”,即利空消息对波动率的冲击比利好消息更大。这时候要用非对称模型。

  • GJR-GARCH:在模型中引入一个虚拟变量,捕捉好消息和坏消息的不对称性;
  • EGARCH:对波动率取对数建模,不要求参数非负,也能捕捉不对称性。

我的经验是:对股票收益率序列,非对称效应几乎总是显著的。如果你用GARCH(1,1)做出来但觉得拟合不够好,可以先试试GJR-GARCH,模型复杂度没有明显增加,但对波动率尖峰的把控更好。

注意事项:GARCH家族的参数估计非常依赖初始值,有时候会收敛到局部最优。建议多试几组随机种子或更换优化算法,对比结果是否稳定。如果omega为负或者alpha+beta大于1,说明模型可能选错了。

5. 一次完整的建模实战:从数据到报告的复现流程

5.1 案例背景与数据准备

为了把这套流程完整串起来,我用模拟项目X做一次全流程演练。模拟项目X的原始数据是某指数在过去5年的日收盘价,共约1250个观测值。目标是构建一个收益率均值和波动率的联合预测模型。

第一步是加载数据并计算对数收益率:

import pandas as pd import numpy as np df = pd.read_csv('simulated_index.csv', parse_dates=['date'], index_col='date') close = df['close'] log_returns = np.log(close / close.shift(1)).dropna()

对数收益率的好处是它在数学上可以累加,并且可以让序列更接近正态分布。当然,这只是转换收益率的计算方式,不会改变数据本身的分布特征。

5.2 平稳性检验与ARMA定阶

先做ADF检验:

from statsmodels.tsa.stattools import adfuller result = adfuller(log_returns) print(f'ADF p值: {result[1]:.4f}')

p值小于0.01,可以认为对数收益率序列平稳。接着看ACF和PACF图,滞后1阶到3阶都出现了明显的相关性,之后快速衰减。用AIC遍历找到的模型是ARMA(2,2),但BIC更倾向于ARMA(1,1)。我用滚动窗口对比了两个模型,发现ARMA(1,1)在样本外的MAE略高一点点,但差异小到可以忽略。最终选择ARMA(1,1),因为参数更少、更稳健。

拟合ARMA(1,1)后,对残差做了Ljung-Box检验:

from statsmodels.tsa.arima.model import ARIMA from statsmodels.stats.diagnostic import acorr_ljungbox arma_model = ARIMA(log_returns, order=(1, 0, 1)) arma_result = arma_model.fit() lb = acorr_ljungbox(arma_result.resid, lags=[10, 20], return_df=True) print(lb)

滞后10阶和20阶的p值都大于0.05,说明残差没有明显的自相关性,均值模型部分过得去。

5.3 加入GARCH模型联合建模

但对残差的平方再检验时,het_arch检验的p值远小于0.01,说明存在ARCH效应。于是把均值部分和波动率部分放在一起建模:

from arch import arch_model final_model = arch_model( log_returns, mean='AR', lags=1, vol='GARCH', p=1, q=1 ) final_result = final_model.fit(disp='off') print(final_result.summary())

最终得到的alpha约0.08,beta约0.89,alpha+beta约为0.97,说明波动率衰减速度较慢,一个冲击的影响会持续很久。这个数字在股票市场数据里非常典型:波动率是高持续的。

5.4 波动率预测的实际应用:VaR计算

波动率模型最有价值的应用之一是计算VaR。在正态性假设下,95%置信水平的单日VaR可以近似为:

VaR = -(均值 - 1.65 * 预测波动率)

这意味着明天有95%的把握,损失不会超过这个值。用上面拟合的GARCH模型做滚动预测,得到每日的条件波动率序列,再代入公式得到动态VaR:

forecasts = final_result.forecast(start=1000, horizon=1) cond_var = forecasts.variance.dropna() cond_vol = np.sqrt(cond_var.iloc[:, 0]) var_95 = -1.65 * cond_vol print(f'最后一期95% VaR: {var_95.iloc[-1]:.2%}')

你会发现传统历史均值VaR是固定的,而GARCH给出的VaR是高波动时期扩大、低波动时期收缩。这就是条件波动率的优势:能根据最新的市场信息调整风险预估。

需要强调的是,这个例子里的VaR计算采用的是简化正态假设,实际风险管理中还会结合历史模拟法或极值理论做修正。但作为课程笔记和入门实践,这条路径已经把金融时间序列建模的核心环节全部趟了一遍。

6. 那些年我踩过的坑,整理成了一份避坑速查表

6.1 常见问题与解决方案

问题表现解决方案
ADF检验总是不通过p值始终大于0.05检查是否用了未复权价格;尝试对数差分;检查样本区间是否包含极端跳空
ACF和PACF图都拖尾定阶困难用AIC/BIC遍历对比若干组(p,q),不要只看图形
模型残差Ljung-Box通过但平方残差不通过忽略ARCH效应做ARCH检验,均值模型与GARCH模型联合建模
GARCH模型不收敛optimizer返回错误或参数为负收益率乘以100;更换优化器;增加迭代次数;简化模型阶数
预测结果“太完美”训练集表现远好于测试集检查是否泄漏未来信息;使用滚动窗口评估;警惕高阶模型过拟合
出现伪回归两个非平稳序列回归R平方很高先做协整检验;确保变量平稳后再建模

6.2 三条独家实操心得

第一,处理金融数据时,一定要保留数据来源和预处理步骤的记录。金融数据源经常会更新或修正历史数据,如果没有快照,你的结果可能过几个月就无法复现了。我后来做任何数据分析项目,第一步就是生成数据的哈希值并保存预处理脚本,至少保证自己随时能复现。

第二,不要迷信某个模型“先进”或“更高级”。在金融时序分析里,GARCH(1,1)和ARMA(1,1)这种简单模型在预测中表现稳定,反而一些复杂模型因为参数过多,样本外表现一塌糊涂。先跑通简单模型,再逐步扩展,这个顺序永远不会错。

第三,收益率序列建模的天花板需要明确认知。课程里教的模型能让我们理解数据背后的规律,但在实际交易中,靠单纯的时序模型做收益率预测几乎不可能稳定盈利。这些模型真正的主战场是风险管理和资产配置。把课程内容和应用场景对不上号,是新手最容易产生的认知落差。

6.3 给课程学习者的进阶建议

如果你是配合课程看这篇笔记,我强烈建议私下做一件事:找一段自己感兴趣的资产数据,把上面整个流程完整跑一遍,然后试着把模型输出的结果和课程里的理论概念一一对应。所有知识点通过一组真实数据串起来之后,才算是真正学会了。

至于后续还可以怎么扩展,路径也很清晰:把单变量ARMA-GARCH扩展到多元的DCC-GARCH去研究多个资产间的动态相关性,或者把均值方程里的AR换成机器学习模型如随机森林或LSTM,再用GARCH族模型去拟合残差的波动率。这种混合架构在学术和工业界都有不少探索,可以作为下一个阶段的练习方向。

我个人在把这套流程跑通之后,最大的体会是:金融时间序列建模的门槛不在数学推导,而在对整个分析流程每条决策背后原因的理解。当你遇到一个数据异常时能立刻判断出該回到哪一步重新处理,才是从“会调包”走向“会建模”的真正分水岭。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进