Skip to content

2.4 时间序列分析

概念详解

时间序列分析的量化意义

金融数据本质上都是时间序列——从tick级的价格跳动到月度的宏观经济指标,时间维度是量化分析中最核心的组织维度。时间序列分析回答的是一个根本问题:过去的数据模式能否为未来提供有用信息?

传统计量经济学假定数据是"独立同分布"的(横截面数据通常满足),但金融时间序列天然不满足这个假设——今天和昨天的价格是高度相关的。这既是挑战(许多标准统计方法不再适用),也是机遇(相关性本身可能包含可交易的信号)。

名词解释:平稳性

时间序列的统计特性(均值、方差、自相关)不随时间变化。ADF检验常用于检测单位根。

名词解释:协整

两个或多个非平稳序列的线性组合是平稳的,表明它们之间存在长期均衡关系,常用于配对交易。

名词解释:自相关 (Autocorrelation)

时间序列与其自身滞后值之间的相关性。ACF图展示各阶滞后的相关系数,是识别序列结构的基本工具。

时间序列的分解

经典时间序列分解将序列分解为三个成分:

Y_t = T_t + S_t + R_t

其中:

  • Tt趋势成分 (Trend) —— 长期方向性变化
  • St季节成分 (Seasonal) —— 固定周期重复的模式(金融中的月初效应、季末效应)
  • Rt残差成分 (Residual) —— 扣除趋势和季节后的剩余变化

在金融中,我们通常关心的是 Rt ——残差序列是否有可预测的结构,即是否存在alpha。

数学原理

平稳性的正式定义

一个时间序列 {Yt}弱平稳(协方差平稳)的,当且仅当:

  1. E[Yt]=μ(均值恒定)
  2. Var(Yt)=γ0(方差恒定)
  3. Cov(Yt,Ytk)=γk(自协方差仅依赖于滞后阶数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
  • H0γ=0(存在单位根,序列非平稳)
  • H1γ<0(序列平稳)

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的基础上增加了差分项:

  • 如果原始序列需要差分 d 次才能平稳,则 d 为差分阶数
  • 金融价格序列通常需要 d=1(一次差分变为收益率)

GARCH模型(波动率建模)

收益率序列通常没有均值方向的预测力("预测明天涨跌很难"),但波动率具有强烈的可预测结构:

\sigma_t^2 = \omega + \alpha \varepsilon_{t-1}^2 + \beta \sigma_{t-1}^2

GARCH(1,1) 是金融中最常用的波动率模型:

  • ω:基础波动水平
  • α:最近冲击对波动率的影响(α 大 = 对近期事件敏感)
  • β:波动率的持续性(β 大 = 波动率变化慢)
  • α+β 接近1时,波动率具有长记忆性

协整检验

Engle-Granger两步法:

  1. 估计长期均衡关系:Yt=α+βXt+εt
  2. 检验残差 εt 是否平稳

Johansen检验可以同时检测多个协整关系,适用于3个及以上的时间序列。

Python实战

案例1:Python进行协整检验

PYTHON3 行 · 128 B
📄此处有展示代码3 行 · 128 B展开 ▼
python
from statsmodels.tsa.stattools import coint
score, pvalue, _ = coint(df['X'], df['Y'])
print(f"协整检验p值: {pvalue:.4f}")
点击展开可浏览运行结果
📘 本段为代码片段(仅展示函数/类定义,未提供运行入口,无需运行)

案例2:完整的平稳性诊断与ARIMA建模

PYTHON140 行 · 4.8 KB
📄此处有展示代码140 行 · 4.8 KB展开 ▼
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:协整配对发现与交易

PYTHON129 行 · 5.2 KB
📄此处有展示代码129 行 · 5.2 KB展开 ▼
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)进行网格搜索,同时兼顾模型的简洁性和经济学解释,通常优于纯粹基于图形的判断。

实战练习

  1. 平稳性转化:取A股沪深300指数的日收盘价(显然非平稳),尝试以下平稳化方法:(a) 对数差分(收益率),(b) 去线性趋势,(c) HP滤波去趋势。对每种结果进行ADF检验,比较三种方法的平稳化效果。

  2. ARIMA预测挑战:用ARIMA模型尝试预测某只股票的次日收盘价。将数据分为训练集(前80%)和测试集(后20%),用滚动预测(每次用最新的数据重新拟合)的方法生成测试集的预测。比较预测方向和真实方向的准确率——能否显著高于50%?

  3. 配对交易完整实现:在同一个行业板块中(如银行、白酒、新能源),寻找至少三组潜在的协整配对。对每组分别做协整检验,选出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模型用于捕捉波动率的时变性
  • 协整是配对交易的理论基础:非平稳序列的线性组合是平稳的,意味着二者之间存在长期的"引力"
  • 金融时间序列建模的核心挑战是:信噪比极低,过度拟合极为容易
  • 始终在建模前执行平稳性检验,在建模后执行残差诊断,在样本外验证模型表现