Skip to main content

Building a Basic Order Book in C++

Order books are fundamental data structures in trading systems. They maintain lists of buy and sell orders, match them, and provide real-time market depth information. In this article, we'll build a basic order book implementation in C++ that demonstrates core trading system concepts.

Overview

We'll create:

  • An Order struct to represent individual orders
  • An OrderBook class 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 operations
  • string: String handling
  • map: Ordered data structures for price levels
  • vector: Collections of orders
  • iomanip: 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 order
  • price: Limit price (floating-point)
  • quantity: Number of units to buy/sell
  • is_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:

  • bids uses std::greater<double> to sort prices in descending order (highest bid first)
  • asks uses 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:

  1. Check if new order crosses the spread
  2. Match against opposite side at best price
  3. 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::map maintains order (needed for best bid/ask), but std::unordered_map is faster for lookups
  • Memory: Aggregating by price reduces memory usage vs. storing individual orders
  • Locking: In multi-threaded systems, consider using std::shared_mutex for concurrent reads

Common Pitfalls

  1. Empty order book: Always check empty() before accessing begin()
  2. Price precision: Floating-point prices can cause precision issues—consider using fixed-point arithmetic
  3. Integer overflow: Large quantities might overflow—use int64_t for 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.


Read more from Cryptogrammar