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:
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):
- Position snapshot is saved to the database
- Strategy run is ended in the database
- All orders are canceled across all exchanges
- All feeds are stopped