Quant Trading Systems: Architecture & Infrastructure › Execution Management System

Michael BrenndoerferJanuary 21, 20266 min read
Quantitative FinanceAll chapters
Part 15 of 15Trading Systems and Production Deployment
Chapter 77Quant Trading Systems: Architecture & Infrastructure › Execution Management System
Reading section 7 of 14 · Execution Management System · 6 minSection 7 of 14 · 6 min

Book contents

Section 7 of 14 · 6 min read

Quant Trading Systems: Architecture & Infrastructure › Execution Management SystemOne section · Sections in this chapter7 / 14 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

The Execution Management System (EMS) turns trading decisions into executed orders. Building on the execution algorithms covered in the previous chapter, it manages order routing, protocol translation, and fill tracking, serving as the bridge between internal trading logic and external exchanges.

A well-designed EMS abstracts away the complexity of individual exchange protocols, allowing your strategy to work with a uniform interface regardless of which venues receive the orders. It also handles the many operational details that arise in real trading: partial fills, order modifications, exchange outages, and communication failures.

FIX Protocol Integration

The Financial Information eXchange (FIX) protocol is the industry standard for electronic trading communication. Understanding FIX is essential for anyone building trading infrastructure. FIX defines a standard message format for communicating orders, executions, and other trading-related information between counterparties.

The protocol uses a tag-value format where each field in a message is identified by a numeric tag. For example, tag 35 identifies the message type, tag 55 identifies the symbol, and tag 54 identifies the order side. This standardization allows systems from different vendors to communicate without custom integration for each pairing.

In[24]:
Code
# Demonstrate FIX message structure (simplified representation)
# Real FIX uses tag=value format with SOH delimiters


class SimpleFIXMessage:
    """Simplified FIX message builder for demonstration."""

    # Common FIX tags
    TAGS = {
        "BeginString": "8",
        "MsgType": "35",
        "SenderCompID": "49",
        "TargetCompID": "56",
        "ClOrdID": "11",
        "Symbol": "55",
        "Side": "54",
        "OrderQty": "38",
        "OrdType": "40",
        "Price": "44",
        "TransactTime": "60",
    }

    # Message types
    MSG_TYPES = {
        "NewOrderSingle": "D",
        "ExecutionReport": "8",
        "OrderCancelRequest": "F",
    }

    def __init__(self, msg_type: str):
        self.fields = {
            "BeginString": "FIX.4.4",
            "MsgType": self.MSG_TYPES.get(msg_type, msg_type),
        }

    def set_field(self, name: str, value: str):
        self.fields[name] = value
        return self

    def to_string(self) -> str:
        """Convert to FIX-like string representation."""
        parts = []
        for name, value in self.fields.items():
            tag = self.TAGS.get(name, name)
            parts.append(f"{tag}={value}")
        return "|".join(parts)


# Build a sample new order message
new_order = SimpleFIXMessage("NewOrderSingle")
new_order.set_field("SenderCompID", "QUANT_FIRM")
new_order.set_field("TargetCompID", "EXCHANGE")
new_order.set_field("ClOrdID", "ORD-2024-001")
new_order.set_field("Symbol", "AAPL")
new_order.set_field("Side", "1")  # 1 = Buy
new_order.set_field("OrderQty", "100")
new_order.set_field("OrdType", "2")  # 2 = Limit
new_order.set_field("Price", "185.50")
Out[25]:
Console
Sample FIX New Order Single Message:
8=FIX.4.4|35=D|49=QUANT_FIRM|56=EXCHANGE|11=ORD-2024-001|55=AAPL|54=1|38=100|40=2|44=185.50

Production systems use FIX libraries like QuickFIX to handle the complexities of session management, message sequencing, and heartbeat monitoring. The message structure above is simplified, but illustrates the tag-value format that defines FIX communication. In the output above, tag 35 (MsgType) is 'D' for New Order Single, tag 54 (Side) is '1' for Buy, and tag 40 (OrdType) is '2' for Limit. Understanding these conventions is essential for debugging order routing issues and integrating with new execution venues.

Order Routing and Smart Order Routing

Smart Order Routing (SOR) determines the optimal destination for each order. In fragmented markets with multiple venues (NYSE, NASDAQ, BATS, IEX, dark pools), routing decisions significantly impact execution quality. The same order routed to different venues can result in different execution prices and fill rates.

The challenge of smart order routing arises from market fragmentation. In the US equity market, the same stock can be traded on over a dozen different venues, each with different prices, liquidity, and fee structures. A naive approach of always routing to the exchange with the best displayed price ignores factors like queue position, fill probability, and maker-taker fee economics.

In[26]:
Code
@dataclass
class VenueQuote:
    """Quote from a specific trading venue."""

    venue_id: str
    bid_price: float
    bid_size: int
    ask_price: float
    ask_size: int
    latency_ms: float  # Expected latency to venue
    fee_per_share: float  # Negative means rebate


class SmartOrderRouter:
    """Simple smart order router for demonstration."""

    def __init__(self, venues: list[str]):
        self.venues = venues
        self.venue_quotes: dict[str, VenueQuote] = {}

    def update_quote(self, quote: VenueQuote):
        """Update quote for a venue."""
        self.venue_quotes[quote.venue_id] = quote

    def route_order(
        self, side: str, quantity: int, strategy: str = "best_price"
    ) -> list[tuple[str, int]]:
        """
        Route order across venues.
        Returns list of (venue_id, quantity) allocations.
        """
        if not self.venue_quotes:
            return []

        if strategy == "best_price":
            return self._route_best_price(side, quantity)
        elif strategy == "minimize_impact":
            return self._route_minimize_impact(side, quantity)
        else:
            raise ValueError(f"Unknown routing strategy: {strategy}")

    def _route_best_price(
        self, side: str, quantity: int
    ) -> list[tuple[str, int]]:
        """Route to venues with best prices, sweeping through order book."""
        allocations = []
        remaining = quantity

        if side == "BUY":
            # Sort venues by ask price (lowest first), then by fee
            sorted_venues = sorted(
                self.venue_quotes.values(),
                key=lambda v: (v.ask_price, v.fee_per_share),
            )

            for venue in sorted_venues:
                if remaining <= 0:
                    break
                fill_qty = min(remaining, venue.ask_size)
                if fill_qty > 0:
                    allocations.append((venue.venue_id, fill_qty))
                    remaining -= fill_qty
        else:
            # Sort by bid price (highest first)
            sorted_venues = sorted(
                self.venue_quotes.values(),
                key=lambda v: (-v.bid_price, v.fee_per_share),
            )

            for venue in sorted_venues:
                if remaining <= 0:
                    break
                fill_qty = min(remaining, venue.bid_size)
                if fill_qty > 0:
                    allocations.append((venue.venue_id, fill_qty))
                    remaining -= fill_qty

        return allocations

    def _route_minimize_impact(
        self, side: str, quantity: int
    ) -> list[tuple[str, int]]:
        """Spread order across venues to minimize market impact."""
        total_liquidity = sum(
            v.ask_size if side == "BUY" else v.bid_size
            for v in self.venue_quotes.values()
        )

        if total_liquidity == 0:
            return []

        allocations = []
        for venue in self.venue_quotes.values():
            venue_liquidity = (
                venue.ask_size if side == "BUY" else venue.bid_size
            )
            proportion = venue_liquidity / total_liquidity
            venue_qty = int(quantity * proportion)
            if venue_qty > 0:
                allocations.append((venue.venue_id, venue_qty))

        return allocations
In[27]:
Code
# Demonstrate smart order routing
router = SmartOrderRouter(["NYSE", "NASDAQ", "BATS", "IEX"])

# Set up venue quotes
quotes = [
    VenueQuote("NYSE", 185.48, 500, 185.50, 300, 0.5, 0.003),
    VenueQuote("NASDAQ", 185.47, 200, 185.49, 400, 0.3, -0.002),  # Rebate
    VenueQuote("BATS", 185.48, 300, 185.50, 200, 0.4, 0.001),
    VenueQuote("IEX", 185.47, 400, 185.51, 600, 0.8, 0.0009),
]

for quote in quotes:
    router.update_quote(quote)

# Route a buy order using different strategies
buy_qty = 500
allocations_best = router.route_order("BUY", buy_qty, "best_price")
allocations_impact = router.route_order("BUY", buy_qty, "minimize_impact")
Out[28]:
Console
Buy 500 shares - Venue Quotes:
------------------------------------------------------------
  NYSE    : 185.48 ( 500) x ( 300) 185.50  fee: $0.0030
  NASDAQ  : 185.47 ( 200) x ( 400) 185.49  fee: $-0.0020
  BATS    : 185.48 ( 300) x ( 200) 185.50  fee: $0.0010
  IEX     : 185.47 ( 400) x ( 600) 185.51  fee: $0.0009

Best Price Routing:
  NASDAQ: 400 shares
  BATS: 100 shares

Minimize Impact Routing:
  NYSE: 100 shares
  NASDAQ: 133 shares
  BATS: 66 shares
  IEX: 200 shares

The best-price strategy fills at NASDAQ first (lowest ask at 185.49), while the minimize-impact strategy spreads the order proportionally across venues based on available liquidity. The best-price approach optimizes for immediate execution cost, while the minimize-impact approach reduces the information leakage that occurs when a large order sweeps all liquidity at a single venue.