Execution Engine Module
The src/execution/ module converts trading signals into executed orders: position sizing via Half-Kelly criterion, smart order routing (market/limit/TWAP), broker adapter integration (IBKR/PaperStub/Simulated), and execution quality tracking (slippage, latency).
Purpose
The execution engine provides:
- Order generation from signals with Half-Kelly position sizing
- Smart order router selecting order type based on size, spread, and urgency
- Broker adapters for real (IBKR), paper (demo data), and simulated trading
- Execution quality tracker measuring slippage and fill latency
- Order lifecycle management (pending → submitted → filled/cancelled/rejected)
Key Components
Order Generator
OrderGenerator (src/execution/order_generator.py)
Generates orders from trading signals using the Kelly criterion for position sizing.
Kelly Criterion:
Optimal fraction of capital to allocate:
f* = (p * b - q) / b
where:
p = win rate (e.g., 0.55)
b = average win / average loss (e.g., 2.0 / 1.0 = 2.0)
q = 1 - p = loss rate
f* = Kelly fraction (e.g., 0.15 = 15% of capital)
Half-Kelly (Conservative):
position_size = portfolio.cash * f* / 2 / current_price
Half-Kelly reduces risk of ruin from estimation errors.
Order Generation Flow:
- Extract
win_rate,avg_win_pct,avg_loss_pctfrom signal metadata. - Calculate Kelly fraction.
- Calculate quantity:
cash * kelly_frac / 2 / price. - Apply position limits (max 5% of portfolio per position).
- Set order type (MARKET or LIMIT based on config).
- Set stop-loss price (2% below entry for longs, 2% above for shorts).
Usage:
from src.execution.order_generator import OrderGenerator
from src.core.models import Signal, PortfolioSnapshot
generator = OrderGenerator()
signal = Signal(
symbol="AAPL",
direction=SignalDirection.LONG,
strength=0.75,
metadata={"win_rate": 0.55, "avg_win_pct": 2.0, "avg_loss_pct": 1.0}
)
portfolio = PortfolioSnapshot(total_equity=100_000, cash=50_000, ...)
order = generator.generate_order(signal, portfolio)
# order.quantity calculated via Half-Kelly
# order.limit_price set if default_order_type=limit
# order.stop_price set to 2% stop-loss
Configuration (config/settings.yaml):
execution:
default_order_type: "limit" # market | limit
limit_offset_pct: 0.05 # 5 bps offset for limit orders
max_slippage_pct: 0.10 # 10 bps max acceptable slippage
position_sizing: "half_kelly" # half_kelly | fixed | percent_equity
Smart Order Router
SmartOrderRouter (src/execution/router.py)
Selects order type and slicing strategy based on market conditions.
Routing Logic:
| Condition | Order Type | Reasoning |
|---|---|---|
| Signal strength > 0.9 | MARKET | High urgency → accept slippage |
| Spread > 20 bps | LIMIT at mid + offset | Wide spread → passive fill |
| Order size > 1.0% ADV | TWAP (5 slices, 60s interval) | Large order → minimize market impact |
| Order size < 0.1% ADV | LIMIT at best bid/ask | Small order → passive fill |
| Default | LIMIT at bid+offset (buy) or ask-offset (sell) | Standard execution |
Quote Data Source:
- Router reads
bid_price,ask_price,volumefrommarket:quotes:{symbol}Redis channel. - Published by
IBKRMarketFeed(see Data Ingestion Module).
TWAP Slicing:
For large orders (> 1% of average daily volume):
# Split 1000 shares into 5 slices of 200 each
# Submit one slice every 60 seconds
# Reduces market impact vs. single large order
slices = router._create_twap_slices(order, num_slices=5)
# Returns list of 5 Order objects with metadata:
# - twap_slice: 1, 2, 3, 4, 5
# - twap_total_slices: 5
# - twap_interval_seconds: 60
# - parent_order_id: original order ID
Configuration (config/settings.yaml):
router:
size_threshold_small_pct: 0.1 # < 0.1% ADV → passive limit
size_threshold_large_pct: 1.0 # > 1.0% ADV → TWAP
twap_num_slices: 5 # Split large orders into 5 slices
twap_interval_seconds: 60 # 60s between slices
wide_spread_threshold_bps: 20.0 # > 20 bps → use limit at mid
Usage:
from src.execution.router import SmartOrderRouter
router = SmartOrderRouter(feed=market_feed)
orders = await router.route(order)
# Returns list (usually 1 order, but 5 for TWAP)
for routed_order in orders:
await broker.submit_order(routed_order)
if "twap_interval_seconds" in routed_order.metadata:
await asyncio.sleep(routed_order.metadata["twap_interval_seconds"])
Broker Adapters
All broker adapters implement the BrokerAdapter interface (see Core Module).
IBKRBrokerAdapter (src/execution/broker/ibkr_broker.py)
Real trading via IBKR API using ib-async library.
Methods:
submit_order(order: Order) -> Order: Submits order to IB Gateway / TWS. Returns order withbroker_order_idandstatus=SUBMITTED.cancel_order(broker_order_id: str) -> bool: Cancels pending order.get_order_status(broker_order_id: str) -> OrderStatus: Polls order status (PENDING/SUBMITTED/FILLED/CANCELLED/REJECTED).get_positions() -> list[Position]: Fetches current open positions.get_portfolio() -> PortfolioSnapshot: Fetches portfolio summary (equity, cash, positions, P&L).health_check() -> bool: Validates connection to IB Gateway / TWS.
Client ID Scheme:
- Broker adapter:
client_id(default: 1) - Data adapter:
client_id + 1(default: 2) - Market feed:
client_id + 2(default: 3)
Never reuse IDs across connections (IBKR rejects duplicate client IDs).
Configuration:
broker:
provider: "ibkr" # ibkr | simulated | paper_stub
paper_trading: true # true = paper account, false = live
ibkr:
host: "127.0.0.1"
port: 4002 # 4002=Gateway paper, 4001=Gateway live, 7497=TWS paper, 7496=TWS live
client_id: 1
account_id: "" # blank for single-account setups
Order Lifecycle:
submit_order()sends order to IBKR → receivesbroker_order_id(e.g., “12345”).- Order status transitions: PENDING → SUBMITTED → (PARTIALLY_FILLED) → FILLED.
- Fills trigger
order_filledevent → published to Redisexecution:fillschannel. - Execution engine updates order in DB and portfolio snapshot.
PaperStubBroker (src/execution/broker/paper_stub.py)
Demo broker with seed data. Does not execute real orders.
Behavior:
submit_order(): Immediately marks order as FILLED with simulated fill price (current market price + random slippage).get_portfolio(): Returns static demo portfolio (cash=$100K, 3 positions: AAPL, MSFT, GOOG).get_positions(): Returns demo positions with mock P&L.
Used when:
SA_BROKER__PROVIDERis not “ibkr” and not “simulated” (e.g., “demo”, “stub”, or unset).- IBKR connection fails → system falls back to PaperStubBroker.
SimulatedBroker (src/execution/broker/simulated_broker.py)
In-memory simulation for backtesting and testing.
Behavior:
submit_order(): Validates order against available cash, updates in-memory positions, marks as FILLED.get_portfolio(): Returns current simulated portfolio state.- Tracks commission and slippage (0.005% commission, 0.05% slippage).
- Resets state between backtest runs.
Used when:
SA_BROKER__PROVIDER=simulated- Backtesting via
BacktraderEngine(if configured to use SimulatedBroker instead of Backtrader’s internal broker).
Execution Quality Tracker
ExecutionQualityTracker (src/execution/quality.py)
Measures order execution performance.
Metrics:
| Metric | Definition | Formula |
|---|---|---|
slippage_bps | Fill price vs. limit price | (filled_price - limit_price) / limit_price * 10000 |
arrival_slippage_bps | Fill price vs. signal price | (filled_price - signal_price) / signal_price * 10000 |
fill_latency_ms | Time from signal to fill | (fill_timestamp - signal_timestamp).total_seconds() * 1000 |
Storage:
- Metrics logged to
ExecutionQualityMetricsmodel. - Stored in TimescaleDB
execution_qualitytable. - Aggregated daily/weekly for performance reports.
Usage:
from src.execution.quality import ExecutionQualityTracker
tracker = ExecutionQualityTracker()
metrics = tracker.compute_metrics(
order_id="abc123",
symbol="AAPL",
side=OrderSide.BUY,
expected_price=150.00,
filled_price=150.05,
signal_timestamp=datetime(...),
fill_timestamp=datetime(...)
)
# metrics.slippage_bps = 3.33 bps
# metrics.fill_latency_ms = 1250 ms
await tracker.store_metrics(metrics)
Dashboard Integration:
ExecutionQualityChartcomponent displays average slippage and latency over time.- Alerts triggered if slippage > 10 bps or latency > 5 seconds.
Data Flow
- Signal Generation:
- Strategy (or MLSignalStrategy) generates
Signalobjects. - Execution engine receives signals via
execution:signalsRedis channel.
- Strategy (or MLSignalStrategy) generates
- Order Generation:
OrderGenerator.generate_order()converts signal → order with Kelly sizing.- Order includes metadata:
signal_strength,strategy_name,signal_timestamp.
- Pre-Trade Risk Checks:
RiskManager.check_pre_trade()validates order against position/portfolio limits, PDT guard, circuit breakers.- If rejected, order status set to REJECTED, logged, and not submitted.
- Smart Routing:
SmartOrderRouter.route()selects order type (market/limit/TWAP).- Reads real-time quotes from Redis
market:quotes:{symbol}channel. - Returns list of orders (1 for standard, 5 for TWAP).
- Order Submission:
BrokerAdapter.submit_order()sends order to broker (IBKR, PaperStub, or Simulated).- Order status transitions to SUBMITTED.
- Broker order ID stored in
order.broker_order_id.
- Fill Notification:
- Broker publishes fill event to Redis
execution:fillschannel. - Execution engine updates order status to FILLED.
- Portfolio snapshot recalculated.
- Broker publishes fill event to Redis
- Quality Tracking:
ExecutionQualityTracker.compute_metrics()calculates slippage and latency.- Metrics stored in TimescaleDB.
- Alerts triggered if thresholds exceeded.
Configuration
Settings (config/settings.yaml):
broker:
provider: "ibkr" # ibkr | simulated | paper_stub
paper_trading: true
ibkr:
host: "127.0.0.1"
port: 4002
client_id: 1
account_id: ""
execution:
default_order_type: "limit"
limit_offset_pct: 0.05 # 5 bps
max_slippage_pct: 0.10 # 10 bps
position_sizing: "half_kelly"
router:
size_threshold_small_pct: 0.1
size_threshold_large_pct: 1.0
twap_num_slices: 5
twap_interval_seconds: 60
wide_spread_threshold_bps: 20.0
Environment Variable Overrides:
# Switch to live IBKR
export SA_BROKER__PROVIDER=ibkr
export SA_BROKER__PAPER_TRADING=false
export SA_BROKER__IBKR__PORT=4001 # Gateway live
# Use simulated broker for testing
export SA_BROKER__PROVIDER=simulated
# Adjust router thresholds
export SA_ROUTER__SIZE_THRESHOLD_LARGE_PCT=0.5
export SA_ROUTER__TWAP_NUM_SLICES=10
Integration with Other Modules
- Strategy Engine (
src/strategy/): Strategies generate signals → execution engine. - Risk Management (
src/risk/): Pre-trade checks gate all orders before submission. - Data Ingestion (
src/data/): Market feed provides real-time quotes for router. - Scheduler (
src/scheduling/): TWAP slices executed at scheduled intervals. - API (
src/api/routes/trades.py): Endpoints for order history, execution metrics. - Dashboard (
web/src/pages/Trades.tsx): Displays orders, fills, execution quality.
Critical Patterns
- Half-Kelly sizing: Conservative position sizing reduces risk of ruin.
- Smart routing: Order type selection minimizes slippage and market impact.
- TWAP for large orders: Splits orders > 1% ADV into 5 slices over 5 minutes.
- Client ID isolation: Broker (+0), data (+1), feed (+2) use separate IDs.
- Execution quality tracking: All fills measured for slippage and latency.
- Broker fallback: If IBKR connection fails, system falls back to PaperStubBroker.
- Order lifecycle: Pending → Submitted → Filled/Cancelled/Rejected (never directly to Filled without Submitted).
Glossary Links
- IBKR — Interactive Brokers
- PDT — Pattern Day Trader rule
- OHLCV — Open/High/Low/Close/Volume bar data
- Redis — In-memory data store
- Kelly Criterion — Optimal position sizing formula