All projects

Pairs-trading backtest

A Mastercard–Visa pairs trade in Python, with an out-of-sample test that produced too few signals to judge.

  • Role Sole author — design, implementation, testing
  • Stack Python, pandas, statsmodels, yfinance
  • Completed
  • Aug–Sep 2026

Backtest results

The same trade, tested twice

The real results exported from my notebook. Pick a threshold setting to see what it does to the 2025 signal.

Look-ahead against locked parameters

Same code, same pair. On the left the hedge ratio is fitted on the whole of 2021–2025 and then tested on that same span. On the right it is fitted on 2021–24, locked, and tested on 2025 after 5bp costs. Both panels share a y-axis.

Fitted on 2021–2025, tested on 2021–2025Full-sample run with look-ahead bias+25.6%
1.001.101.201.3020212022202320242025
Fitted on 2021–24, tested on 2025Parameters locked, net of 5bp costs−0.09%
1.001.101.201.30FebMayAugNov

Growth of $1. The left panel's Sharpe of 1.12 comes from letting the hedge ratio see the period it trades. Fit it on earlier data only and the result goes with it.

Every threshold gives a different answer

Entry and exit z-scores are free parameters. Pick a cell to redraw the 2025 signal. Nothing else changes: same pair, same hedge ratio, same year.

Spread z-score, 2025Entry ±2.0, exit ±0.5

−4−20+2FebAprJunAugOctDec
Short entry +2.0, never reached
  • Entry ±2.0
  • Exit ±0.5
  • Position opened
  • Position closed
  • In position
Sharpe ratio by threshold
Entry by exitExit 0.0Exit 0.5Exit 1.0
Entry 1.5
Entry 2.0
Entry 2.5

Committed before seeing 2025

Net return
−0.09%
Sharpe
+0.02
Max drawdown
−5.05%
Completed round trips
1
Entries
2
Days in position
192

The setting I committed to before seeing 2025: 2 entries, 192 days in position, and no short ever taken.

Sharpe runs from −0.31 to +0.73 across the nine settings. Choosing the best one would mean picking parameters on the test set, which is exactly what an out-of-sample test exists to prevent. Returns come from the backtest grid; only the signals are recomputed here.

11 pairs below 0.05, about 6 expected by chance

Each dot is one of the 120 pairs, placed by its Engle–Granger p-value on a log scale.

11 of 120 pairs fall below p = 0.05. Testing 120 pairs at the 5% level produces about 6 false positives on its own, so 11 is barely more than noise, and none survive a Bonferroni correction for120 tests.

Problem

Pairs trading bets that two stocks whose prices move together will drift back together after they diverge. I wanted to know whether that survives a test on data the strategy has never seen, rather than on the data its parameters were fitted to.

Approach

I screened all 120 pairs from 16 liquid equities with the Engle–Granger cointegration test, on daily adjusted closes from 2021 to 2025. Eleven came in below p = 0.05, but testing 120 pairs at that level produces about six false positives by chance, and none survive a Bonferroni correction for 120 tests.

The best p-value belonged to JPMorgan and Walmart (0.0061), whose daily returns correlate at 0.23 and whose businesses have nothing in common. I took Mastercard and Visa instead (p = 0.0101, return correlation 0.88). Two card networks running the same business are a reason for a spread to revert; a low p-value on its own isn’t. Coca-Cola and Pepsi are the mirror case: the obvious pair, and not cointegrated at all (p = 0.90).

I fitted the hedge ratio by OLS on 2021–24 only, traded the spread’s z-score against its 2021–24 mean and standard deviation, and fixed the thresholds before looking at 2025: enter at ±2.0, exit at ±0.5. Costs are 5bp per unit of turnover. The signal is a stateful loop, because a position is held between its entry and exit events. My first vectorised version silently produced one entry and no exits.

Result

Fitted on the whole 2021–2025 history and backtested over that same span, the strategy returns 25.6% with a Sharpe of 1.12. That is a full-sample run with look-ahead bias: the hedge ratio already knows the period it trades. Fitted on 2021–24 and locked, 2025 returned −0.09% after costs, with a Sharpe of 0.02 and a maximum drawdown of −5.05%.

That does not show the strategy loses money. 2025 produced two entries and one completed round trip, and a Sharpe ratio on one closed trade is not a measurement. The honest conclusion is that the year contained too few signals to evaluate the strategy at all.

The spread explains why. It never crossed +2 in 2025, ranging from −4.72 to +0.85, so the short side could not trigger at any threshold I tested. Instead of oscillating around its training mean, it re-levelled below it and stayed there, reaching −4.7 standard deviations in April. A fixed hedge ratio assumes the relationship keeps its shape; a rolling estimate exists to handle exactly this.

Changing the thresholds moves the 2025 Sharpe anywhere from −0.31 to +0.73. Picking the best of them after the fact would mean choosing parameters on the test set, which is exactly what fixing them in advance prevents. Next I would test walk-forward across many one-year windows, so a result rests on dozens of trades rather than two.

Next project

Keep reading

SoleSense

A sensor insole that detects foot tapping, leg shaking and heart rate, built by a team of three at the UpLYFT hackathon.