Pairs trading is one of the oldest quantitative strategies. The idea: two correlated assets diverge sometimes, then re-converge. Buy the underperformer, short the outperformer, profit when they reconverge. Because you are long one asset and short another, you are roughly market-neutral. The market can do whatever it likes; you are betting on the spread between two things, not on direction.
On crypto perpetuals, pairs trading still works in 2026. Not as well as it did in 2018, but the edge is real. Most retail attempts fail though. Worth understanding why before deploying capital.
Two assets X and Y are cointegrated if their long-term spread is mean-reverting even when each one individually trends. Test with the Engle-Granger or Johansen test. Without cointegration, "pairs trading" is just two random directional bets pointing in opposite directions.
If X and Y cointegrate, model their relationship:
\`\`\`
Y = α + β·X + ε
\`\`\`
ε (the residual) is what you trade. When ε is more than 2 standard deviations above its mean, short Y and long β·X (Y is overpriced relative to X). When ε is more than 2 standard deviations below, do the opposite.
The 2-sigma threshold is a starting point. Calibrate to your specific pair using backtest results.
This is the hard part. Most asset pairs in crypto are correlated but not cointegrated. They move together most of the time but their spread does not mean-revert reliably.
Pairs that tend to work on LMEX:
Pairs that look correlated but tend not to cointegrate reliably:
Test on at least 6 months of data. If the Engle-Granger p-value is above 0.05, the pair is not reliable enough to trade.
Minimum viable pairs trading bot for ETH-PERP / SOL-PERP. Not production-ready, but the bones are right:
\`\`\`python
import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import coint
import ccxt
exchange = ccxt.lmex({'apiKey': '...', 'secret': '...'})
def fetch_history(symbol, days=30, timeframe='1h'):
candles = exchange.fetch_ohlcv(symbol, timeframe, limit=days*24)
df = pd.DataFrame(candles, columns=['ts', 'o', 'h', 'l', 'c', 'v'])
return df['c']
def check_cointegration(x, y, threshold=0.05):
score, pvalue, _ = coint(x, y)
return pvalue < threshold, pvalue
def calculate_hedge_ratio(x, y):
return np.cov(x, y)[0, 1] / np.var(x)
def calculate_spread(x, y, hedge_ratio):
return y - hedge_ratio * x
def trade_signal(spread, lookback=20, threshold=2.0):
mean = spread.tail(lookback).mean()
std = spread.tail(lookback).std()
z_score = (spread.iloc[-1] - mean) / std
if z_score > threshold:
return 'short_spread'
elif z_score < -threshold:
return 'long_spread'
elif abs(z_score) < 0.5:
return 'close'
return 'hold'
x = fetch_history('ETH-PERP')
y = fetch_history('SOL-PERP')
is_cointegrated, pvalue = check_cointegration(x, y)
if not is_cointegrated:
print(f"Pair not cointegrated (p={pvalue:.3f}), skipping")
exit()
hedge_ratio = calculate_hedge_ratio(x, y)
spread = calculate_spread(x, y, hedge_ratio)
signal = trade_signal(spread)
\`\`\`
This produces a signal. Actual trading requires position sizing, order placement, exit logic — none trivial.
The hedge ratio sets relative sizing. Hedge ratio 0.4 means you long 1 unit of Y for every 0.4 units of X you short. In dollars: long \$10,000 of SOL-PERP, short \$4,000 of ETH-PERP.
Total capital per pair: no more than 10% of account, and that is aggressive. Pairs strategies look low-risk in backtest but produce large drawdowns when the relationship breaks down. Size accordingly.
Two exit conditions:
**Reversion to mean.** Close when z-score returns to within ±0.5. The normal happy path.
**Stop on time and divergence.** If the position is open more than 5 days and the z-score is still beyond ±3, the relationship may have broken. Close and reassess.
The second condition is what saves you from the "this time is different" scenario where the spread keeps widening because something fundamental changed. Without it, pairs strategies can carry losing trades indefinitely.
**Regime change.** A pair that cointegrates in 2024 may stop cointegrating in 2026 because the assets are now driven by different factors. Re-test weekly.
**Funding rate asymmetry.** Long one perp, short another — you pay funding on one leg, receive on the other. If both legs have the same direction of funding (both positive or both negative), you pay/receive twice. Funding can eat the spread profit. Account for it in your backtest.
**Liquidity events on one leg.** SOL gets liquidation cascades but ETH does not. The spread blows out for non-fundamental reasons. Your model says "great, more divergence to capture." You are actually catching a knife.
**Slippage scales worse than expected.** Closing two legs simultaneously while the market is moving creates directional exposure during the close. For small positions this is noise. For large positions it eats the spread.
Q: What is the typical Sharpe ratio of a working pairs trading strategy?
1.0 to 2.0 after costs. Anything above 3.0 in backtest is almost certainly overfit. Real pairs trading is steady not spectacular — small profits per trade, lots of trades, drawdowns that test your conviction but rarely break the strategy.
Q: How often should I re-check cointegration?
Weekly minimum, daily if you can. If the rolling 60-day cointegration p-value drifts above 0.10, reduce position size. Above 0.20, close out and stop trading the pair until it stabilises.
Q: Can I run multiple pairs trading strategies simultaneously?
Yes, and this is where the strategy shines. A portfolio of 5-10 uncorrelated pairs produces smoother equity curves than any single one. Just make sure the pairs are genuinely uncorrelated — running ETH/SOL and ETH/BNB simultaneously doubles your ETH exposure.
Q: What is the smallest account size that makes pairs trading viable?
Around \$25,000. Below that the minimum position sizes plus fees plus slippage eat too much of the per-trade profit. Pairs trading is high turnover (5-15 trades per pair per month) and economics matter.