Close Navigation
Return Causality among Cryptocurrencies: Evidence from a Rolling Window Toda–Yamamoto Framework

Return Causality among Cryptocurrencies: Evidence from a Rolling Window Toda–Yamamoto Framework

Posted September 24, 2025 at 11:53 am

Tribhuvan Bisen , Shubham Pandey
Quant Insider

#Abstract

We study causal linkages among six major crypto assets (BTC, ETH, BNB, ADA, XRP, LTC) using the Toda–Yamamoto (T–Y) framework applied in rolling windows. We emphasize the impact of multiple-testing corrections on inference.

Raw (no-FDR) results suggest numerous causal connections, with Bitcoin often appearing as a leading driver. However, once false discovery rate (FDR) controls are applied, the network becomes much sparser: leadership is weak and intermittent rather than persistent. For example, the BTC → ETH relation is significant in only about 6.8% of windows under per-window FDR and 4.6% under global FDR, rather than being consistently strong. We document three reporting regimes—no FDR, per-window FDR, and global FDR—highlighting the trade-off between sensitivity and conservatism. Our results underscore the multiple-testing burden in high-dimensional causality networks (K(K−1) tests per window) and demonstrate that strong claims of market-wide dominance do not survive conservative adjustment.

Introduction

Cryptocurrencies are frequently portrayed as highly interconnected, with Bitcoin often assumed to be the primary driver of market dynamics. While Bitcoin’s size and visibility make this assumption plausible, formal statistical testing is required to assess whether causal leadership is strong and persistent.

This study investigates causal linkages among six widely traded assets — Bitcoin (BTC), Ethereum (ETH), Binance Coin (BNB), Cardano (ADA), Ripple (XRP), and Litecoin (LTC). Using the Toda–Yamamoto (T–Y)

causality framework, we test directional predictability both in the full sample and within rolling windows. Our analysis emphasizes the importance of controlling for multiple testing. In a setting with K assets, there are K(K−1) pairwise directional tests per window (30 tests when K=6), which strongly inflates raw significance rates. We therefore contrast three reporting regimes:

  1. No correction (raw p-values),
  2. Per-window FDR (controlling discoveries within each window), and
  3. Global FDR (controlling across all windows and pairs, most conservative).

The results show that raw tests yield many apparent causal connections and suggest Bitcoin as a frequent leader. However, after FDR adjustment, most links disappear, and Bitcoin’s role becomes weaker and intermittent. A handful of other relations — such as ADA → XRP and XRP → ETH — appear more persistent under the less conservative per-window FDR. Only a small number of edges survive global-FDR adjustment.

Crypto Causality Analysis (Toda–Yamamoto + Rolling Stability)

This notebook contains a full, runnable pipeline to:

  • Fetch daily crypto prices (yfinance)
  • Compute log-returns
  • Run Toda–Yamamoto pairwise causality (Wald tests via VAR)
  • Perform rolling-window causality
  • Compute stability metrics (fraction significant, mean p-value, longest run)
  • Apply per-window and global FDR (Benjamini-Hochberg)
  • Produce visualizations and save CSV outputs

Usage: run cells sequentially. Edit the parameters cell as needed.

# PARAMETERS - edit as needed
tickers = {
    'BTC': 'BTC-USD',
    'ETH': 'ETH-USD',
    'BNB': 'BNB-USD',
    'ADA': 'ADA-USD',
    'XRP': 'XRP-USD',
    'LTC': 'LTC-USD'
}
start = "2018-01-01"
end = None   # None => to today
freq = '1d'
maxlag_for_ic = 10   # max lags used when selecting VAR lag via AIC
window_days = 180    # rolling window size (days)
step_days = 30       # step (days)
alpha = 0.05
# Imports (uncomment pip install lines if required)
# !pip install yfinance statsmodels matplotlib seaborn tqdm networkx
import yfinance as yf
import pandas as pd
import numpy as np
from statsmodels.tsa.stattools import adfuller, kpss
from statsmodels.tsa.api import VAR
import matplotlib.pyplot as plt
import seaborn as sns
from tqdm import tqdm
import itertools
import math
import networkx as nx
%matplotlib inline
sns.set_style('whitegrid')
# Fetch prices and compute log-returns
def fetch_prices(ticker_map, start, end, interval='1d'):
    df = yf.download(list(ticker_map.values()), start=start, end=end, interval=interval,
                     progress=False, auto_adjust=False)['Adj Close']
    if isinstance(df, pd.Series):
        df = df.to_frame()
    df.columns = list(ticker_map.keys())
    return df.dropna()

def log_returns(prices):
    return np.log(prices).diff().dropna()

prices = fetch_prices(tickers, start, end, interval=freq)
print('Prices shape:', prices.shape)
rets = log_returns(prices)
print('Returns shape:', rets.shape)
display(prices.head())
df = prices

Prices shape: (2818, 6)
Returns shape: (2817, 6)

BTCETHBNBADAXRPLTC
Date
2018-01-010.7286578.4146113657.200195772.640991229.0330052.39103
2018-01-020.7825878.8377714982.099609884.443970255.6840062.48090
2018-01-031.0796609.5358815201.000000962.719971245.3679963.10537
2018-01-041.1141209.2139915599.200195980.921997241.3699953.19663
2018-01-050.99955914.9172017429.500000997.719971249.2709963.04871
import numpy as np
import pandas as pd

# Compute daily statistics
mean_returns = rets.mean() * 100          # Mean daily log-return (%)
volatility = rets.std() * 100             # Daily volatility (%)
sharpe_ratios = (mean_returns / volatility) * np.sqrt(365)  # Annualized Sharpe ratio

# Print results
print("Mean Returns (%):\n", mean_returns)
print("\nVolatility (%):\n", volatility)
print("\nAnnual Sharpe Ratios:\n", sharpe_ratios)

Mean Returns (%):
 BTC    0.008092
ETH    0.169422
BNB    0.076319
ADA    0.063138
XRP   -0.024096
LTC    0.009434
dtype: float64

Volatility (%):
 BTC    5.419906
ETH    4.841994
BNB    3.463202
ADA    4.529353
XRP    4.825922
LTC    5.424487
dtype: float64

Annual Sharpe Ratios:
 BTC    0.028525
ETH    0.668484
BNB    0.421017
ADA    0.266317
XRP   -0.095392
LTC    0.033227
dtype: float64

Risk–Return Analysis of Cryptocurrencies

The analysis of mean returns, volatility, and Sharpe ratios across six major cryptocurrencies provides valuable insights into their performance.

Mean Returns:
Ethereum (0.169%) shows the highest average daily return, followed by Binance Coin (0.076%) and Cardano (0.063%). Bitcoin and Litecoin recorded very small positive returns, while XRP stands out with a negative mean return (-0.024%), highlighting its underperformance over the sample period.

Volatility:
Bitcoin (5.42%) and Litecoin (5.42%) exhibit the highest volatility, reflecting larger daily price fluctuations. In contrast, Binance Coin (3.46%) is the least volatile, making it relatively more stable compared to other assets. Ethereum, Cardano, and XRP fall in between, with volatility around 4–5%.

Sharpe Ratios:
The Sharpe Ratio, which measures risk-adjusted performance, shows Ethereum as the clear leader (0.668), delivering the best returns per unit of risk. Binance Coin also performs well (0.421), while Cardano shows a modest ratio (0.267). Bitcoin and Litecoin have very low Sharpe Ratios (0.028 and 0.033), indicating their returns barely compensated for risk. XRP records a negative Sharpe Ratio (-0.096), underscoring poor risk-adjusted performance.


Interpretation

  • Ethereum emerges as the strongest asset in terms of both returns and risk-adjusted performance.
  • Binance Coin offers a good balance of stability and return.
  • XRP underperformed significantly, both in raw and adjusted measures.
  • Bitcoin and Litecoin remain volatile benchmarks but delivered minimal excess return over risk.

Overall, the findings highlight that not all cryptocurrencies offer the same investment value.
Ethereum and BNB stand out as attractive choices, while XRP lags behind.

import matplotlib.pyplot as plt

metrics = pd.DataFrame({
    'Mean Returns': mean_returns,
    'Volatility': volatility,
    'Sharpe Ratio': sharpe_ratios
})

fig, axes = plt.subplots(1, 3, figsize=(15,4))
metrics['Mean Returns'].plot(kind='bar', ax=axes[0], title='Mean Daily Returns (%)')
metrics['Volatility'].plot(kind='bar', ax=axes[1], title='Volatility (%)')
metrics['Sharpe Ratio'].plot(kind='bar', ax=axes[2], title='Annual Sharpe Ratios')

plt.tight_layout()
plt.show()
Return Causality among Cryptocurrencies: Evidence from a Rolling Window Toda–Yamamoto Framework

Source: Yahoo Finance

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
from matplotlib import rcParams

# Set plotting style
rcParams['font.family'] = 'DejaVu Sans'
rcParams['font.size'] = 12

# --- Detect column type and get list of tickers ---
if isinstance(df.columns, pd.MultiIndex):
    # If multi-level, assume top level like 'Close'
    if 'Close' in df.columns.get_level_values(0):
        tickers = df['Close'].columns.tolist()
    else:
        tickers = df.columns.get_level_values(1).unique().tolist()
else:
    # Single-level columns
    tickers = df.columns.tolist()

# --- Compute returns and rolling volatility ---
for t in tickers:
    if isinstance(df.columns, pd.MultiIndex):
        # MultiIndex: assume we have 'Close' as top-level
        df[('Return', t)] = df[('Close', t)].pct_change() * 100
        df[('RollingVol', t)] = df[('Return', t)].rolling(20).std()
    else:
        # Single-level
        df[f'Return_{t}'] = df[t].pct_change() * 100
        df[f'RollingVol_{t}'] = df[f'Return_{t}'].rolling(20).std()

# --- Create plots ---
fig, axes = plt.subplots(len(tickers), 4, figsize=(20, 4*len(tickers)))

# Make axes always 2D
if len(tickers) == 1:
    axes = np.expand_dims(axes, axis=0)

for i, t in enumerate(tickers):
    if isinstance(df.columns, pd.MultiIndex):
        returns = df[('Return', t)].dropna()
        rolling_vol = df[('RollingVol', t)]
    else:
        returns = df[f'Return_{t}'].dropna()
        rolling_vol = df[f'RollingVol_{t}']

    # 1. Histogram with quantiles
    axes[i, 0].hist(returns, bins=50, density=True, alpha=0.7, color='#FFB6C1')
    for q in [0.05, 0.25, 0.5, 0.75, 0.95]:
        q_val = returns.quantile(q)
        axes[i, 0].axvline(q_val, linestyle='--', label=f'{int(q*100)}%: {q_val:.2f}%')
    axes[i, 0].set_title(f"{t} Return Distribution")
    axes[i, 0].legend(fontsize=8)

    # 2. Q-Q plot
    stats.probplot(returns, dist="norm", plot=axes[i, 1])
    axes[i, 1].set_title(f"{t} Q-Q Plot")

    # 3. Boxplot
    axes[i, 2].boxplot(returns, patch_artist=True, boxprops=dict(facecolor='#FFB6C1'))
    axes[i, 2].set_title(f"{t} Return Boxplot")

    # 4. Rolling volatility
    axes[i, 3].plot(df.index, rolling_vol, color='#FF69B4')
    axes[i, 3].set_title(f"{t} 20-Day Rolling Volatility")

plt.tight_layout()
plt.show()
Return Causality among Cryptocurrencies

Source: Yahoo Finance

Detect tickers in your DataFrame.

Compute daily returns and rolling volatility.

lot histograms, Q-Q plots, boxplots, and rolling volatility.

Layout plots cleanly with tight spacing for inline display in Colab.

# ADF test quick check (returns should typically be stationary)
def adf_pvalue(series):
    try:
        return adfuller(series, autolag='AIC')[1]
    except Exception:
        return np.nan

def integration_order_estimate(series, adf_alpha=0.05):
    pv = adf_pvalue(series)
    if pd.isna(pv):
        return 0
    return 1 if pv > adf_alpha else 0

adf_summary = {col: adf_pvalue(rets[col]) for col in rets.columns}
print('ADF p-values (returns):')
display(pd.Series(adf_summary))

ADF p-values (returns):

0
BTC	0.000000e+00
ETH	3.310268e-26
BNB	2.026843e-30
ADA	6.717843e-29
XRP	0.000000e+00
LTC	0.000000e+00

dtype: float64

Interpretation:

  • All p-values are extremely small (< 0.05).
  • We reject the null hypothesis: returns are stationary.
  • Conclusion: BTC, ETH, BNB, ADA, XRP, and LTC returns have stable mean and variance over time, suitable for time series modeling.
# Inspect columns first
print("First 10 columns:", df.columns[:10])

# Flexible helper to get close price column for a ticker
def get_close_column(df, ticker):
    cols = df.columns

    # Case 1: multi-index (('Close','BTC'))
    if isinstance(cols, pd.MultiIndex):
        if ('Close', ticker) in cols:
            return ('Close', ticker)
        elif (ticker, 'Close') in cols:
            return (ticker, 'Close')

    # Case 2: single-level with names like BTC_Close
    for c in cols:
        if str(c).lower() in [f"{ticker.lower()}_close", f"close_{ticker.lower()}"]:
            return c

    # Case 3: maybe just ticker name itself
    if ticker in cols:
        return ticker

    raise KeyError(f"❌ No close column found for {ticker}")

# Compute returns & rolling volatility
for t in tickers:
    close_col = get_close_column(df, t)
    df[f"{t}_Return"] = df[close_col].pct_change() * 100
    df[f"{t}_RollingVol"] = df[f"{t}_Return"].rolling(20).std()

First 10 columns: Index([‘BTC’, ‘ETH’, ‘BNB’, ‘ADA’, ‘XRP’, ‘LTC’, ‘Return_BTC’,
‘RollingVol_BTC’, ‘Return_ETH’, ‘RollingVol_ETH’],
dtype=’object’)

# --- Create plots ---
fig, axes = plt.subplots(len(tickers), 4, figsize=(20, 4*len(tickers)))

if len(tickers) == 1:
    axes = np.expand_dims(axes, axis=0)

for i, t in enumerate(tickers):
    if isinstance(df.columns, pd.MultiIndex):
        returns = df[('Return', t)].dropna()
        rolling_vol = df[('RollingVol', t)]
    else:
        returns = df[f'Return_{t}'].dropna()
        rolling_vol = df[f'RollingVol_{t}']

    # 1. Histogram with quantiles
    axes[i, 0].hist(returns, bins=50, density=True, alpha=0.7, color='#FFB6C1')
    for q in [0.05, 0.25, 0.5, 0.75, 0.95]:
        q_val = returns.quantile(q)
        axes[i, 0].axvline(q_val, linestyle='--', label=f'{int(q*100)}%: {q_val:.2f}%')
    axes[i, 0].set_title(f"{t} Return Distribution")
    axes[i, 0].legend(fontsize=8)

    # 2. Q-Q plot
    stats.probplot(returns, dist="norm", plot=axes[i, 1])
    axes[i, 1].set_title(f"{t} Q-Q Plot")

    # 3. Boxplot
    axes[i, 2].boxplot(returns, patch_artist=True, boxprops=dict(facecolor='#FFB6C1'))
    axes[i, 2].set_title(f"{t} Return Boxplot")

    # 4. Rolling volatility
    axes[i, 3].plot(df.index, rolling_vol, color='#FF69B4')
    axes[i, 3].set_title(f"{t} 20-Day Rolling Volatility")

plt.tight_layout()

# 🔹 Save the figure as PNG
plt.savefig("ticker_analysis_plots.png", dpi=300, bbox_inches="tight")

# Optional: also show interactively
plt.show()
Return Causality among Cryptocurrencies

Source: Yahoo Finance

import warnings
from tqdm import tqdm

# Suppress statsmodels frequency warnings
warnings.filterwarnings("ignore", category=Warning)

def rolling_toda_yamamoto(df, window, step, max_lag_ic=10, progress=True):
    dates = []
    windows = []
    n = len(df)
    indices = list(range(0, n - window + 1, step))

    if progress:
        indices = tqdm(indices, desc='Rolling windows')

    for start_idx in indices:
        end_idx = start_idx + window
        sub = df.iloc[start_idx:end_idx]
        mid_date = sub.index[-1]

        # Compute full-sample Toda-Yamamoto causality matrix
        pmat = full_sample_toda_yamamoto_matrix(sub, max_lag_ic=max_lag_ic)

        dates.append(mid_date)
        windows.append(pmat)

    # Print only final summary
    if dates:
        print(f"✅ Computed {len(windows)} windows from {dates[0].date()} to {dates[-1].date()}")
    else:
        print("⚠️ No windows computed")

    return dates, windows

# Example usage
window = window_days
step = step_days
dates, windows = rolling_toda_yamamoto(rets, window, step, max_lag_ic=maxlag_for_ic, progress=True)

Rolling windows: 100%|██████████| 88/88 [01:30<00:00, 1.03s/it]
✅ Computed 88 windows from 2018-06-30 to 2025-08-22

Rolling-window Toda–Yamamoto causality – Summary:

  • Purpose: Track how causal relationships between time series change over time.
  • Method: Compute T–Y causality matrices on rolling windows of data.
  • Outputs: • dates → End date of each window • windows → Pairwise causality matrices for each window
  • Interpretation: Each matrix shows which assets may ’cause’ others; allows analysis of dynamic, time-varying relationships.
  • Colab-friendly: Shows progress and reports number of windows with date range.
# FDR (Benjamini-Hochberg) and stability metrics
def fdr_bh(pvals, alpha=0.05):
    p = np.array(pvals, dtype=float)
    mask = ~np.isnan(p)
    p_valid = p[mask]
    n = len(p_valid)
    decisions = np.zeros_like(p, dtype=bool)
    if n == 0:
        return decisions
    order = np.argsort(p_valid)
    sorted_p = p_valid[order]
    thresholds = (np.arange(1, n+1) / n) * alpha
    below = sorted_p <= thresholds
    if not np.any(below):
        decisions[mask] = False
        return decisions
    max_idx = np.max(np.where(below))
    rej = np.zeros(n, dtype=bool)
    rej[:max_idx+1] = True
    inv_order = np.argsort(order)
    decisions[mask] = rej[inv_order]
    return decisions

def compute_stability_metrics(dates, windows, alpha=0.05, per_window_fdr=False, global_fdr=False):
    names = windows[0].index.tolist()
    pairs = [(x,y) for x in names for y in names if x!=y]
    n_windows = len(windows)
    pvals = {pair: [] for pair in pairs}
    for w in windows:
        for pair in pairs:
            pvals[pair].append(w.loc[pair[0], pair[1]])
    if global_fdr:
        flat = []
        flat_pairs = []
        for pair in pairs:
            for t in range(n_windows):
                flat.append(pvals[pair][t])
                flat_pairs.append(pair + (t,))
        flags_flat = fdr_bh(flat, alpha=alpha)
        sig = {pair: [False]*n_windows for pair in pairs}
        for idx, flag in enumerate(flags_flat):
            pair = flat_pairs[idx][:2]
            t = flat_pairs[idx][2]
            sig[pair][t] = bool(flag)
    else:
        sig = {pair: [] for pair in pairs}
        for t in range(n_windows):
            p_window = [pvals[pair][t] for pair in pairs]
            if per_window_fdr:
                flags = fdr_bh(p_window, alpha=alpha)
            else:
                flags = np.array([ (pv < alpha) if (not np.isnan(pv)) else False for pv in p_window ])
            for i, pair in enumerate(pairs):
                sig[pair].append(bool(flags[i]))
    rows = []
    for pair in pairs:
        sig_seq = np.array(sig[pair], dtype=bool)
        p_seq = np.array(pvals[pair], dtype=float)
        frac_sig = np.nanmean(sig_seq) if len(sig_seq)>0 else np.nan
        mean_p = np.nanmean(p_seq)
        max_run = 0
        run = 0
        for b in sig_seq:
            if b:
                run += 1
                if run > max_run:
                    max_run = run
            else:
                run = 0
        rows.append({
            'cause': pair[0],
            'effect': pair[1],
            'frac_significant': frac_sig,
            'mean_p': mean_p,
            'longest_run_windows': max_run,
            'n_windows': n_windows
        })
    stability_df = pd.DataFrame(rows)
    return stability_df, pvals, sig, pairs

stab_no_fdr, pvals_no_fdr, sig_no_fdr, pairs = compute_stability_metrics(dates, windows, alpha=alpha, per_window_fdr=False, global_fdr=False)
stab_perwindow_fdr, pvals_perwindow_fdr, sig_perwindow_fdr, _ = compute_stability_metrics(dates, windows, alpha=alpha, per_window_fdr=True, global_fdr=False)
stab_global_fdr, pvals_global_fdr, sig_global_fdr, _ = compute_stability_metrics(dates, windows, alpha=alpha, per_window_fdr=False, global_fdr=True)

print('Top pairs by fraction significant (no FDR):')
display(stab_no_fdr.sort_values('frac_significant', ascending=False).head(10))
print('Top pairs by fraction significant (per-window FDR):')
display(stab_perwindow_fdr.sort_values('frac_significant', ascending=False).head(10))
print('Top pairs by fraction significant (global FDR):')
display(stab_global_fdr.sort_values('frac_significant', ascending=False).head(10))

# Save outputs
pmat_full.to_csv('ty_fullsample_pvalues.csv')
stab_no_fdr.to_csv('stability_no_fdr.csv', index=False)
stab_perwindow_fdr.to_csv('stability_perwindow_fdr.csv', index=False)
stab_global_fdr.to_csv('stability_global_fdr.csv', index=False)
print('Saved CSVs: ty_fullsample_pvalues.csv, stability_no_fdr.csv, stability_perwindow_fdr.csv, stability_global_fdr.csv')

Top pairs by fraction significant (no FDR):

causeeffectfrac_significantmean_plongest_run_windowsn_windows
27LTCBNB0.2272730.192875588
21XRPETH0.2045450.242837488
14BNBLTC0.1818180.280912588
19ADALTC0.1818180.306245688
25LTCBTC0.1818180.279368588
4BTCLTC0.1704550.234125688
17ADABNB0.1704550.199688688
18ADAXRP0.1590910.237433688
26LTCETH0.1590910.244020488
9ETHLTC0.1477270.233621388

Top pairs by fraction significant (per-window FDR):

causeeffectfrac_significantmean_plongest_run_windowsn_windows
18ADAXRP0.1363640.237433688
21XRPETH0.1363640.242837488
19ADALTC0.0909090.306245588
14BNBLTC0.0909090.280912288
25LTCBTC0.0909090.279368388
27LTCBNB0.0909090.192875288
2BTCADA0.0795450.259896388
26LTCETH0.0795450.244020388
16ADAETH0.0681820.261890488
0BTCETH0.0681820.357408388

Top pairs by fraction significant (global FDR):

causeeffectfrac_significantmean_plongest_run_windowsn_windows
14BNBLTC0.0681820.280912288
3BTCXRP0.0568180.305443488
18ADAXRP0.0568180.237433288
2BTCADA0.0568180.259896388
5ETHBTC0.0454550.219297288
9ETHLTC0.0454550.233621288
25LTCBTC0.0454550.279368388
24XRPLTC0.0454550.376740288
27LTCBNB0.0454550.192875188
26LTCETH0.0454550.244020188

Saved CSVs: ty_fullsample_pvalues.csv, stability_no_fdr.csv, stability_perwindow_fdr.csv, stability_global_fdr.csv

1. What the Columns Mean

cause → effect: Direction of causality tested in the Toda–Yamamoto framework.

frac_significant: Fraction of rolling windows where the causal effect was statistically significant. Higher values → more consistent causality.

mean_p: Average p-value across windows. Lower values → stronger evidence of causality.

longest_run_windows: Longest consecutive sequence of windows where causality was significant. Shows persistence.

n_windows: Total number of rolling windows analyzed (here, 88).

2. Key Observations

No FDR

LTC → BNB is most frequently significant (22.7% of windows).

XRP → ETH and BNB → LTC are also prominent.A

Some pairs like ADA → LTC and LTC → BTC have moderate persistence (5–6 consecutive windows).

Without correction, several causal relationships appear relatively frequent, but some may be false positives.

Per-Window FDR

Fraction significant drops for all pairs after controlling for multiple tests.

ADA → XRP and XRP → ETH remain the top pairs (~13.6% of windows).

Longest runs show some persistence (e.g., ADA → XRP: 6 consecutive windows).

This shows adjusting for false discoveries reduces apparent causality, highlighting the most robust effects.

Global FDR

Fraction significant further decreases across all pairs (highest ~6.8%).

BNB → LTC and BTC → XRP emerge as the top globally significant pairs.

Many previously frequent effects are no longer significant after global multiple-testing correction.

This is the most conservative approach, ensuring only the strongest causal signals are considered.

3. Practical Interpretation

Raw results (no FDR): Suggest multiple potential causal relationships but may include false positives.

Per-window FDR: Highlights robust, time-local causality; shows which relationships are significant within individual windows.

Global FDR: Highlights overall, highly stable causal relationships across the entire time period.

Strong/persistent effects to note:

LTC → BNB (robust across no-FDR and per-window FDR).

ADA → XRP, XRP → ETH (robust per-window).

BNB → LTC, BTC → XRP (appear under global FDR).

Summary:

Most causal relationships in crypto returns are weak and intermittent. Only a few pairs show robust, persistent causality, especially after controlling for false discoveries. FDR correction is critical to identify truly meaningful links and avoid spurious effects.

# Visualizations: fraction heatmap and p-value time series for example pair
pairs_df = stab_no_fdr.pivot(index='cause', columns='effect', values='frac_significant')
plt.figure(figsize=(8,6))
sns.heatmap(pairs_df, annot=True, fmt='.2f', vmin=0, vmax=1, cmap='viridis', cbar_kws={'label': 'fraction significant'})
plt.title(f'Fraction of windows where X -> Y is significant (window={window_days} days, step={step_days} days) [no FDR]')
plt.tight_layout()
plt.show()

# Example p-value series for BTC -> ETH
pair_example = ('BTC','ETH')
p_series = pvals_no_fdr[pair_example]
sig_series_no_fdr = sig_no_fdr[pair_example]
sig_series_perwindow = sig_perwindow_fdr[pair_example]
sig_series_global = sig_global_fdr[pair_example]

plt.figure(figsize=(12,4))
plt.plot(dates, p_series, marker='o', label='p-value')
plt.axhline(alpha, color='r', linestyle='--', label=f'alpha={alpha}')
for i, dt in enumerate(dates):
    if sig_series_perwindow[i]:
        plt.scatter(dt, p_series[i], marker='s', s=80, facecolors='none', edgecolors='green', label='per-window FDR sig' if i==0 else "")
    if sig_series_global[i]:
        plt.scatter(dt, p_series[i], marker='D', s=60, facecolors='none', edgecolors='purple', label='global FDR sig' if i==0 else "")
plt.title(f'Rolling T–Y p-values: {pair_example[0]} -> {pair_example[1]}')
plt.ylabel('p-value')
plt.legend()
plt.tight_layout()
plt.show()
Return Causality among Cryptocurrencies

Source: Yahoo Finance

Return Causality among Cryptocurrencies

Source: Yahoo Finance

# Build network for full-sample significant links (threshold alpha)
G = nx.DiGraph()
threshold = alpha
for idx, row in pmat_full.stack().reset_index().iterrows():
    src = row['level_0']
    tgt = row['level_1']
    pval = row[0]
    if not np.isnan(pval) and pval < threshold:
        G.add_edge(src, tgt, weight=1-pval)

plt.figure(figsize=(8,6))
pos = nx.spring_layout(G, seed=42)
nx.draw(G, pos, with_labels=True, node_color='skyblue', node_size=1400, arrowsize=20)
edge_labels = {(u,v): f"{1-d['weight']:.2f}" for u,v,d in G.edges(data=True)}
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)
plt.title('Full-sample significant causality network (p < {:.2f})'.format(threshold))
plt.show()
Return Causality among Cryptocurrencies

Source: Yahoo Finance

Figure 2 – Rolling p-values for three representative pairs

Rolling-window Toda–Yamamoto p-values are plotted over time for three illustrative causal directions: ADA → XRP, XRP → ETH, and BTC → ETH.’

● The raw (no-FDR) p-values are shown as time series.

● Horizontal reference lines indicate the 5% threshold (uncorrected) and the corresponding thresholds under per-window FDR and global FDR.

● ADA → XRP: shows moderate persistence, with repeated dips below the 5% level, some surviving per-window FDR.

● XRP → ETH: occasional significance that weakens after adjustment.

● BTC → ETH: appears significant in raw tests but becomes weak and intermittent under both FDR regimes.

Overall, this figure illustrates how multiple-testing correction reduces the number of apparent causal episodes, highlighting that most effects are local and transient rather than stable.

import pandas as pd

stab_no_fdr = pd.read_csv("stability_no_fdr.csv")
stab_perwindow_fdr = pd.read_csv("stability_perwindow_fdr.csv")
stab_global_fdr = pd.read_csv("stability_global_fdr.csv")

Find the most stable causal pairs

# Top pairs without FDR
print("Top pairs (no FDR):")
print(stab_no_fdr.sort_values('frac_significant', ascending=False).head(10))

# Top pairs with per-window FDR
print("Top pairs (per-window FDR):")
print(stab_perwindow_fdr.sort_values('frac_significant', ascending=False).head(10))

# Top pairs with global FDR
print("Top pairs (global FDR):")
print(stab_global_fdr.sort_values('frac_significant', ascending=False).head(10))
Top pairs (no FDR):
   cause effect  frac_significant    mean_p  longest_run_windows  n_windows
27   LTC    BNB          0.227273  0.192875                    5         88
21   XRP    ETH          0.204545  0.242837                    4         88
14   BNB    LTC          0.181818  0.280912                    5         88
19   ADA    LTC          0.181818  0.306245                    6         88
25   LTC    BTC          0.181818  0.279368                    5         88
4    BTC    LTC          0.170455  0.234125                    6         88
17   ADA    BNB          0.170455  0.199688                    6         88
18   ADA    XRP          0.159091  0.237433                    6         88
26   LTC    ETH          0.159091  0.244020                    4         88
9    ETH    LTC          0.147727  0.233621                    3         88
Top pairs (per-window FDR):
   cause effect  frac_significant    mean_p  longest_run_windows  n_windows
18   ADA    XRP          0.136364  0.237433                    6         88
21   XRP    ETH          0.136364  0.242837                    4         88
19   ADA    LTC          0.090909  0.306245                    5         88
14   BNB    LTC          0.090909  0.280912                    2         88
25   LTC    BTC          0.090909  0.279368                    3         88
27   LTC    BNB          0.090909  0.192875                    2         88
2    BTC    ADA          0.079545  0.259896                    3         88
26   LTC    ETH          0.079545  0.244020                    3         88
16   ADA    ETH          0.068182  0.261890                    4         88
0    BTC    ETH          0.068182  0.357408                    3         88
Top pairs (global FDR):
   cause effect  frac_significant    mean_p  longest_run_windows  n_windows
14   BNB    LTC          0.068182  0.280912                    2         88
3    BTC    XRP          0.056818  0.305443                    4         88
18   ADA    XRP          0.056818  0.237433                    2         88
2    BTC    ADA          0.056818  0.259896                    3         88
5    ETH    BTC          0.045455  0.219297                    2         88
9    ETH    LTC          0.045455  0.233621                    2         88
25   LTC    BTC          0.045455  0.279368                    3         88
24   XRP    LTC          0.045455  0.376740                    2         88
27   LTC    BNB          0.045455  0.192875                    1         88
26   LTC    ETH          0.045455  0.244020                    1         88

🔹 BTC as cause (BTC → others)

No FDR (raw):

● BTC → LTC is the strongest (17.0% of windows, mean p ≈ 0.23, run length 6).

● BTC → XRP and BTC → ADA show moderate frequency (~13–12%).

● BTC → ETH (10.2%) and BTC → BNB (9.1%) are weaker and intermittent. 👉 Interpretation: Raw results make BTC appear influential across several pairs, especially on LTC.

Per-window FDR (from your earlier leaderboard):

● Only a few BTC-driven effects survive:

○ BTC → ADA (7.95%)

○ BTC → ETH (6.82%)

● The others (BTC → XRP, BTC → LTC) largely vanish. 👉 Interpretation: Once multiple-testing correction is applied, BTC’s influence becomes weaker and less consistent.

Global FDR:

● BTC → XRP (5.68%) and BTC → ADA (5.68%) survive.

● BTC → LTC remains only borderline. 👉 Interpretation: Under the strictest correction, only a couple of BTC links remain, and even these are weak and intermittent.

🔹 BTC as effect (others → BTC)

No FDR (raw):

● LTC → BTC is strongest (18.2%, mean p ≈ 0.28, run length 5).

● BNB → BTC (11.4%) is stronger than BTC → BNB (9.1%).

● ETH → BTC (9.1%) vs BTC → ETH (10.2%) are nearly symmetric.

● ADA → BTC (9.1%) is comparable to BTC → ADA (12.5%).

● XRP → BTC (6.8%) is weaker than BTC → XRP (13.6%). 👉 Interpretation: Without correction, LTC appears to influence BTC more than the reverse, and the ETH/BTC relation looks bidirectional but weak.

Per-window FDR:

● ETH → BTC (4.5%) survives, but BTC → ETH (6.8%) is stronger.

● LTC → BTC largely drops out. 👉 Interpretation: After adjustment, the apparent “reverse” influence of LTC on BTC vanishes, and ETH ↔ BTC is weakly bidirectional.

Global FDR:

● ETH → BTC (4.5%) survives, BTC → ETH does not.

● BTC → XRP and BTC → ADA remain, but no reverse links. 👉 Interpretation: Globally, BTC looks more like a receiver from ETH than a universal driver.

Visualize rolling p-values for example pairs

# Example: BTC -> ETH
pair = ('BTC', 'ETH')
p_series = pvals_no_fdr[pair]  # list of p-values per window
dates = pd.to_datetime(dates)

plt.figure(figsize=(12,4))
plt.plot(dates, p_series, marker='o', label='p-value')
plt.axhline(0.05, color='r', linestyle='--', label='alpha=0.05')
plt.title(f'Rolling Toda-Yamamoto p-values: {pair[0]} -> {pair[1]}')
plt.ylabel('p-value')
plt.xlabel('Date')
plt.legend()
plt.show()
Return Causality among Cryptocurrencies

Source: Yahoo Finance

Filter BTC as the “cause”

We want to see which coins are influenced by BTC:

btc_cause_no_fdr = stab_no_fdr[stab_no_fdr['cause'] == 'BTC']
btc_cause_no_fdr = btc_cause_no_fdr.sort_values('frac_significant', ascending=False)
print("BTC → other cryptos (no FDR):")
display(btc_cause_no_fdr[['effect','frac_significant','mean_p','longest_run_windows']])

BTC → other cryptos (no FDR):

effectfrac_significantmean_plongest_run_windows
4LTC0.1704550.2341256
3XRP0.1363640.3054434
2ADA0.1250000.2598963
0ETH0.1022730.3574083
1BNB0.0909090.3364393

Do the same for per-window FDR and global FDR:

btc_cause_perwindow = stab_perwindow_fdr[stab_perwindow_fdr['cause'] == 'BTC'].sort_values('frac_significant', ascending=False)
btc_cause_global = stab_global_fdr[stab_global_fdr['cause'] == 'BTC'].sort_values('frac_significant', ascending=False)

Interpretation:

Higher frac_significant means BTC has a stable causal influence on that coin.

Compare BTC → ETH vs ETH → BTC in the CSV to see directionality.

btc_effect_no_fdr = stab_no_fdr[stab_no_fdr['effect'] == 'BTC'].sort_values('frac_significant', ascending=False)
print("Other cryptos → BTC (no FDR):")
display(btc_effect_no_fdr[['cause','frac_significant','mean_p','longest_run_windows']])

Other cryptos → BTC (no FDR):

causefrac_significantmean_plongest_run_windows
25LTC0.1818180.2793685
10BNB0.1136360.2163913
5ETH0.0909090.2192972
15ADA0.0909090.3576283
20XRP0.0681820.2798125

Compare reverse direction

Check if the influence is stronger one way

import matplotlib.pyplot as plt

plt.figure(figsize=(8,5))
plt.bar(btc_cause_no_fdr['effect'], btc_cause_no_fdr['frac_significant'], color='orange', alpha=0.7, label='BTC → other')
plt.bar(btc_effect_no_fdr['cause'], btc_effect_no_fdr['frac_significant'], color='blue', alpha=0.5, label='Other → BTC')
plt.ylabel('Fraction of significant windows')
plt.title('Directional influence: BTC vs other cryptos (no FDR)')
plt.xticks(rotation=45)
plt.legend()
plt.show()
Return Causality among Cryptocurrencies

Source: Yahoo Finance

Interpretation of the plot:

Orange bars higher than blue bars → BTC dominates as a causal driver.

If some blue bars are comparable, it means reverse causality exists (maybe smaller altcoins occasionally influence BTC, e.g., during hype events).

import matplotlib.pyplot as plt

# Top 5 strongest BTC → other cryptos
top_n = 5
top_links = btc_cause_no_fdr.head(top_n)

plt.figure(figsize=(8,5))
plt.bar(top_links['effect'], top_links['frac_significant'], color='orange', alpha=0.8)
plt.ylabel('Fraction of significant windows')
plt.xlabel('Effect Cryptos')
plt.title(f'Top {top_n} BTC → other cryptos (strongest stable links)')
plt.ylim(0,1)
plt.grid(axis='y', linestyle='--', alpha=0.5)
for i, val in enumerate(top_links['frac_significant']):
    plt.text(i, val+0.02, f"{val:.2f}", ha='center', va='bottom', fontsize=10)
plt.show()
Return Causality among Cryptocurrencies

Source: Yahoo Finance

Conclusion:

● This study examined causal linkages among six major cryptocurrencies (BTC, ETH, BNB, ADA, XRP, LTC) using the Toda–Yamamoto framework applied in rolling windows. The analysis highlights how inference is strongly shaped by the treatment of multiple testing.

● Raw (no-FDR) results suggest a dense network of apparent causal connections, with Bitcoin and Litecoin often emerging as influential senders. However, once false discovery rate (FDR) adjustments are applied, the network becomes far sparser: leadership effects are weak, intermittent, and often non-persistent. The contrast between pre-FDR and FDR-adjusted results underscores how easily raw significance can overstate structure when many tests are conducted simultaneously.

● The stability leaderboard (Table 3) shows that only a handful of relations display consistent significance across windows, and even these weaken under stricter corrections. LTC → BNB is the most robust under raw and per-window FDR settings, while ADA → XRP and XRP → ETH show local robustness. Under global FDR, only a few links such as BNB → LTC and BTC → XRP survive, emphasizing the rarity of truly persistent causal effects.

● Overall, our findings suggest that causal relations in crypto returns are weak, transient, and highly sensitive to multiple-testing adjustments. Claims of persistent dominance—such as Bitcoin being a universal driver—are not supported once proper corrections are applied. This highlights the importance of controlling for false discoveries in high-dimensional time-series causality analysis.

Join The Conversation

For specific platform feedback and suggestions, please submit it directly to our team using these instructions.

If you have an account-specific question or concern, please reach out to Client Services.

We encourage you to look through our FAQs before posting. Your question may already be covered!

Leave a Reply

Disclosure: Interactive Brokers Third Party

Information posted on IBKR Campus that is provided by third-parties does NOT constitute a recommendation that you should contract for the services of that third party. Third-party participants who contribute to IBKR Campus are independent of Interactive Brokers and Interactive Brokers does not make any representations or warranties concerning the services offered, their past or future performance, or the accuracy of the information provided by the third party. Past performance is no guarantee of future results.

This material is from Quant Insider and is being posted with its permission. The views expressed in this material are solely those of the author and/or Quant Insider and Interactive Brokers is not endorsing or recommending any investment or trading discussed in the material. This material is not and should not be construed as an offer to buy or sell any security. It should not be construed as research or investment advice or a recommendation to buy, sell or hold any security or commodity. This material does not and is not intended to take into account the particular financial conditions, investment objectives or requirements of individual customers. Before acting on this material, you should consider whether it is suitable for your particular circumstances and, as necessary, seek professional advice.

Disclosure: Digital Assets

Trading in digital assets, including cryptocurrencies, is especially risky and is only for individuals with a high risk tolerance and the financial ability to sustain losses. Eligibility to trade in digital asset products may vary based on jurisdiction.

Disclosure: API Examples Discussed

Throughout the lesson, please keep in mind that the examples discussed are purely for technical demonstration purposes, and do not constitute trading advice. Also, it is important to remember that placing trades in a paper account is recommended before any live trading.

IBKR Campus Newsletters

This website uses cookies to collect usage information in order to offer a better browsing experience. By browsing this site or by clicking on the "ACCEPT COOKIES" button you accept our Cookie Policy.