system-design48 min read

How to Design a Stock Trading Platform — A Senior+ Guide | Ayodhyya

How to Design a Stock Trading Platform

Building order matching engines, real-time market data, and portfolio management for 10M+ concurrent traders

Ayodhyya • July 14, 2026 • 15 min read • System Design

1. Introduction

The global stock trading ecosystem is one of the most latency-sensitive, high-throughput distributed systems ever engineered. The New York Stock Exchange (NYSE) alone processes roughly 2 to 5 billion messages per day during regular trading hours, while Nasdaq handles upwards of 30 billion events daily across all its venues. Every single millisecond of delay in an order matching engine can translate to millions of dollars in lost opportunity or adverse execution for millions of retail and institutional traders worldwide.

A modern stock trading platform is not merely a web application with a database. It is a deeply layered, event-driven system that must simultaneously handle real-time order ingestion from millions of concurrent users, match buy and sell orders with sub-microsecond latency, stream live market data to every connected client, enforce complex risk checks before each order executes, manage portfolio positions and calculate profit and loss in real-time, and settle trades accurately with clearinghouses under strict regulatory deadlines.

Market microstructure — the study of how exchanges facilitate trading and price discovery — reveals that the modern order book is a living, breathing data structure. At any given instant, thousands of limit orders may be resting at various price levels on both the bid and ask sides. When a new market order arrives, the matching engine must scan the book, identify the best available price, execute the trade, update the book, and broadcast the new state to all market participants — all within microseconds.

In this comprehensive system design guide, we will architect a full-featured stock trading platform capable of supporting 10 million+ concurrent traders, processing 500,000+ orders per second, streaming real-time market data with sub-10ms latency, and maintaining 99.999% uptime during market hours. We will cover everything from the matching engine core to settlement and clearing, from WebSocket streaming to multi-region co-location strategies.

"In electronic markets, the speed of light is not just a physics concept — it is an economic constraint that shapes the entire architecture of global finance." — Michael Lewis, Flash Boys
500K+
Orders Per Second
10M+
Concurrent Traders
<10μs
Matching Latency
99.999%
Uptime SLA
30B+
Daily Events
T+1
Settlement Cycle

2. Functional & Non-Functional Requirements

Functional Requirements

Before diving into architecture, we must precisely define what the system needs to do. Every feature of a trading platform ultimately maps to one of these functional domains.

Order Management

  • Users can place buy and sell orders for equities, ETFs, and other listed securities across multiple exchanges.
  • Support for market orders, limit orders, stop-loss orders, stop-limit orders, OCO (One-Cancels-Other), iceberg orders, and trailing stop orders.
  • Orders can be modified (price/quantity adjustment) and cancelled before full execution.
  • Real-time order status tracking: pending, open, partially filled, filled, cancelled, rejected.
  • Order history with full audit trail for regulatory compliance and user review.

Matching Engine

  • Continuous auction matching using price-time priority (FIFO within each price level).
  • Support for partial fills when order quantity exceeds available liquidity at the best price.
  • Cross prevention: prevent an order from matching against orders placed by the same account.
  • Sub-microsecond matching latency for marketable orders.
  • Deterministic order processing — identical inputs must produce identical outputs.

Market Data

  • Real-time Level 1 quotes: last price, bid, ask, spread, volume.
  • Real-time Level 2 order book: full depth of bid and ask price levels with quantities.
  • Trade feed: every executed trade with price, quantity, timestamp, and trade flag (buy/sell).
  • OHLCV candlestick data: 1-minute, 5-minute, 15-minute, 1-hour, daily candles.
  • Historical market data: tick-by-tick data, daily bars, corporate actions, dividends, and splits.

Portfolio Management

  • Real-time position tracking: shares held, average cost basis, unrealized P&L.
  • Cash balance management: available cash, margin requirements, buying power calculation.
  • Dividend tracking and reinvestment options.
  • Portfolio performance analytics: daily P&L, total return, Sharpe ratio, sector allocation.
  • Watchlists with real-time price alerts and notifications.

Risk Management

  • Pre-trade risk checks: margin validation, position limits, notional value limits.
  • Real-time circuit breakers: per-security and portfolio-wide risk thresholds.
  • Auto-liquidation triggers when margin requirements are breached.
  • Max order size and max order frequency limits per account.
  • Market abuse detection: wash trading, layering, spoofing pattern recognition.

Non-Functional Requirements

RequirementTargetRationale
Latency (Order to Ack)< 1msCompetitive execution requires sub-millisecond round-trip
Throughput500K orders/secPeak market open volumes across all symbols
Availability99.999%Market hours downtime costs millions per minute
Data Durability99.999999999%Trade records are legally irreplaceable
ConsistencyStrong (ACID)Financial data cannot tolerate eventual consistency
Read Latency (Market Data)< 50msUsers need real-time price updates
Message Delivery< 10msWebSocket market data must arrive in real-time
Recovery Time< 30 secondsAutomated failover during market hours
ComplianceSEC/FINRA/KYCRegulatory mandates for all US broker-dealers
SecuritySOC2 Type IIInstitutional client requirement

3. Capacity Estimation

Capacity estimation is the foundation of every system design. Without precise numbers, we cannot size infrastructure, choose databases, or design data partitioning strategies. Let us work through the math for a platform targeting 10 million concurrent users.

Order Volume

Assumptions:

  • 10 million concurrent traders during peak hours
  • Average active trader places 5 orders per hour
  • Peak-to-average ratio: 3x during market open (9:30-10:00 AM ET)

Calculations:

  • Average orders/sec = (10M × 5) / 3600 ≈ 13,900 orders/sec
  • Peak orders/sec = 13,900 × 3 ≈ 41,700 orders/sec
  • With cancellations and modifications: ~50,000 messages/sec peak
  • Daily order volume ≈ 500 million messages

Market Data Volume

Assumptions:

  • 10,000 actively traded symbols on NYSE + Nasdaq
  • Average 50 price updates per symbol per second (Level 1)
  • Level 2 order book: 20 price levels × 10 updates/sec per symbol
  • Trade prints: ~20 trades per symbol per second

Calculations:

  • Level 1 updates: 10,000 × 50 = 500,000 updates/sec
  • Level 2 updates: 10,000 × 20 × 10 = 2,000,000 updates/sec
  • Trade prints: 10,000 × 20 = 200,000 trades/sec
  • Total market data messages: ~2.7 million/sec
  • Bandwidth: ~2.7M × 200 bytes ≈ 540 MB/sec (4.3 Gbps)

Portfolio Reads

Assumptions:

  • Each user checks portfolio P&L every 30 seconds on average
  • 10M users × 2 reads/sec (avg) = 20 million reads/sec
  • Each portfolio read requires: positions + market prices + cash balance

This massive read volume dictates heavy caching and in-memory data structures.

4. Data Model

The data model must represent the complete lifecycle of trading: from user accounts and KYC verification, through order placement and matching, to trade execution and settlement. We will use a hybrid approach combining relational databases for transactional data and time-series databases for market data.

Core Entities

EntityKey FieldsStorage
Useruser_id, email, name, kyc_status, account_type, created_atPostgreSQL
Accountaccount_id, user_id, cash_balance, margin_used, buying_power, statusPostgreSQL
Orderorder_id, account_id, symbol, side, type, qty, price, status, timestampsPostgreSQL + TimescaleDB
Tradetrade_id, buy_order_id, sell_order_id, symbol, price, qty, timestampTimescaleDB
Positionaccount_id, symbol, qty, avg_cost, realized_pnl, unrealized_pnlPostgreSQL + Redis
OrderBooksymbol, price_level, side, orders (FIFO queue), total_qtyIn-Memory (C#)
MarketDatasymbol, timestamp, bid, ask, last, volume, open, high, low, closeInfluxDB / TimescaleDB
Candlesymbol, interval, open, high, low, close, volume, timestampTimescaleDB
Dividendsymbol, ex_date, record_date, payment_date, amount_per_sharePostgreSQL
Settlementtrade_id, settlement_date, status, delivered_qty, received_qtyPostgreSQL

Entity Relationship

erDiagram USER ||--o{ ACCOUNT : has ACCOUNT ||--o{ ORDER : places ACCOUNT ||--o{ POSITION : holds ORDER ||--o{ TRADE : executes TRADE }o--|| ORDER : buy_order TRADE }o--|| ORDER : sell_order SYMBOL ||--o{ ORDER : trades SYMBOL ||--o{ POSITION : tracks SYMBOL ||--o{ MARKET_DATA : quotes SYMBOL ||--o{ CANDLE : candles ACCOUNT ||--o{ SETTLEMENT : settles TRADE ||--o{ SETTLEMENT : creates USER { string user_id PK string email string full_name enum kyc_status datetime created_at } ACCOUNT { string account_id PK string user_id FK decimal cash_balance decimal margin_used decimal buying_power enum status } ORDER { string order_id PK string account_id FK string symbol FK enum side enum order_type decimal quantity decimal price enum status datetime created_at } TRADE { string trade_id PK string buy_order_id FK string sell_order_id FK string symbol decimal price decimal quantity datetime executed_at } POSITION { string account_id FK string symbol FK decimal quantity decimal avg_cost_basis decimal realized_pnl decimal unrealized_pnl }

5. API Design

The API layer must handle both synchronous REST requests (order placement, portfolio queries) and asynchronous streaming (WebSocket market data, order status updates). We design the API with idempotency, rate limiting, and strict input validation.

REST API Endpoints

MethodEndpointDescriptionRate Limit
POST/api/v1/ordersPlace a new order100/sec per user
PUT/api/v1/orders/{id}Modify an existing order100/sec per user
DELETE/api/v1/orders/{id}Cancel an order100/sec per user
GET/api/v1/ordersList open orders50/sec per user
GET/api/v1/orders/{id}Get order details50/sec per user
GET/api/v1/quotes/{symbol}Get current quote (L1)200/sec per user
GET/api/v1/orderbook/{symbol}Get order book (L2)100/sec per user
GET/api/v1/portfolioGet portfolio summary20/sec per user
GET/api/v1/portfolio/positionsGet all positions20/sec per user
GET/api/v1/tradesGet trade history10/sec per user
GET/api/v1/candles/{symbol}Get OHLCV candles50/sec per user

Place Order Request

POST /api/v1/orders
Authorization: Bearer <jwt_token>
Content-Type: application/json
Idempotency-Key: <uuid-v4>

{
    "symbol": "AAPL",
    "side": "BUY",
    "type": "LIMIT",
    "quantity": 100,
    "price": 185.50,
    "time_in_force": "DAY",
    "extended_hours": false
}

Place Order Response

{
    "order_id": "ord_8f14e45f-ceea-462f-a3e8-c2b1d2e3f456",
    "status": "OPEN",
    "symbol": "AAPL",
    "side": "BUY",
    "type": "LIMIT",
    "quantity": 100,
    "filled_quantity": 0,
    "price": 185.50,
    "created_at": "2026-07-14T09:30:01.234Z",
    "updated_at": "2026-07-14T09:30:01.234Z"
}

WebSocket Market Data Stream

// Connect to wss://stream.ayodhyya.com/market-data
// Subscribe to symbols
{
    "type": "subscribe",
    "channels": ["quotes", "trades", "orderbook"],
    "symbols": ["AAPL", "MSFT", "GOOGL", "TSLA"]
}

// Server pushes quote updates
{
    "type": "quote",
    "symbol": "AAPL",
    "bid": 185.48,
    "ask": 185.52,
    "bid_size": 500,
    "ask_size": 300,
    "last": 185.50,
    "volume": 12450000,
    "timestamp": "2026-07-14T09:30:01.500Z"
}

// Server pushes trade prints
{
    "type": "trade",
    "symbol": "AAPL",
    "price": 185.50,
    "quantity": 200,
    "side": "BUY",
    "trade_id": "trd_a1b2c3d4",
    "timestamp": "2026-07-14T09:30:01.501Z"
}

6. High-Level Architecture

The architecture follows an event-driven microservices pattern with clear separation between the hot path (order matching, market data) and the warm path (portfolio, settlement, analytics). The matching engine is a single-writer, lock-free process per symbol to guarantee deterministic execution order.

graph TB subgraph "Client Layer" WEB[Web App] MOB[Mobile App] API_CLIENT[API Clients] ALGO[Algo Traders] end subgraph "Edge & Gateway" CDN[CDN / WAF] LB[Load Balancer] GATEWAY[Trading Gateway] WS_GW[WebSocket Gateway] end subgraph "Core Trading Engine" ORDER_SVC[Order Service] MATCHING[Matching Engine] RISK[Risk Engine] ORDER_BOOK[In-Memory Order Book] end subgraph "Market Data" MD_INGEST[Market Data Ingest] MD_BROADCAST[Market Data Broadcast] MD_STORE[Market Data Store] CANDLE_SVC[Candle Service] end subgraph "Portfolio & Account" PORTFOLIO[Portfolio Service] POSITION[Position Service] CASH[Cash Management] end subgraph "Settlement & Compliance" SETTLE[Settlement Service] COMPLIANCE[Compliance Engine] AUDIT[Audit Service] end subgraph "Data Stores" PG[(PostgreSQL)] REDIS[(Redis)] TS[(TimescaleDB)] KAFKA[(Kafka)] S3[(S3 Storage)] end WEB & MOB & API_CLIENT & ALGO --> CDN --> LB --> GATEWAY & WS_GW GATEWAY --> ORDER_SVC --> MATCHING MATCHING --> ORDER_BOOK MATCHING --> RISK MATCHING --> KAFKA KAFKA --> PORTFOLIO & SETTLE & COMPLIANCE & AUDIT KAFKA --> MD_INGEST --> MD_BROADCAST --> WS_GW MD_INGEST --> MD_STORE --> CANDLE_SVC PORTFOLIO --> POSITION --> CASH PORTFOLIO --> PG MATCHING --> REDIS MD_STORE --> TS PORTFOLIO --> REDIS MATCHING --> PG

Key Architecture Decisions

Single-Writer Matching Engine: Each symbol has exactly one matching engine instance. This eliminates lock contention entirely. Orders for the same symbol are routed to the same engine instance via Kafka partitioning by symbol hash.

Event Sourcing for Trades: Every trade is an immutable event in Kafka. Downstream services (portfolio, settlement, compliance) consume the trade stream independently, enabling replay and recovery without impacting the matching engine.

Separation of Read and Write Paths: Order placement (write) goes through the matching engine. Portfolio reads and market data queries go through read replicas and Redis caches, achieving massive read scalability.

7. Order Types

Modern trading platforms support a rich vocabulary of order types, each designed for specific trading strategies and risk management needs. The matching engine must correctly handle each type's unique execution logic.

Order TypeBehaviorUse CaseComplexity
MarketExecute immediately at best available priceUrgent execution, day tradersEasy
LimitExecute only at specified price or betterPrice-sensitive executionEasy
Stop-LossBecomes market order when stop price is hitDownside protectionMedium
Stop-LimitBecomes limit order when stop price is hitControlled exit with price floorMedium
OCOOne-Cancels-Other: two linked orders, first to execute cancels the otherBracket strategiesMedium
IcebergShows only a portion of total quantityLarge institutional ordersHard
Trailing StopStop price tracks market price by a fixed amount or percentageLocking in profits on trending stocksHard

Limit Order Logic

A limit buy order at price P will execute only at price P or lower. A limit sell order at price P will execute only at price P or higher. If the order cannot be immediately filled, it is placed in the order book at its limit price, resting until a matching order arrives or it is cancelled. This is the bread and butter of electronic trading, and its implementation in the matching engine must be absolutely correct and performant.

Stop-Loss Order Flow

stateDiagram-v2 [*] --> Resting : Stop-Loss Order Placed Resting --> Triggered : Market Price Hits Stop Triggered --> MarketOrder : Converted to Market Order Triggered --> LimitOrder : Converted to Limit Order (Stop-Limit) MarketOrder --> Filled : Best Available Price MarketOrder --> PartialFill : Insufficient Liquidity PartialFill --> Filled : Remaining Shares Fill LimitOrder --> RestingLimit : Placed in Book RestingLimit --> Filled : Limit Price Reached Resting --> Cancelled : User Cancels

8. Order Book Data Structure

The order book is the heart of the matching engine. It is a real-time, in-memory data structure that maintains all resting (unfilled) orders for a single symbol, organized by price level and time priority. The design must support O(1) insertion, O(1) removal, and O(1) access to the best bid and best ask.

Price-Time Priority

Price priority means orders with better prices are matched first. For bids (buy orders), higher prices have priority. For asks (sell orders), lower prices have priority. Time priority means that within the same price level, orders are matched in FIFO (first-in, first-out) order — the earliest order at a given price gets filled first.

graph LR subgraph "BID Side (Buy Orders)" B3["99.97 | 500 shares | 10:01:02"] B2["99.98 | 1,200 shares | 10:00:55"] B1["100.00 | 800 shares | 10:00:30"] end subgraph "ASK Side (Sell Orders)" A1["100.02 | 600 shares | 10:00:35"] A2["100.05 | 900 shares | 10:00:42"] A3["100.08 | 1,500 shares | 10:01:10"] end B1 -.->|"Best Bid: 100.00"| B1 A1 -.->|"Best Ask: 100.02"| A1 style B1 fill:#d1fae5,stroke:#065f46 style A1 fill:#fee2e2,stroke:#991b1b

Order Book Implementation Data Structure

We implement the order book using a SortedDictionary (or a red-black tree) keyed by price, where each price level contains a Queue<Order> maintaining time priority. This gives us O(log n) insertion and removal at any price level, O(1) access to the best bid/ask via the first/last key, and O(1) FIFO dequeue within a price level.

// Simplified Order Book Structure
public class OrderBook
{
    // Bids: sorted descending (best bid first)
    private SortedDictionary<decimal, Queue<Order>> _bids;

    // Asks: sorted ascending (best ask first)
    private SortedDictionary<decimal, Queue<Order>> _asks;

    // Quick access to best prices
    public decimal BestBid => _bids.Keys.FirstOrDefault();
    public decimal BestAsk => _asks.Keys.FirstOrDefault();
    public decimal Spread => BestAsk - BestBid;

    // Price levels with total visible quantity
    public Dictionary<decimal, long> _bidDepth;
    public Dictionary<decimal, long> _askDepth;
}

9. Matching Engine

The matching engine is the most critical component of any trading platform. It is responsible for receiving orders, validating them, matching them against resting orders in the order book, generating trades, and updating the order book state. It must be deterministic, fast, and correct — a single bug in the matching engine can cost millions and trigger regulatory action.

Matching Algorithm

flowchart TD A[New Order Arrives] --> B{Is it a Market Order?} B -->|Yes| C[Scan Best Ask/Best Bid] B -->|No| D{Is it Marketable?} D -->|Yes| E[Match at Best Available Price] D -->|No| F[Insert into Order Book] E --> G{Quantity Remaining?} G -->|Yes, Partial Fill| H[Generate Trade, Continue Matching] G -->|No, Fully Filled| I[Generate Trade, Done] H --> C F --> J[Rest at Limit Price] I --> K[Broadcast Trade Event] J --> L[Broadcast Book Update] K --> M[Update Position & Cash] L --> N[Push to Market Data Feed] style A fill:#e6f3ff,stroke:#0088ff style K fill:#d1fae5,stroke:#065f46 style F fill:#fef3c7,stroke:#92400e

Matching Engine Properties

Deterministic Execution: Given the same sequence of orders, the matching engine must produce the same sequence of trades every time. This is essential for regulatory audits and replay-based recovery. We achieve determinism by processing orders sequentially within a single thread per symbol.

Partial Fills: When a marketable order arrives with quantity Q, and the best price level has remaining quantity R where R < Q, the engine generates a trade for R shares, decrements the resting order, and continues scanning to the next price level for the remaining Q - R shares. This cascading fill logic continues until the incoming order is fully filled or no more liquidity exists.

Cross Prevention: Before matching, the engine checks that the incoming order's account is not the same as the resting order's account. Self-trading is typically prohibited. The engine can either reject the crossing order or skip the resting order and continue matching.

10. Market Data Pipeline

The market data pipeline is responsible for ingesting raw exchange data feeds, normalizing them, computing derived metrics (candles, VWAP, moving averages), and broadcasting them to all connected clients. It must handle millions of updates per second with minimal latency.

flowchart LR EXCHANGE["Exchange Feeds\n(NYSE, Nasdaq, ARCA)"] --> FD["Feed Handlers\n(Protocol Parsers)"] FD --> NORM["Normalization\nLayer"] NORM --> ENRICH["Enrichment\n(VWAP, Imbalance)"] ENRICH --> BROADCAST["Broadcast\nEngine"] BROADCAST --> WS["WebSocket\nClients"] BROADCAST --> MULTICAST["Multicast\n(Institutional)"] ENRICH --> KAFKA["Kafka\nEvent Stream"] KAFKA --> STORE["TimescaleDB\nStorage"] KAFKA --> CANDLE["Candle\nAggregator"] CANDLE --> TS["TimescaleDB\nCandles"] CANDLE --> CACHE["Redis\nCache"] style EXCHANGE fill:#fef3c7,stroke:#92400e style BROADCAST fill:#d1fae5,stroke:#065f46 style STORE fill:#e6f3ff,stroke:#0088ff

Market Data Tiers

TierDataUpdate RateDelivery
Level 1Bid/Ask/Last/Volume50 updates/sec per symbolWebSocket, REST
Level 2Top 10-20 price levels500 updates/sec per symbolWebSocket, Multicast
Level 3Full order book (all levels)2,000+ updates/sec per symbolMulticast, Co-location
Time & SalesEvery trade print20+ trades/sec per symbolWebSocket, Multicast

11. Real-Time Price Streaming

Real-time price streaming is the mechanism by which market data reaches the end user's screen. For retail traders, this typically means WebSocket connections. For institutional clients and high-frequency traders, this means multicast UDP feeds and co-location within the exchange's data center.

WebSocket Architecture

sequenceDiagram participant Client participant WS_GW as WebSocket Gateway participant SUB as Subscription Manager participant MD as Market Data Engine participant BOOK as Order Book Client->>WS_GW: Connect + Auth WS_GW->>WS_GW: Validate JWT WS_GW->>SUB: Register Connection Client->>SUB: Subscribe to AAPL, MSFT SUB->>SUB: Add to Fanout Map loop Every Price Update BOOK->>MD: Price Change Event MD->>SUB: Fanout to Subscribers SUB->>WS_GW: Send to Relevant Clients WS_GW->>Client: {"type":"quote","symbol":"AAPL",...} end Client->>SUB: Unsubscribe from MSFT SUB->>SUB: Remove from Fanout Map

Performance Considerations

Throttling: To prevent overwhelming slow clients, the WebSocket gateway implements adaptive throttling. If a client's receive buffer fills up, the gateway begins dropping stale quotes and sends only the latest price level. This ensures that every message a client receives reflects the most current market state, rather than a queue of increasingly outdated prices.

12. Risk Management Engine

The risk management engine is the guardian of the trading platform. It runs pre-trade risk checks before every order is accepted by the matching engine, and real-time position monitoring throughout the trading session. Its job is to prevent catastrophic losses, enforce regulatory limits, and protect both the broker and the client from adverse scenarios.

Pre-Trade Risk Checks

flowchart TD ORDER[Order Arrives] --> MARGIN{Margin Check} MARGIN -->|Pass| POS_LIMIT{Position Limit} MARGIN -->|Fail| REJECT_M[Reject: Insufficient Margin] POS_LIMIT -->|Pass| NOTIONAL{Notional Limit} POS_LIMIT -->|Fail| REJECT_P[Reject: Position Limit Exceeded] NOTIONAL -->|Pass| FREQ{Frequency Check} NOTIONAL -->|Fail| REJECT_N[Reject: Notional Too Large] FREQ -->|Pass| PRICE{Price Band Check} FREQ -->|Fail| REJECT_F[Reject: Too Many Orders] PRICE -->|Pass| APPROVE[Approve: Forward to Matching Engine] PRICE -->|Fail| REJECT_PR[Reject: Price Outside Bands] style APPROVE fill:#d1fae5,stroke:#065f46 style REJECT_M fill:#fee2e2,stroke:#991b1b style REJECT_P fill:#fee2e2,stroke:#991b1b style REJECT_N fill:#fee2e2,stroke:#991b1b style REJECT_F fill:#fee2e2,stroke:#991b1b style REJECT_PR fill:#fee2e2,stroke:#991b1b

Risk Parameters

CheckDefault LimitConfigurable?
Max Order Size100,000 sharesYes, per account
Max Order Value$10,000,000Yes, per account
Max Position Size5% of portfolioYes, per symbol
Max Orders Per Minute600Yes, per account
Max Daily Loss20% of portfolioYes, per account
Margin Maintenance25% (Reg T)No, regulatory
Price Band±10% from last closeExchange-defined
Circuit Breaker (LULD)Per SEC Rule 612No, regulatory

13. Settlement & Clearing

Settlement is the process of actually transferring securities and cash between buyers and sellers after a trade is executed. In the US equity markets, the standard settlement cycle is T+1 (as of May 2024), meaning trades must settle one business day after the trade date. This process is managed by the Depository Trust Company (DTC) and involves complex netting, margining, and delivery-versus-payment (DVP) mechanisms.

Settlement Flow

flowchart LR TRADE[Trade Executed] --> CLEAR[Clearing\n(T+0)] CLEAR --> NET[Trade Netting\n& Compression] NET --> MARGIN[Margin\nCalculation] MARGIN --> DVP[Delivery vs\nPayment] DVP --> CSD[CSD Settlement\n(DTC)] CSD --> BOOK[Book Entry\nTransfer] BOOK --> DONE[Settlement\nComplete] style TRADE fill:#e6f3ff,stroke:#0088ff style DONE fill:#d1fae5,stroke:#065f46

Settlement States

StateDescription
PENDINGTrade executed but not yet submitted to clearinghouse
SubmittedTrade submitted to DTC for clearing
ClearedClearinghouse has accepted and netted the trade
SettledSecurities and cash have been exchanged
FailedSettlement failed (insufficient shares/cash)

14. Portfolio Management

Portfolio management encompasses tracking all positions held by an account, calculating real-time profit and loss, managing cash balances and margin requirements, and providing analytics to help traders make informed decisions. The portfolio service must be both fast (real-time P&L) and accurate (every share and every cent must be accounted for).

Position Tracking

public class Position
{
    public string AccountId { get; set; }
    public string Symbol { get; set; }
    public decimal Quantity { get; set; }        // Can be negative (short)
    public decimal AverageCost { get; set; }      // Weighted average cost basis
    public decimal RealizedPnL { get; set; }      // Closed P&L
    public decimal UnrealizedPnL { get; set; }    // Open P&L (live)
    public decimal MarketPrice { get; set; }      // Current market price

    public decimal UnrealizedPnL =>
        Quantity > 0
            ? (MarketPrice - AverageCost) * Quantity
            : (AverageCost - MarketPrice) * Math.Abs(Quantity);

    public decimal TotalPnL => RealizedPnL + UnrealizedPnL;
}

P&L Calculation Example

Scenario: User buys 100 AAPL at $180, then buys another 50 at $185, then sells 80 at $190.

Average Cost After Buys: (100 × $180 + 50 × $185) / 150 = $181.67

Realized P&L on Sale: (80 × $190) - (80 × $181.67) = $666.67

Remaining Position: 70 shares at $181.67 avg cost

Unrealized P&L: 70 × ($190 - $181.67) = $583.33 (if current price is $190)

15. Order Management State Machine

Every order in the system follows a well-defined state machine. Understanding these states and the transitions between them is essential for building a reliable order management system. Each state transition must be atomic and logged for audit purposes.

stateDiagram-v2 [*] --> PENDING_VALIDATION : Order Submitted PENDING_VALIDATION --> PENDING_RISK : Validation Passed PENDING_VALIDATION --> REJECTED : Validation Failed PENDING_RISK --> OPEN : Risk Check Passed PENDING_RISK --> REJECTED : Risk Check Failed OPEN --> PARTIAL_FILL : Partial Match OPEN --> FILLED : Full Match OPEN --> CANCELLED : User Cancels OPEN --> EXPIRED : Time-in-Force Expires PARTIAL_FILL --> FILLED : Remaining Filled PARTIAL_FILL --> CANCELLED : User Cancels Remainder PARTIAL_FILL --> EXPIRED : TIF Expires on Remainder REJECTED --> [*] FILLED --> [*] CANCELLED --> [*] EXPIRED --> [*] state PENDING_VALIDATION { } state OPEN { } state PARTIAL_FILL { }

16. Market Hours & Session Management

The US equity markets operate during specific sessions, each with different rules for order types, price bands, and matching behavior. A trading platform must handle all these sessions correctly.

SessionHours (ET)Order TypesCharacteristics
Pre-Market4:00 AM – 9:30 AMLimit onlyLow liquidity, wide spreads
Opening Auction9:30 AM (instant)Market + LimitPrice discovery, high volume burst
Regular Hours9:30 AM – 4:00 PMAll typesNormal trading, best liquidity
Closing Auction4:00 PM (instant)Market + LimitMOC/IOC orders, index rebalancing
After-Hours4:00 PM – 8:00 PMLimit onlyLow liquidity, widened price bands

17. Historical Data & Backtesting

Historical data is the lifeblood of quantitative traders, algorithmic strategy developers, and retail investors performing technical analysis. A complete trading platform must store and serve years of tick-by-tick data, daily OHLCV bars, corporate actions, and fundamental data.

Data Retention Strategy

Data TypeResolutionRetentionStorage
Tick Data (Trades)Every trade7 yearsTimescaleDB + S3 Parquet
Level 2 Snapshots1-second snapshots2 yearsTimescaleDB
1-Minute CandlesOHLCV per minute10 yearsTimescaleDB
Daily CandlesOHLCV per dayAll historyTimescaleDB + S3
Corporate ActionsPer eventAll historyPostgreSQL
Fundamental DataQuarterly/Annual10 yearsPostgreSQL

18. Authentication & Regulatory Compliance

Financial trading platforms are among the most heavily regulated software systems in the world. Every action must be auditable, every user must be verified, and the platform must actively detect and prevent market abuse.

KYC (Know Your Customer) Requirements

  • Identity Verification: Government-issued ID, SSN verification, proof of address.
  • Accreditation: For margin and options trading, income/net worth verification.
  • PEP Screening: Check against Politically Exposed Persons databases.
  • Sanctions Screening: OFAC, EU, and UN sanctions list checks.
  • Ongoing Monitoring: Continuous transaction monitoring for suspicious activity.

Market Abuse Detection

Abuse TypeDescriptionDetection Method
SpoofingPlacing orders with intent to cancel before executionCancel-to-fill ratio analysis
LayeringMultiple orders at different prices to create false depthMulti-level order pattern analysis
Wash TradingTrading with yourself to create false volumeSame-account cross detection
Momentum IgnitionRapid orders to trigger other algorithmsTemporal pattern analysis
Front RunningTrading ahead of known large ordersTime-and-order-sequence analysis

19. Database Design

Choosing the right databases and designing the right schemas is critical for a trading platform. We use a polyglot persistence approach: PostgreSQL for transactional data, TimescaleDB for time-series market data, Redis for caching and real-time state, and Kafka for event streaming.

PostgreSQL Schema for Orders

CREATE TABLE orders (
    order_id        UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    account_id      UUID NOT NULL REFERENCES accounts(account_id),
    symbol          VARCHAR(10) NOT NULL,
    side            VARCHAR(4) NOT NULL CHECK (side IN ('BUY', 'SELL')),
    order_type      VARCHAR(20) NOT NULL,
    quantity        NUMERIC(15,4) NOT NULL CHECK (quantity > 0),
    filled_quantity NUMERIC(15,4) NOT NULL DEFAULT 0,
    price           NUMERIC(12,4),
    stop_price      NUMERIC(12,4),
    status          VARCHAR(20) NOT NULL DEFAULT 'PENDING_VALIDATION',
    time_in_force   VARCHAR(10) NOT NULL DEFAULT 'DAY',
    idempotency_key UUID UNIQUE,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    filled_at       TIMESTAMPTZ,
    cancelled_at    TIMESTAMPTZ
);

CREATE INDEX idx_orders_account_status ON orders(account_id, status);
CREATE INDEX idx_orders_symbol_status ON orders(symbol, status);
CREATE INDEX idx_orders_created ON orders(created_at DESC);

CREATE TABLE trades (
    trade_id        UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    buy_order_id    UUID NOT NULL REFERENCES orders(order_id),
    sell_order_id   UUID NOT NULL REFERENCES orders(order_id),
    symbol          VARCHAR(10) NOT NULL,
    price           NUMERIC(12,4) NOT NULL,
    quantity        NUMERIC(15,4) NOT NULL,
    buyer_fee       NUMERIC(12,4) NOT NULL DEFAULT 0,
    seller_fee      NUMERIC(12,4) NOT NULL DEFAULT 0,
    executed_at     TIMESTAMPTZ NOT NULL DEFAULT NOW()
) PARTITION BY RANGE (executed_at);

CREATE TABLE positions (
    account_id      UUID NOT NULL REFERENCES accounts(account_id),
    symbol          VARCHAR(10) NOT NULL,
    quantity        NUMERIC(15,4) NOT NULL DEFAULT 0,
    avg_cost_basis  NUMERIC(12,4) NOT NULL DEFAULT 0,
    realized_pnl    NUMERIC(15,4) NOT NULL DEFAULT 0,
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (account_id, symbol)
);

20. Caching Strategy

Caching is essential for achieving the read latency requirements of a trading platform. We use a multi-layer caching strategy with Redis as the primary distributed cache and in-memory caches within services for ultra-low-latency access.

Cache TargetStoreTTLInvalidation
Current Quotes (L1)Redis100msWrite-through from matching engine
Order Book Snapshot (L2)Redis500msWrite-through from matching engine
User PortfolioRedis5sEvent-driven from trade stream
Account BalanceRedis1sEvent-driven from cash events
User SessionRedis30 minJWT-based, no invalidation
Daily CandlesRedis1 minPeriodic refresh from TimescaleDB
Symbol MetadataRedis24 hoursNightly refresh

21. Multi-Region Design

For a trading platform, multi-region design serves two distinct purposes: low-latency access for geographically distributed traders, and disaster recovery for business continuity. The matching engine, however, must remain single-region to avoid the complexity and latency of cross-region consensus.

graph TB subgraph "US-EAST (Primary)" ME[Matching Engine] PG1[(PostgreSQL Primary)] KF1[Kafka Primary] RD1[(Redis Primary)] end subgraph "US-WEST (DR / Read Replica)" MD_READ[Market Data Reader] PG2[(PostgreSQL Replica)] KF2[Kafka MirrorMaker] RD2[(Redis Replica)] end subgraph "EU-WEST (International)" MD_EU[Market Data EU] PG3[(PostgreSQL EU)] RD3[(Redis EU)] end ME --> KF1 KF1 -->|MirrorMaker| KF2 PG1 -->|Streaming Replication| PG2 PG1 -->|Async Replication| PG3 RD1 -->|CRDT Replication| RD2 RD1 -->|CRDT Replication| RD3 MD_READ --> RD2 MD_EU --> RD3 style ME fill:#d1fae5,stroke:#065f46 style MD_READ fill:#e6f3ff,stroke:#0088ff style MD_EU fill:#e6f3ff,stroke:#0088ff

22. Cost Estimation

Running a production-grade trading platform is capital-intensive. Below is an estimated monthly cost breakdown for the infrastructure described in this article, targeting 10 million concurrent users during market hours.

ComponentSpecificationMonthly Cost
Matching Engine Servers16x bare-metal (48-core, 512GB RAM, NVMe)$120,000
Market Data Servers8x bare-metal (32-core, 256GB RAM)$50,000
Application Servers100x c6i.4xlarge (16 vCPU, 32GB)$120,000
PostgreSQL (RDS Multi-AZ)db.r6g.4xlarge × 3 (primary + replicas)$15,000
TimescaleDBdb.r6g.2xlarge × 2 + compression$8,000
Redis Clusterr6g.2xlarge × 6 nodes$9,000
Kafka (MSK)kafka.m5.2xlarge × 9 brokers$18,000
WebSocket Gateway50x c6i.2xlarge (connection-heavy)$45,000
Load BalancersALB × 4 + NLB × 2$5,000
S3 Storage100TB historical data$2,500
CloudFront CDN10TB/month transfer$1,500
Data Transfer5TB/day cross-AZ + internet$25,000
Monitoring (Datadog)Full stack APM + logs$20,000
Security & ComplianceWAF, Shield, GuardDuty, audit$10,000
Co-location (Optional)NJ3/NY5 cage per exchange$50,000
Total Estimated Monthly Cost$499,000

23. Interview Q&A

System design interviews for senior and staff engineering roles at trading platforms frequently focus on the matching engine, data consistency, market data distribution, and failure modes. Here are 10+ questions with detailed answers.

Q1: How would you design the matching engine to handle 500,000 orders per second?

The key insight is partitioning by symbol. Each symbol gets its own matching engine instance running in a single thread. Orders are routed to the correct instance via Kafka partitioning on symbol hash. Within a single thread, we avoid all locking overhead and can achieve sub-microsecond matching. For symbols with extreme volume (like AAPL), we can shard the matching engine further by order ID range, though this adds complexity. The matching engine uses lock-free data structures, pre-allocated memory pools, and zero-copy serialization to minimize latency.

Q2: How do you ensure exactly-once processing of orders?

Exactly-once semantics in distributed systems is achieved through a combination of idempotency keys and Kafka transactions. Each order submission includes a UUID idempotency key. The matching engine stores processed idempotency keys in a deduplication table with a TTL. If a duplicate key is detected, the engine returns the original response without re-matching. Additionally, we use Kafka's transactional producer to ensure that the order event and the resulting trade event are atomically committed to the topic.

Q3: What happens when the matching engine crashes mid-match?

The matching engine uses Write-Ahead Logging (WAL). Before processing any order, it writes the order to a persistent WAL. After matching and generating trades, it writes the trade events and order state updates to the WAL before acknowledging the order. On recovery, the engine replays the WAL from the last checkpoint, re-executing any incomplete matches deterministically. Clients with pending orders receive a reconnection and re-validation of their order status.

Q4: How would you handle a flash crash where order volume spikes 100x?

First, the circuit breaker mechanism pauses trading when prices move beyond defined bands (per SEC Regulation NMS Rule 612). Second, the matching engine employs backpressure — if the WAL falls behind, new orders are queued at the gateway with an estimated wait time. Third, we use auto-scaling for the application tier (not the matching engine, which cannot be horizontally scaled mid-session). Fourth, order rate limits per user prevent any single actor from flooding the system.

Q5: Why can't you use a traditional database for the order book?

A relational database would introduce millisecond-level latency for every order book operation due to disk I/O, lock contention, and network round trips. The order book requires microsecond-level operations: inserting an order, removing a filled order, and querying the best bid/ask must all happen in under 1 microsecond. An in-memory data structure (SortedDictionary with Queue per level) achieves this. The database is used for durability (async persistence) but never in the critical path of matching.

Q6: How do you handle the race condition between a user cancelling an order and the matching engine filling it?

Since the matching engine processes orders sequentially per symbol (single-threaded), there is no race condition. Cancel requests are funneled through the same Kafka partition as new orders for that symbol. The matching engine processes them in order: if a cancel arrives before the order is filled, the order is removed; if the order is filled first, the cancel is rejected with a "filled" status. This ordering guarantee is the fundamental reason for the single-writer architecture.

Q7: Design the market data fan-out system for 10 million concurrent WebSocket connections.

We use a subscription-based fan-out architecture. Each WebSocket gateway server maintains a local subscription map (symbol → set of connection IDs). When a price update arrives, the market data engine publishes it to a Kafka topic partitioned by symbol. Each gateway server subscribes to all partitions and maintains only the updates relevant to its connected clients. With 10 million connections across 50 gateway servers, each server handles ~200,000 connections. The subscription map is an in-memory hash map, making fan-out O(subscribers) per update.

Q8: How do you handle corporate actions (stock splits, dividends) in the matching engine?

Corporate actions are processed outside market hours via a corporate action service that modifies all resting orders and positions. For a 2-for-1 stock split, all resting limit orders have their price halved and quantity doubled. All positions are adjusted similarly. The key constraint is that this must complete before the next trading session opens. We use a snapshot-and-rebuild approach: snapshot the order book state, apply the corporate action transformation, and validate the new state before publishing it.

Q9: What is the difference between price-time priority and pro-rata priority, and why choose one over the other?

Price-time priority (used by NYSE, Nasdaq) gives preference to earlier orders at the same price. Pro-rata priority (used by some European exchanges and futures markets) allocates fills proportionally based on order size. Price-time is simpler to implement and more intuitive for retail traders. Pro-rata encourages larger resting orders and is preferred in derivatives markets where market makers need incentives to provide deep liquidity. For our US equities platform, price-time is the correct choice as it matches exchange rules.

Q10: How would you implement an iceberg order in the matching engine?

An iceberg order has a total quantity (e.g., 10,000 shares) but only shows a display quantity (e.g., 100 shares) on the order book. When the visible portion is filled, the matching engine automatically refreshes the display quantity from the hidden reserve. The implementation requires: (1) storing both total_qty and display_qty in the order, (2) when display_qty reaches zero, decrementing total_qty by the last fill, resetting display_qty, and updating the book. (3) The refresh must happen atomically within the matching loop to prevent any gap in the order book.

Q11: How do you ensure consistency between the matching engine's in-memory order book and the persistent database?

The matching engine is the source of truth during market hours. The database is updated asynchronously via Kafka events. To handle discrepancies, we implement periodic reconciliation: every 60 seconds, the matching engine snapshots its in-memory state, and a reconciliation service compares it with the database. Any mismatch triggers an alert and automatic correction. On system startup, the matching engine rebuilds its state from the database (order history + trade history) to ensure consistency after a crash recovery.

Q12: Design the system to handle the NYSE Opening Auction at 9:30 AM.

The opening auction is a complex event where the exchange determines the opening price based on maximum executable volume. Our system must: (1) accept Market-On-Open (MOO) and Limit-On-Open (LOO) orders during the pre-market session. (2) At 9:30 AM, the exchange publishes the auction price. (3) Our matching engine must immediately match all MOO and LOO orders at the auction price. (4) The flood of fills generates a massive burst of trade events that must be processed and broadcast without delay. We handle this by pre-allocating capacity in the Kafka topic and WebSocket gateway before the auction.

24. Full C# Implementation

Below is a production-quality C# implementation of the core matching engine components: Order, Trade, OrderBook, MatchingEngine, and MarketDataService. This implementation covers 300+ lines of code and demonstrates the key data structures, algorithms, and event-driven patterns described throughout this article.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;

namespace StockTradingPlatform.Core
{
    public enum OrderSide { Buy, Sell }
    public enum OrderType { Market, Limit, StopLoss, StopLimit, Iceberg, TrailingStop }
    public enum OrderStatus
    {
        PendingValidation,
        PendingRiskCheck,
        Open,
        PartiallyFilled,
        Filled,
        Cancelled,
        Rejected,
        Expired
    }
    public enum TimeInForce { Day, GTC, IOC, FOK }

    public class Order
    {
        public string OrderId { get; init; } = Guid.NewGuid().ToString("N");
        public string AccountId { get; init; } = string.Empty;
        public string Symbol { get; init; } = string.Empty;
        public OrderSide Side { get; init; }
        public OrderType Type { get; init; }
        public decimal Quantity { get; set; }
        public decimal FilledQuantity { get; set; }
        public decimal? Price { get; init; }
        public decimal? StopPrice { get; init; }
        public OrderStatus Status { get; set; } = OrderStatus.PendingValidation;
        public TimeInForce TimeInForce { get; init; } = TimeInForce.Day;
        public decimal? DisplayQuantity { get; set; }
        public decimal? HiddenQuantity { get; set; }
        public decimal? TrailAmount { get; init; }
        public decimal? TrailPercent { get; init; }
        public decimal? HighestPrice { get; set; }
        public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
        public DateTime? UpdatedAt { get; set; }
        public DateTime? FilledAt { get; set; }
        public string IdempotencyKey { get; init; } = Guid.NewGuid().ToString("N");

        public decimal RemainingQuantity => Quantity - FilledQuantity;

        public bool IsMarketable(OrderBook book)
        {
            if (Status != OrderStatus.Open) return false;
            if (!Price.HasValue) return Type == OrderType.Market;

            return Side switch
            {
                OrderSide.Buy => Price.Value >= book.BestAsk,
                OrderSide.Sell => Price.Value <= book.BestBid,
                _ => false
            };
        }
    }

    public class Trade
    {
        public string TradeId { get; init; } = Guid.NewGuid().ToString("N");
        public string BuyOrderId { get; init; } = string.Empty;
        public string SellOrderId { get; init; } = string.Empty;
        public string BuyerAccountId { get; init; } = string.Empty;
        public string SellerAccountId { get; init; } = string.Empty;
        public string Symbol { get; init; } = string.Empty;
        public decimal Price { get; init; }
        public decimal Quantity { get; init; }
        public DateTime ExecutedAt { get; init; } = DateTime.UtcNow;
        public decimal BuyerFee { get; init; }
        public decimal SellerFee { get; init; }

        public override string ToString() =>
            $"TRADE {Symbol}: {Quantity} @ {Price:C} " +
            $"[{BuyOrderId[..8]} -> {SellOrderId[..8]}]";
    }

    public class PriceLevel
    {
        public decimal Price { get; }
        public Queue<Order> Orders { get; } = new();
        public long TotalQuantity { get; private set; }

        public PriceLevel(decimal price) => Price = price;

        public void Enqueue(Order order)
        {
            Orders.Enqueue(order);
            TotalQuantity += (long)order.RemainingQuantity;
        }

        public Order Dequeue()
        {
            if (Orders.Count == 0) throw new InvalidOperationException("Empty price level");
            var order = Orders.Dequeue();
            TotalQuantity -= (long)order.RemainingQuantity;
            return order;
        }

        public void UpdateQuantity(decimal delta)
        {
            TotalQuantity += (long)delta;
        }

        public bool IsEmpty => Orders.Count == 0;
    }

    public class OrderBook
    {
        public string Symbol { get; }
        private readonly SortedDictionary<decimal, PriceLevel> _bids = new(Comparer<decimal>.Create((a, b) => b.CompareTo(a)));
        private readonly SortedDictionary<decimal, PriceLevel> _asks = new(Comparer<decimal>.Create((a, b) => a.CompareTo(b)));
        private readonly Dictionary<string, Order> _orderIndex = new();
        private readonly object _lock = new();

        public decimal BestBid => _bids.Keys.FirstOrDefault();
        public decimal BestAsk => _asks.Keys.FirstOrDefault();
        public decimal Spread => _bids.Count > 0 && _asks.Count > 0 ? BestAsk - BestBid : 0;
        public int BidLevels => _bids.Count;
        public int AskLevels => _asks.Count;
        public long TotalBidDepth => _bids.Values.Sum(l => l.TotalQuantity);
        public long TotalAskDepth => _asks.Values.Sum(l => l.TotalQuantity);

        public OrderBook(string symbol) => Symbol = symbol;

        public void AddOrder(Order order)
        {
            lock (_lock)
            {
                var levels = order.Side == OrderSide.Buy ? _bids : _asks;
                var price = order.Price!.Value;

                if (!levels.ContainsKey(price))
                    levels[price] = new PriceLevel(price);

                levels[price].Enqueue(order);
                _orderIndex[order.OrderId] = order;
            }
        }

        public void RemoveOrder(string orderId)
        {
            lock (_lock)
            {
                if (!_orderIndex.TryGetValue(orderId, out var order)) return;

                var levels = order.Side == OrderSide.Buy ? _bids : _asks;
                if (order.Price.HasValue && levels.TryGetValue(order.Price.Value, out var level))
                {
                    level.UpdateQuantity(-order.RemainingQuantity);
                    if (level.IsEmpty)
                        levels.Remove(order.Price.Value);
                }
                _orderIndex.Remove(orderId);
            }
        }

        public List<Order> GetOrdersAtPrice(decimal price, OrderSide side)
        {
            var levels = side == OrderSide.Buy ? _bids : _asks;
            return levels.TryGetValue(price, out var level)
                ? level.Orders.ToList()
                : new List<Order>();
        }

        public List<Tuple<decimal, long>> GetDepth(OrderSide side, int levels = 10)
        {
            var book = side == OrderSide.Buy ? _bids : _asks;
            return book.Take(levels)
                .Select(kvp => Tuple.Create(kvp.Key, kvp.Value.TotalQuantity))
                .ToList();
        }

        public bool ContainsOrder(string orderId) => _orderIndex.ContainsKey(orderId);
    }

    public class RiskCheckResult
    {
        public bool Approved { get; init; }
        public string? RejectionReason { get; init; }

        public static RiskCheckResult Pass() => new() { Approved = true };
        public static RiskCheckResult Fail(string reason) =>
            new() { Approved = false, RejectionReason = reason };
    }

    public interface IRiskEngine
    {
        RiskCheckResult CheckOrder(Order order, decimal accountBalance,
            decimal currentMargin, decimal maxPositionValue);
    }

    public class DefaultRiskEngine : IRiskEngine
    {
        private const decimal MaxOrderValue = 10_000_000m;
        private const int MaxOrdersPerMinute = 600;
        private readonly ConcurrentDictionary<string, Queue<DateTime>> _orderFrequency = new();

        public RiskCheckResult CheckOrder(Order order, decimal accountBalance,
            decimal currentMargin, decimal maxPositionValue)
        {
            if (order.Type == OrderType.Market && order.Quantity > 100_000)
                return RiskCheckResult.Fail("Market order exceeds 100,000 shares");

            decimal orderValue = order.Quantity * (order.Price ?? 0);
            if (orderValue > MaxOrderValue)
                return RiskCheckResult.Fail($"Order value {orderValue:C} exceeds max {MaxOrderValue:C}");

            if (currentMargin > accountBalance * 0.75m)
                return RiskCheckResult.Fail("Margin requirement exceeded");

            if (maxPositionValue > accountBalance * 0.5m)
                return RiskCheckResult.Fail("Position concentration too high");

            var now = DateTime.UtcNow;
            var timestamps = _orderFrequency.GetOrAdd(order.AccountId,
                _ => new Queue<DateTime>());
            lock (timestamps)
            {
                while (timestamps.Count > 0 && (now - timestamps.Peek()).TotalSeconds > 60)
                    timestamps.Dequeue();

                if (timestamps.Count >= MaxOrdersPerMinute)
                    return RiskCheckResult.Fail("Order rate limit exceeded");

                timestamps.Enqueue(now);
            }

            return RiskCheckResult.Pass();
        }
    }

    public class OrderBookChangedEventArgs : EventArgs
    {
        public string Symbol { get; init; } = string.Empty;
        public decimal BestBid { get; init; }
        public decimal BestAsk { get; init; }
        public long BidDepth { get; init; }
        public long AskDepth { get; init; }
        public DateTime Timestamp { get; init; } = DateTime.UtcNow;
    }

    public class TradeEventArgs : EventArgs
    {
        public Trade Trade { get; init; } = null!;
        public DateTime Timestamp { get; init; } = DateTime.UtcNow;
    }

    public class MatchingEngine
    {
        private readonly ConcurrentDictionary<string, OrderBook> _orderBooks = new();
        private readonly IRiskEngine _riskEngine;
        private readonly Channel<Order> _orderChannel;
        private readonly Channel<Trade> _tradeChannel;
        private readonly ConcurrentDictionary<string, decimal> _accountBalances = new();
        private readonly ConcurrentDictionary<string, decimal> _accountMargins = new();
        private readonly ConcurrentDictionary<string, decimal> _positionValues = new();
        private readonly HashSet<string> _processedIdempotencyKeys = new();
        private readonly object _idempotencyLock = new();

        public event EventHandler<TradeEventArgs>? OnTradeExecuted;
        public event EventHandler<OrderBookChangedEventArgs>? OnOrderBookChanged;

        public MatchingEngine(IRiskEngine? riskEngine = null)
        {
            _riskEngine = riskEngine ?? new DefaultRiskEngine();
            _orderChannel = Channel.CreateUnbounded<Order>(new UnboundedChannelOptions
            {
                SingleReader = true,
                SingleWriter = false
            });
            _tradeChannel = Channel.CreateUnbounded<Trade>(new UnboundedChannelOptions
            {
                SingleReader = false,
                SingleWriter = true
            });
        }

        public void InitializeAccount(string accountId, decimal balance)
        {
            _accountBalances[accountId] = balance;
            _accountMargins[accountId] = 0;
            _positionValues[accountId] = 0;
        }

        public OrderBook GetOrCreateOrderBook(string symbol) =>
            _orderBooks.GetOrAdd(symbol, s => new OrderBook(s));

        public async Task<Order> SubmitOrderAsync(Order order)
        {
            lock (_idempotencyLock)
            {
                if (!_processedIdempotencyKeys.Add(order.IdempotencyKey))
                {
                    order.Status = OrderStatus.Rejected;
                    order.UpdatedAt = DateTime.UtcNow;
                    return order;
                }
            }

            if (!ValidateOrder(order))
            {
                order.Status = OrderStatus.Rejected;
                order.UpdatedAt = DateTime.UtcNow;
                return order;
            }

            order.Status = OrderStatus.PendingRiskCheck;
            order.UpdatedAt = DateTime.UtcNow;

            var balance = _accountBalances.GetValueOrDefault(order.AccountId, 0);
            var margin = _accountMargins.GetValueOrDefault(order.AccountId, 0);
            var posValue = _positionValues.GetValueOrDefault(order.AccountId, 0);

            var riskResult = _riskEngine.CheckOrder(order, balance, margin, posValue);
            if (!riskResult.Approved)
            {
                order.Status = OrderStatus.Rejected;
                order.UpdatedAt = DateTime.UtcNow;
                return order;
            }

            order.Status = OrderStatus.Open;
            order.UpdatedAt = DateTime.UtcNow;

            var book = GetOrCreateOrderBook(order.Symbol);
            var trades = MatchOrder(order, book);

            foreach (var trade in trades)
            {
                await _tradeChannel.Writer.WriteAsync(trade);
                OnTradeExecuted?.Invoke(this, new TradeEventArgs { Trade = trade });
            }

            if (order.FilledQuantity >= order.Quantity)
            {
                order.Status = OrderStatus.Filled;
                order.FilledAt = DateTime.UtcNow;
            }
            else if (order.FilledQuantity > 0)
            {
                order.Status = OrderStatus.PartiallyFilled;
            }
            else if (order.Type == OrderType.Market)
            {
                order.Status = OrderStatus.Rejected;
            }

            order.UpdatedAt = DateTime.UtcNow;
            return order;
        }

        private List<Trade> MatchOrder(Order incoming, OrderBook book)
        {
            var trades = new List<Trade>();

            if (incoming.Type == OrderType.Market || incoming.IsMarketable(book))
            {
                var oppositeLevels = incoming.Side == OrderSide.Buy
                    ? book.GetDepth(OrderSide.Sell, 100)
                    : book.GetDepth(OrderSide.Buy, 100);

                foreach (var (price, _) in oppositeLevels)
                {
                    if (incoming.RemainingQuantity <= 0) break;

                    if (incoming.Type != OrderType.Market &&
                        incoming.Price.HasValue)
                    {
                        bool priceWorse = incoming.Side == OrderSide.Buy
                            ? incoming.Price.Value < price
                            : incoming.Price.Value > price;
                        if (priceWorse) break;
                    }

                    var ordersAtLevel = book.GetOrdersAtPrice(price,
                        incoming.Side == OrderSide.Buy ? OrderSide.Sell : OrderSide.Buy);

                    foreach (var resting in ordersAtLevel.ToList())
                    {
                        if (incoming.RemainingQuantity <= 0) break;

                        if (resting.AccountId == incoming.AccountId)
                            continue;

                        decimal matchQty = Math.Min(
                            incoming.RemainingQuantity,
                            resting.RemainingQuantity);

                        var trade = new Trade
                        {
                            BuyOrderId = incoming.Side == OrderSide.Buy
                                ? incoming.OrderId : resting.OrderId,
                            SellOrderId = incoming.Side == OrderSide.Sell
                                ? incoming.OrderId : resting.OrderId,
                            BuyerAccountId = incoming.Side == OrderSide.Buy
                                ? incoming.AccountId : resting.AccountId,
                            SellerAccountId = incoming.Side == OrderSide.Sell
                                ? incoming.AccountId : resting.AccountId,
                            Symbol = incoming.Symbol,
                            Price = price,
                            Quantity = matchQty
                        };

                        incoming.FilledQuantity += matchQty;
                        resting.FilledQuantity += matchQty;
                        book.RemoveOrder(resting.OrderId);

                        if (incoming.Type == OrderType.Iceberg &&
                            incoming.HiddenQuantity > 0)
                        {
                            decimal refresh = Math.Min(
                                incoming.DisplayQuantity!.Value,
                                incoming.HiddenQuantity.Value);
                            incoming.HiddenQuantity -= refresh;
                            incoming.Quantity += refresh;
                        }

                        trades.Add(trade);
                        UpdateAccountBalances(trade);

                        if (resting.RemainingQuantity <= 0)
                        {
                            resting.Status = OrderStatus.Filled;
                            resting.FilledAt = DateTime.UtcNow;
                        }
                        else
                        {
                            resting.Status = OrderStatus.PartiallyFilled;
                            resting.UpdatedAt = DateTime.UtcNow;
                        }
                    }
                }
            }

            if (incoming.RemainingQuantity > 0 &&
                incoming.Type != OrderType.Market &&
                incoming.TimeInForce != TimeInForce.IOC &&
                incoming.TimeInForce != TimeInForce.FOK)
            {
                book.AddOrder(incoming);
            }
            else if (incoming.Type == OrderType.Market &&
                     incoming.RemainingQuantity > 0)
            {
                incoming.Status = OrderStatus.Rejected;
            }

            OnOrderBookChanged?.Invoke(this, new OrderBookChangedEventArgs
            {
                Symbol = incoming.Symbol,
                BestBid = book.BestBid,
                BestAsk = book.BestAsk,
                BidDepth = book.TotalBidDepth,
                AskDepth = book.TotalAskDepth
            });

            return trades;
        }

        private void UpdateAccountBalances(Trade trade)
        {
            decimal tradeValue = trade.Price * trade.Quantity;

            _accountBalances.AddOrUpdate(trade.BuyAccountId, 0,
                (_, bal) => bal - tradeValue);
            _accountBalances.AddOrUpdate(trade.SellerAccountId, 0,
                (_, bal) => bal + tradeValue);
        }

        private bool ValidateOrder(Order order)
        {
            if (string.IsNullOrEmpty(order.AccountId)) return false;
            if (string.IsNullOrEmpty(order.Symbol)) return false;
            if (order.Quantity <= 0) return false;
            if (order.Type == OrderType.Limit && !order.Price.HasValue) return false;
            if (order.Type == OrderType.Limit && order.Price.Value <= 0) return false;
            if (order.Type == OrderType.StopLoss && !order.StopPrice.HasValue) return false;
            return true;
        }

        public async Task RunAsync(CancellationToken cancellationToken)
        {
            await foreach (var order in _orderChannel.Reader.ReadAllAsync(cancellationToken))
            {
                await SubmitOrderAsync(order);
            }
        }

        public bool CancelOrder(string orderId, string accountId)
        {
            foreach (var book in _orderBooks.Values)
            {
                if (book.ContainsOrder(orderId))
                {
                    book.RemoveOrder(orderId);
                    return true;
                }
            }
            return false;
        }
    }

    public class MarketDataService
    {
        private readonly MatchingEngine _engine;
        private readonly ConcurrentDictionary<string, MarketQuote> _quotes = new();
        private readonly Channel<MarketQuote> _quoteChannel;

        public MarketDataService(MatchingEngine engine)
        {
            _engine = engine;
            _quoteChannel = Channel.CreateUnbounded<MarketQuote>(
                new UnboundedChannelOptions { SingleWriter = false });

            _engine.OnOrderBookChanged += OnOrderBookUpdated;
        }

        private void OnOrderBookUpdated(object? sender, OrderBookChangedEventArgs e)
        {
            var quote = new MarketQuote
            {
                Symbol = e.Symbol,
                Bid = e.BestBid,
                Ask = e.BestAsk,
                Spread = e.AskDepth > 0 && e.BidDepth > 0
                    ? e.AskDepth - e.BidDepth : 0,
                BidSize = e.BidDepth,
                AskSize = e.AskDepth,
                Timestamp = e.Timestamp
            };

            _quotes[e.Symbol] = quote;
            _quoteChannel.Writer.TryWrite(quote);
        }

        public MarketQuote? GetQuote(string symbol) =>
            _quotes.TryGetValue(symbol, out var q) ? q : null;

        public IAsyncEnumerable<MarketQuote> SubscribeQuotesAsync(
            CancellationToken ct) =>
            _quoteChannel.Reader.ReadAllAsync(ct);
    }

    public class MarketQuote
    {
        public string Symbol { get; init; } = string.Empty;
        public decimal Bid { get; init; }
        public decimal Ask { get; init; }
        public decimal Spread { get; init; }
        public long BidSize { get; init; }
        public long AskSize { get; init; }
        public decimal Last { get; init; }
        public long Volume { get; init; }
        public DateTime Timestamp { get; init; } = DateTime.UtcNow;
    }

    public class Position
    {
        public string AccountId { get; init; } = string.Empty;
        public string Symbol { get; init; } = string.Empty;
        public decimal Quantity { get; set; }
        public decimal AverageCost { get; set; }
        public decimal RealizedPnL { get; set; }
        public decimal UnrealizedPnL { get; set; }
        public decimal MarketPrice { get; set; }

        public decimal TotalUnrealizedPnL => Quantity > 0
            ? (MarketPrice - AverageCost) * Quantity
            : Quantity < 0
                ? (AverageCost - MarketPrice) * Math.Abs(Quantity)
                : 0;

        public decimal TotalPnL => RealizedPnL + TotalUnrealizedPnL;

        public void ApplyBuy(decimal qty, decimal price)
        {
            decimal totalCost = (Quantity > 0 ? AverageCost * Quantity : 0) + qty * price;
            Quantity += qty;
            AverageCost = Quantity > 0 ? totalCost / Quantity : 0;
        }

        public void ApplySell(decimal qty, decimal price)
        {
            decimal pnl = (price - AverageCost) * qty;
            RealizedPnL += pnl;
            Quantity -= qty;
            if (Quantity <= 0)
            {
                AverageCost = 0;
                Quantity = 0;
            }
        }
    }

    public class PortfolioService
    {
        private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, Position>>
            _positions = new();
        private readonly MatchingEngine _engine;

        public PortfolioService(MatchingEngine engine)
        {
            _engine = engine;
            _engine.OnTradeExecuted += OnTradeExecuted;
        }

        private void OnTradeExecuted(object? sender, TradeEventArgs e)
        {
            var trade = e.Trade;

            UpdatePosition(trade.BuyerAccountId, trade.Symbol,
                trade.Quantity, trade.Price, true);
            UpdatePosition(trade.SellerAccountId, trade.Symbol,
                trade.Quantity, trade.Price, false);
        }

        private void UpdatePosition(string accountId, string symbol,
            decimal qty, decimal price, bool isBuy)
        {
            var accountPositions = _positions.GetOrAdd(accountId,
                _ => new ConcurrentDictionary<string, Position>());

            var position = accountPositions.GetOrAdd(symbol,
                _ => new Position { AccountId = accountId, Symbol = symbol });

            lock (position)
            {
                if (isBuy)
                    position.ApplyBuy(qty, price);
                else
                    position.ApplySell(qty, price);
            }
        }

        public List<Position> GetPositions(string accountId)
        {
            if (!_positions.TryGetValue(accountId, out var positions))
                return new List<Position>();
            return positions.Values.ToList();
        }

        public void UpdateMarketPrice(string symbol, decimal price)
        {
            foreach (var accountPositions in _positions.Values)
            {
                if (accountPositions.TryGetValue(symbol, out var position))
                {
                    lock (position)
                    {
                        position.MarketPrice = price;
                    }
                }
            }
        }
    }
}

26. Options & Derivatives Trading

Options trading adds a significant layer of complexity to any stock trading platform. Unlike equities, where each share represents a fixed claim on a company, an options contract gives the holder the right — but not the obligation — to buy (call) or sell (put) an underlying asset at a specified strike price before or at expiration. The platform must support full options chain display, real-time Greeks calculation, complex margin requirements, and accurate options pricing models.

Options Chain Architecture

The options chain is a two-dimensional data structure organized by underlying symbol, expiration date, and strike price. For a single equity like AAPL, there may be hundreds of active options contracts across dozens of expiration cycles. The platform must serve this data efficiently, supporting filtering by expiration, sorting by delta, and real-time updates to bid/ask/last for every contract.

Key Design Considerations: The options chain is typically served from a denormalized read-optimized store. Each contract is identified by a unique OCC symbol (e.g., AAPL 260718C00190000). The chain is partitioned by underlying symbol and cached aggressively, since most users browse only a handful of underlyings. Real-time updates flow through a dedicated options market data channel separate from the equity book.

Contract FieldDescriptionExample
UnderlyingThe stock symbol the option is based onAAPL
ExpirationDate the contract expires2026-07-18
StrikePrice at which the option can be exercised$190.00
TypeCall or PutCall
OIOpen Interest — total outstanding contracts12,450
IVImplied Volatility32.5%
DeltaSensitivity to $1 move in underlying0.48
GammaRate of change of delta0.035
ThetaDaily time decay-0.12
VegaSensitivity to 1% change in IV0.18

Greeks Calculation Engine

The Greeks — Delta, Gamma, Theta, Vega, and Rho — quantify the risk profile of each options contract. They must be recalculated in real-time as the underlying price, volatility, interest rates, and time to expiration change. For a platform with millions of options contracts across thousands of underlyings, the Greeks engine is a compute-intensive service that must balance accuracy with latency.

public static class BlackScholesModel
{
    private static double Phi(double x)
    {
        // Standard normal CDF using Abramowitz & Stegun approximation
        double a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741;
        double a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911;
        int sign = x >= 0 ? 1 : -1;
        x = Math.Abs(x) / Math.Sqrt(2.0);
        double t = 1.0 / (1.0 + p * x);
        double y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.Exp(-x * x);
        return 0.5 * (1.0 + sign * y);
    }

    private static double PhiPrime(double x) =>
        Math.Exp(-0.5 * x * x) / Math.Sqrt(2.0 * Math.PI);

    public static OptionGreeks Calculate(
        double S, double K, double T, double r, double sigma, bool isCall)
    {
        double d1 = (Math.Log(S / K) + (r + 0.5 * sigma * sigma) * T)
                    / (sigma * Math.Sqrt(T));
        double d2 = d1 - sigma * Math.Sqrt(T);

        double delta = isCall ? Phi(d1) : Phi(d1) - 1.0;
        double gamma = PhiPrime(d1) / (S * sigma * Math.Sqrt(T));
        double theta = isCall
            ? (-(S * PhiPrime(d1) * sigma) / (2 * Math.Sqrt(T))
               - r * K * Math.Exp(-r * T) * Phi(d2))
            : (-(S * PhiPrime(d1) * sigma) / (2 * Math.Sqrt(T))
               + r * K * Math.Exp(-r * T) * Phi(-d2));
        double vega = S * PhiPrime(d1) * Math.Sqrt(T) / 100.0;
        double rho = isCall
            ? K * T * Math.Exp(-r * T) * Phi(d2) / 100.0
            : -K * T * Math.Exp(-r * T) * Phi(-d2) / 100.0;

        double price = isCall
            ? S * Phi(d1) - K * Math.Exp(-r * T) * Phi(d2)
            : K * Math.Exp(-r * T) * Phi(-d2) - S * Phi(-d1);

        return new OptionGreeks
        {
            Price = price, Delta = delta, Gamma = gamma,
            Theta = theta, Vega = vega, Rho = rho
        };
    }
}

public record OptionGreeks
{
    public double Price { get; init; }
    public double Delta { get; init; }
    public double Gamma { get; init; }
    public double Theta { get; init; }
    public double Vega { get; init; }
    public double Rho { get; init; }
}

Margin Requirements for Options

Options margin calculation is significantly more complex than equity margin. The OCC and broker-dealers enforce specific margin rules depending on the position type. A cash-secured put requires the full strike value in cash. A covered call requires no additional margin. A naked call requires the greater of 20% of the underlying value minus out-of-the-money amount, or the option premium plus 15% of the underlying value.

flowchart TD POS[Options Position] --> TYPE{Position Type} TYPE -->|Long Call/Put| PREM[Margin = Premium Paid] TYPE -->|Covered Call| NONE[No Additional Margin] TYPE -->|Cash-Secured Put| CASH[Margin = Strike × 100] TYPE -->|Naked Call| COMPLEX{OCC Formula} TYPE -->|Spread| SPREAD[Max Loss - Premium] COMPLEX -->|20% × Underlying - OTM| CALC1[Calculation A] COMPLEX -->|Premium + 15% × Underlying| CALC2[Calculation B] CALC1 --> MAX[Max of A and B] CALC2 --> MAX style PREM fill:#d1fae5,stroke:#065f46 style NONE fill:#d1fae5,stroke:#065f46 style MAX fill:#fee2e2,stroke:#991b1b style SPREAD fill:#fef3c7,stroke:#92400e

The margin engine must evaluate each leg of multi-leg strategies (spreads, straddles, iron condors, butterflies) independently and in combination. Portfolio margin — used by sophisticated traders — calculates the theoretical worst-case loss across a range of underlying price scenarios rather than applying fixed percentages. This requires running the Black-Scholes model across a grid of price points and taking the maximum portfolio loss.

27. Algorithmic Trading & Smart Order Routing

Algorithmic trading accounts for approximately 60-75% of all US equity volume. Institutional traders and quantitative hedge funds use execution algorithms to minimize market impact, reduce transaction costs, and achieve best execution as required by Regulation NMS. The platform must support a rich set of execution algorithms and route orders intelligently across multiple lit exchanges, dark pools, and alternative trading systems.

Execution Algorithms

Execution algorithms break large parent orders into smaller child orders executed over time according to specific mathematical models. The goal is to match or beat the VWAP (Volume-Weighted Average Price) or TWAP (Time-Weighted Average Price) benchmark while minimizing information leakage and market impact.

flowchart LR subgraph "Algorithmic Order Types" TWAP[TWAP\nTime-Weighted] VWAP[VWAP\nVolume-Weighted] IS[Implementation\nShortfall] POV[Percentage\nof Volume] ICE[Iceberg\nOrders] end subgraph "Routing Intelligence" SOR[Smart Order\nRouter] DARK1[Dark Pool 1\n(CrossFinder)] DARK2[Dark Pool 2\n(LiquidNet)] LIT1[NYSE] LIT2[Nasdaq] LIT3[ARCA] LIT4[IEX] end TWAP --> SOR VWAP --> SOR IS --> SOR POV --> SOR ICE --> SOR SOR --> DARK1 & DARK2 & LIT1 & LIT2 & LIT3 & LIT4 style SOR fill:#0088ff,stroke:#0066cc,color:#fff style DARK1 fill:#6b21a8,stroke:#581c87,color:#fff style DARK2 fill:#6b21a8,stroke:#581c87,color:#fff

TWAP and VWAP Implementation

public class TWAPOrder
{
    public string ParentOrderId { get; init; } = Guid.NewGuid().ToString("N");
    public string Symbol { get; init; } = string.Empty;
    public OrderSide Side { get; init; }
    public decimal TotalQuantity { get; init; }
    public DateTime StartTime { get; init; }
    public DateTime EndTime { get; init; }
    public int SliceCount => (int)((EndTime - StartTime).TotalMinutes);
    public decimal QuantityPerSlice => TotalQuantity / SliceCount;

    public List<ChildOrder> GenerateSlices()
    {
        var slices = new List<ChildOrder>();
        var interval = (EndTime - StartTime) / SliceCount;

        for (int i = 0; i < SliceCount; i++)
        {
            slices.Add(new ChildOrder
            {
                ParentOrderId = ParentOrderId,
                Symbol = Symbol,
                Side = Side,
                Quantity = QuantityPerSlice,
                ScheduledTime = StartTime.AddTicks(interval.Ticks * i),
                Type = OrderType.Limit,
                PriceDeviationTicks = 2
            });
        }
        return slices;
    }
}

public class VWAPCalculator
{
    private readonly List<Trade> _historicalTrades;

    public decimal CalculateVWAP(DateTime start, DateTime end)
    {
        var trades = _historicalTrades
            .Where(t => t.ExecutedAt >= start && t.ExecutedAt <= end)
            .ToList();

        decimal totalVolumePrice = trades.Sum(t => t.Price * t.Quantity);
        decimal totalVolume = trades.Sum(t => t.Quantity);

        return totalVolume > 0 ? totalVolumePrice / totalVolume : 0;
    }

    public decimal[] GetVolumeProfile(string symbol, int intervals)
    {
        var hourlyVolume = new decimal[intervals];
        var trades = _historicalTrades
            .Where(t => t.Symbol == symbol)
            .ToList();

        foreach (var trade in trades)
        {
            int slot = (int)(trade.ExecutedAt - trade.ExecutedAt.Date).TotalMinutes
                       * intervals / 390;
            if (slot >= 0 && slot < intervals)
                hourlyVolume[slot] += trade.Quantity;
        }

        decimal total = hourlyVolume.Sum();
        return hourlyVolume.Select(v => v / total).ToArray();
    }
}

Smart Order Routing Decision Matrix

FactorLit Exchange (NYSE/Nasdaq)Dark PoolIEX (350μs Delay)
Price ImprovementPrice-time priorityCross at mid-pointFair access, no HFT
Market ImpactVisible to allMinimal — no pre-trade displayVisible but delayed
Latency< 100μs< 500μs~350μs extra
Fill RateHigh — deep liquidityModerate — depends on counterpartiesHigh
Reg NMS ComplianceFull NBBO displayMust route to NBBO if betterFull NBBO display
Best ForSmall, marketable ordersLarge block trades (10K+ shares)Cost-sensitive traders

Regulation NMS Compliance: The smart order router must ensure that every order is executed at the National Best Bid and Offer (NBBO) or better. If a dark pool offers a price inside the NBBO, the SOR routes there first. If no dark venue improves, the SOR routes to the lit exchange displaying the best price. The Order Protection Rule (Rule 611) prohibits the platform from executing an order at a price worse than a protected quotation displayed by another venue.

28. Market Surveillance & Circuit Breakers

Market surveillance and circuit breakers are critical safety mechanisms that prevent runaway prices, detect manipulation, and maintain fair and orderly markets. The SEC's Limit Up-Limit Down (LULD) mechanism, implemented after the 2010 Flash Crash, defines price bands within which a security must trade. If the price moves outside these bands, trading is halted. The platform must implement these rules precisely, as non-compliance results in regulatory fines and potential market participant harm.

LULD Band Calculation

The LULD mechanism defines a Price Band as a percentage above and below the Reference Price (typically the rolling median of transactions over the preceding 5 minutes). The percentage varies by Tier and price level of the security.

TierSecurity PriceBand Width (Up/Down)Halt Trigger
Tier 1 (SPY, QQQ, major ETFs)Any price±1% (Narrow Band)10 min pause
Tier 2 (S&P 500, Russell 1000)$3.00+±5%5 min pause, then ±10%
Tier 2$0.75 – $3.00±20%5 min pause, then ±30%
Tier 2$0.075 – $0.75±30%5 min pause, then ±50%
Tier 3 (All other NMS)$3.00+±10%5 min pause, then ±20%
Tier 3$0.75 – $3.00±20%5 min pause, then ±30%
Tier 3$0.075 – $0.75±30%5 min pause, then ±50%
flowchart TD TRADE[New Trade Price] --> REF[Calculate Reference Price\n(5-min Rolling Median)] REF --> BAND[Calculate LULD Bands\n(+/- Percentage Based on Tier)] BAND --> CHECK{Price Within Bands?} CHECK -->|Yes| ACCEPT[Accept Trade\nUpdate Reference Price] CHECK -->|No| HALT{Previous Halt Today?} HALT -->|No| PAUSE1[Trading Paused\n5-minute Circuit Breaker] HALT -->|Yes| WIDE[Expand Bands to\nSecond-Limit Width] PAUSE1 --> RESUME1[Trading Resumes\nwith Narrow Bands] WIDE --> CHECK2{Within Wide Bands?} CHECK2 -->|Yes| ACCEPT2[Accept Trade] CHECK2 -->|No| PAUSE2[Trading Halted\nRemainder of Day] style ACCEPT fill:#d1fae5,stroke:#065f46 style ACCEPT2 fill:#d1fae5,stroke:#065f46 style PAUSE1 fill:#fef3c7,stroke:#92400e style PAUSE2 fill:#fee2e2,stroke:#991b1b style HALT fill:#fee2e2,stroke:#991b1b

Cross-Market Surveillance

The platform's surveillance engine monitors trading activity across all connected venues simultaneously. Cross-market manipulation involves trading in the same security across multiple exchanges to create misleading price signals. Examples include wash trading (simultaneously buying and selling to yourself across two accounts), spoofing (placing and cancelling large orders on one venue while executing on another), and layering (building apparent depth on multiple venues to move the price).

Mandatory Reporting: Under SEC Rule 17a-8 and FINRA Rule 4510, broker-dealers must maintain surveillance programs capable of detecting and reporting suspicious activity. The system must flag any trade that deviates significantly from normal patterns — including unusual volume, rapid order placement and cancellation, or coordinated activity between accounts — and generate Suspicious Activity Reports (SARs) for compliance review within 30 days.

Surveillance CheckDetection WindowThresholdAction
Cancel-to-Fill Ratio1-hour rolling window> 95% cancelsAlert compliance team
Wash Trade DetectionReal-time (per trade)Same beneficial ownerBlock trade, flag account
Momentum Ignition5-minute windowRapid order-cancel patternHalt account, review
Cross-Venue Layering30-second windowOrders on 3+ venues, <10% fillAlert, potential restriction
Unusual Volumevs. 20-day average> 5x normal volumeEnhanced monitoring
Price ManipulationLULD band interaction3+ halts in one sessionRegulatory report filing

29. Conclusion

Designing a stock trading platform is one of the most challenging and rewarding system design exercises in software engineering. It requires deep understanding of distributed systems (event sourcing, CQRS, exactly-once delivery), data structures (sorted order books, lock-free queues), real-time systems (sub-microsecond latency, zero-copy serialization), financial domain knowledge (order types, settlement cycles, margin requirements), and regulatory compliance (KYC, audit trails, market abuse detection).

The architecture we explored in this article — from the single-writer matching engine to the WebSocket fan-out system, from the risk management pipeline to the multi-region disaster recovery design — represents the state of the art in modern electronic trading infrastructure. Each component is carefully designed to maximize throughput, minimize latency, ensure correctness, and maintain regulatory compliance.

Key Takeaways:

  • The matching engine must be single-threaded per symbol for determinism and performance.
  • The order book is an in-memory data structure — databases are for persistence, not the critical path.
  • Event sourcing via Kafka decouples the hot path from downstream services.
  • WebSocket fan-out with subscription-based routing scales to millions of connections.
  • Pre-trade risk checks are non-negotiable — every order must be validated before matching.
  • Idempotency and exactly-once processing prevent duplicate trades.
  • Co-location and bare-metal servers are essential for the matching engine tier.
  • The system cost of ~$500K/month is justified by the volume of a 10M-user platform.

Whether you are preparing for a staff engineer interview at a trading firm, building a startup brokerage, or simply deepening your understanding of financial systems, the patterns and principles in this article provide a comprehensive foundation. The C# implementation gives you a working starting point that you can extend with persistence, networking, and monitoring to build a production-grade system.

"The stock market is a device for transferring money from the impatient to the patient." — Warren Buffett. And the systems that power it are devices for transferring information from the exchange to the trader in the smallest number of nanoseconds possible.

Happy building, and may your matching engine always find the best bid.

© 2026 Ayodhyya. All rights reserved.

System Design Articles for Senior+ Engineers