Skip to main content
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

from horizon import Engine, RiskConfig

Paper exchange (default)

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)
api_key is optional if you’ve set HORIZON_API_KEY as an environment variable. See Authentication.

Polymarket

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

Kalshi

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

With persistence

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

Full signature

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).
# 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.

Multi-Exchange

add_exchange

Add a secondary exchange to the engine.
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.
Each exchange type can only be registered once. Calling add_exchange("polymarket", ...) when Polymarket is already registered raises ValueError.

exchange_names

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

exchange_count

count = engine.exchange_count()  # 2

exchange_name

Get the primary exchange name (backward compatible):
name = engine.exchange_name()  # "polymarket"

set_netting_pair

Register a netting pair for cross-exchange risk reduction:
engine.set_netting_pair("market_a", "market_b")

netting_pairs

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

Order Submission

All order methods support optional exchange parameter for routing:

submit_order

order_id = engine.submit_order(request, exchange=None)
Submit an order through the risk pipeline. Returns the exchange-assigned order ID.

submit_quotes

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

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.
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 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

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

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.
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.
triggered_count = engine.check_contingent_triggers(market_id, current_price)
hz.run() calls check_contingent_triggers automatically on every tick. You only need to call this manually when using the engine directly.

cancel_contingent

Cancel a specific contingent order by its ID.
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.
pending = engine.pending_contingent_orders()
# Returns: list[ContingentOrder]
See ContingentOrder type 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.
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).
Smart routing requires active feeds for each exchange. Start feeds before using submit_order_smart.

Cancel

cancel

Cancel a single order by ID. Looks up the exchange automatically:
engine.cancel(order_id)

cancel_all

Cancel all orders across all exchanges:
count = engine.cancel_all()  # Returns total canceled

cancel_market

Cancel all orders for a specific market across all exchanges:
count = engine.cancel_market(market_id)

Fills & Positions

tick

Tick the paper exchange with a price and process fills:
fill_count = engine.tick(market_id, mid_price, exchange=None)

poll_fills

Poll fills from all live exchanges:
fill_count = engine.poll_fills()

process_fill

Manually inject a fill:
engine.process_fill(fill)

update_mark_price

Update mark price for unrealized P&L calculation:
engine.update_mark_price(market_id, Side.Yes, 0.60)

sync_positions

Fetch positions from an exchange and reconcile:
count = engine.sync_positions(exchange=None)

Queries

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 for details.

Runtime Parameters

# Set a single runtime parameter
engine.update_param("spread", 0.05)

# Set multiple parameters at once
engine.update_params_batch({"spread": 0.05, "gamma": 0.3})

# Get a single parameter (returns None if not set)
spread = engine.get_param("spread")

# Get all runtime parameters
params = engine.get_all_params()  # dict[str, float]

# Remove a parameter
engine.remove_param("spread")
Runtime parameters are injected into ctx.params every cycle when using hz.run() with hot_reload() in the pipeline.

Risk Controls

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

# 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

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 for the full guide.

register_event

Register an event with its outcome market IDs:
engine.register_event("election-2024", ["trump-win", "biden-win", "desantis-win"])

event_exposure

Total exposure across all outcome markets in an event:
exposure = engine.event_exposure("election-2024")  # float

event_positions

All positions in markets belonging to an event:
positions = engine.event_positions("election-2024")  # list[Position]

event_parity_check

Check whether outcome prices sum to ~1.0 using current feed data:
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?
event_id = engine.market_event_id("trump-win")  # "election-2024" or None

registered_events

All registered events and their market IDs:
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.
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 for the full guide including arb_scanner() and arb_sweep().

Maintenance

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