---
title: "Advanced Orders"
description: "Stop-loss, take-profit, bracket orders, OCO linking, and order amendment."
icon: "shield-halved"
---

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

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

## Core Concepts

<CardGroup cols={2}>
  <Card title="Contingent Orders" icon="clock">
    Synthetic orders that wait for a trigger condition before submitting to the exchange. They go through the full risk pipeline when fired.
  </Card>
  <Card title="OCO (One-Cancels-Other)" icon="link">
    Two contingent orders linked together. When one triggers, the other is automatically canceled.
  </Card>
  <Card title="Bracket Orders" icon="brackets-curly">
    An entry order paired with a stop-loss and take-profit, automatically linked as OCO.
  </Card>
  <Card title="Order Amendment" icon="pen">
    Modify price or size of a live order. Paper exchange amends in-place; live exchanges do cancel + resubmit.
  </Card>
</CardGroup>

## Types

### TriggerType

```python
from horizon import TriggerType

TriggerType.StopLoss
TriggerType.TakeProfit
```

### ContingentOrder

Each contingent order exposes the following fields:

| Field | Type | Description |
|---|---|---|
| `id` | `str` | Unique contingent order ID |
| `trigger_type` | `TriggerType` | `StopLoss` or `TakeProfit` |
| `market_id` | `str` | Target market |
| `side` | `Side` | `Yes` or `No` |
| `order_side` | `OrderSide` | `Buy` or `Sell` |
| `size` | `float` | Order size |
| `trigger_price` | `float` | Price threshold |
| `trigger_pnl` | `float or None` | PnL threshold (take-profit only) |
| `linked_order_id` | `str or None` | OCO partner ID |
| `exchange` | `str or None` | Target exchange override |
| `triggered` | `bool` | Whether the order has fired |
| `child_order_id` | `str or None` | ID of the submitted order after trigger |

---

## Trigger Logic

Understanding when contingent orders fire is critical.

<Tabs>
  <Tab title="Stop-Loss">
    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` |

    ```python
    # 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.
    ```
  </Tab>
  <Tab title="Take-Profit">
    Take-profits lock in gains when price or PnL reaches a target.

    | Order Side | Trigger Condition |
    |---|---|
    | **Sell** take-profit | Fires when `current_price >= trigger_price` **OR** `unrealized_pnl >= trigger_pnl` |
    | **Buy** take-profit | Fires when `current_price <= trigger_price` **OR** `unrealized_pnl >= trigger_pnl` |

    ```python
    # 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,
    )
    ```
  </Tab>
</Tabs>

---

## Engine Methods

### add_stop_loss

```python
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

```python
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

```python
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

```python
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

```python
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

```python
engine.pending_contingent_orders() -> list[ContingentOrder]
```

Returns all pending (not yet triggered) contingent orders.

### amend_order

```python
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:

<Tabs>
  <Tab title="Paper Exchange">
    Amends the order **in-place** and returns the **same order ID**. The order's `amendment_count` is incremented.

    ```python
    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
    ```
  </Tab>
  <Tab title="Live Exchanges">
    Performs a **cancel + resubmit** under the hood and returns a **new order ID**. The new order carries forward the `amendment_count`.

    ```python
    order_id = engine.submit_order(request)
    new_id = engine.amend_order(order_id, new_price=0.55)
    # new_id != order_id on live exchanges
    ```
  </Tab>
</Tabs>

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

---

## Examples

### Standalone Stop-Loss

<Steps>
  <Step title="Set up the engine and enter a position">
    ```python
    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)
    ```
  </Step>
  <Step title="Add a stop-loss to protect the position">
    ```python
    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}")
    ```
  </Step>
  <Step title="Check triggers on each tick">
    ```python
    # 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
    ```
  </Step>
</Steps>

### Bracket Order

A bracket order is the most common advanced order pattern: enter a position with automatic downside protection and profit target.

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

```python
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

```python
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

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

```python
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],
)
```

<Tip>
  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()`.
</Tip>
