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.
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?
- 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. - Correlation matrix — build the
N × Ncross-correlation matrixCof the standardized returns. - Eigenspectrum vs. Marchenko–Pastur — diagonalize
Cand compare the empirical eigenvalue density to the MP law with boundsλ± = (1 ± √(N/T))². Eigenvalues aboveλ₊are candidate signal. - Inverse participation ratio (IPR / NPR) — measure how localized each eigenvector is, i.e. how many assets contribute to each mode.
- 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). - 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?
- Rolling regime detection — recompute the spectrum over a 30-day window
stepped weekly and track market collectivity
λ_max / Nand the spectral effective rank against known crypto events. - 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.
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 toN, 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.16at 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.64and 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.62is essentially DOGE alone (loading 0.84),λ₄ = 0.51is 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 untilk = 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/Nranges 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 ownq = 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,expof 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. - the level —
-
A methodological negative result is kept deliberately. The entry-permutation null cannot work: the analytic bound is
1 + (N−1)ρ̄ = 11.277, the shuffledλ_maxaverages11.284 ± 0.002, and the real value is11.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 |
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
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 .gitattributesTwo of the steps deserve a note, both easy to trip over:
- Step 4 — why register a kernel at all. The default
python3kernelspec belongs to whichever interpreter installed it — with conda, usuallybase. A notebook left on it starts in an environment wherecrypto_rmtis absent and fails on the first import. Registering under the namecrypto-rmtmakes the choice explicit and survives environment changes.--userwrites to~/.local/share/jupyter/kernels/crypto-rmt; nothing outside your home directory is touched. - Step 6 — why it is per clone.
.gitattributesbinds*.ipynbto thenbstripoutfilter 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 acleanfilter that strips outputs on the way into a commit and a no-opsmudgefilter, so your working copy keeps every rendered figure and execution count while the committed notebook stays free of them. The same install adds anipynbdiff driver, sogit diffshows source changes rather than pages of base64 image data. Confirm withnbstripout --status.
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 *.ipynbA newly created notebook defaults to the python3 kernel — pick crypto-rmt
from the kernel selector once, and it is saved into that file.
pytest # unit tests + a golden integration test on the real data
ruff check src tests
black src testsfrom 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.
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.
- 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).
Released under the MIT License — see LICENSE.