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.
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.
All execution algorithms inherit from ExecAlgo and share a common interface.
Copy
from horizon.algos import ExecAlgoclass 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.""" ...
import timeimport horizon as hzfrom horizon import Side, OrderSide, OrderRequestfrom horizon.algos import TWAPengine = hz.Engine()# We want to buy 500 contracts over 5 minutes, in 10 slicestwap = 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 loopstart_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}")
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.
import timeimport horizon as hzfrom horizon import Side, OrderSide, OrderRequestfrom horizon.algos import VWAPengine = hz.Engine()# Volume profile: heavier trading at open and close# These weights are automatically normalizedvolume_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}")
You can derive volume_profile from historical trade data. Use hz.backtest() results or exchange trade history to build a realistic intraday volume curve.
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.
import timeimport horizon as hzfrom horizon import Side, OrderSide, OrderRequestfrom horizon.algos import Icebergengine = hz.Engine()# We want to buy 200 contracts but only show 15 at a timeiceberg = 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}")
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().
Execution algorithms work best when managed inside a pipeline function that has access to the engine.
Copy
import timeimport horizon as hzfrom horizon import Side, OrderSide, OrderRequestfrom horizon.algos import TWAPactive_algo = Nonedef model(ctx): fair_value = 0.60 # Your model logic here return fair_valuedef 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],)
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.
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.
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.