Skip to main content
When you need to fill a large order without moving the market, Horizon provides three execution algorithms: TWAP (time-weighted), VWAP (volume-weighted), and Iceberg (hidden size). Each splits a parent order into smaller child orders managed by the engine.

TWAP (Time-Weighted Average Price)

Splits a large order into equal-sized slices submitted at regular time intervals:
"""TWAP execution: split a large order into time-weighted slices."""

from horizon import Engine, OrderRequest, Side, OrderSide, RiskConfig
from horizon import TWAP

engine = Engine(risk_config=RiskConfig(max_position_per_market=10000))

# Create a large parent order
request = OrderRequest(
    market_id="btc-100k",
    side=Side.Yes,
    order_side=OrderSide.Buy,
    size=100.0,
    price=0.55,
)

# TWAP: 100 contracts over 60 seconds in 10 slices
twap = TWAP(engine, duration_secs=60.0, num_slices=10)
twap.start(request)

# Each cycle, call on_tick with current price
twap.on_tick(current_price=0.55, timestamp=0.0)
print(f"TWAP complete: {twap.is_complete}")
print(f"Child orders: {len(twap.child_order_ids)}")

How TWAP works

Time:   0s    6s    12s   18s   24s   30s   36s   42s   48s   54s
Slice:  10    10    10    10    10    10    10    10    10    10
        |     |     |     |     |     |     |     |     |     |
        v     v     v     v     v     v     v     v     v     v
        [=====|=====|=====|=====|=====|=====|=====|=====|=====|=====]
        0                          60s
        Total: 100 contracts in 10 equal slices
Each slice is submitted as an independent limit order at the current market price. The algorithm:
  1. Divides duration_secs by num_slices to get the interval (6 seconds above).
  2. On each on_tick() call, checks if enough time has passed for the next slice.
  3. Submits a child order of total_size / num_slices contracts.
  4. Tracks total filled across all children and marks complete when the target is reached.

When to use TWAP

  • Low-urgency fills where minimizing market impact matters more than speed.
  • Markets with thin books where placing the full size at once would walk the book.
  • Predictable scheduling: TWAP gives you even participation over the time window.

VWAP (Volume-Weighted Average Price)

Slices the order proportionally to a volume profile, concentrating execution during high-volume periods:
"""VWAP execution: volume-weighted slicing based on a historical profile."""

from horizon import Engine, OrderRequest, Side, OrderSide, RiskConfig
from horizon import VWAP

engine = Engine(risk_config=RiskConfig(max_position_per_market=10000))

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

# Volume profile: more volume in first and last buckets (U-shaped)
volume_profile = [3.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 3.0]

# VWAP: 100 contracts over 60 seconds following the volume profile
vwap = VWAP(engine, duration_secs=60.0, volume_profile=volume_profile)
vwap.start(request)

# Drive the algo. In a real loop this runs each tick cycle
vwap.on_tick(current_price=0.55, timestamp=0.0)
print(f"VWAP complete: {vwap.is_complete}")
print(f"Child orders: {len(vwap.child_order_ids)}")
print(f"Total filled: {vwap.total_filled}")

How VWAP works

The volume profile determines how much of the total order goes into each time slice:
Volume profile: [3, 1, 1, 1, 1, 1, 1, 1, 1, 3]
Total weight:   14
Slice sizes:    21.4, 7.1, 7.1, 7.1, 7.1, 7.1, 7.1, 7.1, 7.1, 21.4
                ^^^^                                              ^^^^
                Heavy at open and close (U-shaped pattern)
Each weight in the profile is normalized by the total weight, then multiplied by the target size. Slices are evenly spaced in time across duration_secs.

When to use VWAP

  • Benchmarking against market VWAP: your fills will match the volume pattern.
  • High-volume windows: concentrate execution when liquidity is deepest.
  • Relative value strategies where you want to match the market’s natural flow.

Building a volume profile

You can construct volume profiles from historical data or use common patterns:
# U-shaped: heavy at open and close
u_shape = [3.0, 1.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.5, 3.0]

# Front-loaded: execute mostly at the start
front_loaded = [5.0, 3.0, 2.0, 1.0, 1.0, 0.5, 0.5, 0.5, 0.5, 0.5]

# Uniform (equivalent to TWAP)
uniform = [1.0] * 10

# From historical data
import csv
def load_volume_profile(csv_path: str) -> list[float]:
    """Load hourly volume buckets from a CSV."""
    volumes = []
    with open(csv_path) as f:
        reader = csv.DictReader(f)
        for row in reader:
            volumes.append(float(row["volume"]))
    return volumes

Iceberg (Hidden Size)

Shows only a small visible portion of the total order. When the visible slice fills, a new one is placed:
"""Iceberg: hide the true order size behind a small visible clip."""

from horizon import Engine, OrderRequest, Side, OrderSide, RiskConfig
from horizon import Iceberg

engine = Engine(risk_config=RiskConfig(max_position_per_market=10000))

request = OrderRequest(
    market_id="btc-100k",
    side=Side.Yes,
    order_side=OrderSide.Buy,
    size=100.0,   # True size: 100 contracts
    price=0.55,
)

# Iceberg: show only 10 contracts at a time
iceberg = Iceberg(engine, show_size=10.0)
iceberg.start(request)

# On each tick, the algo checks if the visible order has been filled
# and submits a new visible slice if needed
iceberg.on_tick(current_price=0.55, timestamp=0.0)
print(f"Iceberg complete: {iceberg.is_complete}")
print(f"Child orders placed: {len(iceberg.child_order_ids)}")
print(f"Total filled: {iceberg.total_filled}")

How Iceberg works

Total order: 100 contracts
Show size:   10 contracts

Visible:   [10]  -> filled -> [10] -> filled -> [10] -> ... -> [10]
Hidden:    [===========90 remaining=============]    ...    [===0===]
                                                            Complete!
The algorithm:
  1. Places a limit order for show_size contracts.
  2. On each on_tick(), checks whether the visible order is still open.
  3. When the visible order fills, calculates remaining size and places a new visible slice.
  4. Repeats until the full target size is filled.

When to use Iceberg

  • Avoid signaling: other market participants cannot see your true order size.
  • Thin prediction markets where showing 100 contracts would discourage counterparty flow.
  • Passive fills: each slice rests at your limit price, earning the spread.

Running an Algo in a Loop

All three algorithms follow the same ExecAlgo interface. Here is a complete example driving TWAP inside a loop:
"""Complete TWAP execution loop with fill tracking."""

import time
from horizon import Engine, OrderRequest, Side, OrderSide, RiskConfig
from horizon import TWAP

engine = Engine(risk_config=RiskConfig(max_position_per_market=10000))

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

twap = TWAP(engine, duration_secs=30.0, num_slices=5)
twap.start(request)

start_time = time.time()
while not twap.is_complete:
    elapsed = time.time() - start_time
    current_price = 0.55  # In production, fetch from feed

    # Tick the paper exchange (simulates matching)
    engine.tick("btc-100k", current_price)

    # Drive the algo
    twap.on_tick(current_price=current_price, timestamp=elapsed)

    print(
        f"  t={elapsed:.1f}s | "
        f"children={len(twap.child_order_ids)} | "
        f"filled={twap.total_filled:.0f}/{request.size:.0f}"
    )
    time.sleep(1.0)

print(f"\nDone! Total filled: {twap.total_filled}")
print(f"Child order IDs: {twap.child_order_ids}")

Comparison

FeatureTWAPVWAPIceberg
SlicingEqual time intervalsVolume-weighted intervalsOn-fill replacement
Market impactLow (spread out)Lowest (follows liquidity)Low (small visible)
SpeedFixed durationFixed durationDepends on fill rate
Best forLow urgency, even executionMatching market flowHiding order size
Inputsduration_secs, num_slicesduration_secs, volume_profileshow_size

API Reference

All algorithms inherit from ExecAlgo and share these properties and methods:
MemberTypeDescription
start(request)MethodBegin execution of a parent order.
on_tick(current_price, timestamp)MethodCalled each cycle to manage child orders.
is_completePropertyTrue when the target size has been fully filled.
child_order_idsPropertyList of all child order IDs submitted so far.
total_filledPropertyTotal contracts filled across all children.

Constructor signatures

TWAP(engine, duration_secs=60.0, num_slices=10)
VWAP(engine, duration_secs=60.0, volume_profile=[1.0, 2.0, 3.0, ...])
Iceberg(engine, show_size=10.0)