---
title: "Arbitrage Executor"
description: "Atomic cross-exchange arbitrage execution with auto-rollback, pipeline scanner, and one-shot sweep."
icon: "arrows-split-up-and-left"
---

# Arbitrage Executor

Horizon provides atomic arbitrage execution across exchanges. The `execute_arbitrage()` engine method submits simultaneous buy and sell legs with automatic rollback if one leg fails. The Python `arb_scanner()` and `arb_sweep()` utilities add pipeline integration and one-shot scanning.

<Note>
  Cross-exchange arbitrage exploits price differences for the same event between Polymarket and Kalshi (or paper exchanges). Horizon's executor handles both legs atomically  - if the sell leg fails after the buy succeeds, the buy is automatically canceled.
</Note>

## Overview

<CardGroup cols={2}>
  <Card title="Atomic Execution" icon="atom">
    `engine.execute_arbitrage()` submits both legs through the risk pipeline with auto-rollback.
  </Card>
  <Card title="Pipeline Scanner" icon="radar">
    `hz.arb_scanner()` integrates with `hz.run()` for continuous arbitrage monitoring.
  </Card>
  <Card title="One-Shot Sweep" icon="broom">
    `hz.arb_sweep()` scans for and executes arbitrage opportunities in a single call.
  </Card>
  <Card title="Cooldown Control" icon="clock">
    Built-in cooldown timer prevents over-trading after an execution.
  </Card>
</CardGroup>

---

## Engine Method: execute_arbitrage

Submit a cross-exchange arbitrage trade with atomic rollback.

```python
buy_id, sell_id = engine.execute_arbitrage(
    market_id="will-btc-hit-100k",
    buy_exchange="kalshi",
    sell_exchange="polymarket",
    buy_price=0.48,
    sell_price=0.52,
    size=10.0,
    side=None,           # Side.Yes or Side.No (optional)
    token_id=None,       # Polymarket token ID (optional)
    neg_risk=False,      # Polymarket neg-risk flag
)
print(f"Buy: {buy_id}, Sell: {sell_id}")
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `market_id` | `str` | required | Market identifier |
| `buy_exchange` | `str` | required | Exchange to buy on |
| `sell_exchange` | `str` | required | Exchange to sell on |
| `buy_price` | `float` | required | Buy leg price |
| `sell_price` | `float` | required | Sell leg price |
| `size` | `float` | required | Trade size |
| `side` | `Side \| None` | `None` | Side (Yes/No) |
| `token_id` | `str \| None` | `None` | Polymarket token ID |
| `neg_risk` | `bool` | `False` | Polymarket neg-risk flag |

**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
5. Raises an exception if risk rejects either leg

---

## Pipeline Factory: hz.arb_scanner

Continuous arbitrage monitoring inside `hz.run()`.

```python
scanner = hz.arb_scanner(
    market_id="will-btc-hit-100k",
    exchanges=["polymarket", "kalshi"],
    feed_map={
        "polymarket": "poly_feed",
        "kalshi": "kalshi_feed",
    },
    min_edge=0.01,         # Minimum net edge to execute
    max_size=50.0,         # Maximum execution size
    fee_rates=None,        # dict[str, float] fee rates per exchange
    auto_execute=False,    # Auto-execute via engine.execute_arbitrage()
    cooldown=5.0,          # Seconds between executions
)
```

| 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 key mapping |
| `min_edge` | `float` | `0.01` | Minimum net edge threshold |
| `max_size` | `float` | `50.0` | Max execution size |
| `fee_rates` | `dict[str, float] \| None` | `None` | Fee rates per exchange |
| `auto_execute` | `bool` | `False` | Auto-execute opportunities |
| `cooldown` | `float` | `5.0` | Cooldown between executions (seconds) |

The scanner uses `engine.scan_arbitrage()` internally (if available in context params). When `auto_execute=True`, it calls `engine.execute_arbitrage()` automatically when an opportunity exceeds the minimum edge.

---

## One-Shot: hz.arb_sweep

Scan for and optionally execute arbitrage in a single call.

```python
result = hz.arb_sweep(
    engine=engine,
    market_id="will-btc-hit-100k",
    feed_map={
        "polymarket": "poly_feed",
        "kalshi": "kalshi_feed",
    },
    min_edge=0.01,
    max_size=50.0,
)

if result:
    print(f"Found arb: buy on {result.buy_exchange} at {result.buy_price}")
    print(f"           sell on {result.sell_exchange} at {result.sell_price}")
    print(f"           edge: {result.net_edge:.4f}")
```

Returns `ArbResult` or `None`.

---

## ArbResult

Dataclass representing an arbitrage opportunity or executed trade.

```python
result = ArbResult(
    market_id="will-btc-hit-100k",
    buy_exchange="kalshi",
    sell_exchange="polymarket",
    buy_price=0.48,
    sell_price=0.52,
    size=10.0,
    raw_edge=0.04,           # sell - buy
    net_edge=0.02,           # After fees
    buy_order_id="order1",   # Set after execution
    sell_order_id="order2",  # Set after execution
    timestamp=1700000000.0,  # Auto-set if omitted
)
```

| Field | Type | Description |
|-------|------|-------------|
| `market_id` | `str` | Market identifier |
| `buy_exchange` | `str` | Buy leg exchange |
| `sell_exchange` | `str` | Sell leg exchange |
| `buy_price` | `float` | Buy price |
| `sell_price` | `float` | Sell price |
| `size` | `float` | Execution size |
| `raw_edge` | `float` | Raw price difference (sell - buy) |
| `net_edge` | `float` | Edge after fees |
| `buy_order_id` | `str \| None` | Buy order ID (after execution) |
| `sell_order_id` | `str \| None` | Sell order ID (after execution) |
| `timestamp` | `float` | Unix timestamp |

---

## Examples

### Cross-Exchange Arb with Pipeline

```python
import horizon as hz

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

hz.run(
    name="arb_bot",
    exchanges=[
        hz.Polymarket(private_key="0x..."),
        hz.Kalshi(api_key="..."),
    ],
    markets=["election-winner"],
    feeds={
        "poly": hz.PolymarketBook("election-winner"),
        "kalshi": hz.KalshiBook("KXELECTION-25"),
    },
    pipeline=[scanner],
    interval=0.5,
)
```

### Manual Arbitrage Execution

```python
import horizon as hz

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

# Check feeds for price divergence
poly_snap = engine.feed_snapshot("poly")
kalshi_snap = engine.feed_snapshot("kalshi")

if poly_snap and kalshi_snap:
    if kalshi_snap.ask < poly_snap.bid - 0.02:
        # Buy on Kalshi, sell on Polymarket
        buy_id, sell_id = engine.execute_arbitrage(
            "election-winner",
            buy_exchange="kalshi",
            sell_exchange="polymarket",
            buy_price=kalshi_snap.ask,
            sell_price=poly_snap.bid,
            size=10.0,
        )
```

### Combining Arb with Market Making

```python
import horizon as hz

def arb_or_mm(ctx):
    """Scan for arb, fall back to market making."""
    result = hz.arb_sweep(
        engine=ctx.params.get("engine"),
        market_id=ctx.market.id,
        feed_map={"polymarket": "poly", "kalshi": "kalshi"},
        min_edge=0.02,
    )
    if result:
        return []  # Arb executed, no quotes needed

    # No arb, do market making
    mm = hz.market_maker(feed_name="poly", size=5.0)
    return mm(ctx)

hz.run(
    name="arb_mm_hybrid",
    pipeline=[arb_or_mm],
    ...
)
```

<Warning>
  Cross-exchange arbitrage requires active positions on both exchanges with sufficient margin. Ensure your exchange credentials and risk limits are properly configured. The atomic rollback only cancels the buy leg  - it cannot guarantee the cancel succeeds on a live exchange if the order was already filled.
</Warning>
