Horizon provides synthetic advanced order types managed entirely by the engine. Stop-losses, take-profits, bracket orders, and OCO links are not sent to the exchange. They live in-engine and fire through the normal risk pipeline when triggered.
Contingent orders (stop-loss, take-profit) are evaluated every tick via engine.check_contingent_triggers(). When using hz.run(), this is called automatically. If you manage the loop yourself, you must call it explicitly.
Understanding when contingent orders fire is critical.
Stop-Loss
Take-Profit
Stop-losses protect against adverse price movement.
Order Side
Trigger Condition
Sell stop-loss
Fires when current_price <= trigger_price
Buy stop-loss
Fires when current_price >= trigger_price
Copy
# You hold a Yes position bought at 0.60.# Set a sell stop-loss at 0.45 to limit downside.sl_id = engine.add_stop_loss( market_id="will-it-rain-tomorrow", side=Side.Yes, order_side=OrderSide.Sell, size=10.0, trigger_price=0.45,)# If the price drops to 0.45 or below, a sell order is submitted.
Take-profits lock in gains when price or PnL reaches a target.
Order Side
Trigger Condition
Sell take-profit
Fires when current_price >= trigger_priceORunrealized_pnl >= trigger_pnl
Buy take-profit
Fires when current_price <= trigger_priceORunrealized_pnl >= trigger_pnl
Copy
# You hold a Yes position bought at 0.60.# Take profit at 0.80 or when PnL exceeds $50.tp_id = engine.add_take_profit( market_id="will-it-rain-tomorrow", side=Side.Yes, order_side=OrderSide.Sell, size=10.0, trigger_price=0.80, trigger_pnl=50.0,)
Submits the entry order immediately and creates linked stop-loss and take-profit contingent orders. Returns (entry_id, sl_id, tp_id). The SL and TP are automatically linked as OCO.
Performs a cancel + resubmit under the hood and returns a new order ID. The new order carries forward the amendment_count.
Copy
order_id = engine.submit_order(request)new_id = engine.amend_order(order_id, new_price=0.55)# new_id != order_id on live exchanges
On live exchanges, there is a brief window between cancel and resubmit where you have no order in the book. If you need atomic amendment, use the exchange’s native amend API directly.
A bracket order is the most common advanced order pattern: enter a position with automatic downside protection and profit target.
Copy
import horizon as hzfrom horizon import Side, OrderSide, OrderRequest, RiskConfigengine = hz.Engine(risk_config=RiskConfig(max_position_per_market=1000, max_order_size=200))# Define the entry orderentry_request = OrderRequest( market_id="election-winner", side=Side.Yes, order_side=OrderSide.Buy, price=0.55, size=100.0,)# Submit bracket: entry + stop-loss at 0.40 + take-profit at 0.75entry_id, sl_id, tp_id = engine.submit_bracket( request=entry_request, stop_trigger=0.40, take_profit_trigger=0.75, take_profit_pnl=200.0, # Also take profit if PnL >= $200)print(f"Entry: {entry_id}")print(f"Stop-loss: {sl_id}")print(f"Take-profit: {tp_id}")# The SL and TP are automatically OCO-linked.# If the stop-loss fires at 0.40, the take-profit is canceled.# If the take-profit fires at 0.75 (or PnL >= $200), the stop-loss is canceled.
When using hz.run(), contingent triggers are checked automatically. You can set up contingent orders inside your pipeline functions.
Copy
import horizon as hzfrom horizon import Side, OrderSidedef my_model(ctx): return 0.60def my_quoter(ctx, fair): return hz.quotes(fair - 0.05, spread=0.04, size=20)def my_risk_manager(ctx): """Add stop-loss after first fill if none exists.""" engine = ctx.params["engine"] pending = engine.pending_contingent_orders() has_sl = any( o.trigger_type == hz.TriggerType.StopLoss and o.market_id == ctx.market_id for o in pending ) if ctx.inventory.net != 0 and not has_sl: engine.add_stop_loss( market_id=ctx.market_id, side=Side.Yes, order_side=OrderSide.Sell, size=abs(ctx.inventory.net), trigger_price=ctx.feed.price - 0.15, )hz.run( name="advanced-orders-demo", markets=["btc-above-100k"], feeds={"btc-above-100k": "polymarket_book"}, pipeline=[my_model, my_quoter, my_risk_manager],)
Contingent orders survive for the lifetime of the engine. If you want to reset them (e.g., after a position is fully closed), cancel them explicitly with engine.cancel_contingent().