---
title: "Execution Algorithms"
description: "TWAP, VWAP, and Iceberg algorithms for intelligent order splitting with minimal market impact."
icon: "chart-line"
---

# Horizon Execution Algorithms

Horizon ships with three execution algorithms for splitting large orders into smaller child orders: **TWAP**, **VWAP**, and **Iceberg**. All are implemented in pure Python, use `engine.submit_order()` internally, and do **not** participate in cancel-before-requote.

<Note>
  Execution algorithms are designed for situations where you need to fill a large order with minimal market impact. They manage their own child orders and should not be mixed with manual order management for the same market.
</Note>

## Overview

<CardGroup cols={3}>
  <Card title="TWAP" icon="clock">
    Time-Weighted Average Price. Splits the order into equal slices submitted at regular intervals over a duration.
  </Card>
  <Card title="VWAP" icon="chart-bar">
    Volume-Weighted Average Price. Slices the order proportionally based on a volume profile (historical or predicted).
  </Card>
  <Card title="Iceberg" icon="iceberg">
    Shows only a small visible portion of the full order. Automatically replenishes when the visible portion fills.
  </Card>
</CardGroup>

---

## Base Class: ExecAlgo

All execution algorithms inherit from `ExecAlgo` and share a common interface.

```python
from horizon.algos import ExecAlgo

class ExecAlgo:
    def start(self, request: OrderRequest) -> None:
        """Begin execution of the given order request."""
        ...

    def on_tick(self, current_price: float, timestamp: float) -> None:
        """Called each tick to advance the algorithm."""
        ...

    @property
    def is_complete(self) -> bool:
        """Whether the full order has been executed."""
        ...

    @property
    def child_order_ids(self) -> list[str]:
        """IDs of all child orders submitted so far."""
        ...

    @property
    def total_filled(self) -> float:
        """Total size filled across all child orders."""
        ...
```

### Usage Pattern

Every algo follows the same lifecycle:

<Steps>
  <Step title="Create the algo with engine reference and parameters">
    ```python
    algo = TWAP(engine, duration_secs=300, num_slices=10)
    ```
  </Step>
  <Step title="Start with an OrderRequest">
    ```python
    algo.start(request)
    ```
  </Step>
  <Step title="Call on_tick() each cycle">
    ```python
    algo.on_tick(current_price=0.55, timestamp=time.time())
    ```
  </Step>
  <Step title="Check completion">
    ```python
    if algo.is_complete:
        print(f"Done! Filled {algo.total_filled} across {len(algo.child_order_ids)} orders")
    ```
  </Step>
</Steps>

---

## TWAP (Time-Weighted Average Price)

Splits the total order into `num_slices` equal pieces and submits one slice at each interval over the specified duration.

```python
from horizon.algos import TWAP
```

### Constructor

```python
TWAP(
    engine: Engine,
    duration_secs: float,
    num_slices: int,
)
```

| Parameter | Type | Description |
|---|---|---|
| `engine` | `Engine` | The Horizon engine instance |
| `duration_secs` | `float` | Total execution window in seconds |
| `num_slices` | `int` | Number of equal child orders |

### How It Works

1. The total size is divided into `num_slices` equal parts.
2. The interval between slices is `duration_secs / num_slices`.
3. On each `on_tick()`, if enough time has elapsed since the last slice, a new child order is submitted at the current market price.
4. The algo is complete when all slices have been submitted.

### Full Example

```python
import time
import horizon as hz
from horizon import Side, OrderSide, OrderRequest
from horizon.algos import TWAP

engine = hz.Engine()

# We want to buy 500 contracts over 5 minutes, in 10 slices
twap = TWAP(engine, duration_secs=300, num_slices=10)

request = OrderRequest(
    market_id="election-winner",
    side=Side.Yes,
    order_side=OrderSide.Buy,
    price=0.55,
    size=500.0,  # Total size
)

twap.start(request)

# Simulation loop
start_time = time.time()
while not twap.is_complete:
    current_time = time.time()
    current_price = 0.55  # In practice, get from feed

    twap.on_tick(current_price, current_time)

    print(f"Filled: {twap.total_filled} / 500.0")
    print(f"Child orders: {len(twap.child_order_ids)}")

    time.sleep(1)

print(f"TWAP complete. Total filled: {twap.total_filled}")
print(f"All child order IDs: {twap.child_order_ids}")
```

<Tip>
  Choose `num_slices` based on market liquidity. More slices means smaller individual orders but more time in the market. For thin prediction markets, 5-10 slices is usually sufficient.
</Tip>

---

## VWAP (Volume-Weighted Average Price)

Splits the total order proportionally according to a volume profile. Heavier slices are placed during high-volume periods.

```python
from horizon.algos import VWAP
```

### Constructor

```python
VWAP(
    engine: Engine,
    duration_secs: float,
    volume_profile: list[float],
)
```

| Parameter | Type | Description |
|---|---|---|
| `engine` | `Engine` | The Horizon engine instance |
| `duration_secs` | `float` | Total execution window in seconds |
| `volume_profile` | `list[float]` | Relative volume weights per slice (auto-normalized) |

### How It Works

1. The `volume_profile` is normalized so weights sum to 1.0.
2. Each slice's size is `total_size * normalized_weight[i]`.
3. Slices are submitted at regular intervals (`duration_secs / len(volume_profile)`).
4. Heavier slices are placed during time windows with higher expected volume.

### Full Example

```python
import time
import horizon as hz
from horizon import Side, OrderSide, OrderRequest
from horizon.algos import VWAP

engine = hz.Engine()

# Volume profile: heavier trading at open and close
# These weights are automatically normalized
volume_profile = [
    3.0,  # Slice 1: heavy (market open)
    2.0,  # Slice 2: moderate
    1.0,  # Slice 3: light (midday)
    1.0,  # Slice 4: light
    2.0,  # Slice 5: moderate
    4.0,  # Slice 6: heaviest (market close)
]

vwap = VWAP(engine, duration_secs=600, volume_profile=volume_profile)

request = OrderRequest(
    market_id="fed-rate-decision",
    side=Side.Yes,
    order_side=OrderSide.Buy,
    price=0.62,
    size=1000.0,  # Total size to fill
)

# With the profile above, slice sizes will be approximately:
# Slice 1: 230.8 (3/13 * 1000)
# Slice 2: 153.8 (2/13 * 1000)
# Slice 3:  76.9 (1/13 * 1000)
# Slice 4:  76.9 (1/13 * 1000)
# Slice 5: 153.8 (2/13 * 1000)
# Slice 6: 307.7 (4/13 * 1000)

vwap.start(request)

while not vwap.is_complete:
    current_time = time.time()
    current_price = 0.62

    vwap.on_tick(current_price, current_time)
    time.sleep(1)

print(f"VWAP complete. Total filled: {vwap.total_filled}")
```

<Tip>
  You can derive `volume_profile` from historical trade data. Use `hz.backtest()` results or exchange trade history to build a realistic intraday volume curve.
</Tip>

---

## Iceberg

Shows only a small portion (`show_size`) of the total order at any time. When the visible portion is filled, a new visible order is automatically submitted until the full size is complete.

```python
from horizon.algos import Iceberg
```

### Constructor

```python
Iceberg(
    engine: Engine,
    show_size: float,
)
```

| Parameter | Type | Description |
|---|---|---|
| `engine` | `Engine` | The Horizon engine instance |
| `show_size` | `float` | Maximum visible size per child order |

### How It Works

1. A child order of size `min(show_size, remaining_size)` is submitted.
2. On each `on_tick()`, the algo checks if the current visible order has been filled.
3. If filled, a new visible order is submitted with the next chunk.
4. The algo is complete when the entire original size has been filled.

### Full Example

```python
import time
import horizon as hz
from horizon import Side, OrderSide, OrderRequest
from horizon.algos import Iceberg

engine = hz.Engine()

# We want to buy 200 contracts but only show 15 at a time
iceberg = Iceberg(engine, show_size=15.0)

request = OrderRequest(
    market_id="btc-above-100k",
    side=Side.Yes,
    order_side=OrderSide.Buy,
    price=0.70,
    size=200.0,
)

iceberg.start(request)

while not iceberg.is_complete:
    current_time = time.time()
    current_price = 0.70

    iceberg.on_tick(current_price, current_time)

    print(f"Filled: {iceberg.total_filled} / 200.0")
    print(f"Visible orders placed: {len(iceberg.child_order_ids)}")

    time.sleep(0.5)

print(f"Iceberg complete. Total filled: {iceberg.total_filled}")
```

<Warning>
  The iceberg algo relies on fills being drained from the exchange. Make sure the engine's fill polling is active. In `hz.run()`, this happens automatically. In manual loops, call `engine.tick()` or `engine.drain_fills()` before `on_tick()`.
</Warning>

---

## Using Algos in hz.run()

Execution algorithms work best when managed inside a pipeline function that has access to the engine.

```python
import time
import horizon as hz
from horizon import Side, OrderSide, OrderRequest
from horizon.algos import TWAP

active_algo = None

def model(ctx):
    fair_value = 0.60  # Your model logic here
    return fair_value

def algo_manager(ctx, fair):
    """Manage a TWAP algo to accumulate a position."""
    global active_algo
    engine = ctx.params["engine"]

    # Start a TWAP if we don't have one running
    if active_algo is None or active_algo.is_complete:
        positions = engine.positions()
        current_pos = next((p for p in positions if p.market_id == ctx.market_id), None)
        current_size = current_pos.size if current_pos else 0.0

        # Target 100 contracts; start TWAP if below target
        if current_size < 100.0:
            request = OrderRequest(
                market_id=ctx.market_id,
                side=Side.Yes,
                order_side=OrderSide.Buy,
                price=fair,
                size=100.0 - current_size,
            )
            active_algo = TWAP(engine, duration_secs=120, num_slices=6)
            active_algo.start(request)

    # Advance the algo each tick
    if active_algo and not active_algo.is_complete:
        active_algo.on_tick(ctx.feed.price, time.time())

hz.run(
    name="twap-accumulator",
    markets=["election-winner"],
    feeds={"election-winner": "polymarket_book"},
    pipeline=[model, algo_manager],
)
```

---

## Comparison

<AccordionGroup>
  <Accordion title="When to use TWAP">
    Use TWAP when you want to spread execution evenly over time and don't have a strong opinion about volume patterns. Good for markets with consistent liquidity throughout the day.
  </Accordion>
  <Accordion title="When to use VWAP">
    Use VWAP when you have historical volume data and want to minimize market impact by trading more during high-volume periods. Best for markets with predictable volume patterns.
  </Accordion>
  <Accordion title="When to use Iceberg">
    Use Iceberg when you want to hide the full size of your order from other participants. Best for markets where showing a large order would move the price against you.
  </Accordion>
</AccordionGroup>

| Feature | TWAP | VWAP | Iceberg |
|---|---|---|---|
| Execution timing | Fixed intervals | Fixed intervals | On fill |
| Size distribution | Equal slices | Weighted slices | Fixed visible size |
| Requires volume data | No | Yes | No |
| Hides order size | Partially | Partially | Yes |
| Best for | Steady markets | Volume-patterned markets | Thin orderbooks |
