Pocket Matching Engine
A limit order book that matches buys and sells by price-time priority, the rule most stock exchanges use, with a dashboard that shows every fill as it happens. The matching core is correct for whole-number quantities and handles more than a million random orders a second.
A four-level sweep



Engine and UI, cleanly apart
engine.js holds three classes: Order, Trade and MatchingEngine. It touches no DOM, which is why the unmodified file could be loaded into Node and benchmarked. ui.js validates input, calls the engine and re-renders both books, the trade log and a stats bar.
Each side of the book is one plain array kept in priority order. Bids sort by price high to low, asks by price low to high, and ties sort by timestamp. The best price is always at index 0.
| File | Lines | Role |
|---|---|---|
js/engine.js | 296 | Order book, matching, spread, volume |
js/ui.js | 328 | Input, rendering, demo, theme, keys |
index.html | 346 | Three-column layout, Tailwind config |
favicon.svg | 10 | Icon |
The match loop and the insert
// engine.js:102-116 · matching an incoming buy while (buyOrder.quantity > 0 && this.sellOrders.length > 0) { const bestSell = this.sellOrders[0]; if (buyOrder.price < bestSell.price) break; const tradeQty = Math.min(buyOrder.quantity, bestSell.quantity); const tradePrice = bestSell.price; // maker's price ... }
The sell side mirrors this with the comparison reversed.
// engine.js:201-216 · resting a bid for (let i = 0; i < this.buyOrders.length; i++) { const existing = this.buyOrders[i]; if (order.price > existing.price) { insertIndex = i; break; } if (order.price === existing.price && order.timestamp < existing.timestamp) { insertIndex = i; break; } } this.buyOrders.splice(insertIndex, 0, order);
First-in-first-out at a price, as long as the wall clock never steps back.
| Operation | Cost | Why |
|---|---|---|
| Insert a resting order | O(n) | Linear scan, then splice shifts the tail |
| Remove a filled head order | O(n) | Array.shift() moves every element |
| Sweep producing k fills | O(k·n) | One shift per full fill |
| Best bid / ask, spread | O(1) | Index 0 of each side |
| Total volume | O(trades) | Recomputed on every UI update |
Throughput, and where it falls off
Random flow (prices around $100 with a $2 spread of values, sizes 1 to 100) keeps about 21% of orders resting. Throughput drops from 1.3M to 0.5M orders a second as the book deepens to 21,523 orders.
The adversarial case is quadratic. Going from 10k to 50k orders (5×) took 30.6× longer: 1.78 seconds for 50k orders. That matters only for a deep book, and the UI adds a full re-render of both tables on every order anyway.
| Correctness scenario | Result |
|---|---|
| Buy 3 @ 101 against asks at 101, 99, 100 | Pass fills at 99, then 100, then 101 |
| Two bids at 100, then sell 7 @ 100 | Pass first bid fills 5, second fills 2 and rests as partial |
| Maker price rule, both sides | Pass ask 99 hit by buy 101 prints 99; bid 101 hit by sell 99 prints 101 |
| Incoming partial fill | Pass remainder of 3 rests with original quantity 8 |
| No cross | Pass both rest, spread $1.00 |
| Random invariants, 170k+ orders | Pass sorted, never crossed, quantity conserved |