← Retour aux articles
August 7, 2026
5 min de lecture

Toda-Yamamoto vs Differenced Granger: Does the BTC Lead-Lag Survive?

Toda-Yamamoto vs Differenced Granger: Does the BTC Lead-Lag Survive?
#causal-inference
#Granger-causality
#lead-lag
#crypto
#VAR

Presque tous les résultats de causalité Granger publiés sur la cryptographie sont calculés sur les résultats des journaux. Ce choix n’est pas libre. La différence rend la série stationnaire, ce que requiert le test F standard, mais elle élimine également la relation de niveau – et si deux pièces sont cointégrées, le VAR différencié est mal spécifié et le test répond à une question légèrement différente de celle que vous avez posée.

Toda et Yamamoto (1995) proposent une solution : installez le VAR sur des niveaux avec des décalages supplémentaires, testez uniquement les niveaux d'origine et obtenez un résultat valide. χ2\chi^2 indépendamment des racines unitaires ou de la cointégration. Il est bien connu en économétrie et ne s’applique quasiment jamais au lead-lag des cryptos.

Cet article fait donc une chose : exécutez les deux tests sur les mêmes paires, sur la même fenêtre, et signalez s'ils ne sont pas d'accord. Appliquez ensuite le suivi honnête : une correction de tests multiples de familles corrélées sur la matrice résultante et une valeur p mobile pour voir si le décalage "significatif" est suffisamment stable pour être échangé ou scintille simplement au-delà du seuil.

La théorie ci-dessous ne constitue que ce qui est nécessaire pour rendre la comparaison lisible. Le blog couvre déjà les racines unitaires, la cointégration, le chargement des données, le dimensionnement et les coûts ; ce sont des liens, pas des sections.

Ce que prétend réellement la causalité de Granger

Voies directionnelles avance-retard dans les événements de marché

La causalité de Granger est une préséance prédictive, pas un mécanisme : XX Granger-causes YY si passé XX réduit la variance de l'erreur de prévision de YY au-delà de quel passé YY explique déjà.

XX Granger-causes YY si σ2(YtYt1,,Ytp)>σ2(YtYt1,,Ytp,Xt1,,Xtp)\sigma^2(Y_t \mid Y_{t-1}, \ldots, Y_{t-p}) > \sigma^2(Y_t \mid Y_{t-1}, \ldots, Y_{t-p}, X_{t-1}, \ldots, X_{t-p})

Deux séries motivées par un facteur caché commun peuvent montrer une causalité Granger sans lien direct entre elles. En crypto, cette mise en garde n'est pas académique - [BTC est un facteur dominant unique dans l'ensemble du complexe altcoin] (/en/blog/post/signal-correlation-pairs), donc presque toute « causalité » alt-alt est un artefact candidat de vitesses de réponse différentes au même choc BTC.

Le cadre VAR

Le test se trouve dans une autorégression vectorielle. Pour deux variables avec pp décalages :

Yt=α0+i=1pαiYti+i=1pβiXti+ϵ1tY_t = \alpha_0 + \sum_{i=1}^{p} \alpha_i Y_{t-i} + \sum_{i=1}^{p} \beta_i X_{t-i} + \epsilon_{1t} Xt=γ0+i=1pγiXti+i=1pδiYti+ϵ2tX_t = \gamma_0 + \sum_{i=1}^{p} \gamma_i X_{t-i} + \sum_{i=1}^{p} \delta_i Y_{t-i} + \epsilon_{2t}

Sous forme matricielle, un VAR(p) avec kk variables :

yt=c+A1yt1++Apytp+ut\mathbf{y}_t = \mathbf{c} + \mathbf{A}_1 \mathbf{y}_{t-1} + \cdots + \mathbf{A}_p \mathbf{y}_{t-p} + \mathbf{u}_t

Tester si XX Granger-causes YY teste la restriction conjointe H0:β1==βp=0H_0: \beta_1 = \cdots = \beta_p = 0 dans la première équation. La voie standard est un test F sur les sommes des carrés résiduelles restreintes et non restreintes,

F=(RSSrRSSu)/pRSSu/(T2p1)F = \frac{(RSS_r - RSS_u) / p}{RSS_u / (T - 2p - 1)}

avec la forme de Wald asymptotiquement équivalente W=T(RSSrRSSu)/RSSudχ2(p)W = T \cdot (RSS_r - RSS_u)/RSS_u \xrightarrow{d} \chi^2(p). Tout ce qui suit est une question de savoir à quels coefficients cette restriction s'applique et sur quelles données le VAR a été ajusté.

Sélection du décalage

pp compte plus que ne l'admettent la plupart des articles : trop peu de décalages manquent la dynamique, trop brûlent des degrés de liberté et détruisent la puissance.

AIC(p)=lnΣ^u(p)+2pk2T,BIC(p)=lnΣ^u(p)+pk2lnTT\text{AIC}(p) = \ln|\hat{\Sigma}_u(p)| + \frac{2pk^2}{T}, \qquad \text{BIC}(p) = \ln|\hat{\Sigma}_u(p)| + \frac{pk^2 \ln T}{T} HQIC(p)=lnΣ^u(p)+2pk2ln(lnT)T\text{HQIC}(p) = \ln|\hat{\Sigma}_u(p)| + \frac{2pk^2 \ln(\ln T)}{T}

Plages de recherche raisonnables : 1 à 60 sur les données en secondes, 1 à 30 sur les données en minutes, 1 à 48 sur les données horaires. BIC est ici la bonne valeur par défaut – c’est le critère le plus parcimonieux, et le décalage dans les paires de crypto liquides est un phénomène de mémoire courte. Chaque résultat ci-dessous rapporte le BIC sélectionné pp par paire plutôt que de corriger un décalage à travers l'univers, car un décalage fixe convertit silencieusement un choix de sélection de décalage en une revendication de signification.

Pourquoi le test différencié n'est pas la bonne valeur par défaut

La différenciation peut fragmenter une relation avance-retard

Les séries de prix des cryptomonnaies sont I(1). La solution habituelle – prendre des retours logarithmiques – achète la stationnarité au détriment du niveau de cointégration, et si la paire partage un équilibre à long terme, la différence VAR est mal spécifiée. La machinerie de racine unitaire et de cointégration derrière cette phrase (ADF, Engle-Granger avec ses valeurs critiques de Monte-Carlo, Johansen sur un système VAR) est déjà couverte dans arbitrage statistique et trading de paires ; supposons-le ici. Le problème est simplement que le remède habituel – la différence, puis le test – est une décision de modélisation lourde de conséquences, et Toda-Yamamoto est le moyen d’éviter de la prendre.

La procédure Toda-Yamamoto

Ajuster un VAR avec p+dmaxp + d_{max} est en retard, où pp est l'ordre de décalage optimal et dmaxd_{max} est l'ordre d'intégration maximum de la série, alors testez les restrictions uniquement sur le premier pp est en retard. Le supplément dmaxd_{max} les décalages absorbent la non-stationnarité ; la statistique de Wald sur le premier pp les coefficients suivent une norme χ2(p)\chi^2(p) que les séries soient I(0), I(1) ou cointégrées.

  1. Déterminer dmaxd_{max} — ADF et KPSS sur chaque série. Pour les prix des cryptomonnaies dmax=1d_{max} = 1 presque toujours.
  2. Sélectionner pp — installez un VAR sur les niveaux, choisissez par BIC.
  3. Estimez le VAR augmenté (p+dmaxp + d_{max}) par niveaux, pas de différence.
  4. Wald-testez le premier pp est uniquement en retard, en ignorant le dmaxd_{max} des extras. Le résultat est χ2(p)\chi^2(p).

Le résultat : pas de pré-test de cointégration, pas de différenciation et une taille de test correcte – le taux de rejet sous la valeur nulle reste proche du nominal quelles que soient les propriétés d'intégration. Le coût est que l'étape 4 n'est pas celle test_causality le fait par défaut, c'est là que la plupart des implémentations tournent mal.

Implémentation

Flux de mise en œuvre de la matrice de décalage

Supposons que vous ayez déjà aligné les barres de minutes dans un DataFrame - le passe-partout de récupération et d'indexation ccxt est dans [détection de régime avec HMM] (/en/blog/post/regime-detection-hmm-adaptive-trading), et le wrapper ADF que vous utiliseriez pour l'étape 1 est dans [arbitrage statistique et trading de paires] (/en/blog/post/statistical-arbitrage-pairs-trading-crypto).

Une note de version avant que tout cela ne s'exécute : grangercausalitytests(..., verbose=False) était obsolète puis supprimé dans statsmodels 0.15. Supprimez l'argument (la fonction ne s'imprime plus par défaut) ou épinglez statsmodels<0.15. Le code ci-dessous assume la signature moderne.

Toda-Yamamoto, fait correctement

La restriction doit être construite à la main. VARResults.test_causality teste tous les retards de la variable provoquante dans le modèle ajusté — sur un VAR augmenté (p+1p+1) qui comprend le dmaxd_{max} retard dans la restriction, ce qui est exactement ce que Toda-Yamamoto dit de ne pas faire. Le correctif est explicite RR matrice par rapport à la covariance complète du coefficient :

import numpy as np
from scipy.stats import chi2
from statsmodels.tsa.api import VAR


def toda_yamamoto_test(data, target, predictor, max_lag=15, d_max=1,
                       significance=0.05):
    """
    Toda-Yamamoto Granger causality on levels (no differencing).

    Fits VAR(p + d_max) and applies a Wald test to the predictor's
    coefficients at lags 1..p ONLY, leaving the d_max augmenting lags
    unrestricted. Statistic is chi2(p).
    """
    pair = data[[target, predictor]].dropna()
    n_vars = pair.shape[1]
    target_idx = list(pair.columns).index(target)
    pred_idx = list(pair.columns).index(predictor)

    model = VAR(pair)
    p = model.select_order(maxlags=max_lag).bic or 1
    res = model.fit(p + d_max)

    beta = res.params.values.ravel(order="F")
    cov = res.cov_params()
    cov = cov.values if hasattr(cov, "values") else np.asarray(cov)

    n_per_eq = res.params.shape[0]
    idx = [target_idx * n_per_eq + 1 + lag * n_vars + pred_idx
           for lag in range(p)]                      # first p lags only

    R = np.zeros((p, beta.size))
    R[np.arange(p), idx] = 1.0

    Rb = R @ beta
    V = R @ cov @ R.T
    wald = float(Rb @ np.linalg.solve(V, Rb))        # no pinv: V must be PD
    p_value = float(chi2.sf(wald, df=p))

    return {
        "target": target, "predictor": predictor,
        "p": p, "augmented_lag": p + d_max, "d_max": d_max,
        "wald_stat": wald, "p_value": p_value,
        "significant": p_value < significance,
    }

Deux détails sur lesquels il est facile de se tromper et qui invalident le test si vous le faites. Premièrement, l'arithmétique de l'index doit correspondre à la façon dont les modèles de statistiques s'aplatissent params — vérifier sur un modèle équipé que beta[target_idx * n_per_eq + 1] est égal res.params.iloc[1, target_idx] avant de faire confiance à une valeur p. Deuxièmement, utilisez np.linalg.solve, pas pinv: si VV est singulier, la restriction est dégénérée et l'exécution devrait échouer bruyamment plutôt que de renvoyer un nombre apparemment plausible. Un pseudo-inverse ici est la façon dont les tests Wald brisés survivent à la révision du code.

Granger différencié standard, à titre de comparaison

La référence par rapport à laquelle la comparaison est effectuée : log-retours, mêmes paires, même décalage BIC :

from statsmodels.tsa.stattools import grangercausalitytests


def differenced_granger(data, target, predictor, p):
    """Standard Granger test on log-returns at a fixed lag p."""
    pair = data[[target, predictor]].dropna()   # returns, not levels
    out = grangercausalitytests(pair, maxlag=[p])   # statsmodels >= 0.15
    f_stat, p_value = out[p][0]["ssr_ftest"][:2]
    return {"target": target, "predictor": predictor, "lag": p,
            "f_stat": f_stat, "p_value": p_value}

Note maxlag=[p] plutôt que maxlag=p: passer un int s'exécute à chaque décalage de 1 à pp et vous incite à signaler le meilleur, qui est un test multiple non enregistré en plus du test multiple que vous exécutez déjà.

La comparaison

Pour chaque paire, effectuez le test différentiel sur les rendements et Toda-Yamamoto sur les niveaux, au même BIC sélectionné. pp, et tabulez là où les deux ne sont pas d'accord. Le désaccord est la cellule intéressante : une paire significative sur les différences mais pas sur les niveaux est un artefact candidat de différenciation dans une relation de cointégration ; l'inverse suggère que la relation de niveau contient des informations que le test de retour ne peut pas voir.

Comparaisons multiples sur une matrice de causalité

Filtrer une grande matrice de causalité

Le plein n×nn \times n c'est dans la matrice que cela devient dangereux. Un balayage de 20 actifs et 10 décalages correspond à 3 800 tests d'hypothèse, et Bonferroni est la mauvaise correction pour une famille aussi corrélée - les tests partagent des données, partagent le facteur BTC et sont loin d'être indépendants, donc Bonferroni est à la fois trop conservateur dans son ensemble et trompeur quant aux cellules qui survivent. Utilisez la machinerie effective-N de l'article dégonflé de Sharpe, qui est conçue exactement pour cette situation : regroupez la famille de tests corrélés, comptez les essais indépendants plutôt que les essais bruts, et établissez un seuil par rapport à cela.

def granger_matrix(data, max_lag=15, d_max=1):
    """Toda-Yamamoto p-value matrix. Entry (i, j): does col_j cause col_i?"""
    cols = list(data.columns)
    out = pd.DataFrame(np.nan, index=cols, columns=cols, dtype=float)
    for target in cols:
        for predictor in cols:
            if target == predictor:
                continue
            r = toda_yamamoto_test(data, target, predictor,
                                   max_lag=max_lag, d_max=d_max)
            out.loc[target, predictor] = r["p_value"]
    return out

Le nombre qui compte n’est pas le nombre de cellules inférieures à 0,05, mais le nombre de cellules qui survivent au seuil corrigé de N effectif.

Lisez la matrice survivante de manière structurelle, et non cellule par cellule : les lignes où un actif en entraîne plusieurs autres confirment une hiérarchie d'informations ; une colonne provoquée par rien suggère une dynamique idiosyncrasique spécifique à un jeton plutôt qu'une découverte.

Le test honnête : la stabilité au roulement

Signal stable via des fenêtres déroulantes

Une seule valeur p dans l’échantillon est presque sans valeur pour le trading. La question pertinente est de savoir si la signification persiste. Les relations entre actifs dans la cryptographie dépendent du régime — la structure de corrélation diffère fortement entre le calme et la panique, et le régime lui-même est estimable (HMM régime Detection) — donc un décalage estimé sur une fenêtre est une déclaration sur cette fenêtre.

def rolling_granger(data, target, predictor, window=500, lag=5, step=50):
    """Rolling-window p-value: when is the lead-lag active vs dormant?"""
    rows = []
    for start in range(0, len(data) - window, step):
        chunk = data.iloc[start:start + window][[target, predictor]].dropna()
        if len(chunk) < window * 0.8:
            continue
        try:
            out = grangercausalitytests(chunk, maxlag=[lag])
            f_stat, p_value = out[lag][0]["ssr_ftest"][:2]
        except Exception:
            f_stat, p_value = np.nan, np.nan
        rows.append({"timestamp": data.index[start + window - 1],
                     "f_stat": f_stat, "p_value": p_value})
    return pd.DataFrame(rows).set_index("timestamp")

Rapportez deux éléments de cette série : la fraction de fenêtres inférieure à 0,05 et le nombre de franchissements de seuil. Une relation significative dans 80 % des fenêtres à trois croisements est un objet différent d'une relation significative dans 55 % des fenêtres à quarante croisements, même si la valeur p poolée est identique. Le second n'est pas négociable : vous ne pouvez pas dimensionner une position sur un signal dont l'existence change toutes les quelques centaines de barres, et le décalage de réestimation garantit que vous négociez toujours le régime précédent.

Le piège à données spécifique à Granger

Les ticks cryptographiques irréguliers perturbent les signaux causals

L’hygiène générique des données cryptographiques est couverte ailleurs – lacunes et bougies manquantes dans backtest-live parity, discipline d’horodatage dans la look-ahead biais taxonomie et prouvant l’absence d’anticipation sur plusieurs périodes. Un mode de défaillance, cependant, est spécifique à Granger et suffisamment grave pour être nommé :

Le remplissage vers l'avant fabrique le résultat. Une bougie remplie vers l'avant répète la clôture précédente, ce qui injecte une autocorrélation pure dans la série - et une autocorrélation de décalage 1 dans YY qui est mécaniquement aligné avec XX est impossible à distinguer, au test F, d'une relation causale à un décalage. La même chose s'applique à tous les sites : un décalage d'horloge d'une seconde entre deux échanges sur les barres des minutes peut créer ou détruire purement et simplement l'importance du décalage 1, car il déplace le côté de la limite de la barre sur lequel un mouvement atterrit. Avant d'exécuter quoi que ce soit, vérifiez que les espaces sont supprimés plutôt que comblés et que les deux séries sont estampillées de la même horloge.

Résultats d'avance et de retard rapportés dans la littérature

Constellation de preuves pour la recherche plomb-lag

Ce sont des citations et non des mesures de cet article. Ils sont ici pour dire ce que prétend le dossier publié, de sorte que la comparaison ci-dessus ait quelque chose avec quoi être d'accord ou en désaccord.

  • BTC vers altcoins. Une étude de 2026 sur Marchés financiers de l'Asie-Pacifique (Springer) rapporte une transmission de prix à haute fréquence du Bitcoin aux altcoins, les pièces à petite capitalisation montrant des réponses considérablement retardées et une liquidité plus faible associée à des réactions plus lentes. Les décalages couramment cités dans cette ligne de travail – environ 1 à 3 minutes entre BTC et ETH, plus longs pour les moyennes et petites capitalisations, de plus en plus unidirectionnels à mesure que la capitalisation boursière diminue – sont des résultats cités, et non redirigés ici. La capitalisation boursière sur laquelle ils reposent est la même échelle utilisée dans la validation multi-symboles.
  • CEX dirige DEX. Recherche sur la microstructure cryptographique (MDPI) rapporte que des sites centralisés dominent la découverte des prix, avec un flux d'informations allant de CEX à DEX et sans causalité inverse significative – ce qui est cohérent avec les moteurs de correspondance inférieurs à la milliseconde par rapport au règlement en temps bloc.
  • ETH en tant que pièce indépendante. Travail VAR-SVAR (MDPI) trouve des régimes dans lesquels Ethereum agit comme source et Bitcoin comme destinataire des retombées, notamment pendant les phases pilotées par DeFi et NFT.

Échange croisé, brièvement

Le décalage entre la même pièce et deux sites est une véritable configuration, mais ses aspects économiques - les seuils de frais, l'échelle de latence, la raison pour laquelle le décalage existe - sont déjà abordés dans [l'arbitrage statistique et le trading de paires] (/en/blog/post/statistical-arbitrage-pairs-trading-crypto) et [la prime kimchi] (/en/blog/post/kimchi-premium-arbitrage). La seule chose que Granger ajoute est une question de stabilité : la causalité entre Binance et Coinbase est-elle persistante significative, ou est-ce qu'elle scintille comme tout le reste ? Exécutez le test de roulement sur la paire de sites ; la réponse est une série chronologique de valeur p, pas un oui.

Transformer un décalage survivant en position

Le seuil de signal devient une position contrôlée

Si une paire efface le seuil corrigé et le test de roulement, le signal lui-même est trivial : retour cumulatif du prédicteur sur la fenêtre de décalage, seuillé.

def lead_lag_signal(btc_returns, alt_returns, lag=5, threshold=0.001):
    """+1 / -1 / 0 on ALT from BTC's cumulative return over `lag` bars."""
    btc_cum = btc_returns.rolling(lag).sum()
    signal = pd.Series(0, index=alt_returns.index)
    signal[btc_cum > threshold] = 1
    signal[btc_cum < -threshold] = -1
    return signal

Trois choses qui ne font pas partie de cet article car déjà réglées ici :

Limites

Limites d'incertitude causale

La causalité de Granger n'est pas une causalité. Les facteurs de confusion (un événement macro déplaçant les deux jambes avec un léger désalignement de l'horodatage), les pilotes communs (deux alts suivant BTC à des vitesses différentes) et les variables omises (test de BTC vers DOGE sans ETH dans le système) produisent tous des statistiques significatives sans flux d'informations direct. Se limiter aux tests bivariés, comme le fait cet article, aggrave le problème des variables omises, au lieu de l'améliorer.

Linéarité. Le test ne voit qu'une structure prédictive linéaire dans la moyenne conditionnelle. La crypto a un regroupement de volatilité, des effets de levier et des changements de régime – voir Prévision de la volatilité GARCH et Corrélation dynamique DCC-GARCH pour l'histoire du deuxième instant. Alternatives non linéaires à connaître : le test du noyau de Diks-Panchenko (2006), l'entropie de transfert comme analogue de la théorie de l'information et la causalité de Granger de la copule sur toute la distribution conjointe.

Ruptures structurelles. Toute relation estimée sur une fenêtre est conditionnelle au régime de cette fenêtre ; le test continu ci-dessus est le diagnostic minimum, et c'est un diagnostic, pas un correctif.

Résumé

Signal avance-retard robuste distillé à partir du bruit

  1. Ajustez le VAR sur niveaux, sélectionnez pp par BIC par paire, augmenté de dmaxd_{max}, et Wald-teste le premier pp seulement des retards. Construisez la matrice de restriction à la main - celle de la bibliothèque test_causality limite également le décalage d'augmentation, ce qui n'est pas Toda-Yamamoto.
  2. Exécutez le test de retour différencié comme référence sur les mêmes paires et décalages, et signalez les désaccords. Ce tableau est la découverte ; une matrice d'astérisques ne l'est pas.
  3. Corrigez la matrice pour une famille de tests corrélés via N efficace, et non Bonferroni, et indiquez le nombre de cellules qui survivent.
  4. Roulez-le. Indiquez la fraction de fenêtres significatives et le nombre de croisements. Il est important que le scintillement ne soit pas un décalage négociable, quelle que soit la valeur p regroupée.
  5. Supprimez les écarts au lieu de combler les écarts et vérifiez l'alignement de l'horloge sur tous les sites, avant de croire à un résultat de décalage 1.

Un très bon résultat de cette procédure est négatif : une relation BTC-alt qui est significative dans l'échantillon, survit à la correction et franchit toujours la ligne 0,05 dans les deux sens toutes les quelques centaines de barres est un résultat réel sur la négociabilité et mérite d'être publié comme tel.

Références

blog.disclaimer

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Gardez une longueur d'avance sur le marché

Abonnez-vous à notre newsletter pour des insights exclusifs sur le trading IA, des analyses de marché et des mises à jour de la plateforme.

Nous respectons votre vie privée. Désabonnement possible à tout moment.