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

Cross-Exchange Arbitrage

Buy on one exchange where the price is low, sell on another where it’s high. Horizon’s executor handles both legs atomically with auto-rollback on failure.

How It Works

When the same market is priced differently across exchanges (e.g., Polymarket bid > Kalshi ask), you can buy on the cheap exchange and sell on the expensive one for risk-free profit.
Kalshi ask      = $0.48  (buy here)
Polymarket bid  = $0.52  (sell here)
Raw edge        = $0.04
Net edge        = $0.04 - fees

Engine Methods

scan_arbitrage

opps = engine.scan_arbitrage(
    "election-winner",
    [("polymarket", "polymarket", 0.002), ("kalshi", "kalshi", 0.002)],
    max_liquidity=100.0,
)
The tuple format is (feed_name, exchange_name, fee_rate).

execute_arbitrage

buy_id, sell_id = engine.execute_arbitrage(
    market_id="election-winner",
    buy_exchange="kalshi",
    sell_exchange="polymarket",
    buy_price=0.48,
    sell_price=0.52,
    size=10.0,
)
Behavior:
  1. Both legs go through the full risk pipeline
  2. Orders use Fill-Or-Kill (FOK) time-in-force
  3. If the buy succeeds but the sell fails, the buy is automatically canceled
  4. Returns (buy_order_id, sell_order_id) on success

Pipeline: arb_scanner

scanner = hz.arb_scanner(
    market_id="election-winner",
    exchanges=["polymarket", "kalshi"],
    feed_map={"polymarket": "polymarket", "kalshi": "kalshi"},
    min_edge=0.02,
    auto_execute=True,
    cooldown=10.0,
)

hz.run(pipeline=[scanner], ...)
ParameterTypeDefaultDescription
market_idstrrequiredMarket to scan
exchangeslist[str]requiredExchanges to compare
feed_mapdict[str, str]requiredExchange name -> feed name
min_edgefloat0.01Minimum net edge
max_sizefloat50.0Max execution size
fee_ratesdict[str, float] or NoneNoneFee rates per exchange
auto_executeboolFalseAuto-execute
cooldownfloat5.0Seconds between executions
Stores ArbResult in ctx.params["last_arb"] on execution.

One-Shot: arb_sweep

result = hz.arb_sweep(
    engine=engine,
    market_id="election-winner",
    feed_map={"polymarket": "polymarket", "kalshi": "kalshi"},
    min_edge=0.01,
)
Returns ArbResult or None.

Example: Full Cross-Exchange Bot

import horizon as hz

scanner = hz.arb_scanner(
    market_id="election-winner",
    exchanges=["polymarket", "kalshi"],
    feed_map={"polymarket": "polymarket", "kalshi": "kalshi"},
    min_edge=0.02,
    auto_execute=True,
    cooldown=10.0,
)

hz.run(
    name="cross_exchange_arb",
    exchanges=[
        hz.Polymarket(private_key="0x..."),
        hz.Kalshi(api_key="..."),
    ],
    markets=["election-winner"],
    feeds={
        "polymarket": hz.PolymarketBook("election-winner"),
        "kalshi": hz.KalshiBook("KXELECTION-25"),
    },
    pipeline=[scanner],
    interval=0.5,
)
Cross-exchange arb requires active accounts on both exchanges with sufficient margin. The atomic rollback only cancels the buy leg. It cannot guarantee cancel success if the order was already filled.