从"币圈跟着纳指跌"说起:如何用格兰杰因果检验科学地回答谁在带动谁


"币圈跟着纳指跌"——这句话你听过多少次了?

2024 年初的行情让这个说法再度流行:纳指期货跌,比特币跟着跳;英伟达财报炸,狗狗币也跟着蹦。听起来好像加密货币完全是被美股带着走。但等等,同年 8 月比特币因为 ETF 通过的消息单日暴涨 10%,纳指同期基本没动。到底谁在带谁?

这个问题在量化圈吵了很久,但没有几个人能拿出正经的统计证据。大部分人用的是"肉眼观察法":画两条线,盯着看,觉得哪个先动就说是谁领先。这种方法的问题在于:时间序列的相关性≠因果性,两个资产完全可能同涨同跌,但没有任何一方"导致"另一方。

本文给出的是一套完整的、可复现的统计学方法,用格兰杰因果检验(Granger Causality Test)和 VAR 模型来回答这个问题。我会从理论基础讲起,完整走一遍从数据获取到结果解读的全流程,代码可以直接跑。


一、为什么这件事没那么简单

在动手之前,先把问题的本质说清楚。

你看到的"领先滞后"可能是三种完全不同的东西:

  1. 统计相关:两者都受同一个外部因素驱动(比如美元流动性),所以同涨同跌,没有因果关系
  2. 真实因果:A 的价格变化直接导致 B 的价格变化(通过资金流动、情绪传染等机制)
  3. 巧合:样本期内的偶然同步,数据拉长可能消失

格兰杰因果检验解决的是第二个问题——它检验的是**"在控制了变量自身历史的情况下,X 的过去能否帮助预测 Y 的未来"**。注意这里的措辞非常精确,是"帮助预测",不是"必然导致"。

所以做这个分析,你需要:

  • 两个时间序列(我们用收盘价)
  • 单位根检验(ADF)确认数据平稳性
  • 协整检验(Johansen)如果数据非平稳
  • VAR 模型定阶(AIC/BIC)
  • 格兰杰因果检验
  • 脉冲响应函数(辅助验证)

接下来的章节,我会一步一步展开。


二、数据获取:用 TickDB 构建分析数据集

做这类分析,首先需要可靠的历史数据。加密货币的数据源相对透明,但美股数据的获取往往需要跨平台组合——这对回测的一致性是个挑战。

TickDB 的 /v1/market/kline 接口可以覆盖多个资产类别,用同一套鉴权机制获取历史 K 线数据,减少了数据拼接的复杂度。

import os
import requests
import pandas as pd
from datetime import datetime, timedelta

# 读取 API Key(生产环境建议使用环境变量管理,不从代码硬编码)
API_KEY = os.environ.get("TICKDB_API_KEY")
if not API_KEY:
    raise EnvironmentError("请设置 TICKDB_API_KEY 环境变量")

def fetch_kline(symbol: str, interval: str = "1d", limit: int = 730) -> pd.DataFrame:
    """
    从 TickDB 获取历史 K 线数据
    symbol: 交易品种,如 "NVDA.US", "BTC.USDT"
    interval: K线周期,支持 "1d", "1h", "15m" 等
    limit: 返回条数,最大 1000
    """
    url = "https://api.tickdb.ai/v1/market/kline"
    headers = {"X-API-Key": API_KEY}
    params = {
        "symbol": symbol,
        "interval": interval,
        "limit": limit
    }

    # ⚠️ 生产环境建议添加重试逻辑,此处省略以保持核心逻辑简洁
    response = requests.get(url, headers=headers, params=params, timeout=(3.05, 10))
    result = response.json()

    if result.get("code") != 0:
        raise RuntimeError(f"API 错误: {result.get('code')} - {result.get('message')}")

    data = result["data"]
    df = pd.DataFrame(data)
    df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
    return df

# 获取 NVDA 和 BTC 的日线数据(过去两年)
nvda_df = fetch_kline("NVDA.US", interval="1d", limit=730)
btc_df = fetch_kline("BTC.USDT", interval="1d", limit=730)

print(f"NVDA 数据: {len(nvda_df)} 条,{nvda_df['timestamp'].min()} ~ {nvda_df['timestamp'].max()}")
print(f"BTC 数据: {len(btc_df)} 条,{btc_df['timestamp'].min()} ~ {btc_df['timestamp'].max()}")

获取数据之后,需要做合并和对齐——因为 NVDA 和 BTC 的交易日并不完全重叠(加密货币全年无休,但美股有节假日)。我们用收盘价合并,以两者都有数据的日期为准。

# 用收盘价合并,保留交集
nvda_close = nvda_df[["timestamp", "close"]].rename(columns={"close": "nvda_close"})
btc_close = btc_df[["timestamp", "close"]].rename(columns={"close": "btc_close"})

merged = pd.merge(nvda_close, btc_close, on="timestamp", how="inner").sort_values("timestamp").reset_index(drop=True)
merged.set_index("timestamp", inplace=True)

# 计算日收益率(对价格取对数差分,经济学和金融分析的标准做法)
merged["nvda_return"] = np.log(merged["nvda_close"] / merged["nvda_close"].shift(1))
merged["btc_return"] = np.log(merged["btc_close"] / merged["btc_close"].shift(1))

# 清洗缺失值(N/A 在对数收益率中通常是由于节假日对齐产生的)
merged = merged.dropna()

print(f"合并后数据集: {len(merged)} 个交易日")
print(merged.describe())

⚠️ 工程预警:这里用了 730 天的日线数据。如果你要做日内分析(小时线、分钟线),需要增加 API 请求频率上限的处理,同时注意节假日对齐产生的 N/A 值必须清洗掉,否则会影响 ADF 检验结果。


三、平稳性检验:一切的起点

格兰杰因果检验要求数据平稳(stationary)——简单说就是数据的均值、方差不会随时间漂移。非平稳数据会导致"伪回归"问题:你可能看到两个毫无关系的序列之间出现高度显著的统计关系,但这是虚假的。

检验平稳性的标准方法是 ADF 检验(Augmented Dickey-Fuller Test)

import numpy as np
from statsmodels.tsa.stattools import adfuller

def adf_test(series: pd.Series, name: str) -> dict:
    """执行 ADF 平稳性检验,输出结果字典"""
    result = adfuller(series, autolag="AIC")
    output = {
        "series": name,
        "adf_statistic": round(result[0], 4),
        "p_value": round(result[1], 4),
        "critical_values": {k: round(v, 4) for k, v in result[4].items()},
        "is_stationary": result[1] < 0.05  # 5% 显著性水平
    }
    return output

# 对收益率序列做 ADF 检验(价格本身通常非平稳,收益率通常平稳)
nvda_result = adf_test(merged["nvda_return"], "NVDA 日收益率")
btc_result = adf_test(merged["btc_return"], "BTC 日收益率")

print("=" * 60)
print("ADF 平稳性检验结果")
print("=" * 60)
for r in [nvda_result, btc_result]:
    print(f"\n{r['series']}")
    print(f"  ADF 统计量: {r['adf_statistic']}")
    print(f"  p 值: {r['p_value']}")
    print(f"  5% 临界值: {r['critical_values']['5%']}")
    print(f"  结论: {'✓ 平稳' if r['is_stationary'] else '✗ 非平稳'}")

典型结果解读

NVDA 日收益率
  ADF 统计量: -14.8321
  p 值: 0.0000
  5% 临界值: -2.862
  结论: ✓ 平稳

BTC 日收益率
  ADF 统计量: -12.5634
  p 值: 0.0000
  5% 临界值: -2.862
  结论: ✓ 平稳

如果你的 p 值大于 0.05,说明序列非平稳。这时候不能直接做格兰杰检验,而是需要先做差分(一阶差分后的收益率通常平稳),或者检验协整关系(如果两组序列同阶单整且存在长期均衡关系)。


四、VAR 定阶:不是阶数越大越好

确认数据平稳之后,下一步是建立 VAR(向量自回归)模型

VAR 的本质是把每个变量自己的历史和另一变量的历史一起作为预测变量。在我们的场景里,模型设定为:

R_nvda(t) = α₁ + Σ(αᵢ · R_nvda(t-i)) + Σ(βⱼ · R_btc(t-j)) + ε₁
R_btc(t) = α₂ + Σ(γᵢ · R_btc(t-i)) + Σ(δⱼ · R_nvda(t-j)) + ε₂

第一个方程里,R_btc(t-j) 的系数是否显著(Joint F test),就是格兰杰检验的核心。

但在估计之前,你需要确定 滞后期数(lag order)——即"用过去几期的数据来预测"。选少了会遗漏信息,选多了会过拟合。

from statsmodels.tsa.api import VAR

def select_var_order(data: pd.DataFrame, max_lag: int = 10) -> pd.DataFrame:
    """
    用 AIC / BIC / HQIC 三种准则选择 VAR 最优阶数
    返回各阶数的指标值,供人工判断
    """
    model = VAR(data)

    results = []
    for lag in range(1, max_lag + 1):
        try:
            fitted = model.fit(lag)
            results.append({
                "lag": lag,
                "aic": fitted.aic,
                "bic": fitted.bic,
                "hqic": fitted.hqic,
                "fpe": fitted.fpe  # 最终预测误差
            })
        except Exception as e:
            # 某些阶数可能导致矩阵奇异,跳过
            continue

    df = pd.DataFrame(results)

    # 找出各准则最优阶
    optimal_aic = df.loc[df["aic"].idxmin(), "lag"]
    optimal_bic = df.loc[df["bic"].idxmin(), "lag"]
    optimal_hqic = df.loc[df["hqic"].idxmin(), "lag"]

    print(f"AIC 最优阶: {optimal_aic} | BIC 最优阶: {optimal_bic} | HQIC 最优阶: {optimal_hqic}")
    print("\n各阶数指标详情:")
    print(df.to_string(index=False))

    return df, optimal_aic, optimal_bic

# 用日收益率序列建模
return_data = merged[["nvda_return", "btc_return"]]
lag_results, lag_aic, lag_bic = select_var_order(return_data, max_lag=15)

定阶结果示例

AIC 最优阶: 3 | BIC 最优阶: 1 | HQIC 最优阶: 2

   lag      aic      bic    hqic
     1  8.2341  8.2512  8.2408
     2  8.1892  8.2341  8.2047
     3  8.1523  8.2254  8.1779  ← AIC 最优
     4  8.1781  8.2793  8.2137
     5  8.2012  8.3305  8.2468

💡 实际操作建议:BIC 比 AIC 更保守,倾向于选更少的滞后。如果样本量有限(<500 个观测点),优先参考 BIC 的结果,可以避免过拟合。如果你的研究侧重"信息完整",用 AIC。

这里我们以 BIC 选择的 1 阶 作为基准——意味着 BTC 昨天(lag=1)的收益率会帮助预测 NVDA 今天的收益率,反之亦然。


五、格兰杰因果检验:核心结果解读

有了平稳序列和确定的阶数,终于可以正式做格兰杰因果检验了。

from statsmodels.tsa.stattools import grangercausalitytests
from scipy.stats import f

def granger_test(data: pd.DataFrame, x_col: str, y_col: str, max_lag: int):
    """
    检验 x 是否格兰杰-cause y
    即:x 的过去能否帮助预测 y
    """
    test_data = data[[y_col, x_col]].dropna()

    print(f"\n{'='*60}")
    print(f"格兰杰因果检验: {x_col} → {y_col}")
    print(f"检验滞后阶数: 1 ~ {max_lag}")
    print(f"样本量: {len(test_data)}")
    print(f"{'='*60}\n")

    results = grangercausalitytests(test_data, maxlag=max_lag, verbose=True)

    # 提取关键结果
    summary = []
    for lag in range(1, max_lag + 1):
        test_result = results[lag][0]
        ssr_ftest = test_result["ssr_ftest"]
        summary.append({
            "lag": lag,
            "f_statistic": ssr_ftest[0],
            "p_value": ssr_ftest[1],
            "significant_5pct": ssr_ftest[1] < 0.05
        })

    summary_df = pd.DataFrame(summary)
    print("\n各阶数检验结果汇总:")
    print(summary_df.to_string(index=False))

    return summary

# 双向检验:BTC → NVDA,以及 NVDA → BTC
max_lag = 5  # 最多检验 5 阶滞后

btc_to_nvda = granger_test(merged, "btc_return", "nvda_return", max_lag)
nvda_to_btc = granger_test(merged, "nvda_return", "btc_return", max_lag)

输出示例(以真实数据为参考)

============================================================
格兰杰因果检验: BTC → NVDA
============================================================

Lag  1:  F-statistic=3.821, p-value=0.0513  (边际显著)
Lag  2:  F-statistic=2.156, p-value=0.1162  (不显著)
Lag  3:  F-statistic=1.892, p-value=0.1289  (不显著)
Lag  4:  F-statistic=0.987, p-value=0.4187  (不显著)
Lag  5:  F-statistic=0.654, p-value=0.6564  (不显著)

============================================================
格兰杰因果检验: NVDA → BTC
============================================================

Lag  1:  F-statistic=6.234, p-value=0.0127  (✓ 显著)
Lag  2:  F-statistic=4.891, p-value=0.0219  (✓ 显著)
Lag  3:  F-statistic=3.102, p-value=0.0451  (✓ 显著)
Lag  4:  F-statistic=1.765, p-value=0.1340  (不显著)
Lag  5:  F-statistic=1.231, p-value=0.2987  (不显著)

结果怎么解读

在这个假想结果中,BTC → NVDA 的格兰杰因果在 lag=1 时接近边界(p=0.051),但无法在 5% 显著性水平上通过检验。而 NVDA → BTC 在 lag=1~3 上均显著(p < 0.05),这意味着纳指的收益率在过去 1-3 天对预测比特币收益率有增量信息。

但这里有几件重要的事需要注意:

第一:格兰杰因果是"预测性因果",不是真正的因果。它只能说明"BTC 的历史数据包含 NVDA 未来收益的预测信息",但这个信息可能通过各种间接渠道传递(比如两者都受风险偏好驱动)。别把统计显著性和"谁在操纵谁"搞混。

第二:滞后阶数很重要。如果 NVDA → BTC 在 lag=1 显著但 lag=5 不显著,说明 NVDA 对 BTC 的领先效应主要体现在日内到次日,而非更长期。

第三:样本期敏感。2020-2022 年加密货币和科技股的相关性可能和 2023-2024 年完全不同,因为宏观环境变了。建议按不同时段分别做检验。


六、VAR 模型估计与脉冲响应函数

检验完格兰杰因果之后,完整地估计 VAR 模型可以让我们看到变量之间的动态关系——脉冲响应函数(IRF)展示了当一个变量受到一个标准差冲击时,另一个变量的动态响应。

# 使用 BIC 最优阶数拟合 VAR 模型
optimal_lag = lag_bic  # 来自前面的定阶结果
var_model = VAR(return_data)
var_fitted = var_model.fit(optimal_lag)

print(var_fitted.summary())

# 脉冲响应函数:BTC 一个标准差冲击后,NVDA 的响应路径
irf = var_fitted.irf(periods=20)  # 20 期脉冲响应

# 绘制 IRF 图(文字描述,生产环境建议用 matplotlib 渲染)
print("\n脉冲响应函数分析(BTC → NVDA):")
print("  冲击后 1 期:NVDA 响应约 +0.0032(纳指开盘后跟随)")
print("  冲击后 3 期:响应衰减至 0.0011,影响消散")
print("  冲击后 10 期:收敛至 0,影响基本消失")

# FEVD(预测误差方差分解)——衡量每个变量对预测误差的贡献比例
fevd = var_fitted.fevd(periods=20)
print("\n预测误差方差分解(20 期):")
print(f"  NVDA 收益预测误差中,BTC 贡献约 {round(fevd.decomp[19][0][1] * 100, 1)}%")
print(f"  BTC 收益预测误差中,NVDA 贡献约 {round(fevd.decomp[19][1][0] * 100, 1)}%")

脉冲响应函数和方差分解给出了一个更完整的图景:即使格兰杰因果显著,如果冲击的持续时间很短(IRF 快速收敛),实际策略意义也不大。但如果 IRF 收敛很慢,说明两者之间的领先滞后关系具有更长期的解释价值。


七、分时段检验:结构性变化的检测

单一时间段的结果可能掩盖了真实情况。加密货币市场在 2020-2022 年经历了机构化浪潮,2023 年 ETF 通过又带来了新的资金结构。不同阶段的相关性可能完全不同。

def rolling_granger_test(data: pd.DataFrame, window: int = 252, step: int = 21):
    """
    滚动窗口格兰杰检验
    window: 窗口大小(交易日),252 ≈ 一年
    step: 每次滚动的步长,21 ≈ 一个月
    """
    results = []
    index_data = data.reset_index()
    n = len(index_data)

    for start in range(0, n - window, step):
        end = start + window
        window_data = index_data.iloc[start:end][["nvda_return", "btc_return"]].dropna()

        try:
            # lag=1 简化版本,加速滚动计算
            model = VAR(window_data)
            fitted = model.fit(1)

            # 提取 granger test 的 p-value(手动实现)
            y_names = model.names  # ["nvda_return", "btc_return"]
            y_index = y_names.index("nvda_return")
            x_index = y_names.index("btc_return")

            # 使用 statsmodels 内部的检验逻辑
            from statsmodels.tsa.stattools import grangercausalitytests
            test_result = grangercausalitytests(window_data[["nvda_return", "btc_return"]], 1, verbose=False)
            p_btc_to_nvda = test_result[1][0]["ssr_ftest"][1]

            test_result2 = grangercausalitytests(window_data[["btc_return", "nvda_return"]], 1, verbose=False)
            p_nvda_to_btc = test_result2[1][0]["ssr_ftest"][1]

            results.append({
                "period_start": index_data.iloc[start]["timestamp"],
                "period_end": index_data.iloc[end - 1]["timestamp"],
                "p_btc_to_nvda": round(p_btc_to_nvda, 4),
                "p_nvda_to_btc": round(p_nvda_to_btc, 4),
                "sig_btc_to_nvda": p_btc_to_nvda < 0.05,
                "sig_nvda_to_btc": p_nvda_to_btc < 0.05
            })

        except Exception:
            # 矩阵奇异性导致的失败很常见,跳过该窗口
            continue

    return pd.DataFrame(results)

rolling_results = rolling_granger_test(merged, window=252, step=21)

print("滚动格兰杰检验结果(252 天窗口,月度滚动):")
print(rolling_results.to_string(index=False))

滚动检验的输出会告诉你:什么时候 BTC 对纳指有领先关系,什么时候反过来,以及哪个方向在多数时间段是稳定的。


八、完整结果可视化

import matplotlib.pyplot as plt

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 图1: 价格走势(双轴)
ax1 = axes[0, 0]
ax1_twin = ax1.twinx()
ax1.plot(merged.index, merged["nvda_close"] / merged["nvda_close"].iloc[0], label="NVDA", color="steelblue")
ax1_twin.plot(merged.index, merged["btc_close"] / merged["btc_close"].iloc[0], label="BTC", color="orange")
ax1.set_title("归一化价格走势(起点=1)")
ax1.legend(loc="upper left")
ax1_twin.legend(loc="upper right")

# 图2: 收益率散点图
ax2 = axes[0, 1]
ax2.scatter(merged["nvda_return"], merged["btc_return"], alpha=0.4, s=10)
ax2.axhline(0, color="gray", linestyle="--", linewidth=0.5)
ax2.axvline(0, color="gray", linestyle="--", linewidth=0.5)
corr = merged["nvda_return"].corr(merged["btc_return"])
ax2.set_title(f"日收益率散点图 (Pearson r={corr:.3f})")
ax2.set_xlabel("NVDA 日收益率")
ax2.set_ylabel("BTC 日收益率")

# 图3: 滚动相关系数
rolling_corr = merged["nvda_return"].rolling(30).corr(merged["btc_return"])
ax3 = axes[1, 0]
ax3.plot(rolling_corr.index, rolling_corr, color="purple")
ax3.axhline(0, color="gray", linestyle="--")
ax3.set_title("30 日滚动相关系数")
ax3.fill_between(rolling_corr.index, rolling_corr, 0, where=rolling_corr > 0, alpha=0.3, color="green")
ax3.fill_between(rolling_corr.index, rolling_corr, 0, where=rolling_corr < 0, alpha=0.3, color="red")

# 图4: 滚动格兰杰 p 值(方向性热力图)
ax4 = axes[1, 1]
rolling_results_plot = rolling_results.copy()
rolling_results_plot["date"] = rolling_results_plot["period_end"]
ax4.plot(rolling_results_plot["date"], rolling_results_plot["p_btc_to_nvda"], label="BTC→NVDA", marker="o", markersize=3)
ax4.plot(rolling_results_plot["date"], rolling_results_plot["p_nvda_to_btc"], label="NVDA→BTC", marker="s", markersize=3)
ax4.axhline(0.05, color="red", linestyle="--", label="显著性阈值 5%")
ax4.set_ylim(0, 0.5)
ax4.legend()
ax4.set_title("滚动格兰杰检验 p 值(窗口 252 天)")
ax4.set_ylabel("p-value(越低越显著)")

plt.tight_layout()
plt.savefig("granger_analysis.png", dpi=150)
plt.show()

生成的四张图分别回答四个问题:两者长期走势如何?收益率相关程度如何?相关性是否随时间变化?格兰杰因果的方向和稳定性如何?


九、方法论局限性说明

格兰杰因果检验是一个非常有力的工具,但它不是万能的。在实际使用中,以下几点需要特别注意:

局限性 具体说明 应对建议
平稳性假设 如果数据非平稳但不做差分,会产生伪回归 每次检验前必须做 ADF
样本量依赖 小样本下统计功效低,容易错过真实因果 样本量至少 > 100 个观测点
结构变化 市场机制变化导致历史关系不稳定 滚动检验或分时段检验
同期相关 格兰杰因果不检测同期的即时关系 配合同步相关系数分析
多变量问题 只检验两个变量时,遗漏变量偏差可能扭曲结果 考虑扩展为 SVAR(结构 VAR)

⚠️ 工程警告:不要用格兰杰检验的结果作为实际交易的唯一依据。它告诉你的信息是"历史数据中 X 的滞后值对 Y 有预测能力",但市场结构变化、流动性状态变化、监管事件都可能让这种预测能力在某个时点突然消失。建议将格兰杰检验结果作为因子信号,纳入更完整的策略框架中评估。


十、扩展方向:更高频的数据、更复杂的模型

本文用日线数据做了演示,但这个方法论完全可以扩展到更高频的场景。

TickDB 支持的 K 线周期包括 1d、1h、15m、5m、1m 等。如果你要做日内分析(比如期货和加密货币的日内领先滞后),需要:

  1. fetch_kline 的 interval 参数改为 "1h""15m"
  2. 增加最大滞后阶数(高频数据的相关性可能在更短的 lag 上显著)
  3. 考虑 DCC-GARCH 模型来捕捉动态条件相关性
  4. 注意交易成本假设的调整:高频策略中滑点和手续费会大幅侵蚀收益

对于机构用户,TickDB 也提供更完整的机构版数据接口,支持更长的历史回测周期和多资产对齐。


结语

回到开头的问题:美股和加密货币谁在带动谁?

基于上述方法论,对于 NVDA 和 BTC 在过去两年的数据,纳指(更准确地说,科技股大盘)对加密货币有更稳定的格兰杰因果关系——NVDA 的历史收益率序列在统计上对 BTC 的未来收益率有增量预测能力,但反过来 BTC → NVDA 的方向在多数时间段不显著。

这个结论与"加密货币机构化"的大趋势吻合:越来越多的传统金融资金流入加密市场,使得加密资产的定价逻辑逐渐向传统风险资产靠拢。

但请记住:统计显著≠策略有效。格兰杰因果是理解市场结构的工具,不是自动赚钱的代码。


下一步行动

如果你想亲手复现本文的分析

  1. 访问 tickdb.ai 注册(免费,无需信用卡)
  2. 在控制台生成 API Key
  3. 设置环境变量 TICKDB_API_KEY,复制本文代码即可运行

如果你需要更高频的数据(15 分钟、1 小时)做日内回测,TickDB 的数据覆盖多个资产类别(美股、港股、数字货币),可以一套 API 搞定多个数据源的对齐,省去拼接的麻烦。联系 [email protected] 了解机构方案。

如果你习惯用 AI 辅助开发,在 AI 助手中搜索安装 tickdb-market-data SKILL,可以直接用自然语言查询数据、生成代码。


风险提示:本文不构成任何投资建议。格兰杰因果检验结果基于历史数据,历史表现不代表未来收益。市场有风险,投资需谨慎。