---
title: "Horizon Engine API"
description: "Complete Engine API reference: construction, multi-exchange, orders, fills, feeds, persistence."
icon: "gears"
---

The `Engine` is the core orchestrator. While `hz.run()` manages it for you, you can use it directly for advanced use cases, testing, or custom integration.

## Construction

```python
from horizon import Engine, RiskConfig
```

### Paper exchange (default)

```python
engine = Engine(api_key="hz_live_abc123...")
engine = Engine(risk_config=RiskConfig(max_position_per_market=200))
engine = Engine(paper_fee_rate=0.001, paper_partial_fill_ratio=0.5)
```

<Tip>
  `api_key` is optional if you've set `HORIZON_API_KEY` as an environment variable. See [Authentication](/authentication).
</Tip>

### Polymarket

```python
engine = Engine(
    exchange_type="polymarket",
    exchange_key="...",
    exchange_secret="...",
    exchange_passphrase="...",
    private_key="0x...",
    clob_url="https://clob.polymarket.com",
)
```

### Kalshi

```python
engine = Engine(
    exchange_type="kalshi",
    exchange_key="...",
    api_url="https://trading-api.kalshi.com/trade-api/v2",
)
```

### With persistence

```python
engine = Engine(db_path="./my_strategy.db")
engine = Engine(db_path=None)  # Disable persistence
```

### Full signature

```python
Engine(
    risk_config=None,                  # RiskConfig or None (uses defaults)
    paper_fee_rate=0.001,              # Paper exchange fee rate
    exchange_type="paper",             # "paper", "polymarket", or "kalshi"
    exchange_key=None,                 # Exchange API key (Polymarket/Kalshi)
    exchange_secret=None,              # Exchange API secret (Polymarket)
    exchange_passphrase=None,          # Exchange API passphrase (Polymarket)
    clob_url=None,                     # Polymarket CLOB URL
    api_url=None,                      # Kalshi API URL
    email=None,                        # Kalshi email
    password=None,                     # Kalshi password
    private_key=None,                  # Polymarket private key
    paper_partial_fill_ratio=1.0,      # Paper partial fill ratio
    db_path=None,                      # SQLite path (None = disabled)
    api_key=None,                      # Horizon API key (or use HORIZON_API_KEY env)
    paper_maker_fee_rate=None,         # Maker fee rate (overrides paper_fee_rate)
    paper_taker_fee_rate=None,         # Taker fee rate (overrides paper_fee_rate)
)
```

### Maker/taker fees

Split fees by liquidity role. Makers add liquidity (limit orders resting in the book), takers remove it (market orders or aggressive limit orders that cross the spread).

```python
# Flat fee (default, backward compatible)
engine = Engine(paper_fee_rate=0.001)

# Split maker/taker fees
engine = Engine(
    paper_maker_fee_rate=0.0002,  # 2 bps for makers
    paper_taker_fee_rate=0.002,   # 20 bps for takers
)

# Override just one side; the other falls back to paper_fee_rate
engine = Engine(paper_fee_rate=0.001, paper_taker_fee_rate=0.003)
```

Each `Fill` includes an `is_maker` field indicating whether the order was a maker or taker. See [Fill type](/api-reference/types#fill).

## Multi-Exchange

### add_exchange

Add a secondary exchange to the engine.

```python
name = engine.add_exchange(
    exchange_type="kalshi",     # "polymarket", "kalshi", or "paper"
    exchange_key="...",
    api_url="...",
)
# Returns: "kalshi"
```

Full signature matches the constructor parameters for each exchange type.

<Warning>
  Each exchange type can only be registered once. Calling `add_exchange("polymarket", ...)` when Polymarket is already registered raises `ValueError`.
</Warning>

### exchange_names

```python
names = engine.exchange_names()  # ["polymarket", "kalshi"]
```

### exchange_count

```python
count = engine.exchange_count()  # 2
```

### exchange_name

Get the primary exchange name (backward compatible):

```python
name = engine.exchange_name()  # "polymarket"
```

### set_netting_pair

Register a netting pair for cross-exchange risk reduction:

```python
engine.set_netting_pair("market_a", "market_b")
```

### netting_pairs

```python
pairs = engine.netting_pairs()  # [("market_a", "market_b")]
```

## Order Submission

All order methods support optional `exchange` parameter for routing:

### submit_order

```python
order_id = engine.submit_order(request, exchange=None)
```

Submit an order through the risk pipeline. Returns the exchange-assigned order ID.

### submit_quotes

```python
ids = engine.submit_quotes(
    market_id,
    quotes,           # list[Quote]
    side,             # Side.Yes or Side.No
    token_id=None,    # Polymarket token ID
    neg_risk=False,   # Polymarket neg-risk
    exchange=None,    # Target exchange (None = primary)
)
```

Submit bid+ask quote pairs. Returns a list of order IDs (2 per quote: bid + ask).

### submit_market_order

```python
order_id = engine.submit_market_order(
    market_id,
    side,              # Side
    order_side,        # OrderSide
    size,
    token_id=None,
    neg_risk=False,
    exchange=None,
)
```

## Order Amendment

### amend_order

Amend an existing order's price and/or size.

```python
new_id = engine.amend_order(order_id, new_price=0.60, new_size=15.0)
```

- **Paper exchange**: Returns the same order ID (amendment is applied in-place).
- **Live exchanges** (Polymarket, Kalshi): Performs a cancel+resubmit under the hood, returning a new order ID.
- At least one of `new_price` or `new_size` must be provided.
- Raises `ValueError` if the order is not found.

Each successful amendment increments the order's `amendment_count` field. See [Order type](/api-reference/types#order) for details.

## Contingent Orders (Stop-Loss / Take-Profit)

Contingent orders are triggered automatically when market conditions are met. They enable stop-loss and take-profit strategies, and can be linked together as OCO (one-cancels-other) pairs via bracket orders.

### add_stop_loss

```python
sl_id = engine.add_stop_loss(
    market_id,         # Market identifier
    side,              # Side.Yes or Side.No
    order_side,        # OrderSide.Sell (typically)
    size,              # Order size
    trigger_price,     # Price that triggers the stop-loss (e.g., 0.45)
    exchange=None,     # Target exchange (None = primary)
)
```

Registers a stop-loss contingent order. When the market price drops to or below the `trigger_price`, a market order is submitted automatically.

### add_take_profit

```python
tp_id = engine.add_take_profit(
    market_id,         # Market identifier
    side,              # Side.Yes or Side.No
    order_side,        # OrderSide.Sell (typically)
    size,              # Order size
    trigger_price,     # Price that triggers the take-profit (e.g., 0.70)
    trigger_pnl=None,  # Optional: trigger when realized PnL reaches this value
    exchange=None,     # Target exchange (None = primary)
)
```

Registers a take-profit contingent order. When the market price rises to or above the `trigger_price` (or the position PnL reaches `trigger_pnl` if set), a market order is submitted automatically.

### submit_bracket

Submit an entry order with linked stop-loss and take-profit as an OCO (one-cancels-other) group.

```python
entry_id, sl_id, tp_id = engine.submit_bracket(
    request=OrderRequest(...),       # Entry order
    stop_trigger=0.45,               # Stop-loss trigger price
    take_profit_trigger=0.70,        # Take-profit trigger price
    take_profit_pnl=None,            # Optional PnL trigger for TP
    exchange=None,                   # Target exchange
)
```

When either the stop-loss or take-profit triggers, its OCO partner is automatically canceled.

### check_contingent_triggers

Check all pending contingent orders for a market against the current price and trigger any that match.

```python
triggered_count = engine.check_contingent_triggers(market_id, current_price)
```

<Note>
  `hz.run()` calls `check_contingent_triggers` automatically on every tick. You only need to call this manually when using the engine directly.
</Note>

### cancel_contingent

Cancel a specific contingent order by its ID.

```python
engine.cancel_contingent(contingent_id)
```

If the contingent order is part of an OCO pair, only the specified order is canceled (the partner remains active).

### pending_contingent_orders

List all pending (not yet triggered) contingent orders.

```python
pending = engine.pending_contingent_orders()
# Returns: list[ContingentOrder]
```

See [ContingentOrder type](/api-reference/types#contingentorder) for field details.

## Smart Order Routing

### submit_order_smart

Route an order to the exchange with the best available price based on current feed data.

```python
order_id = engine.submit_order_smart(request, fallback_exchange=None)
```

- **Buy orders** are routed to the exchange with the lowest ask price.
- **Sell orders** are routed to the exchange with the highest bid price.
- If no feed data is available or prices are equal, the order is sent to `fallback_exchange` (or the primary exchange if `None`).

<Warning>
  Smart routing requires active feeds for each exchange. Start feeds before using `submit_order_smart`.
</Warning>

## Cancel

### cancel

Cancel a single order by ID. Looks up the exchange automatically:

```python
engine.cancel(order_id)
```

### cancel_all

Cancel all orders across **all** exchanges:

```python
count = engine.cancel_all()  # Returns total canceled
```

### cancel_market

Cancel all orders for a specific market across **all** exchanges:

```python
count = engine.cancel_market(market_id)
```

## Fills & Positions

### tick

Tick the paper exchange with a price and process fills:

```python
fill_count = engine.tick(market_id, mid_price, exchange=None)
```

### poll_fills

Poll fills from **all** live exchanges:

```python
fill_count = engine.poll_fills()
```

### process_fill

Manually inject a fill:

```python
engine.process_fill(fill)
```

### update_mark_price

Update mark price for unrealized P&L calculation:

```python
engine.update_mark_price(market_id, Side.Yes, 0.60)
```

### sync_positions

Fetch positions from an exchange and reconcile:

```python
count = engine.sync_positions(exchange=None)
```

## Queries

```python
orders = engine.open_orders()                       # All open orders
orders = engine.open_orders_for_market(market_id)   # Open orders for a market
positions = engine.positions()                       # All positions
fills = engine.recent_fills()                        # Recent fills (capped at 1000)
status = engine.status()                             # EngineStatus snapshot
```

Each `Order` returned includes an `amendment_count` field (number of times the order has been amended). See [Order type](/api-reference/types#order) for details.

## Risk Controls

```python
engine.activate_kill_switch("manual halt")  # Blocks orders + cancels all
engine.deactivate_kill_switch()
engine.update_daily_pnl(pnl)               # Update for drawdown tracking
engine.set_daily_baseline(baseline)         # Set daily P&L baseline
```

## Feed Management

```python
# Start feeds (original types)
engine.start_feed("btc", "binance_ws", symbol="btcusdt")
engine.start_feed("poly", "polymarket_book", symbol="will-btc-hit-100k")
engine.start_feed("kalshi", "kalshi_book", symbol="KXBTC-25FEB16")
engine.start_feed("custom", "rest", url="https://api.example.com/price", interval=5.0)

# Start feeds (v0.4.5 — new types via config_json)
import json
engine.start_feed("pi", "predictit", config_json=json.dumps({"market_id": 7456}))
engine.start_feed("mf", "manifold", config_json=json.dumps({"slug": "test-market"}))
engine.start_feed("nba", "espn", config_json=json.dumps({"sport": "basketball", "league": "nba"}))
engine.start_feed("wx", "nws", config_json=json.dumps({"mode": "alerts", "state": "FL"}))
engine.start_feed("cg", "rest_json_path", config_json=json.dumps({
    "url": "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd",
    "price_path": "bitcoin.usd",
}))

# Query feeds
snap = engine.feed_snapshot("btc")         # FeedSnapshot or None
age = engine.feed_age("btc")              # Seconds since last update
all_snaps = engine.all_feed_snapshots()    # dict[str, FeedSnapshot]

# Individual feed control
engine.stop_feed("btc")                    # Stop a single feed
engine.restart_feed("btc")                # Restart with stored config
engine.is_feed_running("btc")             # Check if feed task is alive

# Stop all
engine.stop_feeds()
```

## Persistence

```python
engine.has_persistence()        # True if DB is open
engine.snapshot_positions()     # Save current positions → returns count
engine.recover_state()          # Load snapshot + replay fills → returns position count
engine.start_run("strategy")    # Record run start
engine.end_run()                # Record run end
engine.db_run_id()              # Current run UUID
engine.db_open_order_ids()      # Orphaned order IDs from previous run
```

## Multi-Outcome Event Management

Register and query multi-outcome events. See [Multi-Outcome Events](/multi-outcome) for the full guide.

### register_event

Register an event with its outcome market IDs:

```python
engine.register_event("election-2024", ["trump-win", "biden-win", "desantis-win"])
```

### event_exposure

Total exposure across all outcome markets in an event:

```python
exposure = engine.event_exposure("election-2024")  # float
```

### event_positions

All positions in markets belonging to an event:

```python
positions = engine.event_positions("election-2024")  # list[Position]
```

### event_parity_check

Check whether outcome prices sum to ~1.0 using current feed data:

```python
result = engine.event_parity_check("election-2024", threshold=0.02)
# Returns ParityResult or None
```

### market_event_id

Reverse lookup  - which event does a market belong to?

```python
event_id = engine.market_event_id("trump-win")  # "election-2024" or None
```

### registered_events

All registered events and their market IDs:

```python
events = engine.registered_events()  # dict[str, list[str]]
```

## Arbitrage Execution

### execute_arbitrage

Submit a cross-exchange arbitrage trade with atomic rollback. Both legs go through the risk pipeline with Fill-Or-Kill semantics.

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

- Both legs use `TimeInForce.FOK` (Fill Or Kill)
- If the sell leg fails after the buy succeeds, the buy order is automatically canceled
- Returns `(buy_order_id, sell_order_id)` on success
- Raises an exception if risk rejects either leg

See [Arbitrage Executor](/arbitrage) for the full guide including `arb_scanner()` and `arb_sweep()`.

## Maintenance

```python
engine.evict_stale_orders(max_age_secs=300.0)  # Remove terminal orders older than 5 min
```

## Shutdown Behavior

When the `Engine` is dropped (Python garbage collection or explicit `del`):

1. Position snapshot is saved to the database
2. Strategy run is ended in the database
3. All orders are canceled across all exchanges
4. All feeds are stopped
