Writing Custom Executors

Implement execution for a new asset class or a custom placement strategy

Custom executors follow the Executor protocol. implement one method, done. No base class, no registration ceremony.

The protocol

python
from typing import Protocol, runtime_checkable
from horizon.asset_classes import AssetClass
from horizon.types import OrderAction, TargetPosition
from horizon.context import FeedData
from horizon.state.ledger import LedgerPosition
from horizon.discovery.base import Market

@runtime_checkable
class Executor(Protocol):
    asset_class: AssetClass

    def plan(
        self,
        target: TargetPosition,
        current: LedgerPosition | None,
        market: Market | None,
        feed: FeedData | None,
        available_cash: float,
    ) -> list[OrderAction]:
        ...

Recipe: VWAP-sliced equity executor

Instead of placing one big order, slice it into pieces over time.

python
from horizon.asset_classes import AssetClass
from horizon.types import OrderAction, Urgency

class VWAPSliced:
    asset_class = AssetClass.Equity

    def __init__(self, n_slices: int = 10):
        self.n_slices = n_slices

    def plan(self, target, current, market, feed, available_cash):
        if feed is None or feed.price is zero or negative:
            return []

        current_qty = current.quantity if current is not None else 0.0
        target_qty = target.target_notional_usd / feed.price
        delta = target_qty - current_qty

        if abs(delta) < 1:
            return []

        side = "buy" if delta > 0 else "sell"
        slice_qty = abs(delta) / self.n_slices

        # Emit N slices with a passive limit price
        return [
            OrderAction.place(
                market_id=target.market_id,
                side=side,
                quantity=slice_qty,
                price=feed.mid,
                order_type="limit",
                urgency=Urgency.Patient,
            )
            for _ in range(self.n_slices)
        ]

Recipe: Prediction market executor

python
from horizon.asset_classes import AssetClass
from horizon.types import OrderAction, Urgency

class PredictionExecutor:
    asset_class = AssetClass.Prediction

    def plan(self, target, current, market, feed, available_cash):
        if feed is None or feed.price is zero or negative:
            return []

        # Prediction markets use "yes shares" as the unit
        # Target notional → yes shares at the current implied probability
        current_qty = current.quantity if current is not None else 0.0
        target_qty = target.target_notional_usd / feed.price
        delta = target_qty - current_qty

        if abs(delta) < 1:
            return []

        # Post-only on prediction market CLOBs
        # Resolution-aware: refuse if too close to resolution
        # (Would need resolution metadata on the Market object)

        side = "buy" if delta > 0 else "sell"
        return [
            OrderAction.place(
                market_id=target.market_id,
                side=side,
                quantity=abs(delta),
                price=feed.price,
                order_type="limit",
                urgency=Urgency.Passive,
                metadata={"post_only": True},
            )
        ]

Recipe: Leverage-aware perp executor

python
from horizon.asset_classes import AssetClass
from horizon.types import OrderAction, Urgency

class PerpExecutor:
    asset_class = AssetClass.Perp

    def __init__(self, max_leverage: float = 3.0):
        self.max_leverage = max_leverage

    def plan(self, target, current, market, feed, available_cash):
        if feed is None or feed.price is zero or negative:
            return []

        # Cap notional at max_leverage × available_cash
        max_notional = available_cash * self.max_leverage
        target_notional = max(
            min(target.target_notional_usd, max_notional),
            -max_notional,
        )

        current_qty = current.quantity if current is not None else 0.0
        target_qty = target_notional / feed.price
        delta = target_qty - current_qty

        if abs(delta) < 0.001:
            return []

        side = "buy" if delta > 0 else "sell"
        return [
            OrderAction.place(
                market_id=target.market_id,
                side=side,
                quantity=abs(delta),
                price=feed.mid,
                order_type="limit",
                urgency=target.urgency,
                metadata={
                    "leverage": self.max_leverage,
                    "reduce_only": delta * current_qty < 0,   # reducing position
                },
            )
        ]

Using your executor

python
import horizon as hz
from horizon.asset_classes import Equity

result = hz.run(
    executors={
        Equity: VWAPSliced(n_slices=10),
    },
    ...
)

The dispatcher routes orders to your executor based on market.asset_class.

Guidelines

Return an empty list for no-op Don't raise exceptions for "nothing to do". Return `[]` instead.
Check for null feed If feed is None or feed.price is zero or negative, you can't size. return `[]`.
Respect urgency `Urgency.Immediate` → market; `Urgency.Patient` → limit; `Urgency.Passive` → post-only.
Split flips into two orders Never one order that crosses zero. Always close then open.

Tests

Write a test that mirrors tests/test_executors.py::TestEquityExecutor:

python
def test_new_long():
    ex = MyExecutor()
    target = TargetPosition(market_id="AAPL", target_notional_usd=10_000)
    actions = ex.plan(target, None, _market(), _feed(price=100), 100_000)
    assert len(actions) >= 1
    assert all(a.side == "buy" for a in actions)
    assert sum(a.quantity for a in actions) == 100.0   # summed across slices

Next