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:
- Both legs go through the full risk pipeline
- Orders use Fill-Or-Kill (FOK) time-in-force
- If the buy succeeds but the sell fails, the buy is automatically canceled
- 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], ...)
| Parameter | Type | Default | Description |
|---|
market_id | str | required | Market to scan |
exchanges | list[str] | required | Exchanges to compare |
feed_map | dict[str, str] | required | Exchange name -> feed name |
min_edge | float | 0.01 | Minimum net edge |
max_size | float | 50.0 | Max execution size |
fee_rates | dict[str, float] or None | None | Fee rates per exchange |
auto_execute | bool | False | Auto-execute |
cooldown | float | 5.0 | Seconds 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.