Building a Python Execution Engine from Scratch
Most quants write strategies. Far fewer build the thing that actually runs them in production. An execution engine is the unglamorous piece between your signal and the exchange — and if it's badly designed, your strategy will leak money quietly until it's dead.
This is what we learned building one. We'll skip the hello-world abstractions and go straight to what actually matters in production.
The Architecture First. Always.
The single biggest mistake: building a request-response execution engine. A function that gets a signal, calls the broker API, waits for a fill, then continues. This blocks everything. One slow API response stalls your entire system during the most volatile moments — exactly when you need it to move fastest.
The correct architecture is event-driven. Every component — market data, signal generation, order management, risk checks — runs independently and communicates through an event queue. Nothing blocks anything.
The Event Loop Core
Here's the skeleton in Python. Every component subscribes to event types. The loop is a single thread processing one event at a time — deceptively simple, practically powerful.
from queue import Queue, Empty from enum import Enum import uuid, time class EventType(Enum): MARKET = "MARKET" SIGNAL = "SIGNAL" ORDER = "ORDER" FILL = "FILL" RISK_REJ = "RISK_REJECTED" class OrderEvent: def __init__(self, symbol, direction, qty, order_type="MKT"): self.id = str(uuid.uuid4()) self.symbol = symbol self.direction = direction # "BUY" | "SELL" self.qty = qty self.type = order_type self.status = "PENDING" self.ts = time.time_ns() # nanosecond precision class ExecutionEngine: def __init__(self, broker, risk_guard): self.queue = Queue() self.broker = broker self.risk_guard = risk_guard self.portfolio = {} def run(self): while True: try: event = self.queue.get(timeout=0.001) self._route(event) except Empty: continue def _route(self, event): handlers = { EventType.SIGNAL : self._on_signal, EventType.ORDER : self._on_order, EventType.FILL : self._on_fill, } handlers.get(event.type, lambda e: None)(event)
The Risk Guard: Non-Negotiable
Risk checks must happen before every order, synchronously, in microseconds. Not as an afterthought. The two checks that prevent most catastrophic losses are a daily max-loss circuit breaker and a position size hard limit.
class RiskGuard: def __init__(self, max_position=5000, daily_loss_limit=-25000): self.max_position = max_position self.daily_loss_lim = daily_loss_limit self.daily_pnl = 0.0 self.positions = {} def approve(self, order) -> bool: # Circuit breaker: halt all trading if loss limit hit if self.daily_pnl <= self.daily_loss_lim: self._alert(f"HALT: Daily loss {self.daily_pnl:.0f} ≤ limit") return False # Position limit check current = self.positions.get(order.symbol, 0) if abs(current + order.qty) > self.max_position: return False return True def _alert(self, msg): # Push to Telegram / log / SMS print(f"[RISK] {msg}")
Never check risk only at strategy level. If you have multiple strategies running, each one thinks the others don't exist. Risk must be enforced at the engine level, against the total portfolio position — not per strategy.
Latency: Where You Actually Lose
Python is not C++. But for most Indian retail algo strategies targeting 5-minute to daily signals, the bottleneck isn't Python's execution speed — it's network round-trip time to the broker. Here's what matters:
| Bottleneck | Typical Latency | Fix |
|---|---|---|
| Python GIL (single-threaded loop) | ~50µs | asyncio or multiprocessing |
| REST API to broker | 80–200ms | Use WebSocket order stream if available |
| Market data WebSocket parse | 1–3ms | ujson over stdlib json |
| Redis for state (vs DB query) | <1ms | Always use Redis for hot state |
| Logging synchronously | 5–15ms/call | Async logging queue — never log inline |
"Synchronous logging inside your hot path will cost you more latency than Python vs. C++ ever will."
Order State Machine: Don't Skip This
Every order must live inside a state machine. PENDING → SUBMITTED → PARTIALLY_FILLED → FILLED (or REJECTED or CANCELLED). If you're storing order state in a mutable dict without transitions, you will eventually process a fill for an order that was already cancelled — and your positions will be wrong from that moment on.
Use UUIDs for order IDs, not incrementing integers. Implement idempotency: if the broker sends the same fill event twice (it happens), your engine must recognise it and skip — not double-count the fill. Store every state transition with a nanosecond timestamp. When something goes wrong in live trading, this log is the only way to reconstruct exactly what happened.
What This Actually Costs to Build Right
A production-grade execution engine — event loop, OMS, risk guard, broker integration, state machine, Telegram alerts, async logging — takes 6–10 weeks to build correctly if you've done it before. It takes much longer if you haven't. And you'll rebuild large parts of it the first time something breaks in live trading that your paper trading never surfaced.
That's not a knock on building it yourself. It's just the honest accounting.
Test Your Strategy on Tick-Level Data — Free
TradeMade's backtesting engine runs on 10+ years of tick data with realistic slippage, brokerage, options Greeks, walk-forward optimisation, and Monte Carlo simulation. Drop your number — we'll set you up.
No spam. No cold calls. Just access.
The execution engine is the least exciting part of a trading system and the most important one. Get the architecture right before you write a single line of strategy logic. The event queue, the risk guard, the order state machine — these aren't optional features you add later. They're the foundation everything else runs on.