---
title: "Statistical Arb Pairs Trading"
description: "Cointegration-based pairs trading between correlated prediction markets."
icon: "wave-sine"
---

<Note>**Pro Feature.** Requires a Pro or Ultra subscription. [Get started at api.mathematicalcompany.com](https://api.mathematicalcompany.com)</Note>

Trade the spread between two cointegrated prediction markets. When the spread deviates from its mean, enter positions expecting reversion.

## Full Code

```python
"""Stat arb pairs trading: GOP Senate vs House markets."""

import horizon as hz
from horizon.context import FeedData


def fair_value(ctx: hz.Context) -> float:
    """Use the Senate feed as primary fair value."""
    senate = ctx.feeds.get("gop_senate_feed", FeedData())
    return senate.price if senate.price > 0 else 0.50


# Check cointegration first
coint = hz.cointegration_test(
    series_a=[0.52, 0.53, 0.51, 0.54, 0.55, 0.53, 0.52, 0.54, 0.56, 0.55],
    series_b=[0.48, 0.49, 0.47, 0.50, 0.51, 0.49, 0.48, 0.50, 0.52, 0.51],
)
print(f"Cointegrated: {coint.is_cointegrated}")
print(f"ADF stat: {coint.adf_statistic:.4f}, p-value: {coint.p_value:.4f}")
print(f"Hedge ratio: {coint.hedge_ratio:.4f}")

# Configure stat arb
config = hz.StatArbConfig(
    market_a="gop-senate",
    market_b="gop-house",
    feed_a="gop_senate_feed",
    feed_b="gop_house_feed",
    hedge_ratio=coint.hedge_ratio,
    entry_z=2.0,        # enter at 2 standard deviations
    exit_z=0.5,         # exit at 0.5 standard deviations
    lookback=50,         # rolling window for z-score
    max_size=20.0,
)

stat_arb_pipeline = hz.stat_arb(config, auto_execute=True)

hz.run(
    name="stat_arb_pairs",
    markets=["gop-senate", "gop-house"],
    feeds={
        "gop_senate_feed": hz.PolymarketBook("gop-senate"),
        "gop_house_feed": hz.PolymarketBook("gop-house"),
    },
    pipeline=[fair_value, stat_arb_pipeline],
    risk=hz.Risk(max_position=100, max_drawdown_pct=5),
    interval=1.0,
    mode="paper",
)
```

## How It Works

1. **`cointegration_test()`** verifies the two price series share a long-run equilibrium
2. **`StatArbConfig`** sets entry/exit z-score thresholds and the hedge ratio
3. **`stat_arb()`** monitors the spread z-score each cycle:
  - When z > `entry_z`: short the spread (sell A, buy B)
  - When z < `-entry_z`: long the spread (buy A, sell B)
  - When |z| < `exit_z`: close the position
4. The hedge ratio scales the B-side size to maintain dollar-neutrality

## Spread Z-Score

You can also compute the z-score manually for custom logic:

```python
z = hz.spread_zscore(
    price_a=0.55,
    price_b=0.51,
    hedge_ratio=0.95,
    mean=0.02,
    std=0.015,
)
print(f"Spread z-score: {z:.2f}")
```

## Run It

```bash
python examples/stat_arb_pairs.py
```

## Simpler Alternative

For a lighter approach without cointegration testing, use `spread_convergence()`:

```python
spread_pipe = hz.spread_convergence(
    market_a="gop-senate",
    market_b="gop-house",
    feed_a="gop_senate_feed",
    feed_b="gop_house_feed",
    threshold=0.03,      # trade when spread exceeds 3 cents
    target_spread=0.01,  # expect convergence to 1 cent
    size=10.0,
)
```

See [Statistical Arbitrage](/arbitrage/stat-arb) for the full method reference.
