Skip to content

About

RMT for cryptocurrency correlation structure: Marchenko–Pastur noise filtering, eigenvalue analysis, and asset clustering.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

crypto-rmt — Random Matrix Theory on Cryptocurrency Correlations

Python License

Separating genuine market structure from statistical noise in a basket of cryptocurrencies, using Random Matrix Theory (RMT). The empirical correlation spectrum is compared against the Marchenko–Pastur law to identify which correlations are real, which eigenvectors carry market-wide signal, and how the assets cluster.


Overview

Correlation matrices estimated from finite time series are noisy: with N assets and T observations, most of the eigenvalue spectrum is indistinguishable from that of a random matrix. RMT provides a principled null model — the Marchenko–Pastur distribution — for the bulk of eigenvalues you'd see from pure noise. Eigenvalues that fall outside that bulk carry genuine information: the largest typically encodes a market-wide mode, and the next few often map onto sectors or groups of co-moving assets.

That second half turns out not to hold here. On this universe the market mode absorbs two-thirds of the variance, no eigenvalue lands inside the noise band at all, and the sub-leading modes are single-asset idiosyncrasies rather than sectors — a near one-factor market. Establishing that, and separating what the data shows from what is forced by construction, is the substance of the analysis.

This project applies that toolkit to 17 cryptocurrencies sampled at hourly resolution over 2021–2026 (48,405 returns per asset), and asks:

  • How much of the measured correlation structure is real vs. noise?
  • Is there a dominant "market mode," and which assets load onto it?
  • Do the deviating eigenvectors reveal interpretable clusters?

Method

  1. Data & returns — load aligned hourly price series, handle missing values, and compute log-returns r_t = log₁₀(p_t / p_{t-1}), then z-score each asset.
  2. Correlation matrix — build the N × N cross-correlation matrix C of the standardized returns.
  3. Eigenspectrum vs. Marchenko–Pastur — diagonalize C and compare the empirical eigenvalue density to the MP law with bounds λ± = (1 ± √(N/T))². Eigenvalues above λ₊ are candidate signal.
  4. Inverse participation ratio (IPR / NPR) — measure how localized each eigenvector is, i.e. how many assets contribute to each mode.
  5. Entry-permutation null (a worked negative result) — permute the off-diagonal entries of C, re-diagonalize, and threshold the resulting largest-eigenvalue distribution. It cannot separate signal: a permutation preserves the mean off-diagonal ρ̄, hence the bound λ_max ≥ 1 + (N−1)ρ̄, so the shuffled top eigenvalue lands on the real one by construction. Kept as a documented trap; the analysis relies on (3).
  6. Hierarchical clustering — cluster assets from the correlation distances and visualize the structure as a dendrogram / cluster map, with a sector colour strip along the margins. Whether the tree agrees with the taxonomy is then tested rather than eyeballed: for each family, is it exactly the leaf set of some subtree, and does any cut of the tree isolate it?
  7. Rolling regime detection — recompute the spectrum over a 30-day window stepped weekly and track market collectivity λ_max / N and the spectral effective rank against known crypto events.
  8. Structure over time — draw the static picture (market-mode composition, dendrogram, correlation map) for the least and most collective windows around an event, so the regime shift is read off the matrix itself rather than off a scalar index.

Key results

N = 17 assets, T = 48,405 hourly returns (2021-01-01 → 2026-07-11), so q = N/T ≈ 3.5 × 10⁻⁴ and the Marchenko–Pastur band is [0.963, 1.038].

  • One eigenvalue lies above λ₊: λ_max = 11.34, carrying 67% of the trace. Under the identity null a single eigenvalue this large is overwhelmingly excluded — the correlation structure is unambiguously real.

  • There is no noise bulk. Of 17 eigenvalues, 0 fall inside the band — 1 above λ₊ and 16 below λ₋. The sub-λ₋ group is not a second finding: the eigenvalues sum to N, so a market mode worth 11.34 mechanically leaves the other sixteen an average of (17 − 11.34)/16 = 0.35. Widening the band by re-estimating over a single 720-hour window (q ≈ 0.024, band [0.72, 1.33]) changes nothing: still 0 of 17 inside, with λ_max = 12.16 at 72% of the trace. The textbook MP bulk appears in equity studies because those matrices are large (N ≳ 400) with a market mode worth only ~25% of the trace; 17 liquid crypto majors are a near one-factor market, with mean pairwise correlation ρ̄ = 0.64 and every pair between 0.45 and 0.84.

  • The market mode is almost perfectly delocalised. Participation ratio 16.7 / 17, with all seventeen loadings sharing one sign and spanning only 0.194 (DOGE) to 0.267 (ETH) — every asset participates, and near-equally. Every other mode is localised by contrast (participation 1.9–7.7): λ₂ = 0.62 is essentially DOGE alone (loading 0.84), λ₄ = 0.51 is SAND. These are idiosyncratic residuals, not sector factors.

  • Hierarchical clustering finds no sector partition, consistent with the above, and the cluster map now says so against a labelled taxonomy rather than by assertion. Average linkage on the Mantegna distance peels off DOGE, then SAND, then XRP as successive singletons while the remaining fourteen stay one block. Tested against the sector tags (crypto_rmt.io.SECTORS, fixed before any correlation is computed and never seen by the linkage), none of the three multi-asset families — L1, DeFi, payments — is a subtree of the tree, and no cut of the tree isolates any of them. The lone surviving fragment is AAVE/UNI/SUSHI, three of the four DeFi assets, which do form a genuine subtree; but no cut recovers it until k = 10, where 7 of the 10 clusters are single assets and the fourth DeFi asset, CRV, sits outside. A group visible only after the tree has been shattered is not a partition the clustering found. The cluster map is informative only once its colour scale is centred on ρ̄; on a [-1, 1] scale every cell renders the same shade.

  • Rolling regime detection (720-hour window, weekly step) is where the temporal signal is. Collectivity λ_max/N ranges 0.40 → 0.85 around a median of 0.73; the effective factor count moves inversely (correlation −0.99, which is largely mechanical) from 9.8 factors down to 2.2. The all-time peak is 0.85 on 2024-05-12; the trough is 0.40 in February 2021. Against the annotated events, the record is mixed and is reported as such: the LUNA/UST collapse produced a clear spike (0.84 within 60 days, effective rank 2.4) and the USDC/SVB depeg a milder one (0.78), while the FTX collapse (0.72) and the spot-BTC-ETF approval (0.74) show no elevation above the median.

  • "Effective number of factors" is reported two ways, and only one of them moves. The threshold reading — count the eigenvalues clearing the per-window MP edge λ₊ (recomputed at the window's own q = 17/720) — is a step function: 1 factor in 276 of 284 windows (97%) and 2 in the remaining 8. It is the honest baseline, and it is why the figures instead plot the entropy reading, exp of the spectral entropy, which reads the shape of the whole spectrum rather than thresholding it and so resolves regimes the count cannot. A per-window entry-permutation threshold is not an alternative: that null is degenerate by construction (below).

  • Collectivity is mean correlation, restated. Across the 284 windows, corr(λ_max/N, ρ̄) = +0.999. That is the Rayleigh bound, not a finding: λ_max ≥ 1 + (N−1)ρ̄ is tight when the off-diagonals are near-uniform, so λ_max/N ≈ (1 + 16ρ̄)/17 — an affine function of the mean pairwise correlation. The temporal section is worth reading with that in mind: the eigenvalue machinery earns its keep on the static spectrum, while the rolling collectivity index is a well-motivated repackaging of average correlation rather than something average correlation cannot say.

  • The regime shift is visible in the matrix, and it has two distinct parts. Contrasting the least collective window in the 180 days before the LUNA collapse with the most collective one in the 60 days after it, the low pick lands on the 30 days to 2021-11-14 — the Nov-2021 cycle top. That is not a "calm" window and is not labelled as one: the top was euphoric and violent, but its assets were moving independently. Low collectivity means decoupled, not quiet. The trough is broad rather than a knife edge — six consecutive weeks at 0.65, 0.55, 0.51, 0.51, 0.55, 0.61 — so the pick is stable for any lookback that reaches it. Between the two windows collectivity rose 0.51 → 0.84 and effective factors fell 7.4 → 2.4, and two genuinely different moments of the correlation distribution moved:

    • the level — ρ̄ 0.454 → 0.824, with the weakest pair rising from 0.065 to 0.708, so no pair stayed uncorrelated;
    • the dispersion — sd of the pairwise correlations 0.163 → 0.049, which is what collapses the market-mode loadings onto the equal-loading level 1/√N = 0.243: they span 0.089 (SAND) to 0.301 (ETH) at the top, a spread of 0.212, but only 0.219 to 0.256 in the crash, a spread of 0.037.

    The second is not implied by the first — a market can grow uniformly more correlated without homogenising — and corr(collectivity, loading spread) is only −0.76, against +0.91 between the loading spread and the correlation sd. So the loadings panel carries information the collectivity trace does not. The dendrogram is the weak one of the three: d = √(2(1−C)) forces the tree to shrink when correlations rise (corr(ρ̄, tree height) = −0.89), so the SAND and DOGE singletons being absorbed into the single block restates the level rise rather than confirming it independently.

  • A methodological negative result is kept deliberately. The entry-permutation null cannot work: the analytic bound is 1 + (N−1)ρ̄ = 11.277, the shuffled λ_max averages 11.284 ± 0.002, and the real value is 11.335 — a 0.4% gap. The permuted matrices are not even positive semi-definite (12.3% of their eigenvalues are negative). See Method (5).

Figure Description
figures/spectrum_vs_mp.png Full-window eigenvalue spectrum vs. the Marchenko–Pastur band
figures/spectrum_vs_mp_window.png The same, re-estimated over one 30-day window (wider band)
figures/participation.png Participation ratio per mode vs. its eigenvalue
figures/cluster_map.png Correlation-based hierarchical cluster map, with a sector colour strip
figures/collectivity.png Rolling market collectivity λ_max / N with event markers
figures/regime_panel.png Collectivity vs. effective factor count, shared time axis
figures/structure_contrast.png Market-mode composition, dendrogram, and correlation map for the least vs. most collective window around the LUNA crash
figures/spectrum_vs_null.png Diagnostic: why the entry-permutation null is degenerate

Project structure

crypto-rmt/
├── src/crypto_rmt/
│   ├── io.py           # load, align, clean series; build the returns matrix
│   ├── rmt.py          # correlation matrix, eigen-decomposition,
│   │                   #   Marchenko–Pastur bounds, IPR/NPR, permutation null
│   ├── clustering.py   # hierarchical clustering; sector-agreement test
│   ├── dynamics.py     # rolling collectivity, effective rank, regime events,
│   │                   #   calm/crash window selection
│   └── plotting.py     # every figure; pure drawing, no analysis
├── notebooks/
│   └── demo.ipynb      # imports the package, reproduces the figures above
├── scripts/
│   └── fetch_data.py   # populate data/ from the exchange API
├── data/               # raw price series (gitignored — see Data)
├── figures/            # generated figures
├── tests/              # pytest smoke tests
├── README.md
└── pyproject.toml

Setup

Six steps, start to finish. Run every command with the environment activated.

# 1. clone
git clone https://github.com/fatemeh-studio/crypto-rmt.git
cd crypto-rmt

# 2. create and activate the environment
conda create -n crypto-rmt python=3.12
conda activate crypto-rmt
#    (or, without conda: python -m venv .venv && source .venv/bin/activate)

# 3. install the package and its extras, editable
pip install -e ".[dev,notebook]"

# 4. register the Jupyter kernel under this project's own name
python -m ipykernel install --user --name crypto-rmt --display-name "crypto-rmt"

# 5. download the price data (data/ is gitignored)
python scripts/fetch_data.py

# 6. strip notebook outputs from commits (keeps them in your working copy)
nbstripout --install --attributes .gitattributes

Two of the steps deserve a note, both easy to trip over:

  • Step 4 — why register a kernel at all. The default python3 kernelspec belongs to whichever interpreter installed it — with conda, usually base. A notebook left on it starts in an environment where crypto_rmt is absent and fails on the first import. Registering under the name crypto-rmt makes the choice explicit and survives environment changes. --user writes to ~/.local/share/jupyter/kernels/crypto-rmt; nothing outside your home directory is touched.
  • Step 6 — why it is per clone. .gitattributes binds *.ipynb to the nbstripout filter and is tracked, but the filter itself lives in .git/config, which cloning does not carry over — so a fresh clone has the binding with nothing behind it. The command installs a clean filter that strips outputs on the way into a commit and a no-op smudge filter, so your working copy keeps every rendered figure and execution count while the committed notebook stays free of them. The same install adds an ipynb diff driver, so git diff shows source changes rather than pages of base64 image data. Confirm with nbstripout --status.

Running the notebooks

The notebooks in notebooks/ are pinned to the crypto-rmt kernel. Open them in Jupyter or VS Code, or re-execute everything headlessly to regenerate figures/:

cd notebooks
jupyter nbconvert --to notebook --execute --inplace *.ipynb

A newly created notebook defaults to the python3 kernel — pick crypto-rmt from the kernel selector once, and it is saved into that file.

Development

pytest                  # unit tests + a golden integration test on the real data
ruff check src tests
black src tests

Usage

from crypto_rmt import plotting
from crypto_rmt.io import TICKERS, load_prices, returns_matrix
from crypto_rmt.rmt import correlation_matrix, eigsystem

prices = load_prices(TICKERS, "data/")
returns = returns_matrix(prices, TICKERS)          # N × T, z-scored log10 returns
n, t = returns.shape

C = correlation_matrix(returns)
eigenvalues, eigenvectors = eigsystem(C)           # descending; eigenvectors in columns

plotting.plot_spectrum_vs_mp(
    eigenvalues, n=n, t=t, save="figures/spectrum_vs_mp.png"
)

Or open notebooks/demo.ipynb to reproduce the full analysis end to end.

Data

Hourly price series for 17 cryptocurrencies, stored as JSON ([{"t": <unix_seconds>, "v": <price | null>}, ...]) with t the bar open time and v the bar close. Raw data files are not committed (size + redistribution); data/ is gitignored.

Source: Binance public spot klines, /api/v3/klines, no API key required — fetched by scripts/fetch_data.py, which is committed so the dataset is reproducible from a free, documented source. The primary host is data-api.binance.vision (Binance's public mirror, not geo-restricted), with api.binance.com and alternates as fallbacks.

Universe: BTC, ETH, BNB, SOL, ADA, XRP, AVAX, DOT, ATOM, LTC, DOGE, LINK, UNI, AAVE, CRV, SUSHI, SAND — chosen for liquidity and a full common history.

Sector tags: each asset carries a conventional market category (crypto_rmt.io.SECTORS, e.g. L1, DeFi/lending, payments). These are not exchange-provided; they are hand-assigned from the usual crypto taxonomy and fixed before any correlation is computed, so "do the clusters follow sectors?" is a question the data answers rather than a label applied after the fact. They annotate the cluster map and drive clustering.sector_agreement; nothing in the correlation, spectrum, or linkage pipeline reads them.

Coverage: 48,405 hourly returns per asset, 2021-01-01 → 2026-07-11. Assets are aligned by timestamp intersection and restricted to hours where every asset has a finite price; null quotes are dropped, never filled. Seven of the 48,404 return intervals consequently span a gap longer than one hour (longest 5 h), which is immaterial — the alternative, forward-filling to keep a strictly hourly grid, would fabricate zero returns and bias correlations downward.

References

  • L. Laloux, P. Cizeau, J.-P. Bouchaud, M. Potters, Noise dressing of financial correlation matrices, Phys. Rev. Lett. 83, 1467 (1999).
  • V. Plerou, P. Gopikrishnan, B. Rosenow, L. A. N. Amaral, T. Guhr, H. E. Stanley, Random matrix approach to cross correlations in financial data, Phys. Rev. E 65, 066126 (2002).
  • V. A. Marchenko, L. A. Pastur, Distribution of eigenvalues for some sets of random matrices, Math. USSR-Sbornik 1, 457 (1967).

License

Released under the MIT License — see LICENSE.

About

RMT for cryptocurrency correlation structure: Marchenko–Pastur noise filtering, eigenvalue analysis, and asset clustering.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages