Tutorial 05 - Pair spread decomposition and stat-arb

Tutorial Navigation

Track Tutorial notebook
Roadmap Tutorial 00 - Roadmap
Strategy Lab 01 Trend-Following Lab
Tutorial Sequence 01 Real Market Data and Feature Factory
Tutorial Sequence 02 Decomposition-aware MA and MACD
Strategy Lab 02 Oscillation-Reversion Lab
Strategy Expansion 03 Method-Specific Variants
Tutorial Sequence 03 Residual Mean Reversion
Strategy Expansion 04 Component Pair Trading
Tutorial Sequence 04 Donchian Breakout
Tutorial Sequence 05 Pair-Spread Stat-Arb
Tutorial Sequence 06 Cross-Sectional Rotation
Native SSA Replay 07 Native SSA High-Return / Low-Drawdown

Executed Notebook

This notebook rewrites pairs trading around spread structure: rolling hedge ratio, spread trend drift, cycle timing and residual deviation.

In [1]
import matplotlib.pyplot as plt
import pandas as pd

from quant_trading.data import load_bundled_real_ohlcv_panel, ohlcv_audit_report
from quant_trading.strategy_pairs import (
    walkforward_pair_spread_features, pair_diagnostics, pair_feature_snapshot,
    make_classic_pair_weight_grid, make_detime_pair_weight_grid,
    compare_pair_suites, run_classical_pair_baselines, run_detime_pair_baselines,
)
from quant_trading.validation import turnover_report, compare_weight_strategies

1. Load real offline market data

In [2]
tickers = ["AUDUSD=X", "NZDUSD=X", "EURUSD=X", "GBPUSD=X"]
pairs = [("AUDUSD=X", "NZDUSD=X"), ("EURUSD=X", "GBPUSD=X")]
ohlcv = load_bundled_real_ohlcv_panel(tickers, min_observations=120)
ohlcv = {field: table.tail(760).copy() for field, table in ohlcv.items()}
prices = ohlcv["Close"]
ohlcv_audit_report(ohlcv)

2. Build walk-forward spread decomposition features

In [3]
spread_features, spread_panel, beta_panel, pair_specs = walkforward_pair_spread_features(
    prices,
    pairs,
    hedge_window=126,
    method="STL",
    period=126,
    train_window=504,
    step=5,
    z_window=63,
)
spread_panel.tail()
In [4]
pair_diagnostics(prices, pair_specs, hedge_window=90)
In [5]
snapshot = pair_feature_snapshot(spread_features, tail=2)
snapshot.query("feature in ['trend_slope', 'cycle_slope', 'residual_z', 'residual_abs_z']").tail(16)

3. Backtest classical baselines and DeTime rewrites

In [6]
classic_weights = make_classic_pair_weight_grid(prices, pairs=pairs, lookback=90)
detime_weights = make_detime_pair_weight_grid(prices, pair_specs, spread_features, spread_panel=spread_panel, beta_panel=beta_panel)
all_weights = {**classic_weights, **detime_weights}
comparison, results = compare_weight_strategies(prices, all_weights, fee_bps=1.0, slippage_bps=2.0)
comparison.insert(0, "strategy_group", ["detime_pair" if str(idx).startswith("detime") else "classical_pair" for idx in comparison.index])
comparison[["strategy_group", "cagr", "sharpe", "max_drawdown", "average_turnover"]].round(4)
In [7]
leaders = comparison.sort_values("sharpe", ascending=False).head(4).index
fig, ax = plt.subplots(figsize=(10, 4))
for name in leaders:
    results[name].equity.plot(ax=ax, linewidth=1.2, label=name)
ax.set_title("Pair-spread equity curves on real FX samples")
ax.set_ylabel("Equity")
ax.legend(loc="best", fontsize=8)
ax.grid(True, alpha=0.25)
plt.tight_layout()
plt.show()
In [8]
turnover_report(all_weights).round(4)

4. Live-data extension

Run run_column_05_pairs_spread_decomposition.py without --use-bundled-sample and pass pairs such as KO:PEP, XOM:CVX, MA:V, or SPY:QQQ.