← Back to Blog
STRATEGY

Statistical Arbitrage with Crypto Pairs: A Python Implementation

June 30, 2026 · 9 min read · LMEX.AI

Statistical arbitrage sounds exotic. The core idea is not. You find two assets that historically move together, wait for them to diverge, then bet they converge again. On crypto perpetuals this is one of the few trades a retail desk can run market-neutral, because you are long one contract and short another of similar size, so a market-wide crash barely touches your book.


This article is the implementation, not the theory tour. We test a pair for cointegration, build a z-score signal, and backtest it against LMEX perpetual data. If you want the conceptual walkthrough first, read our practical pairs trading guide.


Correlation is not enough


Most people pick pairs by correlation. That is a trap. Two assets can be 0.95 correlated and still drift apart forever, because correlation measures co-movement of returns, not whether the price ratio is stable. What you actually want is cointegration: a linear combination of the two prices that is mean-reverting even when each price wanders.


The standard test is Engle-Granger. Regress one price on the other, take the residual spread, and run an Augmented Dickey-Fuller test on that spread. If the ADF p-value is below 0.05, the spread is stationary and you have a tradeable pair.


import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller

def test_cointegration(price_a, price_b):
    # Hedge ratio from OLS: price_a = beta * price_b + resid
    x = sm.add_constant(price_b)
    model = sm.OLS(price_a, x).fit()
    beta = model.params.iloc[1]
    spread = price_a - beta * price_b

    adf_stat, pvalue, *_ = adfuller(spread.dropna())
    return {
        "beta": beta,
        "adf_pvalue": pvalue,
        "cointegrated": pvalue < 0.05,
        "spread": spread,
    }

Run this over candidate pairs from the same sector. ETH-PERP against a large-cap alt, or two L1 tokens, cointegrate far more often than assets from different narratives. BTC against a random meme coin almost never does.


Building the z-score signal


Once you have a stationary spread, the trade is mechanical. Standardise the spread into a rolling z-score and trade the extremes.


def zscore_signal(spread, window=48, entry=2.0, exit=0.5):
    mean = spread.rolling(window).mean()
    std = spread.rolling(window).std()
    z = (spread - mean) / std

    pos = pd.Series(0, index=spread.index)
    pos[z > entry] = -1   # spread rich: short A, long B
    pos[z < -entry] = 1   # spread cheap: long A, short B
    # flatten inside the exit band
    pos[z.abs() < exit] = 0
    return pos.replace(0, np.nan).ffill().fillna(0)

The window matters more than the entry threshold. Too short and you chase noise. Too long and the mean lags a genuine regime change and you hold a broken spread all the way down. On hourly bars a 48 to 72 bar window is a reasonable starting point, but confirm it with a proper backtest rather than trusting the default.


Costs are the whole game


A stat-arb backtest that ignores costs is fiction. You pay taker or maker fees on two legs, you pay funding on both perpetual positions every eight hours, and you pay slippage on entry and exit. Because the edge per trade is small, these costs eat a huge fraction of gross return.


Two rules keep you honest. Post maker orders on both legs so you earn rebates instead of paying the spread, and net your funding: a long-short pair often has offsetting funding, but not always, and a persistently negative net-funding pair can bleed you even when the spread behaves. Model both explicitly before you trust any equity curve. Our backtesting walkthrough covers building cost-aware simulations properly.


When the spread breaks


The failure mode that ends stat-arb accounts is a pair that decouples permanently. A protocol gets hacked, a token gets delisted, a narrative dies. The spread that reverted a hundred times suddenly does not, and your mean-reversion logic keeps adding to the loser.


Defend against it with a hard stop on the z-score itself. If the spread pushes past, say, 4 standard deviations, do not treat it as a better entry. Treat it as evidence the relationship is broken, close the trade, and re-run the cointegration test before you touch that pair again.


Frequently Asked Questions


Q: How many pairs should I trade at once?

More than one, because any single spread can break. A basket of 5 to 10 uncorrelated cointegrated pairs smooths the equity curve far more than optimising a single pair. Just watch aggregate exposure so the "market-neutral" book does not quietly become directional.


Q: What timeframe works best for crypto stat-arb?

Hourly to 4-hour bars are the retail sweet spot. Sub-minute stat-arb exists but competes with colocated firms on latency, which retail cannot win. Slower spreads reward analysis over speed.


Q: Do I need both legs on LMEX?

It is cleaner if you do. Same venue means one margin account, correlated funding, and no transfer latency between exchanges. Cross-venue pairs add settlement and withdrawal risk that usually is not worth the extra candidates.


Q: How do I know if a dead spread is coming back?

Re-run the ADF test on a recent window. If the spread is no longer stationary on fresh data, the relationship has changed and historical mean reversion no longer applies. Trust the current test, not the trade that used to work.


Related Articles


→ Statistical Arbitrage on LMEX: A Practical Pairs Trading Walkthrough
→ Backtesting Your LMEX Trading Bot in Python
→ Delta-Neutral Crypto Strategies: Harvesting Funding Without Directional Risk
← All ArticlesBuild a Bot →