Katkat

Design Exchange and Instrument Abstractions for Quote Arbitrage

Jane StreetSoftware Engineer · Technical Screen

Last updated

Design an in-memory cross-exchange quote aggregator and arbitrage detector that identifies profitable, actionable price discrepancies while handling invalid spreads, quote staleness, and order sizing.

Problem Description

In electronic trading, identical financial instruments trade simultaneously across multiple exchanges (venues). When the highest price an exchange is willing to buy at (best bid) exceeds the lowest price another exchange is willing to sell at (best ask), a cross-exchange arbitrage opportunity exists.

Design and implement a quote processing and arbitrage detection engine that:

  1. Ingests top-of-book market data updates from multiple venues for various instruments.
  2. Validates incoming quotes, rejecting corrupted or crossed single-venue books (where single-venue bid > ask).
  3. Discards quotes that have expired based on a configurable time-to-live (quote_ttl_ms).
  4. Detects cross-venue arbitrage opportunities and returns trade execution instructions maximizing immediate profit.

Interface

Implement the following classes and methods in Python:

from dataclasses import dataclass
from typing import Optional, List

@dataclass
class Quote:
    venue: str
    instrument: str
    bid_price: float
    bid_size: int
    ask_price: float
    ask_size: int
    timestamp_ms: int

@dataclass
class ArbitrageOpportunity:
    instrument: str
    buy_venue: str
    sell_venue: str
    buy_price: float
    sell_price: float
    quantity: int
    profit: float

class ArbitrageEngine:
    def __init__(self, quote_ttl_ms: int):
        """
        Initialize the engine with a maximum quote age in milliseconds.
        """
        pass

    def process_quote(self, quote: Quote) -> Optional[ArbitrageOpportunity]:
        """
        Process a new top-of-book quote. Updates internal state.
        Returns an ArbitrageOpportunity if an immediate cross-venue arb exists,
        otherwise returns None.
        """
        pass

Examples

Example 1

engine = ArbitrageEngine(quote_ttl_ms=500)

# Venue A publishes quote for AAPL
q1 = Quote(venue="EXCH_A", instrument="AAPL", bid_price=150.10, bid_size=100, ask_price=150.20, ask_size=50, timestamp_ms=1000)
engine.process_quote(q1) # Returns None (only 1 venue known)

# Venue B publishes quote for AAPL
q2 = Quote(venue="EXCH_B", instrument="AAPL", bid_price=149.90, bid_size=80, ask_price=150.05, ask_size=40, timestamp_ms=1050)
engine.process_quote(q2)
# Output: ArbitrageOpportunity(
#   instrument="AAPL",
#   buy_venue="EXCH_B",
#   sell_venue="EXCH_A",
#   buy_price=150.05,
#   sell_price=150.10,
#   quantity=40,
#   profit=2.00
# )
# Explanation: Sell on EXCH_A at 150.10, Buy on EXCH_B at 150.05. Volume = min(100, 40) = 40. Profit = (150.10 - 150.05) * 40 = 2.00.

Example 2: Corrupted Spread & Staleness

# Single venue crossed book (corrupted feed)
q3 = Quote(venue="EXCH_C", instrument="AAPL", bid_price=151.00, bid_size=50, ask_price=149.00, ask_size=50, timestamp_ms=1100)
engine.process_quote(q3) # Returns None (invalid quote: bid > ask on same venue)

# Stale quote check
q4 = Quote(venue="EXCH_B", instrument="AAPL", bid_price=149.00, bid_size=10, ask_price=150.00, ask_size=10, timestamp_ms=1600)
# EXCH_A's quote at timestamp 1000 is now 600ms old (> 500ms TTL), so it is stale and ignored.
engine.process_quote(q4) # Returns None

Constraints & Edge Cases

  • Prices are positive floating point numbers rounded to 2 decimal places.
  • Sizes are non-negative integers. If bid_size == 0, bid is inactive. If ask_size == 0, ask is inactive.
  • Input timestamps are monotonically non-decreasing per venue, but can arrive slightly interleaved across venues. Use the latest incoming timestamp as current time.
  • An arbitrage opportunity cannot buy and sell on the same venue.
  • Handle ties by prioritizing the pair with the greatest total profit, or greatest volume if profits are identical.

Hints

Hint 1

First validate incoming quotes by ensuring positive sizes and non-negative spreads (bid_price <= ask_price) on the same venue before updating venue state.

Hint 2

Maintain state partitioned by instrument, tracking the latest valid quote for each venue.

Hint 3

When evaluating potential arbitrage, filter out venues whose quotes are older than the engine's current reference time minus quote_ttl_ms.

Follow-up questions

  • How would you handle legs that partially fill or execute asynchronously, leaving one side of the arbitrage position unhedged (legging risk)?
  • How can you optimize the lookup of best bid and best ask across dozens of venues from O(V) to O(log V) without stale data lingering in priority queues?
  • If fees and rebates differ per exchange (e.g., maker-taker models), how would you adjust the net profit calculation and routing decisions?