---
title: "Multi-Exchange"
description: "Trade on multiple exchanges simultaneously with cross-exchange netting and unified position tracking."
icon: "arrows-split-up-and-left"
---

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

## Quick Start

```python
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

<Steps>
  <Step title="Register exchanges">
    The first exchange in the `exchanges` list becomes the **primary** exchange (default for routing). Additional exchanges are added via `add_exchange()`.
  </Step>
  <Step title="Market resolution">
    Each market is resolved against the appropriate exchange. Markets with `exchange="polymarket"` resolve via Gamma API, `exchange="kalshi"` resolve via ticker mapping.
  </Step>
  <Step title="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()`.
  </Step>
  <Step title="Fill polling">
    `poll_fills()` polls **all** live exchanges each cycle and processes fills from every venue.
  </Step>
  <Step title="Cancel across venues">
    `cancel_all()` and `cancel_market()` operate across **all** registered exchanges.
  </Step>
</Steps>

## Engine API

### Adding exchanges

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

### Querying exchanges

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

### Explicit routing

```python
# 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

```python
hz.run(
    netting_pairs=[
        ("will-btc-hit-100k", "KXBTC-25FEB16"),  # Same event, different exchanges
    ],
    ...
)
```

Or via the Engine API:

```python
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)
```

<Tip>
  Netting only affects the **notional limit** risk check. Position limits per market are still enforced independently.
</Tip>

### 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

```python
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:

```python
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:

```python
fills = engine.recent_fills()
for fill in fills:
    print(f"Fill on {fill.exchange}: {fill.market_id} {fill.size} @ {fill.price}")
```
