Low-Level Design: Stock Trading Platform — Order Matching, Portfolio, and Risk Checks

Core Entities

Account: account_id, user_id, account_type (CASH, MARGIN), buying_power, portfolio_value, status. Order: order_id, account_id, symbol, side (BUY/SELL), type (MARKET, LIMIT, STOP, STOP_LIMIT), quantity, limit_price, stop_price, time_in_force (DAY, GTC, IOC, FOK), status (PENDING, OPEN, PARTIALLY_FILLED, FILLED, CANCELLED, REJECTED), filled_quantity, avg_fill_price, created_at, updated_at. Fill: fill_id, order_id, quantity, price, counterparty_order_id, timestamp. Position: position_id, account_id, symbol, quantity, avg_cost_basis, current_price, unrealized_pnl. OrderBook: in-memory data structure, not persisted — reconstructed from exchange feed. Quote: symbol, bid_price, bid_size, ask_price, ask_size, last_price, last_size, timestamp.

Order Matching Engine

The order book maintains two priority queues per symbol: bids (sorted by price descending, then time ascending) and asks (sorted by price ascending, then time ascending). Price-time priority: the highest bid and lowest ask are matched first; among equal prices, the earlier order is matched first.

import heapq
from dataclasses import dataclass, field
from typing import Optional

@dataclass
class Order:
    order_id: str
    side: str  # BUY or SELL
    price: float
    quantity: int
    timestamp: int
    filled: int = 0

    def remaining(self) -> int:
        return self.quantity - self.filled

class OrderBook:
    def __init__(self):
        # bids: max-heap by price (negate price for max-heap)
        self.bids: list = []  # (-price, timestamp, order)
        # asks: min-heap by price
        self.asks: list = []  # (price, timestamp, order)

    def add_limit_order(self, order: Order) -> list[dict]:
        fills = []
        if order.side == "BUY":
            while self.asks and order.remaining() > 0:
                ask_price, ts, ask = self.asks[0]
                if ask_price > order.price:
                    break
                fill_qty = min(order.remaining(), ask.remaining())
                fill_price = ask_price  # ask price (maker sets price)
                order.filled += fill_qty
                ask.filled += fill_qty
                fills.append({"price": fill_price, "qty": fill_qty,
                              "buyer": order.order_id, "seller": ask.order_id})
                if ask.remaining() == 0:
                    heapq.heappop(self.asks)
            if order.remaining() > 0:
                heapq.heappush(self.bids, (-order.price, order.timestamp, order))
        else:  # SELL
            while self.bids and order.remaining() > 0:
                neg_price, ts, bid = self.bids[0]
                bid_price = -neg_price
                if bid_price  0:
                heapq.heappush(self.asks, (order.price, order.timestamp, order))
        return fills

Pre-Trade Risk Checks

Every order must pass risk checks before reaching the order book: Buying power check: for BUY orders, estimated_cost = quantity * (limit_price or last_price * 1.05 for market orders). Reject if estimated_cost > account.buying_power. Position limit check: total position in symbol after this order <= max_position_size (risk-configured per symbol or account type). Day trading buying power: for margin accounts, day trades use 4:1 intraday leverage but 2:1 overnight — check which limit applies. Order rate limit: max N orders per second per account to prevent runaway algorithms. Market order protection: reject market orders when the spread is > X% (protects against flash crashes filling at extreme prices).

Settlement and Position Tracking

On each fill: Update buying power: for BUY, deduct filled_qty * fill_price from buying_power. For SELL, add to buying_power (after T+1 or T+2 settlement period for equities). Update position: for BUY, add filled_qty to position.quantity; recalculate avg_cost_basis = (old_qty * old_avg + fill_qty * fill_price) / new_qty. For SELL, reduce position.quantity; calculate realized PnL = (fill_price – avg_cost_basis) * fill_qty; record in realized_pnl. Portfolio value: recomputed periodically by multiplying all positions by current market prices. All position updates happen in a transaction with the fill record insert — no fill without a corresponding position update.

See also: Coinbase Interview Prep

See also: Stripe Interview Prep

See also: LinkedIn Interview Prep

Scroll to Top