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