Simulate an exchange and participation-rate trading
Company: Voleon
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Implement a simplified trading simulation in **three parts**.
## Part 1 — Process market data / exchange interaction (order book)
Design a minimal limit-order-book simulator for a **single symbol**.
### Events
Each event is one of:
- `NEW orderId side price qty`
- `side` is `B` (buy) or `S` (sell)
- Add a limit order.
- `CANCEL orderId`
- Remove the remaining quantity of that order (if it exists).
### Matching rules
When a new order arrives, immediately match it against resting orders on the opposite side using:
1. **Price priority**: best price first (highest buy, lowest sell).
2. **Time priority**: FIFO among orders at the same price.
A match produces one or more trades. A trade is:
- `tradePrice` = resting order’s price
- `tradeQty` = min(incomingRemaining, restingRemaining)
### Output
For every input event, output the list of trades generated by that event (may be empty).
---
## Part 2 — Execute for one client with participation rate
A single client submits a parent order:
- `clientId`, `side`, total target quantity `Q`, and participation rate `p` (0 < p <= 1).
You also receive a time-ordered stream of **market prints** (executed market volume):
- `PRINT t marketQty`
At each print, you may execute some quantity for the client, but must satisfy at all times:
\[
executedSoFar \le \lfloor p \cdot marketVolumeSoFar \rfloor
\]
Where `marketVolumeSoFar` is the cumulative sum of `marketQty` from prints up to time `t`.
### Output
For each print, output how many shares you execute for the client at that time (0 if none), until the client reaches `Q` or prints end.
---
## Part 3 — Multiple clients and fairness
Now there are multiple simultaneous clients, each with `(Q_i, p_i)`.
At each `PRINT`, compute executions for all clients such that:
1. For every client `i`: `executed_i <= floor(p_i * marketVolumeSoFar)`.
2. No client executes more than its remaining quantity.
3. Total executed at a print does not exceed that print’s `marketQty`.
4. If there isn’t enough available volume to satisfy everyone’s allowed amount, allocate **fairly** by prioritizing the client(s) with the smallest
\[
\frac{executed_i}{Q_i}
\]
ties broken by smaller `clientId`.
### Task
Implement the simulator for all three parts.
### Notes
- You may assume all inputs are validly formatted.
- You should state and handle edge cases like canceling unknown orders, or `p_i * marketVolumeSoFar` allowing more than remaining.
Quick Answer: This question evaluates competency in designing and implementing event-driven simulators, priority-based matching algorithms, and quota-constrained allocation for a limit order book, participation-rate execution, and multi-client fair-share distribution in the Coding & Algorithms domain.
Part 1: Single-Symbol Limit Order Book
Implement a minimal limit-order-book simulator for one symbol. Each event is either NEW orderId side price qty or CANCEL orderId. NEW adds a limit order after immediately matching it against resting orders on the opposite side. Matching uses price priority first, then FIFO time priority at the same price. A trade price is always the resting order price, and trade quantity is min of incoming remaining quantity and resting remaining quantity. CANCEL removes the remaining quantity of the order if it is still resting; canceling an unknown or already-filled order does nothing.
Constraints
- 0 <= len(events) <= 100000
- 1 <= price, qty <= 1000000000
- orderId contains no whitespace
- A NEW orderId will not duplicate an orderId that is currently resting
- CANCEL for an unknown, canceled, or fully-filled order must be ignored
- The total number of generated trades fits in memory
Examples
Input: ([],)
Expected Output: []
Explanation: No events means no per-event trade lists.
Input: (['NEW b1 B 100 10', 'NEW s1 S 99 4', 'NEW s2 S 100 10', 'NEW b2 B 101 5', 'CANCEL b2'],)
Expected Output: [[], [[100, 4]], [[100, 6]], [[100, 4]], []]
Explanation: Incoming sells trade at resting buy price 100. The final buy fills the remaining resting sell at price 100, then its remaining 1 share is canceled.
Hints
- Maintain one price-priority structure for buys and one for sells, plus FIFO queues for order ids at each price.
- For cancellation, lazy deletion is simpler: remove the order id from an active-order map and skip inactive ids when they reach the front of a price queue.
Part 2: Single-Client Participation Rate Executor
A single client submits a parent order with client id, side, target quantity Q, and participation rate p. You receive a time-ordered stream of market prints, each giving market volume at that time. After each print, you should execute as much as possible for the client while always maintaining executedSoFar <= floor(p * marketVolumeSoFar), and without exceeding the target quantity Q.
Constraints
- 0 <= len(prints) <= 100000
- 0 <= target_qty <= 1000000000
- 0 <= marketQty <= 1000000000
- 0 < participation_rate <= 1
- prints are already sorted by time
- participation_rate has at most 6 decimal places if given as a decimal
Examples
Input: ('C1', 'B', 100, 0.1, [[1, 50], [2, 70], [3, 500], [4, 100]])
Expected Output: [5, 7, 50, 10]
Explanation: The cumulative caps are 5, 12, 62, and 72, so each print executes the increase in the cap.
Input: ('C1', 'S', 10, 0.5, [[1, 5], [2, 10], [3, 100], [4, 100]])
Expected Output: [2, 5, 3, 0]
Explanation: The target of 10 is reached at the third print, so the fourth print executes 0.
Hints
- Keep cumulative market volume and cumulative client execution.
- At a print, the maximum cumulative client execution is floor(p times cumulative market volume); the new execution is the gap between that cap and what has already been executed, limited by remaining target quantity.
Part 3: Multi-Client Fair Participation Allocator
Multiple clients are active at the same time. Client i has a clientId, target quantity Q_i, and participation rate p_i. At every market print, compute executions for all clients while respecting each client's cumulative cap floor(p_i * marketVolumeSoFar), each client's remaining quantity, and the rule that total execution at that print cannot exceed that print's marketQty. If the print does not have enough volume to satisfy every client's newly allowed quantity, allocate shares fairly by repeatedly choosing the client with the smallest executed_i / Q_i ratio, breaking ties by smaller clientId.
Constraints
- 0 <= len(clients) <= 100
- 0 <= len(prints) <= 1000
- clientId values are unique integers
- 1 <= targetQty <= 1000000000 for each client
- 0 <= marketQty and sum of all marketQty values <= 200000
- 0 < participationRate <= 1
- participationRate has at most 6 decimal places if given as a decimal
Examples
Input: ([[1, 100, 0.1], [2, 50, 0.2]], [[1, 10], [2, 40]])
Expected Output: [[[1, 1], [2, 2]], [[1, 4], [2, 8]]]
Explanation: At both prints, marketQty is enough to give every client their newly allowed quantity.
Input: ([[1, 10, 1.0], [2, 10, 1.0]], [[1, 3], [2, 1]])
Expected Output: [[[1, 2], [2, 1]], [[1, 0], [2, 1]]]
Explanation: At the first print, ratios start tied, so client 1 gets the first share, client 2 gets the second, and client 1 gets the third. At the next print, client 2 has the lower completion ratio.
Hints
- For each print, first compute each client's newly allowed additional quantity from its cumulative cap minus what it has already executed.
- When volume is scarce, a min-heap keyed by executed_i / Q_i and then clientId simulates the fair one-share-at-a-time allocation.