Build an Order Book in C++
Build a limit order book and matching engine in C++ from scratch.
Introduction
30 Second Summary
Every time you buy a stock, your order enters a queue and gets matched against someone else's order in microseconds. The system doing that matching is called a matching engine, and it sits at the heart of every electronic exchange in the world.
In this project, you will build a fully functional simulated order book and matching engine in C++ from scratch, using the same core algorithm that powers exchanges like NYSE and Nasdaq.
What You'll Build
A C++ matching engine that maintains a live order book, accepts limit orders and market orders, matches them using price-time priority, handles partial fills and cancellations, and logs every executed trade.
By the end of this project, you'll have:
- A working order book that maintains sorted bid and ask price levels with FIFO order queues at each level.
- A matching engine that executes trades using price-time priority, supporting limit orders, market orders, and partial fills.
- A simulation that generates random order flow, displays live order book snapshots, and tracks bid-ask spread dynamics in your terminal.
- A market-making bot that posts orders on both sides of the spread, manages inventory, and tracks profit and loss.
Are there any prerequisites?
You should have a solid understanding of C++ including classes, pointers, and STL containers like std::map and std::list. You also need a g++ compiler with C++17 support and basic comfort with the command line.
Set Up the Project Structure
Before you can build a matching engine, you need a clean project structure and the core data types that everything else builds on.
Real exchanges represent prices as integers, not floating-point numbers. This avoids a nasty class of bugs where two prices that should be equal aren't, because of how IEEE 754 floating-point arithmetic works. In this step, you'll set up the project files and define the Order struct using integer tick pricing, the same approach used by NYSE and Nasdaq.
In This Step, Get Ready To:
- Create the project directory and all source files with a Makefile for compilation.
- Define the core types and Order struct with integer tick pricing.
- Compile and run a minimal test to verify everything links correctly.
Create the Project Directory and Files
You'll organize the project into separate header and source files. This keeps the code modular as the engine grows across future steps.
- Open your terminal and create a project directory.
mkdir orderbook-engine && cd orderbook-engine
- Create all the source files you'll need for the project.
touch Order.h OrderBook.h OrderBook.cpp MatchingEngine.h MatchingEngine.cpp main.cpp Makefile
Here's what each file will be responsible for:
- Order.h contains the core data types and the Order struct.
- OrderBook.h and OrderBook.cpp manage the bid and ask price levels.
- MatchingEngine.h and MatchingEngine.cpp handle order matching, market orders, and cancellations.
- main.cpp is the entry point where you'll test and run the simulation.
- Open Makefile in your text editor and add the following build configuration.
CXX = g++
CXXFLAGS = -std=c++17 -O2 -Wall
TARGET = orderbook
SRCS = main.cpp OrderBook.cpp MatchingEngine.cpp
$(TARGET): $(SRCS) Order.h OrderBook.h MatchingEngine.h
$(CXX) $(CXXFLAGS) -o $(TARGET) $(SRCS)
clean:
rm -f $(TARGET)
.PHONY: clean
What Does This Makefile Do?
This Makefile compiles all your .cpp files into a single executable called orderbook.
The -std=c++17 flag enables C++17 features like std::optional that you'll use later. -O2 turns on compiler optimizations for better performance. -Wall enables all compiler warnings so you catch potential issues early.
Pro tip: tab characters in Makefiles
The indented lines in a Makefile MUST use a tab character, not spaces. If you get an error like missing separator, your editor likely converted tabs to spaces. Check your editor settings or use the terminal command cat -A Makefile to verify tabs appear as ^I.
Define the Core Types and Order Struct
Every order in a real exchange has the same basic fields: an ID, a side (buy or sell), a price, a quantity, and a timestamp. The critical design decision here is how you represent prices.
Floating-point numbers seem natural for prices, but they introduce comparison bugs. For example, 100.20 + 100.30 might not equal 100.50 due to IEEE 754 floating-point representation. Real exchanges solve this by using integer ticks. A price of $100.50 becomes 10050 ticks. Integer comparisons are exact and fast.
- Open Order.h and add the following type definitions and struct.
#pragma once
#include <cstdint>
#include <chrono>
#include <string>
// Side of the order: buying or selling
enum class Side { BUY, SELL };
// Price in ticks (e.g., $100.50 = 10050 ticks)
// Integer representation avoids floating-point comparison bugs
using Price = int32_t;
// Quantity of shares/units in the order
using Quantity = uint32_t;
// Unique identifier for each order
using OrderId = uint64_t;
// Represents a single order in the book
struct Order {
OrderId id;
Side side;
Price price;
Quantity quantity;
std::chrono::steady_clock::time_point timestamp;
Order(OrderId id, Side side, Price price, Quantity quantity)
: id(id), side(side), price(price), quantity(quantity),
timestamp(std::chrono::steady_clock::now()) {}
};
// Represents an executed trade between two orders
struct Trade {
Price price;
Quantity quantity;
OrderId aggressor_id; // the incoming order that caused the match
OrderId resting_id; // the order that was already in the book
Side aggressor_side;
std::chrono::steady_clock::time_point timestamp;
};
// Helper to convert tick price to a readable dollar string
inline std::string tickToString(Price ticks) {
return std::to_string(ticks / 100) + "." +
(ticks % 100 < 10 ? "0" : "") +
std::to_string(ticks % 100);
}
Why Integer Ticks Instead of Doubles?
Consider this C++ code: double a = 100.50; double b = 100.20 + 100.30;. You'd expect a == b to be true, but it evaluates to false. This is because floating-point arithmetic introduces tiny rounding errors.
In a matching engine, price comparisons must be exact. A rounding error could cause an order to match at the wrong price level or miss a match entirely. Integer arithmetic has no rounding issues. This is why every real exchange uses integer ticks internally.
What Is a Tick?
A tick is the smallest possible price increment. In this project, one tick equals one cent. So $100.50 is represented as 10050 ticks. The tickToString() helper converts ticks back to a human-readable dollar format for display.
Compile and Verify the Setup
Before building anything complex, you want to make sure all files compile and link correctly. You'll write a minimal main.cpp that creates an Order and prints its fields.
- Open main.cpp and add the following test code.
#include <iostream>
#include "Order.h"
int main() {
// Create a test buy order: ID 1, BUY side, $100.50 (10050 ticks), 100 shares
Order order(1, Side::BUY, 10050, 100);
std::cout << "=== Order Book Engine Test ===" << std::endl;
std::cout << "Order ID: " << order.id << std::endl;
std::cout << "Side: " << (order.side == Side::BUY ? "BUY" : "SELL") << std::endl;
std::cout << "Price: " << tickToString(order.price)
<< " (" << order.price << " ticks)" << std::endl;
std::cout << "Quantity: " << order.quantity << std::endl;
std::cout << "Setup verified!" << std::endl;
return 0;
}
The OrderBook.cpp and MatchingEngine.cpp files need to exist for the Makefile to compile, but they can be empty for now.
- Add minimal placeholder content to both files so the compiler has something to work with.
// OrderBook implementation - to be built in Step 2
#include "Order.h"
// MatchingEngine implementation - to be built in Step 3
#include "Order.h"
You also need placeholder header files for the Makefile dependencies.
- Add include guards to both header files.
#pragma once
// OrderBook class declaration - to be built in Step 2
#pragma once
// MatchingEngine class declaration - to be built in Step 3
- Compile the project by running make in your terminal.
make
- Run the compiled executable.
./orderbook
I see the order details printed
You should see output like this:
=== Order Book Engine Test ===
Order ID: 1
Side: BUY
Price: 100.50 (10050 ticks)
Quantity: 100
Setup verified!
Notice how the price displays as 100.50 for human readability, but is stored internally as 10050 ticks. All matching logic will use the integer representation.
I see an error
Having Trouble Compiling?
Here are the most common issues:
- missing separator error: Your Makefile is using spaces instead of tabs for indentation. Replace the spaces before $(CXX) and rm with actual tab characters.
- g++ not found: Make sure you have a C++ compiler installed. On macOS, run xcode-select --install. On Ubuntu, run sudo apt install g++.
- File not found errors: Make sure all six source files exist in the same directory. Run ls to verify.
Paste your error message for help.
Your project structure is in place and the core Order type is defined with integer tick pricing. Next up, you'll build the order book data structure itself, with sorted bid and ask sides that maintain price-time priority at every level.
Build the Order Book Data Structure
You have the basic types and Order struct ready. Now you need the actual data structure that organizes all those orders into a functioning order book. An order book has two sides: bids (buy orders) and asks (sell orders). Each side is sorted by price, and at each price level, orders wait in a first-in-first-out queue. This is the core data structure that every electronic exchange in the world uses to track available liquidity.
In This Step, Get Ready To:
- Build the OrderBook class with sorted bid and ask sides using std::map and std::list.
- Implement queries for best bid, best ask, spread, and midprice.
- Build a formatted display that prints the order book state to the terminal.
Implement the OrderBook Class With Bid and Ask Sides
The order book needs to keep bids sorted from highest to lowest price, and asks sorted from lowest to highest. This way, the "best" bid (highest price someone will pay) and the "best" ask (lowest price someone will sell at) are always at the front of their respective sides.
The canonical architecture uses a sorted tree of price levels, where each price level contains a FIFO queue of orders. In C++, std::map is implemented as a Red-Black Tree, providing O(log n) insert, find, and erase with ordered iteration. Each map entry holds a std::list of orders at that price, giving you O(1) push to the back and pop from the front for FIFO matching.
- Open OrderBook.h and define the OrderBook class with the following code:
#pragma once
#include "Order.h"
#include <map>
#include <list>
#include <unordered_map>
#include <optional>
#include <iostream>
#include <iomanip>
#include <algorithm>
class OrderBook {
public:
// Add an order to the correct side and price level
void addOrder(const Order& order);
// Remove an order by its ID
bool cancelOrder(OrderId id);
// Query the top of book
std::optional<Price> getBestBid() const;
std::optional<Price> getBestAsk() const;
std::optional<int32_t> getSpread() const;
std::optional<double> getMidprice() const;
// Display the book state
void printBook(int depth = 5) const;
// Access the raw sides (needed by the matching engine)
auto& getBids() { return bids_; }
auto& getAsks() { return asks_; }
auto& getOrderIndex() { return orderIndex_; }
private:
// Bids: highest price first (descending)
std::map<Price, std::list<Order>, std::greater<Price>> bids_;
// Asks: lowest price first (ascending, default)
std::map<Price, std::list<Order>> asks_;
// Fast lookup: order ID -> (price, side) for cancellation
std::unordered_map<OrderId, std::pair<Price, Side>> orderIndex_;
};
Why std::greater for Bids?
By default, std::map sorts keys in ascending order. That works perfectly for asks, where you want the lowest price first. But for bids, you want the highest price first. Passing std::greater<Price> as the comparator reverses the sort order, so the best bid is always at bids_.begin().
What Is the Order Index For?
The orderIndex_ is an std::unordered_map that maps each order ID to its price and side. Without it, cancelling an order would require searching through every price level on both sides. With it, you get average O(1) lookup to find exactly where an order lives.
Now implement the addOrder method.
- Open OrderBook.cpp and add the following:
#include "OrderBook.h"
void OrderBook::addOrder(const Order& order) {
// Insert into the correct side based on BUY or SELL
if (order.side == Side::BUY) {
bids_[order.price].push_back(order);
} else {
asks_[order.price].push_back(order);
}
// Record location in the index for fast cancellation later
orderIndex_[order.id] = {order.price, order.side};
}
What Does This Code Do?
When an order arrives, addOrder does two things. First, it appends the order to the back of the list at its price level. If no orders exist at that price yet, std::map automatically creates a new entry. Second, it records the order's location in the index so you can find it instantly for cancellation.
Implement Best Bid, Best Ask, and Spread Queries
The bid-ask spread is the difference between the lowest ask price and the highest bid price. It represents the fundamental cost of immediacy in financial markets. The midprice is the average of the best bid and best ask, commonly used as a fair price estimate. Both of these values change constantly as orders are added and removed.
- Add the following methods to OrderBook.cpp:
std::optional<Price> OrderBook::getBestBid() const {
// Bids are sorted descending, so begin() is the highest price
if (bids_.empty()) return std::nullopt;
return bids_.begin()->first;
}
std::optional<Price> OrderBook::getBestAsk() const {
// Asks are sorted ascending, so begin() is the lowest price
if (asks_.empty()) return std::nullopt;
return asks_.begin()->first;
}
std::optional<int32_t> OrderBook::getSpread() const {
auto bid = getBestBid();
auto ask = getBestAsk();
// Spread only exists when both sides have orders
if (bid && ask) return *ask - *bid;
return std::nullopt;
}
std::optional<double> OrderBook::getMidprice() const {
auto bid = getBestBid();
auto ask = getBestAsk();
// Midprice is the average of best bid and best ask
if (bid && ask) return (*bid + *ask) / 2.0;
return std::nullopt;
}
Why std::optional?
std::optional is the C++17 way to represent a value that may or may not exist. When one side of the book is empty, there is no best bid or best ask. Returning std::nullopt makes this explicit instead of relying on magic sentinel values like -1 or 0.
Implement the Order Book Display and Test It
A visual display of the order book makes everything concrete. You will print the ask side above a separator (highest to lowest), then the bid side below (highest to lowest). This mirrors how traders see order books on their screens.
- Add the printBook method to OrderBook.cpp:
void OrderBook::printBook(int depth) const {
std::cout << "\n========== ORDER BOOK ==========\n";
std::cout << std::setw(10) << "PRICE" << std::setw(10) << "QTY"
<< std::setw(10) << "ORDERS" << "\n";
std::cout << "--- ASKS (sell side) -----------\n";
// Collect top N ask levels, then print in reverse so highest is on top
std::vector<std::pair<Price, const std::list<Order>*>> askLevels;
int count = 0;
for (auto& [price, orders] : asks_) {
if (count >= depth) break;
askLevels.push_back({price, &orders});
count++;
}
// Print asks from highest to lowest (reverse order)
for (auto it = askLevels.rbegin(); it != askLevels.rend(); ++it) {
Quantity totalQty = 0;
for (auto& o : *(it->second)) totalQty += o.quantity;
// Convert tick price to dollars for display
std::cout << std::setw(10) << std::fixed << std::setprecision(2)
<< (it->first / 100.0)
<< std::setw(10) << totalQty
<< std::setw(10) << it->second->size() << "\n";
}
std::cout << "--------------------------------\n";
std::cout << "--- BIDS (buy side) ------------\n";
// Print top N bid levels (already sorted highest first)
count = 0;
for (auto& [price, orders] : bids_) {
if (count >= depth) break;
Quantity totalQty = 0;
for (auto& o : orders) totalQty += o.quantity;
std::cout << std::setw(10) << std::fixed << std::setprecision(2)
<< (price / 100.0)
<< std::setw(10) << totalQty
<< std::setw(10) << orders.size() << "\n";
count++;
}
// Print spread and midprice if available
auto spread = getSpread();
auto mid = getMidprice();
std::cout << "================================\n";
if (spread) {
std::cout << "Spread: " << std::fixed << std::setprecision(2)
<< (*spread / 100.0) << " | ";
}
if (mid) {
std::cout << "Midprice: " << std::fixed << std::setprecision(2)
<< (*mid / 100.0);
}
std::cout << "\n\n";
}
What Does This Code Do?
The display collects the top N ask levels, reverses them so the highest ask appears at the top (matching how real trading screens look), then prints bid levels below. At each price level, it sums up the total quantity across all orders and shows the number of individual orders. The spread and midprice are printed at the bottom.
Notice the price / 100.0 conversion. Your prices are stored as integer ticks (e.g., 10050 = $100.50), but for display you convert back to dollars.
Now test everything by adding some orders manually.
- Update main.cpp with the following test code:
#include "OrderBook.h"
#include <iostream>
int main() {
OrderBook book;
// Add some buy orders (bids) at different prices
book.addOrder({1, Side::BUY, 10050, 100, 1}); // $100.50, 100 shares
book.addOrder({2, Side::BUY, 10050, 50, 2}); // $100.50, 50 shares (same level)
book.addOrder({3, Side::BUY, 10045, 200, 3}); // $100.45, 200 shares
book.addOrder({4, Side::BUY, 10040, 75, 4}); // $100.40, 75 shares
// Add some sell orders (asks) at different prices
book.addOrder({5, Side::SELL, 10055, 150, 5}); // $100.55, 150 shares
book.addOrder({6, Side::SELL, 10060, 80, 6}); // $100.60, 80 shares
book.addOrder({7, Side::SELL, 10065, 120, 7}); // $100.65, 120 shares
// Display the order book
book.printBook(5);
// Query top of book
auto bid = book.getBestBid();
auto ask = book.getBestAsk();
auto spread = book.getSpread();
if (bid) std::cout << "Best Bid: $" << (*bid / 100.0) << "\n";
if (ask) std::cout << "Best Ask: $" << (*ask / 100.0) << "\n";
if (spread) std::cout << "Spread: $" << (*spread / 100.0) << "\n";
return 0;
}
- Compile and run your project:
make && ./orderbook
What Should I See?
You should see a formatted order book with three ask levels above the separator and three bid levels below. The ask side shows $100.65 at the top, $100.55 at the bottom (closest to the spread). The bid side shows $100.50 at the top (closest to the spread), $100.40 at the bottom. The $100.50 bid level should show 150 total quantity across 2 orders.
The spread should be $0.05 (best ask $100.55 minus best bid $100.50). The midprice should be $100.525.
Getting Compilation Errors?
Make sure your Makefile includes OrderBook.cpp in the source files. Also verify you are compiling with -std=c++17 since std::optional requires C++17. Help me debug my compilation error.
Why This Approach Over a Flat Array?
Production HFT systems sometimes use flat arrays with one slot per tick for O(1) access and better cache locality. However, flat arrays require knowing the price range upfront and waste memory for sparse books. std::map is more general and teaches the same tree-based architecture that most reference implementations describe. The mental model transfers directly to production systems.
Your order book is holding orders, sorting them correctly, and displaying the state of the market. Next up, you will build the matching engine that actually executes trades when buy and sell orders cross.
Implement the Matching Engine
Your order book can store and display orders, but it can't actually execute trades yet. The matching engine is the component that decides when two orders should trade and at what price. It uses price-time priority, the same algorithm used by NYSE, Nasdaq, and most electronic exchanges worldwide.
In This Step, Get Ready To:
- Match incoming limit orders against resting orders on the opposite side of the book.
- Execute market orders that walk the book across multiple price levels.
- Cancel existing orders by ID using a fast lookup index.
Implement Limit Order Matching
When a new limit order arrives, the matching engine checks whether it can immediately trade against resting orders on the opposite side. A buy limit order matches if its price is greater than or equal to the best ask. A sell limit order matches if its price is less than or equal to the best bid. When a match occurs, orders at the best opposing price level are consumed in FIFO order, which is the "time priority" part of price-time priority.
Matching also needs to handle partial fills. If the incoming order wants 50 shares but the first resting order only has 20, you fill that resting order completely and keep matching the remaining 30 against the next order in the queue.
- Create MatchingEngine.h and define the MatchingEngine class that wraps an OrderBook.
- Define a Trade struct to record executed trades with fields for trade price, quantity, aggressor side, and both order IDs.
- Add a std::vector<Trade> member to store the trade log.
- Implement a submitLimitOrder(Order) method with the following matching logic.
// MatchingEngine.h
#pragma once
#include "OrderBook.h"
#include <vector>
#include <cstdint>
struct Trade {
Price price;
Quantity quantity;
Side aggressorSide;
OrderId buyOrderId;
OrderId sellOrderId;
uint64_t timestamp;
};
class MatchingEngine {
public:
MatchingEngine() = default;
// Submit a limit order - matches if possible, then adds remainder to book
void submitLimitOrder(Order order);
// Accessors
const OrderBook& getOrderBook() const { return book_; }
const std::vector<Trade>& getTrades() const { return trades_; }
private:
OrderBook book_;
std::vector<Trade> trades_;
uint64_t tradeTimestamp_ = 0;
// Core matching logic used by both limit and market orders
void matchOrder(Order& order);
};
- Now implement the matching logic in MatchingEngine.cpp.
// MatchingEngine.cpp
#include "MatchingEngine.h"
void MatchingEngine::matchOrder(Order& order) {
// Buy orders match against asks, sell orders match against bids
while (order.quantity > 0) {
// Check if there's a matching price on the opposite side
if (order.side == Side::BUY) {
auto bestAsk = book_.getBestAsk();
if (!bestAsk.has_value() || order.price < bestAsk.value())
break; // No match possible
} else {
auto bestBid = book_.getBestBid();
if (!bestBid.has_value() || order.price > bestBid.value())
break; // No match possible
}
// Get the best opposing price level's order queue
auto& oppositeLevel = book_.getBestLevel(
order.side == Side::BUY ? Side::SELL : Side::BUY
);
// Match against orders in FIFO order at this price level
while (order.quantity > 0 && !oppositeLevel.empty()) {
Order& resting = oppositeLevel.front();
Quantity fillQty = std::min(order.quantity, resting.quantity);
// Record the trade
Trade trade;
trade.price = resting.price; // Trade at the resting order's price
trade.quantity = fillQty;
trade.aggressorSide = order.side;
trade.buyOrderId = (order.side == Side::BUY) ? order.id : resting.id;
trade.sellOrderId = (order.side == Side::SELL) ? order.id : resting.id;
trade.timestamp = ++tradeTimestamp_;
trades_.push_back(trade);
// Update quantities
order.quantity -= fillQty;
resting.quantity -= fillQty;
// Remove fully filled resting order
if (resting.quantity == 0) {
book_.removeTopOrder(order.side == Side::BUY ? Side::SELL : Side::BUY);
}
}
}
}
void MatchingEngine::submitLimitOrder(Order order) {
// First, try to match against the opposite side
matchOrder(order);
// If any quantity remains, add it to the book as a resting order
if (order.quantity > 0) {
book_.addOrder(order);
}
}
Why Does the Trade Execute at the Resting Order's Price?
Notice that trade.price = resting.price, not the incoming order's price. The resting order was there first and set the terms. If you place a buy order at 10050 and the best ask is 10040, you get the better price of 10040. This is called "price improvement" and is how real exchanges work.
This implementation needs two helper methods on the OrderBook class that you haven't written yet: getBestLevel(Side) and removeTopOrder(Side).
- Add these two methods to your OrderBook class.
// Add to OrderBook.h (public methods)
std::list<Order>& getBestLevel(Side side);
void removeTopOrder(Side side);
// Add to OrderBook.cpp
std::list<Order>& OrderBook::getBestLevel(Side side) {
if (side == Side::BUY) {
return bids_.begin()->second; // Highest bid price level
} else {
return asks_.begin()->second; // Lowest ask price level
}
}
void OrderBook::removeTopOrder(Side side) {
if (side == Side::BUY) {
auto it = bids_.begin();
OrderId id = it->second.front().id;
it->second.pop_front();
orderIndex_.erase(id);
// Clean up empty price level
if (it->second.empty()) {
bids_.erase(it);
}
} else {
auto it = asks_.begin();
OrderId id = it->second.front().id;
it->second.pop_front();
orderIndex_.erase(id);
if (it->second.empty()) {
asks_.erase(it);
}
}
}
What Does This Code Do?
The getBestLevel() method returns a reference to the FIFO queue at the best price level on a given side. The removeTopOrder() method pops the front order from the best price level and cleans up the order index. If that was the last order at that price, the entire price level gets removed from the map.
Implement Market Order Execution
A market order has no price limit. It executes immediately against the best available prices, consuming liquidity until it is fully filled or the book is empty. When a large market order exhausts all the quantity at the best price level and continues to the next, it "walks the book." This is where the concept of slippage comes from in real trading. Each successive fill happens at a progressively worse price.
- Add a submitMarketOrder(Order) method to the MatchingEngine class.
// Add declaration to MatchingEngine.h
void submitMarketOrder(Order order);
// Add to MatchingEngine.cpp
void MatchingEngine::submitMarketOrder(Order order) {
// Market orders match at any price - set price to most aggressive possible
if (order.side == Side::BUY) {
order.price = std::numeric_limits<Price>::max();
} else {
order.price = std::numeric_limits<Price>::min();
}
matchOrder(order);
// If any quantity remains unfilled, the book ran out of liquidity
if (order.quantity > 0) {
std::cout << "WARNING: Market order " << order.id
<< " partially filled. Unfilled qty: "
<< order.quantity << std::endl;
}
}
What Does This Code Do?
The trick here is elegant. Instead of writing separate matching logic for market orders, you set the price to the most extreme possible value and reuse the same matchOrder() function. A buy market order with price set to INT_MAX will always be greater than or equal to any ask price, so it matches everything available.
- Add #include <limits> and #include <iostream> to the top of MatchingEngine.cpp if you haven't already.
Implement Order Cancellation and Test Everything
In real markets, the vast majority of orders are cancelled before they ever trade. Your engine needs a way to remove resting orders from the book. The unordered_map index you built in the previous step makes this fast. You look up the order's side and price level, then remove it from the std::list at that level.
- Add a cancelOrder(OrderId) method to the OrderBook class.
// Add declaration to OrderBook.h
bool cancelOrder(OrderId id);
// Add to OrderBook.cpp
bool OrderBook::cancelOrder(OrderId id) {
// Look up where this order lives using the index
auto indexIt = orderIndex_.find(id);
if (indexIt == orderIndex_.end()) {
return false; // Order not found
}
auto [price, side] = indexIt->second;
// Get the correct side's map
auto& sideMap = (side == Side::BUY) ? bids_ : asks_;
auto levelIt = sideMap.find(price);
if (levelIt == sideMap.end()) {
return false;
}
// Find and remove the order from the list at this price level
auto& orderList = levelIt->second;
for (auto it = orderList.begin(); it != orderList.end(); ++it) {
if (it->id == id) {
orderList.erase(it);
// Clean up empty price level
if (orderList.empty()) {
sideMap.erase(levelIt);
}
orderIndex_.erase(indexIt);
return true;
}
}
return false;
}
What About Cancellation Performance?
The index lookup is O(1) on average thanks to std::unordered_map. Finding the order within the std::list at the price level is O(n) where n is the number of orders at that price. This is acceptable for a simulation. Production systems use intrusive doubly-linked lists where prev/next pointers are embedded in the Order struct itself, giving O(1) cancellation.
- Add a cancelOrder(OrderId) pass-through method to MatchingEngine that delegates to the order book.
// Add to MatchingEngine.h
bool cancelOrder(OrderId id);
// Add to MatchingEngine.cpp
bool MatchingEngine::cancelOrder(OrderId id) {
return book_.cancelOrder(id);
}
Now test all three operations together.
- Update main.cpp with a test sequence that exercises limit order matching, partial fills, market orders, and cancellations.
#include "MatchingEngine.h"
#include <iostream>
int main() {
MatchingEngine engine;
OrderId nextId = 1;
// Add some resting sell orders (asks)
engine.submitLimitOrder({nextId++, Side::SELL, 10050, 20, 0});
engine.submitLimitOrder({nextId++, Side::SELL, 10050, 30, 0});
engine.submitLimitOrder({nextId++, Side::SELL, 10060, 50, 0});
// Add some resting buy orders (bids)
engine.submitLimitOrder({nextId++, Side::BUY, 10030, 25, 0});
engine.submitLimitOrder({nextId++, Side::BUY, 10020, 40, 0});
std::cout << "=== Initial Book ===" << std::endl;
engine.getOrderBook().printBook(5);
// Submit a buy limit order that crosses the spread and partially fills
std::cout << "\n=== Buy limit @ 10050, qty 35 ===" << std::endl;
engine.submitLimitOrder({nextId++, Side::BUY, 10050, 35, 0});
engine.getOrderBook().printBook(5);
// Submit a market sell order that walks the book
std::cout << "\n=== Market sell, qty 50 ===" << std::endl;
engine.submitMarketOrder({nextId++, Side::SELL, 0, 50, 0});
engine.getOrderBook().printBook(5);
// Cancel an order
std::cout << "\n=== Cancel order 5 ===" << std::endl;
engine.cancelOrder(5);
engine.getOrderBook().printBook(5);
// Print trade log
std::cout << "\n=== Trade Log ===" << std::endl;
for (const auto& trade : engine.getTrades()) {
std::cout << "Trade: price=" << trade.price
<< " qty=" << trade.quantity
<< " aggressor=" << (trade.aggressorSide == Side::BUY ? "BUY" : "SELL")
<< std::endl;
}
return 0;
}
- Compile and run your program.
make && ./orderbook
What Should I See?
The initial book should show asks at 10050 (qty 50 total) and 10060 (qty 50), with bids at 10030 (qty 25) and 10020 (qty 40). After the buy limit order at 10050 for 35 shares, the first resting sell order (qty 20) should be fully consumed and the second sell order (qty 30) should be partially filled to 15. The trade log should show multiple fills including partial fills.
Getting a Segfault or Empty Book Crash?
The most common bug is calling getBestLevel() when a side of the book is empty. Make sure your matchOrder() function checks getBestBid() or getBestAsk() with has_value() before accessing the level. Help me debug my matching engine crash.
Your matching engine now supports all three core operations: limit orders, market orders, and cancellations. Next up, you'll build a simulation that generates thousands of random orders and watch your order book come alive with realistic price discovery and spread dynamics.
Build the Simulation
You have a fully functional matching engine that can process limit orders, market orders, and cancellations. Now it is time to put it under pressure. In this step, you will build a simulation that generates thousands of random orders, feeds them through your engine, and lets you watch the order book evolve in real time.
In This Step, Get Ready To:
- Create a random order generator that produces a realistic mix of limit orders, market orders, and cancellations.
- Build a simulation loop with live terminal output showing the order book, recent trades, and performance stats.
- Track spread and midprice dynamics across the entire simulation run.
Create the Random Order Generator
A matching engine is only interesting when orders are flowing through it. Real markets see a mix of order types: most activity is limit orders adding and removing liquidity, with market orders occasionally sweeping through the book. Your generator will mimic this pattern.
- Open main.cpp and add the following includes at the top of the file.
#include <random>
#include <chrono>
#include <thread>
- Add a generateRandomOrder function above your main() function.
// Tracks the next available order ID across the simulation
static OrderId nextOrderId = 1;
struct SimOrder {
enum Type { LIMIT, MARKET, CANCEL };
Type type;
Order order; // used for LIMIT and MARKET
OrderId cancelId; // used for CANCEL
};
SimOrder generateRandomOrder(
std::mt19937& rng,
Price referencePrice,
const std::vector<OrderId>& activeOrderIds
) {
// Decide order type: 60% limit, 25% market, 15% cancel
std::uniform_int_distribution<int> typeDist(1, 100);
int roll = typeDist(rng);
// Randomly pick buy or sell
std::uniform_int_distribution<int> sideDist(0, 1);
Side side = sideDist(rng) == 0 ? Side::BUY : Side::SELL;
if (roll <= 60) {
// Limit order: price normally distributed around reference
std::normal_distribution<double> priceDist(referencePrice, 20.0);
Price price = static_cast<Price>(priceDist(rng));
if (price <= 0) price = 1;
std::uniform_int_distribution<uint32_t> qtyDist(1, 100);
Quantity qty = qtyDist(rng);
Order ord{nextOrderId++, side, price, qty, 0};
return SimOrder{SimOrder::LIMIT, ord, 0};
} else if (roll <= 85) {
// Market order: no price, just quantity
std::uniform_int_distribution<uint32_t> qtyDist(1, 50);
Quantity qty = qtyDist(rng);
// Price of 0 signals a market order
Order ord{nextOrderId++, side, 0, qty, 0};
return SimOrder{SimOrder::MARKET, ord, 0};
} else {
// Cancel: pick a random active order
if (activeOrderIds.empty()) {
// Nothing to cancel, fall back to a limit order
std::normal_distribution<double> priceDist(referencePrice, 20.0);
Price price = static_cast<Price>(priceDist(rng));
if (price <= 0) price = 1;
std::uniform_int_distribution<uint32_t> qtyDist(1, 100);
Order ord{nextOrderId++, side, price, qtyDist(rng), 0};
return SimOrder{SimOrder::LIMIT, ord, 0};
}
std::uniform_int_distribution<size_t> idxDist(0, activeOrderIds.size() - 1);
OrderId cancelTarget = activeOrderIds[idxDist(rng)];
return SimOrder{SimOrder::CANCEL, Order{}, cancelTarget};
}
}
What Does This Code Do?
The generator produces a realistic mix of order flow. About 60% of orders are limit orders with prices that cluster around a reference price using a normal distribution (standard deviation of 20 ticks). This creates a natural bell curve of prices around the current market level.
Market orders make up 25% and have smaller quantities (1-50) since large market orders would walk through the entire book. The remaining 15% are cancellations of existing orders. This ratio roughly mirrors real market activity where adds and cancels dominate.
What Is std::mt19937?
std::mt19937 is the Mersenne Twister pseudorandom number generator from C++'s <random> header. It produces high-quality random numbers and is seeded with std::random_device for true randomness. You feed it into distribution objects like std::normal_distribution and std::uniform_int_distribution to shape the output.
Build the Simulation Loop With Live Output
The simulation loop ties everything together. It generates orders, feeds them to the matching engine, and periodically prints a snapshot of the order book so you can watch price discovery happen in real time.
- Replace your existing main() function with the simulation loop below.
int main() {
// Seed the random number generator
std::random_device rd;
std::mt19937 rng(rd());
MatchingEngine engine;
std::vector<OrderId> activeOrderIds;
const int totalOrders = 10000;
const Price referencePrice = 10000; // $100.00 in ticks
// Start the performance timer
auto startTime = std::chrono::high_resolution_clock::now();
for (int i = 0; i < totalOrders; ++i) {
SimOrder sim = generateRandomOrder(rng, referencePrice, activeOrderIds);
switch (sim.type) {
case SimOrder::LIMIT:
engine.submitLimitOrder(sim.order);
activeOrderIds.push_back(sim.order.id);
break;
case SimOrder::MARKET:
engine.submitMarketOrder(sim.order);
break;
case SimOrder::CANCEL:
engine.cancelOrder(sim.cancelId);
// Remove from active list
activeOrderIds.erase(
std::remove(activeOrderIds.begin(), activeOrderIds.end(), sim.cancelId),
activeOrderIds.end()
);
break;
}
// Print a snapshot every 100 orders
if ((i + 1) % 100 == 0) {
// Clear terminal (works on macOS/Linux)
std::cout << "\033[2J\033[H";
std::cout << "=== Order Book Simulation ==="
<< " [Order " << (i + 1) << " / " << totalOrders << "]\n\n";
engine.getOrderBook().printBook(5);
// Print last 5 trades
const auto& trades = engine.getTrades();
std::cout << "\n--- Recent Trades ---\n";
int tradeStart = std::max(0, (int)trades.size() - 5);
for (int t = tradeStart; t < (int)trades.size(); ++t) {
const auto& trade = trades[t];
std::cout << " Price: " << trade.price
<< " Qty: " << trade.quantity
<< " Aggressor: " << (trade.aggressorSide == Side::BUY ? "BUY" : "SELL")
<< "\n";
}
// Print running stats
std::cout << "\n--- Statistics ---\n";
std::cout << " Total trades: " << trades.size() << "\n";
uint64_t totalVolume = 0;
for (const auto& t : trades) totalVolume += t.quantity;
std::cout << " Total volume: " << totalVolume << "\n";
// Brief pause so you can see the output
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
// Calculate throughput
auto endTime = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
std::cout << "\n=== Simulation Complete ===\n";
std::cout << "Processed " << totalOrders << " orders in "
<< elapsed.count() << " ms\n";
if (elapsed.count() > 0) {
double throughput = (totalOrders * 1000.0) / elapsed.count();
std::cout << "Throughput: " << static_cast<int>(throughput)
<< " orders/sec\n";
}
return 0;
}
What Does This Code Do?
The loop processes 10,000 orders. Every 100 orders, it clears the terminal and prints a fresh snapshot of the order book (top 5 levels on each side), the last 5 executed trades, and running statistics. The 50ms pause between snapshots lets you watch the book evolve.
At the end, it uses std::chrono::high_resolution_clock to measure total elapsed time and calculate throughput in orders per second. This gives you a baseline for how fast your engine runs.
Pro tip: Windows terminal clearing
The escape sequence \033[2J\033[H clears the terminal on macOS and Linux. On Windows, you can replace it with system("cls") or enable virtual terminal processing. Most modern Windows terminals (Windows Terminal, VS Code) support the escape sequence natively.
- Make sure you have #include <algorithm> at the top of your file for std::remove.
- Compile and run the simulation.
make && ./orderbook
You should see the order book updating in your terminal every 100 orders, with prices clustering around the 10000 tick level. The bid and ask sides form naturally as limit orders accumulate, and market orders periodically sweep through the best prices.
Compilation Errors?
If you see errors about getTrades() or getOrderBook(), make sure your MatchingEngine class exposes these as public methods that return const references. If you see errors about std::remove, add #include <algorithm>. Help me debug my compilation errors.
Add Spread and Midprice Tracking
Watching the order book update is satisfying, but the real insight comes from tracking how the bid-ask spread and midprice behave over time. The spread is the fundamental cost of immediacy in financial markets. Tracking it reveals how your simulated market behaves under different conditions.
- Add tracking vectors and summary statistics to your main() function. Declare these vectors before the simulation loop.
// Add these before the for loop
std::vector<Price> spreadHistory;
std::vector<Price> midpriceHistory;
- Inside the simulation loop, after processing each order, record the spread and midprice. Add this code right after the switch statement.
// Record spread and midprice after each order
auto spread = engine.getOrderBook().getSpread();
auto midprice = engine.getOrderBook().getMidprice();
if (spread.has_value()) {
spreadHistory.push_back(spread.value());
}
if (midprice.has_value()) {
midpriceHistory.push_back(midprice.value());
}
- After the simulation loop ends (before return 0), add the summary statistics output.
// Print summary statistics
const auto& trades = engine.getTrades();
uint64_t totalVolume = 0;
for (const auto& t : trades) totalVolume += t.quantity;
std::cout << "\n=== Summary Statistics ===\n";
std::cout << "Total trades executed: " << trades.size() << "\n";
std::cout << "Total volume traded: " << totalVolume << "\n";
if (!spreadHistory.empty()) {
Price minSpread = *std::min_element(spreadHistory.begin(), spreadHistory.end());
Price maxSpread = *std::max_element(spreadHistory.begin(), spreadHistory.end());
double avgSpread = 0;
for (Price s : spreadHistory) avgSpread += s;
avgSpread /= spreadHistory.size();
std::cout << "Average spread: " << avgSpread << " ticks\n";
std::cout << "Min spread: " << minSpread << " ticks\n";
std::cout << "Max spread: " << maxSpread << " ticks\n";
}
if (!midpriceHistory.empty()) {
double avgMid = 0;
for (Price m : midpriceHistory) avgMid += m;
avgMid /= midpriceHistory.size();
std::cout << "Average midprice: " << avgMid << " ticks";
std::cout << " ($" << avgMid / 100.0 << ")\n";
}
What Should I See?
The average spread should settle to a relatively small number of ticks. The midprice should hover around 10000 ticks ($100.00) since your limit order prices are normally distributed around that reference. You will likely see the spread widen occasionally when market orders sweep through multiple price levels. This is exactly how real markets behave.
- Compile and run the full simulation with statistics.
make && ./orderbook
Watch the simulation run through all 10,000 orders. When it finishes, you will see the full summary statistics printed below the final order book snapshot.
Why Does This Matter?
These statistics capture the core dynamics of market microstructure. The spread tells you the cost of immediacy. A tighter spread means cheaper trading. The midprice tracks where the market thinks the "fair" price is. In real markets, HFT firms spend millions optimizing systems to tighten spreads by fractions of a tick.
Your simulation is generating realistic order flow, matching trades with price-time priority, and tracking the key metrics that define market behavior. Next up, you will take on the secret mission and build a market-making bot that competes against this random flow.
Secret mission
Build a Market-Making Bot
You have a working matching engine. Now put it to work. Implement a naive market-making strategy that continuously posts limit orders on both sides of the spread, trying to profit from the bid-ask spread while managing inventory risk.
Before you go
Clean Up Your Resources
This project runs entirely locally using only the C++ standard library, so there are no ongoing cloud costs. Decide whether to keep your project files, or delete them entirely.
Resources you used:
- Project directory containing all source files (Order.h, OrderBook.h, OrderBook.cpp, MatchingEngine.h, MatchingEngine.cpp, main.cpp).
- Makefile used for compilation.
- Compiled binary (orderbook executable).
Keep everything running
No action needed. Choose this if you want to keep experimenting with your matching engine, tweak simulation parameters, or continue building on top of it.
- Your project files stay in place and the compiled binary is ready to run anytime.
- There are no background processes or services running, so nothing consumes resources while idle.
Pause - I'll come back to this later
Clean up the compiled binary to free up a small amount of disk space while keeping your source code intact.
- Run make clean in your project directory to remove the compiled binary. If your Makefile doesn't have a clean target, manually delete the orderbook executable.
- Your source files (.h and .cpp files) remain untouched. Recompile anytime with make.
Delete - I don't want to use this again
Remove all project files and start fresh.
- Navigate to the parent directory of your project folder.
- Delete the entire project directory with rm -rf your-project-folder-name (macOS/Linux) or by right-clicking and selecting Delete in your file explorer (Windows).
- This removes all source files, the Makefile, and the compiled binary. Nothing else on your system is affected.
Mission accomplished
That's a Wrap!
You just built the engine that powers every stock exchange in the world. From scratch, using nothing but the C++ standard library.
Here's what you accomplished:
- Built a limit order book with separate bid and ask sides using std::map and std::list to enforce price-time priority, the same matching algorithm used by NYSE and Nasdaq.
- Implemented a matching engine that handles limit orders, market orders, partial fills, and cancellations with integer tick pricing to avoid floating-point bugs.
- Ran a simulation with random order flow and observed spread dynamics, price discovery, and throughput in real time through terminal output.
- Built a market-making bot that posts on both sides of the spread, manages inventory risk, and tracks P&L against random order flow.
Ready to quiz yourself?