Skip to main content
A cross-exchange strategy that monitors the same event on both Polymarket and Kalshi, quoting tight when prices agree and wide when they diverge. Uses the multi-exchange engine with netting pairs.

Full Code

"""Cross-exchange arbitrage between Polymarket and Kalshi."""

import horizon as hz


def fair_value(ctx: hz.Context) -> float:
    """Average of both exchange book prices."""
    poly_price = ctx.feeds.get("poly", hz.context.FeedData()).price or 0.5
    kalshi_price = ctx.feeds.get("kalshi", hz.context.FeedData()).price or 0.5
    return (poly_price + kalshi_price) / 2


def quoter(ctx: hz.Context, fair: float) -> list[hz.Quote]:
    """Tight spread when exchanges agree, wider when they diverge."""
    poly_price = ctx.feeds.get("poly", hz.context.FeedData()).price or fair
    kalshi_price = ctx.feeds.get("kalshi", hz.context.FeedData()).price or fair
    divergence = abs(poly_price - kalshi_price)

    base_spread = 0.02
    spread = base_spread + divergence * 0.5

    return hz.quotes(fair, spread, size=5)


if __name__ == "__main__":
    hz.run(
        name="cross_exchange_arb",
        exchanges=[
            hz.Polymarket(private_key="0x_demo_key"),
            hz.Kalshi(api_key="demo_key"),
        ],
        markets=["will-btc-hit-100k-by-end-of-2025", "KXBTC-25FEB16"],
        feeds={
            "poly": hz.PolymarketBook("will-btc-hit-100k-by-end-of-2025"),
            "kalshi": hz.KalshiBook("KXBTC-25FEB16"),
        },
        pipeline=[fair_value, quoter],
        risk=hz.Risk(max_position=50, max_drawdown_pct=3),
        interval=0.5,
        mode="paper",
        netting_pairs=[
            ("will-btc-hit-100k-by-end-of-2025", "KXBTC-25FEB16"),
        ],
    )

Key Features

Multi-Exchange Engine

Uses exchanges=[...] instead of exchange= to register both Polymarket and Kalshi:
exchanges=[
    hz.Polymarket(private_key="0x_demo_key"),
    hz.Kalshi(api_key="demo_key"),
],
The first exchange (Polymarket) becomes the primary. Orders are routed based on market.exchange.

Netting Pairs

The two markets represent the same underlying event on different exchanges. Registering them as a netting pair reduces the portfolio notional for hedged positions:
netting_pairs=[
    ("will-btc-hit-100k-by-end-of-2025", "KXBTC-25FEB16"),
],
If you hold 50 contracts on Polymarket and 30 on Kalshi, 30 contracts are considered hedged.

Divergence-Based Spread

The spread widens proportionally to the price divergence between exchanges:
spread = 0.02 + |poly_price - kalshi_price| × 0.5
  • When prices agree (divergence ≈ 0): spread = 2 cents (aggressive)
  • When prices diverge by 10 cents: spread = 7 cents (conservative)

Dual Feeds

Both exchange orderbooks provide real-time price data:
feeds={
    "poly": hz.PolymarketBook("will-btc-hit-100k-by-end-of-2025"),
    "kalshi": hz.KalshiBook("KXBTC-25FEB16"),
},

Run It

# Paper mode
python examples/cross_exchange_arb.py

# Live mode with dashboard
python -m horizon run examples/cross_exchange_arb.py --mode=live --dashboard

Strategy Variants

Directional arb

Instead of market-making both sides, take directional positions when divergence exceeds a threshold:
def quoter(ctx: hz.Context, fair: float) -> list[hz.Quote]:
    poly = ctx.feeds.get("poly", hz.context.FeedData()).price or fair
    kalshi = ctx.feeds.get("kalshi", hz.context.FeedData()).price or fair

    if poly - kalshi > 0.05:
        # Poly expensive, Kalshi cheap → buy Kalshi, sell Poly
        return hz.quotes(fair, spread=0.01, size=10)
    elif kalshi - poly > 0.05:
        # Kalshi expensive, Poly cheap → buy Poly, sell Kalshi
        return hz.quotes(fair, spread=0.01, size=10)
    else:
        # No arb opportunity
        return hz.quotes(fair, spread=0.06, size=2)

Using the Arb Scanner

For automated scanning and execution, use hz.arb_scanner():
hz.run(
    name="auto_arb",
    exchanges=[
        hz.Polymarket(private_key="0x_demo_key"),
        hz.Kalshi(api_key="demo_key"),
    ],
    markets=["will-btc-hit-100k-by-end-of-2025"],
    feeds={
        "poly": hz.PolymarketBook("will-btc-hit-100k-by-end-of-2025"),
        "kalshi": hz.KalshiBook("KXBTC-25FEB16"),
    },
    pipeline=[hz.arb_scanner(
        market_id="will-btc-hit-100k-by-end-of-2025",
        exchanges=["polymarket", "kalshi"],
        feed_map={"polymarket": "poly", "kalshi": "kalshi"},
        min_edge=0.02,
        auto_execute=True,
        cooldown=10.0,
    )],
    interval=0.5,
)
Or use hz.arb_sweep() for one-shot scanning:
result = hz.arb_sweep(engine, "will-btc-hit-100k", feed_map={"polymarket": "poly", "kalshi": "kalshi"})
if result:
    print(f"Arb found: {result.net_edge:.4f} edge")
See Arbitrage Executor for the full guide.