Skip to main content
Pro Feature. Requires a Pro or Ultra subscription. Get started at api.mathematicalcompany.com

Statistical Arbitrage

A more rigorous version of 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

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

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

StatArbConfig

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,
)
ParameterTypeDefaultDescription
pairtuple[str, str]requiredMarket IDs
feedstuple[str, str]requiredFeed names
lookbackint200Price history window
entry_zscorefloat2.0Entry threshold
exit_zscorefloat0.5Exit threshold
stop_zscorefloat4.0Stop-loss threshold
recalibrate_everyint50Ticks between recalibration
min_half_lifefloat5.0Min half-life (reject if faster)
max_half_lifefloat200.0Max half-life (reject if slower)

Pipeline: stat_arb

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

FieldTypeDescription
pairtuple[str, str]Market pair
hedge_ratiofloatOLS hedge ratio
zscorefloatCurrent residual z-score
half_lifefloatMean-reversion half-life
adf_statfloatADF test statistic
signalstr"long_a_short_b", "long_b_short_a", "exit", "stop", or "hold"

Signal Logic

ConditionPositionSignal
z > entry_zscoreflatlong_b_short_a
z < -entry_zscoreflatlong_a_short_b
`z< exit_zscore`positionedexit
`z> stop_zscore`positionedstop
otherwiseanyhold

Example

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,
)
ADF critical values (n > 100): 1% = -3.43, 5% = -2.862, 10% = -2.567. More negative values indicate stronger stationarity evidence.