How to Design a Stock Trading Platform
Building order matching engines, real-time market data, and portfolio management for 10M+ concurrent traders
Table of Contents
- Introduction
- Functional & Non-Functional Requirements
- Capacity Estimation
- Data Model
- API Design
- High-Level Architecture
- Order Types
- Order Book Data Structure
- Matching Engine
- Market Data Pipeline
- Real-Time Price Streaming
- Risk Management Engine
- Settlement & Clearing
- Portfolio Management
- Order Management State Machine
- Market Hours & Session Management
- Historical Data & Backtesting
- Authentication & Regulatory Compliance
- Database Design
- Caching Strategy
- Multi-Region Design
- Cost Estimation
- Interview Q&A
- Full C# Implementation
- Options & Derivatives Trading
- Algorithmic Trading & Smart Order Routing
- Market Surveillance & Circuit Breakers
- Conclusion
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
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
| Requirement | Target | Rationale |
|---|---|---|
| Latency (Order to Ack) | < 1ms | Competitive execution requires sub-millisecond round-trip |
| Throughput | 500K orders/sec | Peak market open volumes across all symbols |
| Availability | 99.999% | Market hours downtime costs millions per minute |
| Data Durability | 99.999999999% | Trade records are legally irreplaceable |
| Consistency | Strong (ACID) | Financial data cannot tolerate eventual consistency |
| Read Latency (Market Data) | < 50ms | Users need real-time price updates |
| Message Delivery | < 10ms | WebSocket market data must arrive in real-time |
| Recovery Time | < 30 seconds | Automated failover during market hours |
| Compliance | SEC/FINRA/KYC | Regulatory mandates for all US broker-dealers |
| Security | SOC2 Type II | Institutional 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
| Entity | Key Fields | Storage |
|---|---|---|
| User | user_id, email, name, kyc_status, account_type, created_at | PostgreSQL |
| Account | account_id, user_id, cash_balance, margin_used, buying_power, status | PostgreSQL |
| Order | order_id, account_id, symbol, side, type, qty, price, status, timestamps | PostgreSQL + TimescaleDB |
| Trade | trade_id, buy_order_id, sell_order_id, symbol, price, qty, timestamp | TimescaleDB |
| Position | account_id, symbol, qty, avg_cost, realized_pnl, unrealized_pnl | PostgreSQL + Redis |
| OrderBook | symbol, price_level, side, orders (FIFO queue), total_qty | In-Memory (C#) |
| MarketData | symbol, timestamp, bid, ask, last, volume, open, high, low, close | InfluxDB / TimescaleDB |
| Candle | symbol, interval, open, high, low, close, volume, timestamp | TimescaleDB |
| Dividend | symbol, ex_date, record_date, payment_date, amount_per_share | PostgreSQL |
| Settlement | trade_id, settlement_date, status, delivered_qty, received_qty | PostgreSQL |
Entity Relationship
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
| Method | Endpoint | Description | Rate Limit |
|---|---|---|---|
| POST | /api/v1/orders | Place a new order | 100/sec per user |
| PUT | /api/v1/orders/{id} | Modify an existing order | 100/sec per user |
| DELETE | /api/v1/orders/{id} | Cancel an order | 100/sec per user |
| GET | /api/v1/orders | List open orders | 50/sec per user |
| GET | /api/v1/orders/{id} | Get order details | 50/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/portfolio | Get portfolio summary | 20/sec per user |
| GET | /api/v1/portfolio/positions | Get all positions | 20/sec per user |
| GET | /api/v1/trades | Get trade history | 10/sec per user |
| GET | /api/v1/candles/{symbol} | Get OHLCV candles | 50/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.
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 Type | Behavior | Use Case | Complexity |
|---|---|---|---|
| Market | Execute immediately at best available price | Urgent execution, day traders | Easy |
| Limit | Execute only at specified price or better | Price-sensitive execution | Easy |
| Stop-Loss | Becomes market order when stop price is hit | Downside protection | Medium |
| Stop-Limit | Becomes limit order when stop price is hit | Controlled exit with price floor | Medium |
| OCO | One-Cancels-Other: two linked orders, first to execute cancels the other | Bracket strategies | Medium |
| Iceberg | Shows only a portion of total quantity | Large institutional orders | Hard |
| Trailing Stop | Stop price tracks market price by a fixed amount or percentage | Locking in profits on trending stocks | Hard |
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
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.
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
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.
Market Data Tiers
| Tier | Data | Update Rate | Delivery |
|---|---|---|---|
| Level 1 | Bid/Ask/Last/Volume | 50 updates/sec per symbol | WebSocket, REST |
| Level 2 | Top 10-20 price levels | 500 updates/sec per symbol | WebSocket, Multicast |
| Level 3 | Full order book (all levels) | 2,000+ updates/sec per symbol | Multicast, Co-location |
| Time & Sales | Every trade print | 20+ trades/sec per symbol | WebSocket, 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
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
Risk Parameters
| Check | Default Limit | Configurable? |
|---|---|---|
| Max Order Size | 100,000 shares | Yes, per account |
| Max Order Value | $10,000,000 | Yes, per account |
| Max Position Size | 5% of portfolio | Yes, per symbol |
| Max Orders Per Minute | 600 | Yes, per account |
| Max Daily Loss | 20% of portfolio | Yes, per account |
| Margin Maintenance | 25% (Reg T) | No, regulatory |
| Price Band | ±10% from last close | Exchange-defined |
| Circuit Breaker (LULD) | Per SEC Rule 612 | No, 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
Settlement States
| State | Description |
|---|---|
| PENDING | Trade executed but not yet submitted to clearinghouse |
| Submitted | Trade submitted to DTC for clearing |
| Cleared | Clearinghouse has accepted and netted the trade |
| Settled | Securities and cash have been exchanged |
| Failed | Settlement 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.
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.
| Session | Hours (ET) | Order Types | Characteristics |
|---|---|---|---|
| Pre-Market | 4:00 AM – 9:30 AM | Limit only | Low liquidity, wide spreads |
| Opening Auction | 9:30 AM (instant) | Market + Limit | Price discovery, high volume burst |
| Regular Hours | 9:30 AM – 4:00 PM | All types | Normal trading, best liquidity |
| Closing Auction | 4:00 PM (instant) | Market + Limit | MOC/IOC orders, index rebalancing |
| After-Hours | 4:00 PM – 8:00 PM | Limit only | Low 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 Type | Resolution | Retention | Storage |
|---|---|---|---|
| Tick Data (Trades) | Every trade | 7 years | TimescaleDB + S3 Parquet |
| Level 2 Snapshots | 1-second snapshots | 2 years | TimescaleDB |
| 1-Minute Candles | OHLCV per minute | 10 years | TimescaleDB |
| Daily Candles | OHLCV per day | All history | TimescaleDB + S3 |
| Corporate Actions | Per event | All history | PostgreSQL |
| Fundamental Data | Quarterly/Annual | 10 years | PostgreSQL |
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 Type | Description | Detection Method |
|---|---|---|
| Spoofing | Placing orders with intent to cancel before execution | Cancel-to-fill ratio analysis |
| Layering | Multiple orders at different prices to create false depth | Multi-level order pattern analysis |
| Wash Trading | Trading with yourself to create false volume | Same-account cross detection |
| Momentum Ignition | Rapid orders to trigger other algorithms | Temporal pattern analysis |
| Front Running | Trading ahead of known large orders | Time-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 Target | Store | TTL | Invalidation |
|---|---|---|---|
| Current Quotes (L1) | Redis | 100ms | Write-through from matching engine |
| Order Book Snapshot (L2) | Redis | 500ms | Write-through from matching engine |
| User Portfolio | Redis | 5s | Event-driven from trade stream |
| Account Balance | Redis | 1s | Event-driven from cash events |
| User Session | Redis | 30 min | JWT-based, no invalidation |
| Daily Candles | Redis | 1 min | Periodic refresh from TimescaleDB |
| Symbol Metadata | Redis | 24 hours | Nightly 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.
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.
| Component | Specification | Monthly Cost |
|---|---|---|
| Matching Engine Servers | 16x bare-metal (48-core, 512GB RAM, NVMe) | $120,000 |
| Market Data Servers | 8x bare-metal (32-core, 256GB RAM) | $50,000 |
| Application Servers | 100x c6i.4xlarge (16 vCPU, 32GB) | $120,000 |
| PostgreSQL (RDS Multi-AZ) | db.r6g.4xlarge × 3 (primary + replicas) | $15,000 |
| TimescaleDB | db.r6g.2xlarge × 2 + compression | $8,000 |
| Redis Cluster | r6g.2xlarge × 6 nodes | $9,000 |
| Kafka (MSK) | kafka.m5.2xlarge × 9 brokers | $18,000 |
| WebSocket Gateway | 50x c6i.2xlarge (connection-heavy) | $45,000 |
| Load Balancers | ALB × 4 + NLB × 2 | $5,000 |
| S3 Storage | 100TB historical data | $2,500 |
| CloudFront CDN | 10TB/month transfer | $1,500 |
| Data Transfer | 5TB/day cross-AZ + internet | $25,000 |
| Monitoring (Datadog) | Full stack APM + logs | $20,000 |
| Security & Compliance | WAF, 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 Field | Description | Example |
|---|---|---|
| Underlying | The stock symbol the option is based on | AAPL |
| Expiration | Date the contract expires | 2026-07-18 |
| Strike | Price at which the option can be exercised | $190.00 |
| Type | Call or Put | Call |
| OI | Open Interest — total outstanding contracts | 12,450 |
| IV | Implied Volatility | 32.5% |
| Delta | Sensitivity to $1 move in underlying | 0.48 |
| Gamma | Rate of change of delta | 0.035 |
| Theta | Daily time decay | -0.12 |
| Vega | Sensitivity to 1% change in IV | 0.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.
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.
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
| Factor | Lit Exchange (NYSE/Nasdaq) | Dark Pool | IEX (350μs Delay) |
|---|---|---|---|
| Price Improvement | Price-time priority | Cross at mid-point | Fair access, no HFT |
| Market Impact | Visible to all | Minimal — no pre-trade display | Visible but delayed |
| Latency | < 100μs | < 500μs | ~350μs extra |
| Fill Rate | High — deep liquidity | Moderate — depends on counterparties | High |
| Reg NMS Compliance | Full NBBO display | Must route to NBBO if better | Full NBBO display |
| Best For | Small, marketable orders | Large 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.
| Tier | Security Price | Band 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% |
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 Check | Detection Window | Threshold | Action |
|---|---|---|---|
| Cancel-to-Fill Ratio | 1-hour rolling window | > 95% cancels | Alert compliance team |
| Wash Trade Detection | Real-time (per trade) | Same beneficial owner | Block trade, flag account |
| Momentum Ignition | 5-minute window | Rapid order-cancel pattern | Halt account, review |
| Cross-Venue Layering | 30-second window | Orders on 3+ venues, <10% fill | Alert, potential restriction |
| Unusual Volume | vs. 20-day average | > 5x normal volume | Enhanced monitoring |
| Price Manipulation | LULD band interaction | 3+ halts in one session | Regulatory 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.