📝

Draft article

This draft is visible to admins and superusers only. Sign in with an authorized account.

← 返回文章列表
August 24, 2026
5 分鐘閱讀

Transfer Entropy: Which Way Does Information Flow Between Crypto Assets?

#causal-inference
#transfer-entropy
#information-theory
#crypto
#network

This blog has already established that crypto is close to a one-factor market: signal correlation between pairs puts PC1 (the BTC factor) at 65% of variance and shows that four factors explain 90%, so ten "diversified" pairs carry the information of three or four independent ones. It has also established that the dependence structure is not even constant: DCC-GARCH models the correlation matrix as a time series with its own regimes, and its Limitations section says the quiet part out loud — "correlation is not causation, and not direction."

That last clause is the gap this article addresses. DCC tells you when dependence tightens. It cannot tell you which way it points. Transfer entropy can: it is a model-free, inherently asymmetric measure of how many bits the past of one series contributes to predicting the future of another, beyond what that target's own past already explains. If ETH consistently sends information to AAVE before AAVE reprices, transfer entropy sees the asymmetry that no correlation matrix can represent.

What follows is the measure, its estimator, and — the part that matters more than the measure — the null calibration and sensitivity analysis that determine whether any measured information-flow network is signal or decoration.

The Measured Network

Status: not yet run. The pipeline below is implemented but has not been executed on real data for this article. Until it is, this article is a method writeup, not a result. The blog's own standard — see the honest negative result and deflated Sharpe and multiple testing — is that an uncalibrated network is noise until proven otherwise.

Four things have to be reported here, in this order, and none of them may be asserted from the literature:

  1. The effective-TE matrix. Universe, exchange, date range, bar size, effective TE in bits per ordered pair, p-value per pair, and the directed graph that survives the significance filter. This replaces the usual hand-waving ("BTC is the dominant source, DeFi tokens are sinks") with an out-strength / in-strength / net-flow table.
  2. The false-positive rate of the significance test, measured by running the entire pipeline on shuffled and on synthetic-independent series. See Null Calibration below.
  3. A sensitivity sweep over the two free knobs: n_bins in {3, 5} crossed with k in {1, 2, 3, 5}. If the leader/follower ranking only holds at one cell of that grid, that is the finding.
  4. Stability over time. Does the measured ranking persist out of sample, or does it re-shuffle every window? Nothing downstream is worth writing until this is answered.

The Machinery

Three building blocks

Shannon entropy measures the uncertainty of a random variable XX:

H(X)=i=1np(xi)log2p(xi)H(X) = -\sum_{i=1}^{n} p(x_i) \log_2 p(x_i)

A coin flip is 1 bit; a fair die is log262.58\log_2 6 \approx 2.58 bits; a deterministic variable is 0.

Conditional entropy measures how much uncertainty about YY survives knowing XX:

H(YX)=x,yp(x,y)log2p(x,y)p(x)H(Y|X) = -\sum_{x,y} p(x,y) \log_2 \frac{p(x,y)}{p(x)}

It is 0 when XX determines YY, and equals H(Y)H(Y) when they are independent.

Mutual information is the shared information between them:

I(X;Y)=H(Y)H(YX)=H(X)H(XY)I(X;Y) = H(Y) - H(Y|X) = H(X) - H(X|Y)

It is symmetric — I(X;Y)=I(Y;X)I(X;Y) = I(Y;X) — and that symmetry is exactly what has to be broken.

Transfer entropy

Transfer entropy, introduced by Schreiber (2000), breaks the symmetry by conditioning on the target's own past. It measures how much the past of source XX reduces uncertainty about the future of target YY, beyond what the past of YY already explains.

Let Yt(k)Y_t^{(k)} denote the kk-length history (Yt1,,Ytk)(Y_{t-1}, \dots, Y_{t-k}) and Xt(l)X_t^{(l)} the ll-length source history:

TXY=p(Yt+1,Yt(k),Xt(l))log2p(Yt+1Yt(k),Xt(l))p(Yt+1Yt(k))T_{X \to Y} = \sum p(Y_{t+1}, Y_t^{(k)}, X_t^{(l)}) \log_2 \frac{p(Y_{t+1} \mid Y_t^{(k)}, X_t^{(l)})}{p(Y_{t+1} \mid Y_t^{(k)})}

Equivalently, as a difference of conditional entropies:

TXY=H(Yt+1Yt(k))H(Yt+1Yt(k),Xt(l))T_{X \to Y} = H(Y_{t+1} \mid Y_t^{(k)}) - H(Y_{t+1} \mid Y_t^{(k)}, X_t^{(l)})

If the past of XX does not help predict YY beyond YY's own past, TXY=0T_{X \to Y} = 0. If it does, the magnitude is the number of bits of predictive information that flow.

Key properties:

  • Non-negative: TXY0T_{X \to Y} \geq 0.
  • Asymmetric: in general TXYTYXT_{X \to Y} \neq T_{Y \to X}. This is the whole point.
  • Non-parametric: no model assumptions — linear and nonlinear dependencies alike.
  • Units: bits with log2\log_2, nats with ln\ln.

Net transfer entropy gives the dominant direction:

TXYnet=TXYTYXT_{X \to Y}^{\text{net}} = T_{X \to Y} - T_{Y \to X}

Positive means XX is a net sender to YY; negative means YY leads.

Transfer Entropy vs. Granger Causality

Granger causality (GC) asks whether the past of XX improves a linear autoregressive prediction of YY. This is not the Engle-Granger cointegration test covered in statistical arbitrage and pairs trading — same surname, unrelated concept. Engle-Granger asks whether a linear combination of two non-stationary series is stationary; Granger causality asks whether one series helps forecast another. Readers of this blog will conflate them, so it is worth saying plainly.

Theoretical equivalence for Gaussian processes

Barnett, Barrett, and Seth (2009) proved that for jointly Gaussian processes, Granger causality and transfer entropy are equivalent up to a monotonic transformation:

TXY=12ln(1+FXY)T_{X \to Y} = \frac{1}{2} \ln\left(1 + F_{X \to Y}\right)

where FXYF_{X \to Y} is the Granger causality statistic (a log-likelihood ratio). For linear Gaussian data you recover exactly the same causal structure from either method. Transfer entropy is therefore not a competitor to GC so much as its non-parametric generalization: it reduces to GC precisely where GC's assumptions hold.

Where they diverge

Property Granger causality Transfer entropy
Model assumption Linear VAR None (model-free)
Nonlinear dependencies Misses them Captures them
Distribution assumption Gaussian (for the F-test) None
Sample requirements Moderate High
Computation Fast (OLS) Slow (density estimation)
Interpretation Predictive improvement Information transfer (bits)

The gap matters in crypto in principle: returns are heavy-tailed, volatility clusters, and relationships are regime-dependent, all of which a linear VAR flattens. Whether the gap is large enough to change the measured leader ranking on real data is an empirical question this article must answer, not assume.

Dimpfl and Peter (2013) report that transfer entropy detects flows Granger causality misses in financial series, particularly under stress; Keskin and Aste (2020) report a richer network from nonlinear TE in crypto. Both are cited here as motivation, not as evidence for this dataset.

Effective Transfer Entropy: the Bias Correction

This is the part that is easy to get wrong. Raw TE estimates are biased upward in finite samples: plug in two independent series and you will still estimate TXY>0T_{X \to Y} > 0, purely from sampling noise in the empirical joint distribution. The bias grows with the number of joint states, which grows exponentially in k+lk + l.

Effective transfer entropy subtracts that bias:

TXYeff=TXYE ⁣[TXsurrogateY]T_{X \to Y}^{\text{eff}} = T_{X \to Y} - \mathbb{E}\!\left[T_{X_{\text{surrogate}} \to Y}\right]

where the surrogate destroys the temporal structure of XX while preserving its marginal distribution. Note what this is and is not: the surrogate mean is an estimate of the estimator's own bias, and we subtract it. It is not a confidence interval, and effective TE is not "TE with error bars." The same surrogate ensemble does double duty as a null distribution for a p-value, but the subtraction and the test are separate uses.

The surrogate is generated by a block bootstrap so that the autocorrelation of XX within a block survives — the general mechanics and rationale of block resampling are covered in Monte Carlo and bootstrap for backtests.

Implementation

Data: hourly log returns for a fixed universe of major USDT perpetuals from a single exchange, over a stated contiguous date range. Standard OHLCV fetch and log-return construction is boilerplate covered in statistical arbitrage and pairs trading and is not repeated.

Discretization — and a leakage warning

Transfer entropy needs discrete states. Quantile binning is the usual choice, and it is also the usual place to leak the future.

import numpy as np
import pandas as pd

def discretize_trailing(series, n_bins=3, warmup=500):
    """Discretize using bin edges estimated on a TRAILING window only.

    Computing quantile edges over the full sample is whole-series
    normalization leakage: every bar's label depends on the entire
    future distribution. See the look-ahead bias taxonomy.
    """
    x = np.asarray(series, dtype=float)
    out = np.full(len(x), -1, dtype=int)
    qs = np.linspace(0, 1, n_bins + 1)[1:-1]
    for t in range(warmup, len(x)):
        edges = np.quantile(x[:t], qs)      # strictly past data
        out[t] = np.digitize(x[t], edges)
    return out

The naive full-sample version is fine for a descriptive claim — "over this period, information flowed this way" — and is contaminated for any tradable claim. That distinction is the whole subject of the look-ahead bias taxonomy. If you use full-sample edges, say so and stop before the word "signal."

Transfer entropy from scratch

from collections import Counter

def transfer_entropy(source, target, k=1, l=1):
    """Transfer entropy T_{source -> target} in bits.

    source, target : 1-D integer arrays of discrete states
    k : history length for the target
    l : history length for the source
    """
    source = np.asarray(source)
    target = np.asarray(target)
    n = len(target)
    max_lag = max(k, l)

    y_future = target[max_lag:]
    y_past = np.column_stack([target[max_lag - i - 1:n - i - 1] for i in range(k)])
    x_past = np.column_stack([source[max_lag - i - 1:n - i - 1] for i in range(l)])
    N = len(y_future)

    yf = y_future.tolist()
    yp = [tuple(row) for row in y_past]
    xp = [tuple(row) for row in x_past]

    c_yf_yp_xp = Counter(zip(yf, yp, xp))
    c_yp_xp = Counter(zip(yp, xp))
    c_yf_yp = Counter(zip(yf, yp))
    c_yp = Counter(yp)

    te = 0.0
    for (yf_val, yp_val, xp_val), count in c_yf_yp_xp.items():
        p_joint = count / N
        p_yf_given_yp_xp = count / c_yp_xp[(yp_val, xp_val)]
        p_yf_given_yp = c_yf_yp[(yf_val, yp_val)] / c_yp[yp_val]
        if p_yf_given_yp > 0 and p_yf_given_yp_xp > 0:
            te += p_joint * np.log2(p_yf_given_yp_xp / p_yf_given_yp)

    return te

Effective TE with a block-bootstrap null

def effective_transfer_entropy(source, target, k=1, l=1,
                               n_shuffles=200, block_size=5, rng=None):
    """Effective TE plus a surrogate p-value.

    Returns dict: te, ete, p_value, null_mean, null_std
    """
    rng = rng or np.random.default_rng(0)
    te_observed = transfer_entropy(source, target, k, l)

    n = len(source)
    n_blocks = int(np.ceil(n / block_size))
    null_tes = np.empty(n_shuffles)

    for b in range(n_shuffles):
        starts = rng.integers(0, n, size=n_blocks)
        shuffled = np.concatenate(
            [np.take(source, range(s, s + block_size), mode='wrap')
             for s in starts]
        )[:n]
        null_tes[b] = transfer_entropy(shuffled, target, k, l)

    null_mean = null_tes.mean()
    return {
        'te': te_observed,
        'ete': max(te_observed - null_mean, 0.0),
        'p_value': (np.sum(null_tes >= te_observed) + 1) / (n_shuffles + 1),
        'null_mean': null_mean,
        'null_std': null_tes.std(),
    }

The pairwise matrix

def compute_te_matrix(disc_returns, k=1, n_shuffles=200):
    cols = list(disc_returns.columns)
    m = len(cols)
    te_matrix = np.zeros((m, m))
    pval_matrix = np.ones((m, m))

    for i in range(m):
        for j in range(m):
            if i == j:
                continue
            r = effective_transfer_entropy(
                disc_returns[cols[i]].values,
                disc_returns[cols[j]].values,
                k=k, n_shuffles=n_shuffles,
            )
            te_matrix[i, j] = r['ete']
            pval_matrix[i, j] = r['p_value']

    return (pd.DataFrame(te_matrix, index=cols, columns=cols),
            pd.DataFrame(pval_matrix, index=cols, columns=cols))

The fast path

pyinform wraps an optimized C implementation:

from pyinform.transferentropy import transfer_entropy as te_pyinform

te_btc_to_eth = te_pyinform(btc_disc, eth_disc, k=2)
te_eth_to_btc = te_pyinform(eth_disc, btc_disc, k=2)

Null Calibration

This is the highest-value part of the analysis and the one most often skipped. Before believing any edge in the measured network, run the entire pipeline — discretization, effective TE, surrogate test, significance filter — on data where the true answer is known to be zero:

  1. Shuffled real returns. Destroy the cross-series timing while keeping each series' marginal distribution.
  2. Synthetic independent series. Simulate MM independent heavy-tailed, volatility-clustered processes with no cross-dependence by construction.

Then report the fraction of ordered pairs the test flags at α=0.05\alpha = 0.05. If that fraction is not near 0.05, the test is miscalibrated and every edge in the real network is suspect.

Multiple testing compounds this. A universe of 8 assets is 8×7=568 \times 7 = 56 ordered pairwise tests; 20 assets is 380. The corrections — Bonferroni, Holm, Benjamini-Yekutieli FDR — are derived in full in deflated Sharpe and multiple testing, together with the calibration study where the naive test's FDR is 1.000 against BHY's 0.007.

But do not stop at a raw Bonferroni, because these 56 tests are not 56 independent looks. Crypto assets are heavily correlated with each other — this blog measured PC1 at 65% of variance — which is precisely the correlated-grid failure mode documented in that article's Act 5, where treating a correlated grid's raw cell count as the trial count over-deflates and falsely rejects a genuine edge. The deliverable there was a band of effective-trial estimates, not a point: the average-correlation one-liner Neff=N/(1+(N1)ρˉ)N_{\text{eff}} = N/(1 + (N-1)\bar\rho) at the soft end, the eigenvalue estimators (participation ratio, PCA-95%, Kaiser) in the defensible middle. Report the effective-NN band for the TE grid and check whether the surviving edges are stable across it.

Sensitivity: the Two Free Knobs

Both knobs below are usually set by rule of thumb and then never revisited. They must instead be swept, with the leader/follower ranking reported at every cell.

History length kk

kk controls how much of the target's own past you condition on. Too small and you attribute the target's own autocorrelation to the source; too large and the joint distribution becomes too sparse to estimate — the number of joint states is Ak+l+1|\mathcal{A}|^{k+l+1}, so with 3 bins, k=2k=2, l=1l=1 you have 34=813^4 = 81 states, and k=5k=5 takes you to 37=21873^7 = 2187 against a few thousand observations.

The conventional starting points are k{1,2}k \in \{1, 2\} for hourly data and k{1,,5}k \in \{1, \dots, 5\} for daily, optionally cross-checked with AIC/BIC on the equivalent VAR lag. Treat these as the grid to sweep, not the answer.

Discretization

  • 3 bins (down / flat / up): robust, works with limited data, captures directional structure only.
  • 5 bins: captures magnitude, needs materially more data.
  • Quantile edges: roughly equal occupancy, no empty bins — but see the leakage warning above.
  • Ordinal/symbolic encoding: permutation-entropy style, robust to monotone transforms.

Continuous kernel-based TE avoids discretization loss entirely at the cost of bandwidth selection and much heavier computation.

The sweep

for n_bins in (3, 5):
    for k in (1, 2, 3, 5):
        disc = discretize_all(returns, n_bins=n_bins)
        te_df, p_df = compute_te_matrix(disc, k=k)
        rank = net_flow_ranking(te_df, p_df)
        report(n_bins, k, rank)

The question is not "what is the ranking" but "is the ranking the same at every cell." If an asset is only a net sender at k=2k=2, nbins=3n_{\text{bins}}=3, that instability is the result, and it should be reported as such.

Confounding: Conditional Transfer Entropy

If both XX and YY are driven by a latent factor ZZ, pairwise TE will report a flow between them that is entirely an artifact of the common driver. In a one-factor market this is not a corner case; it is the default expectation. Conditional transfer entropy conditions it away:

TXYZ=H(Yt+1Yt(k),Zt(l))H(Yt+1Yt(k),Xt(l),Zt(l))T_{X \to Y | Z} = H(Y_{t+1} \mid Y_t^{(k)}, Z_t^{(l)}) - H(Y_{t+1} \mid Y_t^{(k)}, X_t^{(l)}, Z_t^{(l)})

Any claim that SOL sends information to AVAX should be re-tested conditioning on BTC. If the flow vanishes, the pairwise result was the market factor wearing a costume.

te_sol_avax_given_btc = te_pyinform(sol_disc, avax_disc, k=2,
                                    condition=btc_disc)

Conditioning is not free: each conditioning variable multiplies the joint state space by Al|\mathcal{A}|^{l}, so conditional TE needs substantially more data than the pairwise version at the same kk.

Stability, and Whether Any of This Is Tradable

Information flow is not static — and neither is anything else in this market, which is the opening premise of regime detection with HMMs and the reason every estimate here has to be windowed rather than fitted once. The rolling-window version of this analysis, though, inherits a known result rather than discovering one. That crypto dependence tightens in a crisis is already published with numbers: signal correlation between pairs tabulates average signal correlation from 0.15 in sideways markets to 0.90 in panic, with NeffN_{\text{eff}} collapsing from 4.2 to 1.1, and DCC-GARCH Application 3 turns average pairwise correlation into a risk-off regime signal with a trailing-quantile flag and an explicit "this is a risk signal, not an alpha signal" caveat.

Freeman centralization of TE out-strength is a different scalar over a different matrix, but it is the same move: collapse a d×dd \times d dependence object into one number and watch it rise. So the bar is not "does centralization rise before drawdowns" — it is does it beat average pairwise correlation as a risk-off signal, measured head to head, on the same windows, against the same drawdowns.

def network_centralization(G):
    """Freeman centralization of out-strength. High = one dominant source."""
    if G.number_of_nodes() < 2:
        return 0.0
    s = [sum(d['weight'] for _, _, d in G.out_edges(v, data=True)) for v in G.nodes()]
    total = sum(s)
    if total == 0:
        return 0.0
    n = len(s)
    return sum(max(s) - x for x in s) / ((n - 1) * total)

Two questions have to be answered before any downstream application is worth writing:

  1. Does the leader ranking persist out of sample? Fit the network on window tt, check the ranking on window t+1t+1. Report the rank correlation across windows. A ranking that re-shuffles every week describes noise.
  2. Does the TE filter add anything to a lead-lag trade? The correct experiment is a single controlled comparison: the same rolling z-score entry/exit machinery covered in statistical arbitrage and pairs trading and the distance approach, run twice, with the presence of the TE significance filter as the only difference, fees included.

Neither has been run for this article. No lead-lag backtest, no information-momentum portfolio, no regime-detection comparison. Weight-construction machinery for a hypothetical information-momentum sleeve is covered in portfolio optimization algorithms compared — but note that a softmax over raw net-strength in bits is scale-arbitrary and would need a normalization argument before it means anything. Untested strategy code is not shipped here.

Cost

TE is O(N)O(N) per pair, but the constant is driven by the joint state count Ak+l+1|\mathcal{A}|^{k+l+1} and by the surrogate count. For MM assets and BB bootstrap replicates you need M(M1)BM(M-1)B TE evaluations — with 50 assets and 200 surrogates that is 490,000. Per-evaluation cost depends entirely on your hardware, pyinform version, and NN; measure it on your own machine rather than trusting a quoted figure. Three levers if it is too slow:

  • Parallelize. Every pair is independent — joblib or multiprocessing.
  • Pre-screen. Compute raw TE first; run surrogates only on pairs above a threshold. (Note this makes the significance test conditional on passing the screen, which needs accounting for.)
  • Shrink the universe. Cluster first, compute TE between cluster representatives.

Summary

Transfer entropy is a principled, model-free measure of directed information flow, and it fills a real gap in this blog's toolkit: DCC-GARCH says when dependence tightens, TE says which way it points. For Gaussian processes it collapses exactly onto Granger causality via T=12ln(1+F)T = \tfrac{1}{2}\ln(1 + F), which makes it a generalization rather than a rival.

What it is not, yet, is a result. The pipeline here is implemented and the failure modes are enumerated — finite-sample bias corrected by surrogates, whole-series binning as look-ahead leakage, a correlated grid of 56 tests that breaks naive multiple-testing corrections, common-driver confounding in a one-factor market, and free parameters that may well determine the answer. Every one of those is a reason to distrust an uncalibrated TE network. Publishing the network without the null calibration, the sensitivity sweep, and the out-of-sample stability check would be publishing the failure modes as if they were findings.

References

  • Schreiber, T. (2000). "Measuring Information Transfer." Physical Review Letters, 85(2), 461-464.
  • Barnett, L., Barrett, A.B., Seth, A.K. (2009). "Granger Causality and Transfer Entropy Are Equivalent for Gaussian Variables." Physical Review Letters, 103(23), 238701.
  • Marschinski, R., Kantz, H. (2002). "Analysing the information flow between financial time series." European Physical Journal B, 30(2), 275-281.
  • Dimpfl, T., Peter, F.J. (2013). "Using Transfer Entropy to Measure Information Flows Between Financial Markets." Studies in Nonlinear Dynamics & Econometrics, 17(1), 85-102.
  • Keskin, Z., Aste, T. (2020). "Information-theoretic measures for nonlinear causality detection: application to social media sentiment and cryptocurrency prices." Royal Society Open Science, 7(9), 200863.
  • Jang, S.M. et al. (2022). "Using transfer entropy to measure information flows between cryptocurrencies." Physica A, 586, 126476.
  • Nicola, G. et al. (2020). "Network Analysis of Multivariate Transfer Entropy of Cryptocurrencies in Times of Turbulence." Entropy, 22(7), 760.
免責宣告:本文提供的資訊僅用於教育和參考目的,不構成財務、投資或交易建議。加密貨幣交易涉及重大損失風險。

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

緊跟市場步伐

訂閱我們的時事通訊,獲取獨家 AI 交易見解、市場分析和平台更新。

我們尊重您的隱私。您可以隨時退訂。