Strategy Expansion 04 - Component pair trading and cointegration

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 turns pair trading into component relationship trading. A pair is interesting when its trend and cycle components are similar enough, while the residual gap creates the temporary tradable deviation.

Component pair hypothesis

For assets A and B, the decomposition-first pair hypothesis is:

trend_A ≈ beta * trend_B
cycle_A ≈ beta * cycle_B
residual_A - beta * residual_B is the tradable gap

The notebook also reports Engle-Granger and ADF diagnostics for cointegration and spread stationarity.

In [1]
import matplotlib.pyplot as plt

from quant_trading.data import load_bundled_real_ohlcv_panel, ohlcv_panel_to_field
from quant_trading.strategy_component_pairs import (
    ComponentPairConfig,
    collect_pair_orders_and_trades,
    run_component_pair_suite,
)
In [2]
pairs = [('AUDUSD=X', 'NZDUSD=X'), ('EURUSD=X', 'GBPUSD=X'), ('CADUSD=X', 'CHFUSD=X')]
assets = sorted({x for pair in pairs for x in pair})
panel = load_bundled_real_ohlcv_panel(assets, min_observations=180)
close = ohlcv_panel_to_field(panel, 'Close')
volume = ohlcv_panel_to_field(panel, 'Volume')
execution_prices = ohlcv_panel_to_field(panel, 'Open').shift(-1).reindex_like(close).ffill()
close.tail()
In [3]
config = ComponentPairConfig(
    method='STL',
    period=126,
    train_window=504,
    step=21,
    z_window=63,
    require_cointegration=False,
)

stats, results, diagnostics, feature_snapshot = run_component_pair_suite(
    close,
    pairs,
    volumes=volume,
    config=config,
    execution_prices=execution_prices,
)

stats
In [4]
plot_stats = stats.sort_values("sharpe", ascending=True).copy()
labels = plot_stats["strategy"].astype(str).str.replace("detime_", "", regex=False)
fig, ax = plt.subplots(figsize=(10, 4))
ax.barh(labels, plot_stats["sharpe"])
ax.axvline(0, color="black", linewidth=0.8)
ax.set_xlabel("Sharpe ratio")
ax.set_title("Component pair strategy Sharpe comparison on real FX samples")
ax.grid(True, axis="x", alpha=0.25)
plt.tight_layout()
plt.show()

Read the diagnostics

  • latest_trend_corr measures whether the two decomposed trends are currently similar.
  • latest_cycle_corr measures whether their cycles are currently aligned.
  • raw_price_coint_pvalue is the Engle-Granger test p-value on log prices.
  • fair_value_coint_pvalue applies the same idea to trend + cycle fair values.
  • raw_spread_adf_pvalue, fair_spread_adf_pvalue, and residual_gap_adf_pvalue test whether the relevant spread is stationary enough for a mean-reversion hypothesis.
In [5]
diagnostics
In [6]
orders, trades = collect_pair_orders_and_trades(results)
print('orders:', len(orders))
print('round-trip trades:', len(trades))
trades.head()

Strategy variants

The suite compares a classical raw spread z-score baseline with three decomposition-first variants:

  • component_residual_gap: trades residual_z differences when trend and cycle are similar;
  • fair_spread_deviation: trades price spread deviation from the decomposed trend+cycle relationship;
  • cointegration_filtered_residual_gap: requires cointegration/stationarity diagnostics before trading residual gaps.