Pairs trading in Python
Pairs trading bets on the spread between two correlated assets, here is an ETH/BTC pairs strategy in Python, backtested on the Rust core.
What is pairs trading?
Pairs trading is a market-neutral strategy: instead of betting on direction, you trade the relative value of two correlated assets. When the spread between them stretches unusually wide, you bet it narrows, long the laggard, short the leader.
Because the two legs hedge each other, a pairs trade is largely insulated from broad market moves. Its risk is that the historical relationship breaks down, the spread can keep widening if the correlation is no longer real.
The logic
We trade ETH against BTC. The execution leg is ETHUSDT; the BTC price comes in as a cross-asset reference via symbol_ref, which must be named in a signal before downstream expressions can use it.
The spread is ETH priced in BTC, and we z-score it over 60 bars. When the spread is 2σ low we go long ETH (it is cheap relative to BTC); when 2σ high we go short; otherwise flat.
pairs trading in Python
Here is the full strategy in the Manifold-BT expression DSL. It imports manifoldbt, builds the signal, configures realistic execution, and runs the backtest against Rust.
import manifoldbt as mbt
from manifoldbt.indicators import close
from manifoldbt.expr import symbol_ref
from manifoldbt.helpers import time_range, Slippage, Interval
# Execution leg = ETH (first symbol); reference leg = BTC via symbol_ref
btc = symbol_ref("binance:BTCUSDT", "close")
spread = close / btc # ETH priced in BTC
z = spread.zscore(60)
strategy = (
mbt.Strategy.create("pairs_eth_btc")
.signal("btc", btc) # cross-asset signals must be named
.signal("z", z)
.size(mbt.when(z < -2.0, 1.0, mbt.when(z > 2.0, -1.0, 0.0)))
)
start, end = time_range("2021-01-01", "2026-01-01")
config = mbt.BacktestConfig(
universe={"binance": ["ETHUSDT", "BTCUSDT"]}, # first symbol fills
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(1),
initial_capital=10_000,
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=60,
)
store = mbt.ingest(provider="binance", symbol="ETHUSDT", symbol_id=1,
interval="1h", start="2021-01-01T00:00:00Z",
end="2026-01-01T00:00:00Z")
mbt.ingest(provider="binance", symbol="BTCUSDT", symbol_id=2,
interval="1h", start="2021-01-01T00:00:00Z",
end="2026-01-01T00:00:00Z", store=store)
result = mbt.run(strategy, config, store)
print(result.summary())
mbt.plot.tearsheet(result, show=True)Backtest it with Manifold-BT
Both legs are ingested into the same DataStore; the first symbol in the universe is the execution target, so fills happen at ETH's prices while BTC only feeds the signal.
Pairs trades hinge on a stable relationship, so backtest across multiple regimes and watch for the spread trending instead of reverting. A cointegration check on the two legs before trading is strongly recommended.
| Universe | {"binance": ["ETHUSDT", "BTCUSDT"]} |
| Bar interval | 1h |
| Fees | FeeConfig.binance_perps() |
| Slippage | fixed 2 bps |
| Warmup | 60 bars |
| Total return | +22.4% |
| Sharpe | 1.33 |
| Max drawdown | -9.6% |
| Win rate | 58% |
| Trades | 318 |
Keep reading
Run your first backtest
Install Manifold-BT and reproduce the backtest above in seconds. The Rust core runs years of bars sub-second so you can sweep parameters instead of waiting.