---
title: "Statistical Arb"
description: "Cointegration-based pairs trading with OLS hedge ratio, ADF test, and half-life filtering."
icon: "chart-line"
---

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

# Statistical Arbitrage

A more rigorous version of [Spread Convergence](/arbitrage/spread-convergence). Uses OLS hedge ratio, ADF stationarity testing, and half-life filtering to identify truly cointegrated pairs.

## How It Works

1. Collect price histories for both markets
2. Compute OLS hedge ratio: `beta = cov(a,b) / var(b)`
3. Compute residuals: `r = a - beta * b`
4. Run ADF test on residuals (more negative = more stationary)
5. Check `signal_half_life()` on residuals (reject if outside bounds)
6. Compute z-score of current residual
7. Generate entry/exit/stop signals

## Rust Functions

### cointegration_test

```python
ratio, residuals, adf = hz.cointegration_test(prices_a, prices_b)
# ratio: OLS hedge ratio
# residuals: a - ratio * b
# adf: ADF statistic (more negative = more stationary)
```

### spread_zscore

```python
z = hz.spread_zscore(residuals, lookback=50)
# Z-score of the last residual over a rolling window
```

## StatArbConfig

```python
config = hz.StatArbConfig(
    pair=("btc-100k", "eth-5k"),
    feeds=("btc_feed", "eth_feed"),
    lookback=200,
    entry_zscore=2.0,
    exit_zscore=0.5,
    stop_zscore=4.0,
    recalibrate_every=50,
    min_half_life=5.0,
    max_half_life=200.0,
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `pair` | `tuple[str, str]` | required | Market IDs |
| `feeds` | `tuple[str, str]` | required | Feed names |
| `lookback` | `int` | `200` | Price history window |
| `entry_zscore` | `float` | `2.0` | Entry threshold |
| `exit_zscore` | `float` | `0.5` | Exit threshold |
| `stop_zscore` | `float` | `4.0` | Stop-loss threshold |
| `recalibrate_every` | `int` | `50` | Ticks between recalibration |
| `min_half_life` | `float` | `5.0` | Min half-life (reject if faster) |
| `max_half_life` | `float` | `200.0` | Max half-life (reject if slower) |

## Pipeline: stat_arb

```python
scanner = hz.stat_arb(
    config=hz.StatArbConfig(
        pair=("btc-100k", "eth-5k"),
        feeds=("btc_feed", "eth_feed"),
    ),
    size=10.0,
    auto_execute=False,
    cooldown=30.0,
)

hz.run(pipeline=[scanner], ...)
```

Stores `StatArbResult` in `ctx.params["last_stat_arb"]`.

## StatArbResult

| Field | Type | Description |
|-------|------|-------------|
| `pair` | `tuple[str, str]` | Market pair |
| `hedge_ratio` | `float` | OLS hedge ratio |
| `zscore` | `float` | Current residual z-score |
| `half_life` | `float` | Mean-reversion half-life |
| `adf_stat` | `float` | ADF test statistic |
| `signal` | `str` | `"long_a_short_b"`, `"long_b_short_a"`, `"exit"`, `"stop"`, or `"hold"` |

## Signal Logic

| Condition | Position | Signal |
|-----------|----------|--------|
| `z > entry_zscore` | flat | `long_b_short_a` |
| `z < -entry_zscore` | flat | `long_a_short_b` |
| `|z| < exit_zscore` | positioned | `exit` |
| `|z| > stop_zscore` | positioned | `stop` |
| otherwise | any | `hold` |

## Example

```python
import horizon as hz

config = hz.StatArbConfig(
    pair=("gop-senate", "gop-house"),
    feeds=("gop_senate_feed", "gop_house_feed"),
    lookback=300,
    entry_zscore=2.5,
    recalibrate_every=100,
    min_half_life=10.0,
    max_half_life=150.0,
)

scanner = hz.stat_arb(config, size=5.0, auto_execute=True, cooldown=60.0)

hz.run(
    name="stat_arb_pairs",
    exchanges=[hz.Polymarket(private_key="0x...")],
    markets=["gop-senate", "gop-house"],
    feeds={
        "gop_senate_feed": hz.PolymarketBook("gop-senate"),
        "gop_house_feed": hz.PolymarketBook("gop-house"),
    },
    pipeline=[scanner],
    interval=1.0,
)
```

<Note>
  ADF critical values (n > 100): 1% = -3.43, 5% = -2.862, 10% = -2.567. More negative values indicate stronger stationarity evidence.
</Note>
