Execution Overview
Per-asset-class translation of target positions to order actions
The execution layer is the only layer where asset-class branching is legitimate. Each executor knows how to convert a USD target notional into native orders for its specific asset class. shares for equity, contracts for options, yes-shares for prediction markets, leverage-aware size for perps.
Available executors
Executor protocol
from typing import Protocol, runtime_checkable
@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]:
...
Every executor implements exactly this method. It takes a target in USD notional, the current position, and market info, and returns a list of concrete OrderActions.
Dispatcher
from horizon.executors import ExecutorDispatcher, EquityExecutor
dispatcher = ExecutorDispatcher()
dispatcher.register(EquityExecutor())
# dispatcher.register(OptionExecutor()) # etc.
# At runtime
actions = dispatcher.plan(target, current, market, feed, cash)
The dispatcher routes per-market by inspecting Market.asset_class and calling the executor registered for that class.
The default wiring
Horizon’s hz.run() auto-registers EquityExecutor for the Equity asset class. For other asset classes you’d register them explicitly:
hz.run(
executors={
Equity: EquityExecutor(),
# Option: OptionExecutor(), # future
# Prediction: PredictionExecutor(),
},
...
)
What an executor decides
Tests
tests/test_executors.py has 14 tests covering every path of EquityExecutor:
- New long / new short
- Reduce existing
- Add to existing
- Flip long → short (two orders)
- Flat target closes existing
- No-op when already at target
- Below-min-shares no-op
- Market vs limit selection
- NaN feed → no action
- Fractional shares
All green.