时间序列计量

预计学习时间:3-4 小时

难度:⭐⭐⭐⭐

核心问题:金融数据天然是时间序列,普通回归方法用在时间序列上会出大问题。如何正确分析时间序列数据?


概述

上一文件讲的是经典回归,适用于截面数据(同一时间点上不同资产)。但金融数据几乎都是时间序列——同一资产在不同时间点上的观测。

时间序列和截面数据有本质区别:

维度截面数据时间序列数据
观测排列不同个体同一个体在不同时间
独立性通常满足几乎不满足(今天和昨天有关)
平稳性不是问题核心问题
因果方向相对清晰可能互为因果
样本量通常够大金融数据常常不够长

如果在非平稳的时间序列上直接跑 OLS,你可能得到完全虚假的结果(回顾上一文件的虚假回归部分)。这是时间序列计量的核心关切。


一、平稳性:分析时间序列的第一步

1.1 什么是平稳性

白话解释:一个时间序列是平稳的,意思是它的”统计特性不随时间改变”。

具体来说,弱平稳(协方差平稳)要求:

  1. 均值恒定,不随时间变化
  2. 方差恒定,不随时间变化
  3. 自协方差只依赖时间间隔 只取决于 ,不取决于

反例:股票价格通常不是平稳的——价格有上升趋势,波动率也会变化。

正例:股票收益率通常是(近似)平稳的——收益率围绕零波动,方差相对稳定。

1.2 为什么不平稳的序列分析结果不可信

  1. 虚假回归:两个不相关的非平稳序列也可能表现出高度”相关”
  2. 均值回归失效:趋势序列会”跑掉”,不会回到均值
  3. 标准推断失效:t 检验、F 检验的统计量分布不再是标准分布

量化中的教训:在做任何时间序列分析之前,先检验平稳性。这就像建房子先检查地基。

1.3 ADF 检验(Augmented Dickey-Fuller Test)

原理

ADF 检验是最常用的单位根检验。它检验的是:

零假设 H0,即序列有单位根(非平稳) 备择假设 H1,即序列无单位根(平稳)

如果 p 值足够小(通常 < 0.05),我们拒绝 H0,认为序列是平稳的。

白话理解:ADF 检验在问”这个序列的当前值是否主要依赖于上一期的值?如果是,它就像随机游走,不是平稳的。“

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.stattools import adfuller
 
np.random.seed(42)
n = 500
 
# ============================================================
# 生成三种序列:平稳、有趋势、随机游走
# ============================================================
# 1. 平稳序列(白噪声 + 微弱自相关)
stationary = np.zeros(n)
for t in range(1, n):
    stationary[t] = 0.3 * stationary[t-1] + np.random.normal(0, 1)
 
# 2. 趋势序列(确定性趋势)
trend = np.arange(n) * 0.02 + np.random.normal(0, 1, n)
 
# 3. 随机游走(非平稳)
random_walk = np.cumsum(np.random.normal(0, 1, n))
 
# ============================================================
# ADF 检验
# ============================================================
def adf_test(series, name):
    """执行 ADF 检验并打印结果"""
    result = adfuller(series, autolag='AIC')
    print(f"\n{name}:")
    print(f"  ADF 统计量 = {result[0]:.4f}")
    print(f"  p 值 = {result[1]:.4f}")
    print(f"  使用滞后阶数 = {result[2]}")
    is_stationary = result[1] < 0.05
    print(f"  结论: {'平稳 ✅' if is_stationary else '非平稳 ❌'}")
    return is_stationary
 
print("=" * 50)
print("ADF 检验:判断序列是否平稳")
print("=" * 50)
 
adf_test(stationary, "平稳序列")
adf_test(trend, "趋势序列")
adf_test(random_walk, "随机游走")

1.4 PP 检验和 KPSS 检验

不同检验从不同角度判断平稳性。实践中建议同时用多种检验,互相验证。

检验零假设检验方向特点
ADF有单位根(非平稳)拒绝 → 平稳考虑高阶自相关
PP有单位根(非平稳)拒绝 → 平稳非参数方法,对异方差稳健
KPSS平稳拒绝 → 非平稳和 ADF 方向相反,适合互补
from statsmodels.tsa.stattools import adfuller, kpss
 
def full_stationarity_test(series, name):
    """综合平稳性检验:ADF + KPSS"""
    # ADF 检验(H0: 非平稳)
    adf_result = adfuller(series, autolag='AIC')
 
    # KPSS 检验(H0: 平稳)
    kpss_result = kpss(series, regression='c', nlags='auto')
 
    print(f"\n{name}:")
    print(f"  ADF:  统计量={adf_result[0]:.3f}, p={adf_result[1]:.4f} "
          f"{'→ 平稳' if adf_result[1] < 0.05 else '→ 非平稳'}")
    print(f"  KPSS: 统计量={kpss_result[0]:.3f}, p={kpss_result[1]:.4f} "
          f"{'→ 非平稳' if kpss_result[1] < 0.05 else '→ 平稳'}")
 
    # 综合判断
    adf_ok = adf_result[1] < 0.05   # ADF 拒绝 → 平稳
    kpss_ok = kpss_result[1] >= 0.05  # KPSS 不拒绝 → 平稳
    if adf_ok and kpss_ok:
        print(f"  综合: 平稳")
    elif not adf_ok and not kpss_ok:
        print(f"  综合: 非平稳")
    else:
        print(f"  综合: 结果不一致,需要进一步分析")
 
# 对三种序列做综合检验
full_stationarity_test(stationary, "平稳序列")
full_stationarity_test(trend, "趋势序列")
full_stationarity_test(random_walk, "随机游走")

1.5 差分平稳

大多数非平稳的金融时间序列,做一次差分后就变得平稳了。

股票价格是非平稳的,但收益率(对数差分)是近似平稳的。这就是为什么量化研究几乎都用收益率而不是价格。

# 对随机游走做一阶差分
diff_rw = np.diff(random_walk)
print(f"\n随机游走差分后的 ADF 检验:")
adf_test(diff_rw, "差分后的随机游走")
print(f"  差分前: 非平稳 → 差分后: 平稳 ✅")

二、协整:两个不平稳序列之间的稳定关系

2.1 协整的本质

白话解释:协整说的是——两个序列各自都不是平稳的(各自有趋势),但它们之间存在一个固定的线性组合,这个组合是平稳的。

例子:两个人的身高各自都在增长(非平稳),但他们的身高差保持相对稳定(协整)。

量化意义:如果你发现两个资产的价格序列有协整关系,意味着它们的价差是平稳的——价差偏离后会回归。这是配对交易的理论基础。

2.2 协整 vs 相关性(关键区别!)

这是一个非常重要的区分,很多人搞混:

维度相关性协整
要求序列平稳吗?理论上要求(但很多人忽略)不要求——序列可以非平稳
衡量什么?同步变化的程度长期均衡关系
假回归风险高(对非平稳序列)低(本身就是为非平稳设计的)
量化含义短期同步性强价差会回归——可做配对交易

核心教训:两个高度相关的序列不一定有协整关系,而有协整关系的序列相关性可能不高。

2.3 Engle-Granger 两步法

最简单的协整检验方法,分两步:

第一步:对两个序列做 OLS 回归,得到残差

第二步:对残差做 ADF 检验。如果残差平稳,则两个序列有协整关系。

import numpy as np
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller
 
np.random.seed(42)
n = 500
 
# ============================================================
# 模拟两个有协整关系的序列
# ============================================================
# 公共随机趋势
common_trend = np.cumsum(np.random.normal(0, 1, n))
 
# 序列 1 和序列 2 共享这个趋势,但有各自的偏移
y1 = common_trend + np.random.normal(0, 0.5, n)  # 序列 1
y2 = 1.5 * common_trend + 2 + np.random.normal(0, 0.8, n)  # 序列 2
 
# ============================================================
# Engle-Granger 两步法
# ============================================================
# 第一步:OLS 回归,得到残差
X = sm.add_constant(y1)
model = sm.OLS(y2, X).fit()
residuals = model.resid
 
# 第二步:对残差做 ADF 检验
# 注意:对残差做 ADF 时,不包含截距项和趋势项
adf_result = adfuller(residuals, maxlag=1, regression='n')
 
print("Engle-Granger 协整检验")
print("=" * 50)
print(f"  回归系数 beta = {model.params[1]:.4f}")
print(f"  残差 ADF 统计量 = {adf_result[0]:.4f}")
print(f"  残差 ADF p 值 = {adf_result[1]:.4f}")
print(f"  R² = {model.rsquared:.4f}")
 
# Engle-Granger 的临界值比标准 ADF 更严格
# 因为残差是估计出来的,不是直接观测的
eg_critical = {  # 近似临界值
    '1%': -3.90, '5%': -3.34, '10%': -3.04
}
print(f"\n  Engle-Granger 临界值(近似):")
for level, cv in eg_critical.items():
    print(f"    {level}: {cv:.2f}")
 
is_cointegrated = adf_result[0] < eg_critical['5%']
print(f"\n  结论: {'存在协整关系 ✅' if is_cointegrated else '不存在协整关系 ❌'}")
 
# ============================================================
# 对比:两个没有协整关系的独立随机游走
# ============================================================
rw1 = np.cumsum(np.random.normal(0, 1, n))
rw2 = np.cumsum(np.random.normal(0, 1, n))
 
X_ind = sm.add_constant(rw1)
model_ind = sm.OLS(rw2, X_ind).fit()
adf_ind = adfuller(model_ind.resid, maxlag=1, regression='n')
 
print(f"\n对比:两个独立随机游走")
print(f"  残差 ADF 统计量 = {adf_ind[0]:.4f}")
print(f"  残差 ADF p 值 = {adf_ind[1]:.4f}")
print(f"  R² = {model_ind.rsquared:.4f}")
print(f"  结论: {'存在协整关系' if adf_ind[0] < eg_critical['5%'] else '不存在协整关系 ❌'}")
print(f"  注意:即使 R² 可能不低,但残差不平稳 → 没有协整")

2.4 Johansen 检验:多变量协整

Engle-Granger 只能处理两个序列。当你有三个或更多序列时,用 Johansen 检验。

from statsmodels.tsa.vector_ar.vecm import coint_johansen
 
np.random.seed(42)
n = 500
 
# 模拟三个有协整关系的序列
common_trend = np.cumsum(np.random.normal(0, 1, n))
y1 = common_trend + np.random.normal(0, 0.5, n)
y2 = 1.5 * common_trend + 2 + np.random.normal(0, 0.8, n)
y3 = -common_trend + np.random.normal(0, 0.6, n)
 
data = np.column_stack([y1, y2, y3])
 
# Johansen 检验
result = coint_johansen(data, det_order=0, k_ar_diff=2)
 
print("=" * 50)
print("Johansen 协整检验")
print("=" * 50)
 
# 迹检验(Trace Test)
print("\n迹检验(Trace Test):")
for i in range(len(result.lr1)):
    print(f"  r <= {i}: 统计量={result.lr1[i]:.2f}, "
          f"临界值(5%)={result.cvt[i, 1]:.2f} → "
          f"{'拒绝' if result.lr1[i] > result.cvt[i, 1] else '不拒绝'}")
 
# 最大特征值检验
print("\n最大特征值检验:")
for i in range(len(result.lr2)):
    print(f"  r <= {i}: 统计量={result.lr2[i]:.2f}, "
          f"临界值(5%)={result.cvm[i, 1]:.2f} → "
          f"{'拒绝' if result.lr2[i] > result.cvm[i, 1] else '不拒绝'}")
 
print(f"\n协整秩(协整向量个数)的判断:")
print(f"  从 r=0 开始,逐个检验。第一个不能拒绝的 r 值就是协整秩。")

三、VAR 模型:多个时间序列互相影响

3.1 向量自回归

白话解释:VAR 模型考虑多个时间序列互相影响——不仅 A 影响 B,B 也会反过来影响 A。

简单的一阶 VAR(1):

量化应用

  • 利率和通胀的交互影响
  • 不同市场之间的溢出效应
  • 宏观因子对股票收益的传导

3.2 模型估计与预测

import numpy as np
import pandas as pd
from statsmodels.tsa.api import VAR
 
np.random.seed(42)
n = 500
 
# 模拟两个互相影响的时间序列
# y1 受自身滞后和 y2 滞后影响
# y2 受自身滞后和 y1 滞后影响
y1 = np.zeros(n)
y2 = np.zeros(n)
y1[0], y2[0] = 0, 0
 
for t in range(1, n):
    y1[t] = 0.5 * y1[t-1] + 0.3 * y2[t-1] + np.random.normal(0, 1)
    y2[t] = 0.2 * y1[t-1] + 0.4 * y2[t-1] + np.random.normal(0, 1)
 
data = pd.DataFrame({'y1': y1, 'y2': y2})
 
# ============================================================
# VAR 模型估计
# ============================================================
model = VAR(data)
 
# 选择最优滞后阶数
lag_order = model.select_order(maxlags=10)
print("滞后阶数选择:")
print(lag_order.summary())
print(f"\n推荐滞后阶数: {lag_order.aic}")
 
# 拟合模型
results = model.fit(maxlags=lag_order.aic, ic='aic')
print(f"\nVAR 模型结果:")
print(results.summary())
 
# ============================================================
# 预测
# ============================================================
forecast = results.forecast(data.values[-results.k_ar:], steps=10)
forecast_df = pd.DataFrame(forecast, columns=['y1', 'y2'],
                           index=pd.RangeIndex(start=n, stop=n+10, name='时间'))
 
print(f"\n未来 10 期预测:")
print(forecast_df.round(3))
 
# 置信区间
forecast_ci = results.forecast_interval(
    data.values[-results.k_ar:], steps=10, alpha=0.05
)
print(f"\n预测置信区间(95%):")
for i, var in enumerate(['y1', 'y2']):
    print(f"\n  {var}:")
    for step in range(min(5, 10)):
        print(f"    第{step+1}步: [{forecast_ci[0][step, i]:.3f}, "
              f"{forecast_ci[1][step, i]:.3f}]")

3.3 脉冲响应函数(IRF)

白话解释:IRF 回答的是——“如果我对变量 A 施加一个标准差的冲击,其他变量在未来各期会如何反应?”

这在量化中非常有用:比如你想知道”如果利率突然上升 1 个标准差,股市在未来 20 天会怎么走?“

# 脉冲响应分析
irf = results.irf(periods=20)
 
print("=" * 50)
print("脉冲响应函数(IRF)")
print("=" * 50)
print("问题:y1 受到一个标准差冲击后,y1 和 y2 如何变化?\n")
 
# y1 对 y1 的冲击的响应
irf_y1_y1 = irf.irfs[:, 0, 0]  # [期数, 响应变量, 冲击变量]
irf_y2_y1 = irf.irfs[:, 1, 0]  # y2 对 y1 冲击的响应
 
print("y1 自身冲击的传导路径:")
for t in range(min(10, len(irf_y1_y1))):
    print(f"  第 {t} 期: y1={irf_y1_y1[t]:.4f}, y2={irf_y2_y1[t]:.4f}")
 
# 绘制 IRF 图
fig = irf.plot(orth=False)
fig.suptitle('脉冲响应函数', fontsize=14)
plt.tight_layout()
plt.show()

3.4 方差分解

白话解释:方差分解回答的是——“在预测 y 的方差中,有多少是由 y 自身过去的冲击解释的?有多少是由其他变量的冲击解释的?“

# 方差分解
fevd = results.fevd(periods=20)
 
print("=" * 50)
print("预测误差方差分解")
print("=" * 50)
print("问题:预测 y1 和 y2 的误差中,各变量贡献了多少?\n")
 
fevd_table = fevd.summary()
print(fevd_table)
 
# 关键信息
print("\n关键结论:")
for i, var in enumerate(['y1', 'y2']):
    print(f"  {var} 的预测方差中:")
    for j, contrib in enumerate(['y1', 'y2']):
        print(f"    {contrib} 的贡献: {fevd.decomp[9][i, j]*100:.1f}%")

四、GARCH:波动率会”聚集”

4.1 ARCH 效应

白话解释:金融市场的波动率不是均匀的——“大波动跟大波动,小波动跟小波动”。这种现象叫做波动率聚集(Volatility Clustering)

2008 年金融危机期间,每天都是巨幅波动。而在平静的 2017 年,市场几乎不动。如果用简单的”固定方差”模型,完全捕捉不到这种现象。

4.2 GARCH(1,1) 模型

GARCH = Generalized ARCH(广义自回归条件异方差)

模型分两部分:

均值方程(建模收益率):

方差方程(建模波动率):

  • :基础波动水平
  • :昨日冲击(残差)对今日波动率的影响
  • :昨日波动率对今日波动率的影响
  • :保证方差有限(平稳条件)

白话理解

  • 大:市场对昨天的大幅波动反应强烈
  • 大:波动率有很强的惯性(持续性)
  • 典型金融数据:

4.3 Python 完整估计和预测

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from arch import arch_model
 
np.random.seed(42)
n = 1000
 
# ============================================================
# 模拟 GARCH(1,1) 过程
# ============================================================
omega_true = 0.02
alpha_true = 0.08
beta_true = 0.90
mu = 0.001  # 均值收益
 
returns = np.zeros(n)
volatility = np.zeros(n)
 
volatility[0] = np.sqrt(omega_true / (1 - alpha_true - beta_true))
returns[0] = mu + np.random.normal(0, volatility[0])
 
for t in range(1, n):
    volatility[t] = np.sqrt(omega_true + alpha_true * returns[t-1]**2
                            + beta_true * volatility[t-1]**2)
    returns[t] = mu + np.random.normal(0, volatility[t])
 
# ============================================================
# 拟合 GARCH(1,1) 模型
# ============================================================
# 使用 arch 库: pip install arch
model = arch_model(returns * 100, vol='Garch', p=1, q=1,
                   mean='Constant', dist='normal')
result = model.fit(disp='off')
 
print("=" * 60)
print("GARCH(1,1) 估计结果")
print("=" * 60)
print(result.summary())
 
print("\n参数对比:")
print(f"  {'参数':>8} {'真实值':>8} {'估计值':>8}")
print(f"  {'omega':>8} {omega_true:>8.4f} {result.params['omega']:>8.4f}")
print(f"  {'alpha':>8} {alpha_true:>8.4f} {result.params['alpha[1]']:>8.4f}")
print(f"  {'beta':>8} {beta_true:>8.4f} {result.params['beta[1]']:>8.4f}")
print(f"  {'alpha+beta':>8} {alpha_true+beta_true:>8.4f} "
      f"{result.params['alpha[1]']+result.params['beta[1]']:>8.4f}")
 
# ============================================================
# 波动率预测
# ============================================================
# 一步向前预测
forecast = result.forecast(horizon=5, reindex=False)
cond_vol = result.conditional_volatility
 
print(f"\n未来 5 期波动率预测:")
for i in range(5):
    print(f"  第 {i+1} 期: {np.sqrt(forecast.variance[-1, i]):.4f}")
 
# ============================================================
# 可视化
# ============================================================
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
 
# 1. 收益率
axes[0].plot(returns, alpha=0.7, linewidth=0.5)
axes[0].set_title('模拟收益率序列')
axes[0].set_ylabel('收益率')
 
# 2. 真实波动率 vs 估计波动率
axes[1].plot(volatility, alpha=0.7, label='真实波动率', linewidth=1)
axes[1].plot(cond_vol / 100, alpha=0.7, label='GARCH 估计波动率', linewidth=1)
axes[1].set_title('真实 vs 估计波动率')
axes[1].legend()
axes[1].set_ylabel('波动率')
 
# 3. 条件波动率
axes[2].plot(cond_vol, alpha=0.7, color='red', linewidth=0.8)
axes[2].set_title('GARCH 条件波动率')
axes[2].set_ylabel('波动率')
 
plt.tight_layout()
plt.show()

4.4 ARCH 效应检验

在做 GARCH 建模之前,应该先检验数据是否存在 ARCH 效应。

from statsmodels.stats.diagnostic import het_arch
 
# 对残差做 ARCH-LM 检验
# 先拟合一个简单的均值模型
from statsmodels.tsa.ar_model import AutoReg
ar_model = AutoReg(returns, lags=1).fit()
residuals = ar_model.resid
 
# ARCH-LM 检验
arch_test = het_arch(residuals, nlags=10)
print("ARCH-LM 检验(检验是否存在波动率聚集):")
print(f"  LM 统计量 = {arch_test[0]:.4f}")
print(f"  p 值 = {arch_test[1]:.4f}")
print(f"  结论: {'存在 ARCH 效应,应使用 GARCH 模型' if arch_test[1] < 0.05 else '不存在 ARCH 效应'}")

五、误差修正模型(VECM)

5.1 长期关系 vs 短期调整

白话解释:VECM 把协整关系(长期均衡)和短期动态调整结合起来。

假设 A 股和 B 股有长期协整关系(价差围绕某个均值波动),那么:

  • 长期:价差保持稳定(协整关系)
  • 短期:如果价差偏离均值,会有一个”拉回”的力量

VECM 把这两种力量都建模了:

其中 就是上一期的偏离, 是调整速度( 表示会拉回)。

5.2 Python 实现

from statsmodels.tsa.vector_ar.vecm import VECM
import numpy as np
import pandas as pd
 
np.random.seed(42)
n = 500
 
# 模拟两个有协整关系的序列
common_trend = np.cumsum(np.random.normal(0, 1, n))
y1 = common_trend + np.random.normal(0, 0.5, n)
y2 = 1.5 * common_trend + 2 + np.random.normal(0, 0.8, n)
 
data = pd.DataFrame({'y1': y1, 'y2': y2})
 
# 确定 coint_rank(协整秩)先用 Johansen 检验
# 这里已知有 1 个协整关系
vecm = VECM(data, k_ar_diff=2, coint_rank=1, deterministic='ci')
result = vecm.fit()
 
print("=" * 50)
print("VECM 估计结果")
print("=" * 50)
print(result.summary())
 
# 关键参数
print("\n关键参数解读:")
print(f"  调整速度系数 alpha: {result.alpha}")
print(f"  协整向量 beta: {result.beta}")
print(f"  alpha < 0 表示:偏离均衡后会被拉回(均值回归)")

六、结构突变检验:关系中是否发生了断裂

6.1 Chow 检验

白话解释:Chow 检验回答的是——“在某个时间点前后,两个序列之间的关系是否发生了变化?”

这在量化中非常重要。市场结构、监管政策、交易制度都可能改变变量之间的关系。如果你忽略结构突变,整个回归结果可能都是错的。

import numpy as np
import statsmodels.api as sm
from scipy import stats
 
np.random.seed(42)
n = 200
 
# ============================================================
# 模拟有结构突变的数据
# ============================================================
# 前半段:y = 1 + 2x
# 后半段:y = 1 + 4x(斜率变化)
breakpoint = 100
 
x = np.random.normal(0, 1, n)
y = np.zeros(n)
 
# 前半段
y[:breakpoint] = 1 + 2 * x[:breakpoint] + np.random.normal(0, 0.5, breakpoint)
# 后半段——关系变了!
y[breakpoint:] = 1 + 4 * x[breakpoint:] + np.random.normal(0, 0.5, n - breakpoint)
 
# ============================================================
# Chow 检验
# ============================================================
X = sm.add_constant(x)
 
# 全样本回归
model_full = sm.OLS(y, X).fit()
SSR_full = np.sum(model_full.resid ** 2)
 
# 前半段回归
model_1 = sm.OLS(y[:breakpoint], X[:breakpoint]).fit()
SSR_1 = np.sum(model_1.resid ** 2)
 
# 后半段回归
model_2 = sm.OLS(y[breakpoint:], X[breakpoint:]).fit()
SSR_2 = np.sum(model_2.resid ** 2)
 
# 计算 F 统计量
k = 2  # 参数个数
n1, n2 = breakpoint, n - breakpoint
SSR_restricted = SSR_1 + SSR_2
SSR_unrestricted = SSR_full
 
# F = [(SSR_r - SSR_u)/k] / [SSR_u / (n1+n2-2k)]
# 注意:这里 SSR_full 是有约束的(全样本用同一组参数)
F_chow = ((SSR_restricted - SSR_unrestricted) / k) / \
         (SSR_unrestricted / (n1 + n2 - 2 * k))
p_value = 1 - stats.f.cdf(F_chow, k, n1 + n2 - 2 * k)
 
print("=" * 50)
print("Chow 结构突变检验")
print("=" * 50)
print(f"  断裂点: 第 {breakpoint} 期")
print(f"  前半段斜率: {model_1.params[1]:.3f}")
print(f"  后半段斜率: {model_2.params[1]:.3f}")
print(f"  F 统计量: {F_chow:.4f}")
print(f"  p 值: {p_value:.4e}")
print(f"  结论: {'存在结构突变!' if p_value < 0.05 else '不存在结构突变'}")
 
# ============================================================
# 对比:没有结构突变的情况
# ============================================================
y_no_break = 1 + 2 * x + np.random.normal(0, 0.5, n)
 
model_full_nb = sm.OLS(y_no_break, X).fit()
SSR_full_nb = np.sum(model_full_nb.resid ** 2)
model_1_nb = sm.OLS(y_no_break[:breakpoint], X[:breakpoint]).fit()
SSR_1_nb = np.sum(model_1_nb.resid ** 2)
model_2_nb = sm.OLS(y_no_break[breakpoint:], X[breakpoint:]).fit()
SSR_2_nb = np.sum(model_2_nb.resid ** 2)
 
F_chow_nb = ((SSR_1_nb + SSR_2_nb - SSR_full_nb) / k) / \
            (SSR_full_nb / (n1 + n2 - 2 * k))
p_value_nb = 1 - stats.f.cdf(F_chow_nb, k, n1 + n2 - 2 * k)
 
print(f"\n对比:没有结构突变的数据")
print(f"  F 统计量: {F_chow_nb:.4f}")
print(f"  p 值: {p_value_nb:.4f}")
print(f"  结论: {'存在结构突变' if p_value_nb < 0.05 else '不存在结构突变 ✅'}")

本文件要点回顾

概念一句话总结
平稳性统计特性不随时间改变,时间序列分析的基石
ADF 检验检验序列是否有单位根(是否非平稳)
协整两个非平稳序列存在一个平稳的线性组合(配对交易基础)
Engle-Granger两步法协整检验:回归 → 残差 ADF
Johansen多变量协整检验
VAR多个时间序列互相影响的建模框架
IRF一个冲击如何传播到各个变量
方差分解预测方差中各变量的贡献比例
GARCH波动率聚集的建模方法
VECM长期协整关系 + 短期调整
Chow 检验检验关系中是否发生了断裂

下一站

掌握了时间序列工具后,我们进入面板数据方法——当你同时有多个资产和多个时间点时,怎么利用这个二维结构提取更多信息。