Building a Basic Order Book in C++
Two traders want to exchange the same asset. One is willing to pay $50,000. The other won't sell for less than $49,995. There's $5 of overlap and a trade should happen. But who notices, and how fast?
The data structure that finds that match is the order book. It works at microsecond speed across thousands of live orders. Every exchange, CEX or DEX, runs on one. This article builds one from scratch in C++.
Repository: cpp-trading
Overview
We'll create:
- An
Orderstruct to represent individual orders - An
OrderBookclass to manage bids and asks - Methods to add/remove orders and query market data
This implementation provides a foundation for more advanced trading systems like DEX market makers and execution engines.
Project Structure
Required Headers
#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <iomanip>
iostream: Input/output operationsstring: String handlingmap: Ordered data structures for price levelsvector: Collections of ordersiomanip: Output formatting
The Order Struct
struct Order {
int id;
double price;
int quantity;
bool is_buy;
Order(int i, double p, int q, bool buy)
: id(i), price(p), quantity(q), is_buy(buy) {}
};
Fields:
id: Unique identifier for the orderprice: Limit price (floating-point)quantity: Number of units to buy/sellis_buy: Boolean flag (true = BUY, false = SELL)
Why boolean instead of string? Booleans are faster and use less memory than string comparisons.
The OrderBook Class
Private Members
class OrderBook {
private:
std::map<double, int, std::greater<double>> bids;
std::map<double, int> asks;
Key design decisions:
bidsusesstd::greater<double>to sort prices in descending order (highest bid first)asksuses default ascending order (lowest ask first)- Both maps associate prices with total quantities at that price level
Adding Orders
void addOrder(const Order& order) {
if (order.is_buy) {
bids[order.price] += order.quantity;
} else {
asks[order.price] += order.quantity;
}
}
The bracket operator ([]) automatically creates entries if they don't exist, or adds to existing quantities. This aggregates orders at the same price level.
Removing Orders
void removeOrder(double price, int quantity, bool is_buy) {
if (is_buy) {
bids[price] -= quantity;
if (bids[price] <= 0) {
bids.erase(price);
}
} else {
asks[price] -= quantity;
if (asks[price] <= 0) {
asks.erase(price);
}
}
}
After subtracting quantity, we check if the price level is empty and remove it if necessary.
Querying Market Data
double getBestBid() const {
if (bids.empty()) {
return 0.0;
}
return bids.begin()->first;
}
double getBestAsk() const {
if (asks.empty()) {
return 0.0;
}
return asks.begin()->first;
}
double getSpread() const {
double bid = getBestBid();
double ask = getBestAsk();
if (bid == 0.0 || ask == 0.0) {
return 0.0;
}
return ask - bid;
}
Best bid/ask: Since bids is sorted descending and asks ascending, begin()->first gives us the best prices.
Spread: The difference between best ask and best bid—a key market metric.
Displaying the Order Book
void printTopOfBook() const {
std::cout << "\n=== ORDER BOOK ===\n";
std::cout << std::fixed << std::setprecision(2);
int count = 0;
for (const auto& [price, qty] : bids) {
if (count >= 5) break;
std::cout << "BID: $" << price << " @ " << qty << "\n";
count++;
}
count = 0;
for (const auto& [price, qty] : asks) {
if (count >= 5) break;
std::cout << "ASK: $" << price << " @ " << qty << "\n";
count++;
}
std::cout << "\nBest Bid: $" << getBestBid() << "\n";
std::cout << "Best Ask: $" << getBestAsk() << "\n";
std::cout << "Spread: $" << getSpread() << "\n";
}
This displays the top 5 price levels on each side, plus best bid/ask and spread.
Complete Example
int main() {
OrderBook book;
std::vector<Order> orders = {
Order(1, 123.50, 100, true), // Buy order
Order(2, 124.00, 150, false), // Sell order
Order(3, 123.75, 200, true), // Buy order
Order(4, 124.25, 75, false), // Sell order
Order(5, 123.25, 50, true) // Buy order
};
// Add all orders
for (const Order& order : orders) {
book.addOrder(order);
}
book.printTopOfBook();
// Remove an order
book.removeOrder(123.50, 100, true);
std::cout << "\nAfter removing order:\n";
book.printTopOfBook();
return 0;
}
Understanding the Output
=== ORDER BOOK ===
BID: $123.75 @ 200
BID: $123.50 @ 100
BID: $123.25 @ 50
ASK: $124.00 @ 150
ASK: $124.25 @ 75
Best Bid: $123.75
Best Ask: $124.00
Spread: $0.25
The order book shows:
- Bids sorted highest to lowest (best bid first)
- Asks sorted lowest to highest (best ask first)
- Spread of $0.25 between best bid and ask
Key Concepts
Price-Time Priority
In real exchanges, orders at the same price are prioritized by time (FIFO). Our simplified version aggregates by price only. A production system would use a more complex structure like:
std::map<double, std::queue<Order>> bids;
Market Depth
The order book provides "market depth"—visibility into liquidity at different price levels. This is crucial for:
- Market makers: Understanding where to place orders
- Traders: Assessing slippage for large orders
- Analytics: Calculating order book imbalance
Order Matching
While this basic implementation doesn't match orders, a full system would:
- Check if new order crosses the spread
- Match against opposite side at best price
- Execute trades and update order book
Extending the Implementation
Adding Order IDs to Price Levels
std::map<double, std::vector<int>> bids; // Track which orders are at each price
Implementing Order Matching
std::vector<Trade> matchOrder(const Order& order) {
std::vector<Trade> trades;
// Match against opposite side
// Return filled trades
}
Adding Timestamps
struct Order {
// ... existing fields
std::chrono::system_clock::time_point timestamp;
};
Performance Considerations
- Map vs. Unordered Map:
std::mapmaintains order (needed for best bid/ask), butstd::unordered_mapis faster for lookups - Memory: Aggregating by price reduces memory usage vs. storing individual orders
- Locking: In multi-threaded systems, consider using
std::shared_mutexfor concurrent reads
Common Pitfalls
- Empty order book: Always check
empty()before accessingbegin() - Price precision: Floating-point prices can cause precision issues—consider using fixed-point arithmetic
- Integer overflow: Large quantities might overflow—use
int64_tfor production
Conclusion
This basic order book implementation demonstrates core concepts:
- Data structure design for efficient price-level access
- Order aggregation at price levels
- Market data queries (best bid/ask, spread)
From here, you can extend it with:
- Order matching logic
- Trade execution
- Market data feeds
- Performance optimizations
The foundation is solid for building more advanced trading systems, which we'll explore in the next article on order execution engines.