gtat-tech-career-kickstarte.../solution/README.md

4.4 KiB
Raw Permalink Blame History

Optivex — Reference Solution

Reference implementation of the Optivex exchange and the system tests used to evaluate participant submissions.

Architecture Overview

Four components communicate over TCP using length-prefixed Protobuf 3 messages:

                   ┌──────────┐
                   │  Admin   │
                   └──┬───┬───┘
                      │   │
              ┌───────┘   └────────┐
              ▼                    ▼
       ┌──────────────┐    ┌──────────────┐
       │  Order Book  │◄───│     Info     │
       └──────────────┘    └──────────────┘
              ▲                    ▲
              │    ┌───────────────┘
              │    │
       ┌──────┴────┴─────┐
       │  Risk Gateway   │
       └─────────────────┘
  • Order Book — Matching engine. No upstream dependencies.
  • Info — Market data. Mirrors order book state to serve TOP_OF_BOOK and PRICE_DEPTH_BOOK subscriptions.
  • Admin — Instrument lifecycle. Orchestrates creation across Order Book and Info.
  • Risk Gateway — Execution + risk. Enforces limits before forwarding orders to Order Book. Implements both the execution and risk_limits protocols.

Protocol Extensions vs Template

The solution adds internal management messages for Admin ↔ Order Book and Admin ↔ Info communication. These are not part of the participant-facing API.

Added to common.proto (numbered at 130+/140+ to avoid collisions with the public API range 067):

Message Type Purpose
INFO_CREATE_INSTRUMENT_REQUEST/RESPONSE (130131) Admin → Info: register instrument with order_book_id
ORDER_BOOK_CREATE_REQUEST/RESPONSE (140141) Admin → Order Book: create order book
ORDER_BOOK_DELETE_REQUEST/RESPONSE (142143) Admin → Order Book: delete order book

Note that info.CreateInstrumentRequest carries order_book_id (book already exists), while admin.CreateInstrumentRequest carries tick_size (book doesn't exist yet). The Admin component bridges this gap with a two-phase flow: create order book first, then register the instrument in Info with the resulting ID.

Non-Obvious Design Decisions

Matching engine uses negated prices for max-heap

Python's heapq is min-heap only. Bids need max-heap behavior (highest price first). The sort key negates the price for bids: (-price, timestamp, order_id) vs (+price, timestamp, order_id) for asks. This gives price-time priority with order_id as a final tiebreaker.

Cancel uses list.remove() + heapify() — O(n) rather than O(log n) with lazy deletion, but sufficient for this scale.

Info mirrors order book state from the event stream

Info doesn't access the matching engine directly. OrderBookClientAggregator reconstructs book state from OnOrderInserted, OnOrderCancelled, and OnTrade events, maintaining quantity-per-price-level aggregates.

A pending trades mechanism handles the ordering issue where OnOrderInserted references trade_ids for trades that haven't arrived yet. While pending trades exist, the book is considered inconsistent and market data updates are suppressed. Reads (get_top_of_book, get_price_depth_book) assert consistency.

Rolling window limits use in-place list pruning

RiskLimitsStore maintains timestamped event lists for each rolling metric (message rate, order quantity, order amount). On each check, entries older than now - window_seconds are pruned with del list[:first_valid]. This mutates the list in place — O(n) per check but amortized O(1) per entry since each is pruned exactly once.

Limit checks follow a check-then-record pattern: check_order_limits() validates without side effects, and record_order_attempt() is called only after the check passes. This avoids polluting rolling windows with rejected orders.

Decimal precision avoids float artifacts

All prices and amounts go through decimal_from_float(): Decimal(str(value)).quantize(Decimal("0.0001")). The str() intermediate avoids the well-known Decimal(0.1)0.100000000000000005... problem.