EquityExecutor

The default executor for equity markets: limit / market orders with flip handling

EquityExecutor is the production executor for equities. It converts target USD notional to share quantities, picks order type based on urgency, and handles reduce / add / flip scenarios correctly.

Import

python
from horizon.executors import EquityExecutor

Signature

python
EquityExecutor(
    min_shares: float = 1.0,
    allow_fractional: bool = False,
    limit_price_strategy: str = "mid",   # "mid" | "last" | "aggressive"
)
min_sharesfloat
Minimum delta in shares before emitting an order. If the delta is smaller, do nothing. Prevents tiny orders that waste fees.
allow_fractionalbool
When True, allow fractional shares. Most retail brokers don't support this; Alpaca and IBKR (with portfolio accounts) do.
limit_price_strategystr
How to pick the limit price. `"mid"` = midpoint of bid/ask. `"last"` = last trade price. `"aggressive"` = cross the spread.

Logic

python
def plan(self, target, current, market, feed, available_cash):
    # Need a price to size
    if feed is None or feed.price <= 0:
        return []

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

    if not self.allow_fractional:
        target_qty = math.trunc(target_qty)   # round toward zero

    delta = target_qty - current_qty

    if abs(delta) < self.min_shares:
        return []   # no-op

    # Determine order type
    urgency = target.urgency
    order_type = "market" if urgency == Urgency.Immediate else "limit"
    limit_price = self._limit_price(feed) if order_type == "limit" else None

    # Flip detection
    if current_qty != 0 and (current_qty > 0) != (target_qty > 0) and target_qty != 0:
        return [
            OrderAction.place(... close current ...),
            OrderAction.place(... open new ...),
        ]

    side = "buy" if delta > 0 else "sell"
    return [OrderAction.place(...)]

Flip handling

Never issue a single order that closes and re-opens in one shot. A flip from long 100 to short 50 becomes two separate orders:

  1. Sell 100 (closes the long)
  2. Sell 50 (opens the short)

This ensures the ledger sees them as distinct events and attributes P&L correctly.

Reduce vs add vs flat

CurrentTargetAction
100 long100 longNo-op (delta is less than min_shares)
100 long150 longBuy 50
100 long50 longSell 50
100 long0Sell 100 (close)
100 long-50 (short)Sell 100 + sell 50 (flip, two orders)
-100 short100 longBuy 100 + buy 100 (flip)

Limit price strategies

Usage

python
import horizon as hz
from horizon.executors import EquityExecutor
from horizon.asset_classes import Equity

result = hz.run(
    executors={
        Equity: EquityExecutor(
            min_shares=10,
            allow_fractional=False,
            limit_price_strategy="mid",
        ),
    },
    ...
)

When not specified, hz.run() uses EquityExecutor() with defaults.

Tests

14 tests in tests/test_executors.py covering every path:

python
def test_new_long(self) -> None:
    ex = EquityExecutor()
    target = TargetPosition(market_id="AAPL", target_notional_usd=10_000)
    actions = ex.plan(target, None, _market(), _feed(price=100.0), 100_000)
    assert actions[0].side == "buy"
    assert actions[0].quantity == 100.0

def test_flip_long_to_short_issues_two_orders(self) -> None:
    ex = EquityExecutor()
    current = _pos(qty=50.0)
    target = TargetPosition(market_id="AAPL", target_notional_usd=-5_000)
    actions = ex.plan(target, current, _market(), _feed(price=100.0), 100_000)
    assert len(actions) == 2
    assert actions[0].side == "sell"   # close long
    assert actions[1].side == "sell"   # open short

def test_urgency_immediate_uses_market_order(self) -> None:
    ex = EquityExecutor()
    target = TargetPosition(
        market_id="AAPL",
        target_notional_usd=10_000,
        urgency=Urgency.Immediate,
    )
    actions = ex.plan(target, None, _market(), _feed(price=100.0), 100_000)
    assert actions[0].order_type == "market"

Source

horizon/executors/equity.py. ~100 lines.

Next