Skip to main content

Horizon Advanced Orders

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.

Core Concepts

Contingent Orders

Synthetic orders that wait for a trigger condition before submitting to the exchange. They go through the full risk pipeline when fired.

OCO (One-Cancels-Other)

Two contingent orders linked together. When one triggers, the other is automatically canceled.

Bracket Orders

An entry order paired with a stop-loss and take-profit, automatically linked as OCO.

Order Amendment

Modify price or size of a live order. Paper exchange amends in-place; live exchanges do cancel + resubmit.

Types

TriggerType

from horizon import TriggerType

TriggerType.StopLoss
TriggerType.TakeProfit

ContingentOrder

Each contingent order exposes the following fields:
FieldTypeDescription
idstrUnique contingent order ID
trigger_typeTriggerTypeStopLoss or TakeProfit
market_idstrTarget market
sideSideYes or No
order_sideOrderSideBuy or Sell
sizefloatOrder size
trigger_pricefloatPrice threshold
trigger_pnlfloat or NonePnL threshold (take-profit only)
linked_order_idstr or NoneOCO partner ID
exchangestr or NoneTarget exchange override
triggeredboolWhether the order has fired
child_order_idstr or NoneID of the submitted order after trigger

Trigger Logic

Understanding when contingent orders fire is critical.
Stop-losses protect against adverse price movement.
Order SideTrigger Condition
Sell stop-lossFires when current_price <= trigger_price
Buy stop-lossFires 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.

Engine Methods

add_stop_loss

engine.add_stop_loss(
    market_id: str,
    side: Side,
    order_side: OrderSide,
    size: float,
    trigger_price: float,
    exchange: str | None = None,
) -> str
Returns the contingent order ID. The order is held in-engine until the trigger condition is met.

add_take_profit

engine.add_take_profit(
    market_id: str,
    side: Side,
    order_side: OrderSide,
    size: float,
    trigger_price: float,
    trigger_pnl: float | None = None,
    exchange: str | None = None,
) -> str
Returns the contingent order ID. If both trigger_price and trigger_pnl are set, the order fires when either condition is met.

submit_bracket

engine.submit_bracket(
    request: OrderRequest,
    stop_trigger: float,
    take_profit_trigger: float,
    take_profit_pnl: float | None = None,
    exchange: str | None = None,
) -> tuple[str, str, str]
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.

check_contingent_triggers

engine.check_contingent_triggers(
    market_id: str,
    current_price: float,
) -> int
Evaluates all pending contingent orders for the given market. Returns the number of orders that triggered. Called automatically each tick in hz.run().

cancel_contingent

engine.cancel_contingent(contingent_id: str) -> bool
Cancels a pending contingent order. Returns True if the order was found and canceled, False if it was already triggered or not found.

pending_contingent_orders

engine.pending_contingent_orders() -> list[ContingentOrder]
Returns all pending (not yet triggered) contingent orders.

amend_order

engine.amend_order(
    order_id: str,
    new_price: float | None = None,
    new_size: float | None = None,
) -> str
Amends an existing order’s price and/or size. Behavior differs by exchange:
Amends the order in-place and returns the same order ID. The order’s amendment_count is incremented.
order_id = engine.submit_order(request)
# Price moved, adjust our quote
same_id = engine.amend_order(order_id, new_price=0.55)
assert same_id == order_id  # Same ID, amended in-place
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.

Examples

Standalone Stop-Loss

1

Set up the engine and enter a position

import horizon as hz
from horizon import Side, OrderSide, OrderRequest

engine = hz.Engine()

# Buy 50 contracts at 0.62
entry = OrderRequest(
    market_id="btc-above-100k",
    side=Side.Yes,
    order_side=OrderSide.Buy,
    price=0.62,
    size=50.0,
)
entry_id = engine.submit_order(entry)
2

Add a stop-loss to protect the position

sl_id = engine.add_stop_loss(
    market_id="btc-above-100k",
    side=Side.Yes,
    order_side=OrderSide.Sell,
    size=50.0,
    trigger_price=0.50,
)
print(f"Stop-loss set: {sl_id}")
3

Check triggers on each tick

# In your strategy loop:
current_price = 0.48  # Price dropped below stop
triggered = engine.check_contingent_triggers("btc-above-100k", current_price)
print(f"{triggered} orders triggered")  # 1

Bracket Order

A bracket order is the most common advanced order pattern: enter a position with automatic downside protection and profit target.
import horizon as hz
from horizon import Side, OrderSide, OrderRequest, RiskConfig

engine = hz.Engine(risk_config=RiskConfig(max_position_per_market=1000, max_order_size=200))

# Define the entry order
entry_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.75
entry_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.

Automatic OCO via Bracket

Use submit_bracket() to automatically link stop-loss and take-profit as OCO. When one triggers, the other is auto-canceled.
import horizon as hz
from horizon import Side, OrderSide, OrderRequest

engine = hz.Engine()

# submit_bracket creates linked OCO orders automatically
request = OrderRequest(
    market_id="fed-rate-cut",
    side=Side.Yes,
    order_side=OrderSide.Buy,
    price=0.55,
    size=25.0,
)
order_id, sl_id, tp_id = engine.submit_bracket(
    request=request,
    stop_trigger=0.30,
    take_profit_trigger=0.80,
)
print(f"Order: {order_id}")
print(f"Stop-loss: {sl_id}")
print(f"Take-profit: {tp_id}")

# Verify the linked orders
pending = engine.pending_contingent_orders()
for order in pending:
    print(f"{order.id} linked to {order.linked_order_id}")

Inspecting Pending Orders

pending = engine.pending_contingent_orders()

for order in pending:
    print(f"ID:        {order.id}")
    print(f"Type:      {order.trigger_type}")
    print(f"Market:    {order.market_id}")
    print(f"Trigger @: {order.trigger_price}")
    print(f"Size:      {order.size}")
    print(f"Linked to: {order.linked_order_id or 'None'}")
    print(f"Triggered: {order.triggered}")
    print("---")

# Cancel a specific contingent order
if pending:
    canceled = engine.cancel_contingent(pending[0].id)
    print(f"Canceled: {canceled}")

Amending Orders

import horizon as hz
from horizon import Side, OrderSide, OrderRequest

engine = hz.Engine()

# Submit initial order
request = OrderRequest(
    market_id="weather-market",
    side=Side.Yes,
    order_side=OrderSide.Buy,
    price=0.50,
    size=30.0,
)
order_id = engine.submit_order(request)

# Market moved, amend just the price
amended_id = engine.amend_order(order_id, new_price=0.48)
print(f"Amended order: {amended_id}")

# Amend both price and size
amended_id = engine.amend_order(amended_id, new_price=0.46, new_size=50.0)
print(f"Amended again: {amended_id}")

Using Advanced Orders in hz.run()

When using hz.run(), contingent triggers are checked automatically. You can set up contingent orders inside your pipeline functions.
import horizon as hz
from horizon import Side, OrderSide

def my_model(ctx):
    return 0.60

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