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
Register exchanges
The first exchange in the exchanges list becomes the primary exchange (default for routing). Additional exchanges are added via add_exchange().
Market resolution
Each market is resolved against the appropriate exchange. Markets with exchange="polymarket" resolve via Gamma API, exchange="kalshi" resolve via ticker mapping.
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().
Fill polling
poll_fills() polls all live exchanges each cycle and processes fills from every venue.
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):
- Compute the absolute exposure for each market
- The hedged amount is
min(exposure_a, exposure_b)
- 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}")