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_sharesfloatMinimum delta in shares before emitting an order. If the delta is smaller, do nothing. Prevents tiny orders that waste fees.
allow_fractionalboolWhen True, allow fractional shares. Most retail brokers don't support this; Alpaca and IBKR (with portfolio accounts) do.
limit_price_strategystrHow 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:
- Sell 100 (closes the long)
- Sell 50 (opens the short)
This ensures the ledger sees them as distinct events and attributes P&L correctly.
Reduce vs add vs flat
| Current | Target | Action |
|---|---|---|
| 100 long | 100 long | No-op (delta is less than min_shares) |
| 100 long | 150 long | Buy 50 |
| 100 long | 50 long | Sell 50 |
| 100 long | 0 | Sell 100 (close) |
| 100 long | -50 (short) | Sell 100 + sell 50 (flip, two orders) |
| -100 short | 100 long | Buy 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.