Back to Research
// Engineering·Mar 10, 2024

Building a Python Execution Engine from Scratch

8 min read · TradeMade Engineering

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.

01
Market Data Handler
WebSocket feed → normalise → push to queue
02
Signal Engine
Consumes ticks, emits MarketEvent on condition
03
Pre-Trade Risk Guard
Position limits, max loss, instrument check
04
Order Management System (OMS)
Creates OrderEvent, assigns UUID, tracks state
05
Execution Handler
Broker API → FillEvent returned to queue
06
Portfolio Updater
Updates positions, P&L, triggers post-trade risk

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.

Python · execution_engine.py

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.

Python · risk_guard.py

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}")
⚠ Common Mistake

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:

BottleneckTypical LatencyFix
Python GIL (single-threaded loop)~50µsasyncio or multiprocessing
REST API to broker80–200msUse WebSocket order stream if available
Market data WebSocket parse1–3msujson over stdlib json
Redis for state (vs DB query)<1msAlways use Redis for hot state
Logging synchronously5–15ms/callAsync 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.

Free Access

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.

🇮🇳 +91

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.