Skip to main content
Horizon supports trading on multiple exchanges simultaneously from a single strategy. This enables cross-exchange arbitrage, hedging, and diversified market making.

Quick Start

hz.run(
    name="cross_venue",
    exchanges=[
        hz.Polymarket(private_key="0x..."),
        hz.Kalshi(api_key="..."),
    ],
    markets=["will-btc-hit-100k", "KXBTC-25FEB16"],
    feeds={"btc": hz.BinanceWS("btcusdt")},
    pipeline=[fair_value, quoter],
    risk=hz.Risk(max_position=50),
    mode="live",
    netting_pairs=[("will-btc-hit-100k", "KXBTC-25FEB16")],
)

How It Works

1

Register exchanges

The first exchange in the exchanges list becomes the primary exchange (default for routing). Additional exchanges are added via add_exchange().
2

Market resolution

Each market is resolved against the appropriate exchange. Markets with exchange="polymarket" resolve via Gamma API, exchange="kalshi" resolve via ticker mapping.
3

Order routing

Orders are routed to the correct exchange based on market.exchange. The pipeline’s _process_result() reads market.exchange and passes it to submit_quotes().
4

Fill polling

poll_fills() polls all live exchanges each cycle and processes fills from every venue.
5

Cancel across venues

cancel_all() and cancel_market() operate across all registered exchanges.

Engine API

Adding exchanges

engine = Engine(exchange_type="polymarket", ...)
engine.add_exchange(
    exchange_type="kalshi",
    exchange_key="...",
    api_url="...",
)

Querying exchanges

engine.exchange_names()   # ["polymarket", "kalshi"]
engine.exchange_count()   # 2
engine.exchange_name()    # "polymarket" (primary)

Explicit routing

# Submit to a specific exchange
engine.submit_order(request, exchange="kalshi")
engine.submit_quotes("market", quotes, Side.Yes, exchange="polymarket")

# Sync positions from a specific exchange
engine.sync_positions(exchange="kalshi")

Netting Pairs

Netting pairs allow correlated positions across exchanges to offset for risk purposes. This reduces the effective portfolio notional when positions are hedged.

Configuration

hz.run(
    netting_pairs=[
        ("will-btc-hit-100k", "KXBTC-25FEB16"),  # Same event, different exchanges
    ],
    ...
)
Or via the Engine API:
engine.set_netting_pair("will-btc-hit-100k", "KXBTC-25FEB16")
pairs = engine.netting_pairs()  # [("will-btc-hit-100k", "KXBTC-25FEB16")]

How Netting Works

For each netting pair (market_a, market_b):
  1. Compute the absolute exposure for each market
  2. The hedged amount is min(exposure_a, exposure_b)
  3. Reduce the portfolio notional by hedged * 0.5 (prediction markets are 0-1 priced)
adjusted_notional = raw_notional - sum(min(exposure_a, exposure_b) * 0.5)
Netting only affects the notional limit risk check. Position limits per market are still enforced independently.

Example

If you hold:
  • 50 contracts on Polymarket will-btc-hit-100k (exposure = 50)
  • 30 contracts on Kalshi KXBTC-25FEB16 (exposure = 30)
The hedged amount is min(50, 30) = 30, reducing notional by 30 * 0.5 = 15.0. This allows larger total positions because 30 contracts are effectively hedged.

Cross-Exchange Strategy Pattern

def fair_value(ctx: hz.Context) -> float:
    """Average price across both exchange books."""
    poly = ctx.feeds.get("poly", hz.context.FeedData())
    kalshi = ctx.feeds.get("kalshi", hz.context.FeedData())

    prices = []
    if poly.price > 0:
        prices.append(poly.price)
    if kalshi.price > 0:
        prices.append(kalshi.price)

    return sum(prices) / len(prices) if prices else 0.5

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

    spread = 0.02 + divergence * 0.5
    return hz.quotes(fair, spread, size=5)

Unified Position Tracking

The PositionTracker maintains positions from all exchanges in a single map. Each position carries its exchange field:
positions = engine.positions()
for pos in positions:
    print(f"{pos.market_id} on {pos.exchange}: {pos.size} @ {pos.avg_entry_price}")
Similarly, fills and orders track their exchange:
fills = engine.recent_fills()
for fill in fills:
    print(f"Fill on {fill.exchange}: {fill.market_id} {fill.size} @ {fill.price}")