Two stocks can rise together for years and still be untradeable as a pair. Correlation measures co-movement; pairs trading needs cointegration — a stationary, mean-reverting spread. This post builds the standard test pipeline in Python and shows where most LATAM pairs fail.
Educational content: methodology, no signals, no return claims.
1. Correlation Lies, Cointegration Trades
Correlated prices drift apart permanently (both trend up, spread widens forever). A cointegrated pair has a spread that reverts to a stable mean, which is what a long/short entry bets on. The Engle–Granger (1987) two-step procedure tests exactly this:
- Regress Y on X by OLS → hedge ratio β.
- Run an Augmented Dickey–Fuller (ADF) test on the residual spread. Only a stationary spread (low ADF p-value) is tradeable.
import numpy as np
import statsmodels.api as sm
from statsmodels.tsa.stattools import coint, adfuller
# y, x: aligned log-price series of the two candidate assets
score, pvalue, _ = coint(y, x) # Engle-Granger cointegration test
print(f"EG p-value: {pvalue:.4f}") # tradeable only if clearly < 0.05
res = sm.OLS(y, sm.add_constant(x)).fit()
beta = res.params[1] # hedge ratio: short beta units of X per 1 of Y
spread = y - beta * x
adf_stat, adf_p, *_ = adfuller(spread)[:2]
A p-value above conventional significance is not a "weak pair" — it is a rejection. Discard it. Most random pairs fail; that is the expected outcome, not bad luck. (Our falsification-first protocol for backtests is documented in Why Perfect Backtests Fail.)
2. Half-Life: How Fast Does It Revert?
A stationary spread that reverts once a year is untradeable. Estimate the half-life with a simple AR(1) regression of spread changes on lagged spread:
d_spread = np.diff(spread)
lag = spread[:-1]
beta_hl = np.polyfit(lag, d_spread, 1)[0]
halflife = -np.log(2) / beta_hl
print(f"Half-life: {halflife:.1f} bars")
Practical rule: half-life should sit comfortably between transaction-cost frequency (too fast = costs eat everything) and your holding horizon (too slow = capital sits idle). Both extremes fail — for opposite reasons.
3. Entries on the Z-Score, Exits on Discipline
z = (spread - spread.mean()) / spread.std()
# Enter when |z| > 2.0 (long the laggard, short the leader),
# exit when z crosses back through ~0.25 — never "wait for full reversion".
Hard stops still apply: cointegration breaks (mergers, regime changes, delistings). A pair that held for three years can decouple in a week. Size every pair as if the spread can go to 3–4σ and stay there.
4. Why LATAM (BVC) Pairs Are Harder
Applying this to Colombian equities (BVC) adds three frictions US textbook examples ignore:
- Liquidity: wider spreads and thin books mean your backtest fill assumptions are fiction unless you model slippage explicitly. Test with volume-capped fills.
- Costs: commissions, exchange fees and taxes compound on a strategy that trades often. Recompute the half-life economics after costs — many "profitable" spreads die here.
- Short access: pairs trading needs both legs. If you cannot reliably short one side, the strategy is long-only spread timing, a different (usually worse) trade. Verify borrow availability before any capital.
No specific pairs are recommended here — universes change, and any named pair would be stale on arrival. Run the pipeline above on current data instead.
5. Validation Checklist
- In-sample cointegration + out-of-sample spread stability (Walk-Forward, not one fit).
- Half-life compatible with costs and horizon.
- Break scenario sized and stop-defined before the first trade.
- 60+ days paper trading the exact execution path.
Study material under Colombia SFC Decreto 2555 de 2010 — financial-engineering education, not investment advice. No signals, no managed accounts.
By **Mahdi Goodarzi* (g.dev/mahdigoodarzi), Gueta Quant — research notes at guetaquant.com/blog. Related: Pine Script v6 guide.*
Top comments (0)