主题切换
2.4 时间序列分析
概念详解
时间序列分析的量化意义
金融数据本质上都是时间序列——从tick级的价格跳动到月度的宏观经济指标,时间维度是量化分析中最核心的组织维度。时间序列分析回答的是一个根本问题:过去的数据模式能否为未来提供有用信息?
传统计量经济学假定数据是"独立同分布"的(横截面数据通常满足),但金融时间序列天然不满足这个假设——今天和昨天的价格是高度相关的。这既是挑战(许多标准统计方法不再适用),也是机遇(相关性本身可能包含可交易的信号)。
名词解释:平稳性
时间序列的统计特性(均值、方差、自相关)不随时间变化。ADF检验常用于检测单位根。
名词解释:协整
两个或多个非平稳序列的线性组合是平稳的,表明它们之间存在长期均衡关系,常用于配对交易。
名词解释:自相关 (Autocorrelation)
时间序列与其自身滞后值之间的相关性。ACF图展示各阶滞后的相关系数,是识别序列结构的基本工具。
时间序列的分解
经典时间序列分解将序列分解为三个成分:
Y_t = T_t + S_t + R_t
其中:
:趋势成分 (Trend) —— 长期方向性变化 :季节成分 (Seasonal) —— 固定周期重复的模式(金融中的月初效应、季末效应) :残差成分 (Residual) —— 扣除趋势和季节后的剩余变化
在金融中,我们通常关心的是
数学原理
平稳性的正式定义
一个时间序列
(均值恒定) (方差恒定) (自协方差仅依赖于滞后阶数k,不依赖于t)
ADF检验 (Augmented Dickey-Fuller)
ADF检验是检测单位根(非平稳)最常用的方法。检验回归方程为:
\Delta Y_t = \alpha + \beta t + \gamma Y_{t-1} + \sum_{i=1}^{p} \delta_i \Delta Y_{t-i} + \varepsilon_t
: (存在单位根,序列非平稳) : (序列平稳)
ARMA/ARIMA模型
ARMA(p, q) 模型结合了自回归和移动平均:
Y_t = c + \sum_{i=1}^{p} \phi_i Y_{t-i} + \sum_{j=1}^{q} \theta_j \varepsilon_{t-j} + \varepsilon_t
ARIMA(p, d, q) 在ARMA的基础上增加了差分项:
- 如果原始序列需要差分
次才能平稳,则 为差分阶数 - 金融价格序列通常需要
(一次差分变为收益率)
GARCH模型(波动率建模)
收益率序列通常没有均值方向的预测力("预测明天涨跌很难"),但波动率具有强烈的可预测结构:
\sigma_t^2 = \omega + \alpha \varepsilon_{t-1}^2 + \beta \sigma_{t-1}^2
GARCH(1,1) 是金融中最常用的波动率模型:
:基础波动水平 :最近冲击对波动率的影响( 大 = 对近期事件敏感) :波动率的持续性( 大 = 波动率变化慢) 接近1时,波动率具有长记忆性
协整检验
Engle-Granger两步法:
- 估计长期均衡关系:
- 检验残差
是否平稳
Johansen检验可以同时检测多个协整关系,适用于3个及以上的时间序列。
Python实战
案例1:Python进行协整检验
此处有展示代码展开 ▼
python
from statsmodels.tsa.stattools import coint
score, pvalue, _ = coint(df['X'], df['Y'])
print(f"协整检验p值: {pvalue:.4f}")点击展开可浏览运行结果
📘 本段为代码片段(仅展示函数/类定义,未提供运行入口,无需运行)
案例2:完整的平稳性诊断与ARIMA建模
此处有展示代码展开 ▼
python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.stattools import adfuller, kpss
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from statsmodels.tsa.arima.model import ARIMA
from statsmodels.tsa.stattools import arma_order_select_ic
def stationarity_diagnosis(series, max_lags=None):
"""
全面诊断时间序列的平稳性特征
"""
series = series.dropna()
# ADF检验 (H0: 有单位根 = 非平稳)
adf_stat, adf_p, adf_lags, adf_obs, adf_crit = adfuller(series, maxlag=max_lags, autolag='AIC')
# KPSS检验 (H0: 序列是平稳的)
kpss_stat, kpss_p, kpss_lags, kpss_crit = kpss(series, regression='c', nlags='auto')
# 第一次差分
diff1 = series.diff().dropna()
adf_diff1_stat, adf_diff1_p, _, _, _ = adfuller(diff1, maxlag=max_lags, autolag='AIC')
# 可视化
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 原始序列
axes[0, 0].plot(series.index, series.values)
axes[0, 0].set_title(f'原始序列 (ADF p={adf_p:.4f}, KPSS p={kpss_p:.4f})')
axes[0, 0].grid(True, alpha=0.3)
# 一阶差分
axes[0, 1].plot(diff1.index, diff1.values)
axes[0, 1].set_title(f'一阶差分 (ADF p={adf_diff1_p:.4f})')
axes[0, 1].grid(True, alpha=0.3)
# ACF
plot_acf(series, ax=axes[1, 0], lags=min(40, len(series)//4))
axes[1, 0].set_title('自相关函数 (ACF)')
# PACF
plot_pacf(series, ax=axes[1, 1], lags=min(40, len(series)//4), method='ywm')
axes[1, 1].set_title('偏自相关函数 (PACF)')
plt.tight_layout()
plt.show()
# 输出诊断结果
print("=" * 50)
print("时间序列平稳性诊断")
print("=" * 50)
print(f"ADF检验: 统计量={adf_stat:.4f}, p值={adf_p:.4f}")
print(f" -> {'平稳 ✅' if adf_p < 0.05 else '非平稳 ❌ (存在单位根)'}")
print(f"KPSS检验: 统计量={kpss_stat:.4f}, p值={kpss_p:.4f}")
print(f" -> {'平稳 ✅' if kpss_p > 0.05 else '非平稳 ❌'}")
print(f"一阶差分后ADF p值: {adf_diff1_p:.4f}")
# 综合判断
if adf_p < 0.05 and kpss_p > 0.05:
conclusion = "序列平稳(两种检验一致)"
elif adf_p > 0.05 and kpss_p < 0.05:
conclusion = "序列非平稳(两种检验一致),建议差分"
else:
conclusion = "检验结果不一致,谨慎判断"
print(f"\n综合结论: {conclusion}")
return {
'adf_stat': adf_stat, 'adf_pvalue': adf_p,
'kpss_stat': kpss_stat, 'kpss_pvalue': kpss_p,
'diff_adf_pvalue': adf_diff1_p
}
def auto_arima_model(series, max_p=5, max_d=2, max_q=5):
"""
自动选择最优ARIMA模型(基于AIC)
"""
series = series.dropna()
best_aic = np.inf
best_order = None
best_model = None
for d in range(max_d + 1):
for p in range(max_p + 1):
for q in range(max_q + 1):
if p == 0 and q == 0:
continue
try:
model = ARIMA(series, order=(p, d, q))
fitted = model.fit()
if fitted.aic < best_aic:
best_aic = fitted.aic
best_order = (p, d, q)
best_model = fitted
except:
continue
if best_model:
print(f"最优模型: ARIMA{best_order}")
print(f"AIC: {best_aic:.2f}")
print(best_model.summary())
return best_model, best_order
def garch_volatility_forecast(returns, p=1, q=1):
"""
使用GARCH模型估计时变波动率
需要安装: pip install arch
"""
try:
from arch import arch_model
# 均值方程设为常数(收益率很难预测),重点在方差方程
model = arch_model(returns * 100, vol='Garch', p=p, q=q, mean='Constant', dist='normal')
fitted = model.fit(disp='off')
# 条件波动率
cond_vol = fitted.conditional_volatility / 100 # 转回小数
# 预测
forecast = fitted.forecast(horizon=22) # 预测未来22个交易日
forecast_vol = np.sqrt(forecast.variance.values[-1, :]) / 100
print(fitted.summary())
print(f"\n参数解读:")
print(f"omega (基础波动): {fitted.params['omega']:.6f}")
print(f"alpha (冲击敏感度): {fitted.params['alpha[1]']:.4f}")
print(f"beta (波动持续性): {fitted.params['beta[1]']:.4f}")
print(f"alpha+beta: {fitted.params['alpha[1]'] + fitted.params['beta[1]']:.4f} "
+ ('(接近1→长记忆性)' if fitted.params['alpha[1]'] + fitted.params['beta[1]'] > 0.95 else ''))
return fitted, cond_vol, forecast_vol
except ImportError:
print("请先安装 arch 包: pip install arch")
return None, None, None点击展开可浏览运行结果
📘 本段代码定义了 3 个函数/类:函数 `stationarity_diagnosis`(全面诊断时间序列的平稳性特征)、函数 `auto_arima_model`(自动选择最优ARIMA模型(基于AIC))、函数 `garch_volatility_forecast`(使用GARCH模型估计时变波动率)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。
案例3:协整配对发现与交易
此处有展示代码展开 ▼
python
import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import coint, adfuller
from scipy import stats
import matplotlib.pyplot as plt
def pair_trading_analysis(price1, price2, name1='Stock1', name2='Stock2',
formation_period=252, trading_period=None):
"""
完整的配对交易分析:从协整检验到交易信号
"""
# 确保是对数价格
log_p1 = np.log(price1)
log_p2 = np.log(price2)
if trading_period is None:
trading_period = len(log_p1) - formation_period
# === 形成期:估计对冲比率和参数 ===
log_p1_form = log_p1[:formation_period]
log_p2_form = log_p2[:formation_period]
# OLS回归得到对冲比率
X = np.column_stack([np.ones(len(log_p2_form)), log_p2_form])
beta = np.linalg.lstsq(X, log_p1_form, rcond=None)[0]
alpha, hedge_ratio = beta[0], beta[1]
# 计算价差
spread = log_p1 - (alpha + hedge_ratio * log_p2)
spread_form = spread[:formation_period]
spread_trade = spread[formation_period:formation_period + trading_period]
# 检验残差平稳性
adf_stat, adf_p, _, _, _ = adfuller(spread_form)
coint_t, coint_p, _ = coint(log_p1_form, log_p2_form)
# 计算开仓阈值
spread_mean = spread_form.mean()
spread_std = spread_form.std()
zscore = (spread_trade - spread_mean) / spread_std
# === 交易期:生成信号 ===
entry_z = 2.0 # 开仓阈值
exit_z = 0.5 # 平仓阈值
position = np.zeros(len(zscore))
for t in range(1, len(zscore)):
if position[t-1] == 0:
if zscore[t] > entry_z:
position[t] = -1 # 做空价差
elif zscore[t] < -entry_z:
position[t] = 1 # 做多价差
else:
position[t] = 0
else:
if abs(zscore[t]) < exit_z:
position[t] = 0 # 平仓
else:
position[t] = position[t-1]
# 计算策略收益
spread_returns = np.diff(spread_trade, prepend=spread_trade[0])
strategy_returns = position[:-1] * spread_returns[1:] # 滞后一日
# 绩效统计
cum_returns = (1 + pd.Series(strategy_returns)).cumprod()
annual_ret = np.mean(strategy_returns) * 252
annual_vol = np.std(strategy_returns) * np.sqrt(252)
sharpe = annual_ret / annual_vol if annual_vol > 0 else 0
# 可视化
fig, axes = plt.subplots(3, 1, figsize=(14, 12))
# 价格走势
axes[0].plot(price1.index[formation_period:],
price1.values[formation_period:formation_period+trading_period] / price1.values[formation_period],
label=name1, alpha=0.7)
axes[0].plot(price2.index[formation_period:],
price2.values[formation_period:formation_period+trading_period] / price2.values[formation_period],
label=name2, alpha=0.7)
axes[0].axvline(x=price1.index[formation_period], color='red', linestyle='--', alpha=0.5, label='交易开始')
axes[0].set_title('标准化价格走势')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# Z-score和交易信号
trade_dates = price1.index[formation_period:formation_period + len(zscore)]
axes[1].plot(trade_dates, zscore, label='Z-score', linewidth=0.8)
axes[1].axhline(y=entry_z, color='red', linestyle='--', alpha=0.5, label=f'开仓阈值 ±{entry_z}')
axes[1].axhline(y=-entry_z, color='red', linestyle='--', alpha=0.5)
axes[1].axhline(y=exit_z, color='green', linestyle=':', alpha=0.5, label=f'平仓阈值 ±{exit_z}')
axes[1].axhline(y=-exit_z, color='green', linestyle=':', alpha=0.5)
axes[1].axhline(y=0, color='gray', alpha=0.3)
axes[1].fill_between(trade_dates, 0, position * max(abs(zscore)),
where=(position > 0), color='green', alpha=0.3, label='多头')
axes[1].fill_between(trade_dates, 0, position * max(abs(zscore)),
where=(position < 0), color='red', alpha=0.3, label='空头')
axes[1].set_title(f'Z-score 和交易信号 (对冲比率={hedge_ratio:.3f})')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
# 累计收益
axes[2].plot(trade_dates, cum_returns, label=f'配对交易 (夏普={sharpe:.2f})', linewidth=0.8)
axes[2].set_title('累计收益')
axes[2].legend()
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("=" * 50)
print(f"配对交易分析: {name1} vs {name2}")
print("=" * 50)
print(f"对冲比率: {hedge_ratio:.4f}")
print(f"常数项(Alpha): {alpha:.4f}")
print(f"ADF检验 (价差平稳性): p={adf_p:.4f} {'✅协整' if adf_p < 0.05 else '❌不协整'}")
print(f"协整检验: p={coint_p:.4f}")
print(f"\n交易统计:")
print(f" 交易次数: {np.sum(np.abs(np.diff(np.insert(position, 0, 0)))) // 2}")
print(f" 年化收益率: {annual_ret:.2%}")
print(f" 年化波动率: {annual_vol:.2%}")
print(f" 夏普比率: {sharpe:.2f}")
return {
'hedge_ratio': hedge_ratio, 'alpha': alpha,
'adf_pvalue': adf_p, 'coint_pvalue': coint_p,
'sharpe': sharpe, 'spread': spread_trade, 'zscore': zscore,
'position': position, 'strategy_returns': strategy_returns
}点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:函数 `pair_trading_analysis`(完整的配对交易分析:从协整检验到交易信号)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。
常见误区
误区一:所有时间序列都可以建模
事实:如果序列是纯白噪声(没有任何自相关结构),任何模型都只是在拟合噪声。在建模之前,总是先用Ljung-Box检验确认序列是否存在可预测的结构。试图在噪声中找到规律,是量化研究中最常见的时间浪费。
误区二:差分可以解决一切非平稳问题
事实:过度差分会引入不必要的MA结构,使模型变得不必要的复杂。通常d=1足够(价格→收益率),d=2非常罕见。此外,如果序列是确定性的趋势平稳(trend-stationary),应该用去趋势(detrending)而非差分。
误区三:协整=可以套利
事实:协整只能说明历史上存在长期均衡关系,但无法保证该关系在未来继续成立。协整关系可能因为企业并购、行业剧变、政策变化等结构性事件而突然破裂。此外,即使协整存在,价差回归的速度是否足够快、过程中是否会触及止损线,才是决定策略能否盈利的关键。
误区四:ACF/PACF图可以直接用于选择ARMA的p和q
事实:样本ACF/PACF图往往非常嘈杂,尤其是在金融数据中。很多看似显著的滞后峰值可能只是抽样波动。在实践中,使用信息准则(AIC/BIC)进行网格搜索,同时兼顾模型的简洁性和经济学解释,通常优于纯粹基于图形的判断。
实战练习
平稳性转化:取A股沪深300指数的日收盘价(显然非平稳),尝试以下平稳化方法:(a) 对数差分(收益率),(b) 去线性趋势,(c) HP滤波去趋势。对每种结果进行ADF检验,比较三种方法的平稳化效果。
ARIMA预测挑战:用ARIMA模型尝试预测某只股票的次日收盘价。将数据分为训练集(前80%)和测试集(后20%),用滚动预测(每次用最新的数据重新拟合)的方法生成测试集的预测。比较预测方向和真实方向的准确率——能否显著高于50%?
配对交易完整实现:在同一个行业板块中(如银行、白酒、新能源),寻找至少三组潜在的协整配对。对每组分别做协整检验,选出p值最小的一组。设定形成期(如1年)和交易期(如6个月),回测配对交易策略,分析其收益的季节性(策略在哪些月份表现更好?)。
延伸阅读
- 《Time Series Analysis》—— James Hamilton,时间序列分析的圣经级教科书
- 《Analysis of Financial Time Series》—— Ruey Tsay,聚焦金融的时间序列教材
- 《Introductory Time Series with R》—— Cowpertwait and Metcalfe,入门友好的实践指南
- Engle, R. (1982): "Autoregressive Conditional Heteroscedasticity" —— GARCH模型的起源
- Engle, R. and Granger, C. (1987): "Co-integration and Error Correction" —— 协整理论奠基之作
本章要点
- 时间序列的平稳性是大多数统计方法的前提,ADF和KPSS检验联合使用可以给出可靠判断
- ARIMA模型用于捕捉序列的线性自相关结构,GARCH模型用于捕捉波动率的时变性
- 协整是配对交易的理论基础:非平稳序列的线性组合是平稳的,意味着二者之间存在长期的"引力"
- 金融时间序列建模的核心挑战是:信噪比极低,过度拟合极为容易
- 始终在建模前执行平稳性检验,在建模后执行残差诊断,在样本外验证模型表现