Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,6 @@
"unordered_set": "cpp",
"iomanip": "cpp",
"codecvt": "cpp"
}
},
"liveServer.settings.port": 5501
}
2 changes: 1 addition & 1 deletion compile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -euo pipefail

BUILD_DIR="${BUILD_DIR:-$(pwd)/build}"
BUILD_TYPE="${BUILD_TYPE:-Debug}"
BUILD_TYPE="${BUILD_TYPE:-Release}"
GENERATOR="${GENERATOR:-Ninja}"

cmake -S . -B "$BUILD_DIR" -G "$GENERATOR" -DCMAKE_BUILD_TYPE="$BUILD_TYPE"
Expand Down
224 changes: 222 additions & 2 deletions gtests/example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ namespace {
uint64_t ts = ltime::rdtsc_lfence();
return BenchmarkOrder{id, type, price, qty, ts, seq};
}

}

TEST(RegularQueueTest, EnqueueDequeueAndFront) {
Expand Down Expand Up @@ -227,4 +226,225 @@ TEST(TestInputsTest, ParseArgsFillsParams) {
EXPECT_EQ(params.thread_count, 4u);
EXPECT_EQ(params.thread_order_limit, 100u);
EXPECT_EQ(params.total_orders, 400u);
}
}

TEST(RegularQueueTest, MultipleEnqueueDequeue) {
RegularQueue<BenchmarkOrder> q;
std::vector<BenchmarkOrder> orders;

for (int i = 0; i < 10; ++i) {
orders.push_back(makeOrder(i, OrderType::BUY, 100.0 + i, i + 1, i));
EXPECT_TRUE(q.enqueueOrder(orders[i]));
}

EXPECT_EQ(q.size(), 10u);

for (int i = 0; i < 10; ++i) {
BenchmarkOrder out{};
EXPECT_TRUE(q.dequeueOrder(out));
EXPECT_EQ(out.order_id, i);
EXPECT_EQ(out.sequence_number, i);
}

EXPECT_TRUE(q.empty());
}

TEST(RegularQueueTest, AlternateEnqueueDequeue) {
RegularQueue<BenchmarkOrder> q;

auto order1 = makeOrder(1, OrderType::SELL, 50.0, 10, 0);
EXPECT_TRUE(q.enqueueOrder(order1));

BenchmarkOrder out1{};
EXPECT_TRUE(q.dequeueOrder(out1));
EXPECT_EQ(out1.order_id, 1);

auto order2 = makeOrder(2, OrderType::BUY, 60.0, 20, 1);
EXPECT_TRUE(q.enqueueOrder(order2));

BenchmarkOrder out2{};
EXPECT_TRUE(q.dequeueOrder(out2));
EXPECT_EQ(out2.order_id, 2);

EXPECT_TRUE(q.empty());
}

// MCLockFreeQueue

TEST(MCLockFreeQueueTest, MultipleEnqueueDequeue) {
MCLockFreeQueue<BenchmarkOrder> q;

for (int i = 0; i < 20; ++i) {
auto order = makeOrder(i, OrderType::BUY, 100.0 + i, i + 1, i);
EXPECT_TRUE(q.enqueueOrder(order));
}

EXPECT_EQ(q.getSize(), 20u);

for (int i = 0; i < 20; ++i) {
BenchmarkOrder out{};
EXPECT_TRUE(q.dequeueOrder(out));
EXPECT_EQ(out.order_id, i);
}

EXPECT_TRUE(q.isEmpty());
}

TEST(MCLockFreeQueueTest, GetFrontConsistency) {
MCLockFreeQueue<BenchmarkOrder> q;

auto order1 = makeOrder(100, OrderType::SELL, 200.0, 50, 5);
EXPECT_TRUE(q.enqueueOrder(order1));

BenchmarkOrder front1{}, front2{};
EXPECT_TRUE(q.getFront(front1));
EXPECT_TRUE(q.getFront(front2));
EXPECT_EQ(front1.order_id, front2.order_id);
EXPECT_EQ(front1.order_id, 100);
}

// MCConcurrentQueue

TEST(MCConcurrentQueueTest, LargeSequence) {
MCConcurrentQueue<BenchmarkOrder> q;

for (int i = 0; i < 100; ++i) {
auto order = makeOrder(i, (i % 2 == 0) ? OrderType::BUY : OrderType::SELL,
100.0 + (i % 20), i + 1, i);
EXPECT_TRUE(q.enqueueOrder(order));
}

EXPECT_EQ(q.getSize(), 100u);

for (int i = 0; i < 100; ++i) {
BenchmarkOrder out{};
EXPECT_TRUE(q.dequeueOrder(out));
EXPECT_EQ(out.order_id, i);
}

EXPECT_TRUE(q.isEmpty());
EXPECT_EQ(q.getSize(), 0u);
}

// MatchingEngine

TEST(MatchingEngineTest, PartialFillScenario) {
MatchingEngine<BenchmarkOrder> engine(100.0);

// sell 50 units at 100.50
auto sell = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(1, OrderType::SELL, 100.50, 50, 1)));
engine.processOrder(sell.get());

// buy 30 units at 100.60 (crosses)
auto buy1 = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(2, OrderType::BUY, 100.60, 30, 2)));
engine.processOrder(buy1.get());

EXPECT_EQ(buy1->quantity, 0u); // fully matched
EXPECT_EQ(sell->quantity, 20u); // partially filled

// buy remaining 20 units
auto buy2 = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(3, OrderType::BUY, 100.60, 20, 3)));
engine.processOrder(buy2.get());

EXPECT_EQ(buy2->quantity, 0u);
EXPECT_EQ(sell->quantity, 0u);
}

TEST(MatchingEngineTest, NoCrossAtDifferentPrices) {
MatchingEngine<BenchmarkOrder> engine(100.0);

// sell at 101.00
auto sell = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(1, OrderType::SELL, 101.00, 10, 1)));
engine.processOrder(sell.get());

// buy at 100.00 (below sell, no cross)
auto buy = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(2, OrderType::BUY, 100.00, 5, 2)));
engine.processOrder(buy.get());

EXPECT_EQ(buy->quantity, 5u); // not filled
EXPECT_EQ(sell->quantity, 10u); // not filled
}

TEST(MatchingEngineTest, BuyTakesSellAtMultipleLevels) {
MatchingEngine<BenchmarkOrder> engine(100.0);

// sells at different price levels
auto sell1 = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(1, OrderType::SELL, 100.50, 5, 1)));
auto sell2 = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(2, OrderType::SELL, 101.00, 8, 2)));
auto sell3 = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(3, OrderType::SELL, 101.50, 10, 3)));

engine.processOrder(sell1.get());
engine.processOrder(sell2.get());
engine.processOrder(sell3.get());

// buy at aggressive price that crosses multiple levels
auto buy = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(4, OrderType::BUY, 102.00, 20, 4)));
engine.processOrder(buy.get());

// sell1 (5) + sell2 (8) + partial sell3 (7)
EXPECT_EQ(sell1->quantity, 0u);
EXPECT_EQ(sell2->quantity, 0u);
EXPECT_EQ(sell3->quantity, 3u);
EXPECT_EQ(buy->quantity, 0u);
}

TEST(MatchingEngineTest, SelfTradePreventionSameBuyLevel) {
MatchingEngine<BenchmarkOrder> engine(100.0);

// add two buy orders at same price level
auto buy1 = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(1, OrderType::BUY, 100.00, 10, 1)));
auto buy2 = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(2, OrderType::BUY, 100.00, 5, 2)));

engine.processOrder(buy1.get());
engine.processOrder(buy2.get());

// add opposing sell - should only match with older buy (buy1 by sequence)
auto sell = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(3, OrderType::SELL, 100.00, 8, 3)));
engine.processOrder(sell.get());

EXPECT_EQ(buy1->quantity, 2u); // matched 8 from its 10
EXPECT_EQ(buy2->quantity, 5u); // not matched yet
EXPECT_EQ(sell->quantity, 0u); // fully matched
}

// PriceBook and PriceLevel Tests

TEST(PriceLevelTest, OrderingBySequenceNumber) {
MatchingEngine<BenchmarkOrder> engine(100.0);

// add three orders at same price with different sequence numbers
auto order1 = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(10, OrderType::BUY, 100.00, 5, 1)));
auto order2 = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(11, OrderType::BUY, 100.00, 3, 3)));
auto order3 = std::make_unique<BookOrder>(engine.convertOrder(makeOrder(12, OrderType::BUY, 100.00, 7, 2)));

engine.processOrder(order1.get());
engine.processOrder(order2.get());
engine.processOrder(order3.get());

// query the price level
int level = engine.buy_book.priceToIndex(order1->price_ticks);
PriceLevel &pl = engine.buy_book.levels[level];

// head should be order1 (sequence 1, earliest)
EXPECT_EQ(pl.head->order_id, 10);
EXPECT_EQ(pl.total_quantity, 15u); // 5 + 3 + 7
}

// Conversion and Price Encoding Tests

TEST(MatchingEngineTest, PriceTickConversion) {
MatchingEngine<BenchmarkOrder> engine(100.0);

double price1 = 100.50;
double price2 = 100.75;

int64_t ticks1 = engine.priceToTicks(price1);
int64_t ticks2 = engine.priceToTicks(price2);

// ticks should be different for different prices
EXPECT_NE(ticks1, ticks2);

// conversion back should be close to original (accounting for tick precision)
double restored1 = engine.ticksToPrice(ticks1);
EXPECT_NEAR(restored1, price1, 0.001);
}
38 changes: 36 additions & 2 deletions include/benchmarking/benchmark.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,52 @@
#include <vector>
#include <string>
#include <mutex>

#include "scenarios/test_inputs.hpp"

#include "utils/timing.hpp"
#include "utils/structs.hpp"

#include "exchange/matching_engine.hpp"
#include "order_simulation/market_state.hpp"

// This will have to either be templ
#include "order_simulation/market_state.hpp"
#include "order_simulation/collection_order_generator.hpp"

#include "hardware_logging/hardware_logger.hpp"
#include "hardware_logging/thread_counter.hpp"

/**
* @class BenchmarkWrapper
* @brief Benchmarking harness for concurrent data structure evaluation.
*
* Orchestrates performance testing of concurrent data-structure implementations
* across latency, ordering, and exchange matching scenarios. Manages thread
* coordination, hardware performance counter collection, and result aggregation.
*
* **Responsibilities:**
* - **Thread Management:** Tracks enqueue/dequeue thread IDs for per-thread metrics
* - **Latency Measurement:** Records nanosecond-precision timestamps for queue operations
* - **Order Preservation:** Tracks sequence numbers and execution order for correctness
* - **Exchange Simulation:** Drives matching engine with generated orders and validates outputs
* - **Hardware Profiling:** Collects CPU cycles, cache misses, branch predictions
* - **Result Serialisation:** Writes benchmarks results and hardware metrics to CSV
*
* **Key Features:**
* - Template-based generic design supporting any queue implementation and order type
* - Hardware counter integration via PAPI for low-level performance analysis
* - Configurable test scenarios (stress, ordering, exchange)
* - Per-thread metrics aggregation with proper thread safety
* - Automatic CSV output generation for post-processing and visualization
*
* **Output Files:**
* - Latency CSV: enqueue/dequeue latencies in nanoseconds
* - Ordering CSV: expected vs actual order sequences
* - Exchange CSV: matching engine execution traces
* - Hardware CSV: aggregated per-thread performance counter data
*
* @tparam DataStructure The queue implementation being benchmarked
* @tparam TOrder The order type being processed (always BenchmarkOrder)
*/
template<typename DataStructure, typename TOrder>
class BenchmarkWrapper {
public:
Expand Down
6 changes: 6 additions & 0 deletions include/data_structures/queues/mc_lockfree_queue.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
#include "data_structures/queues/i_queue.hpp"
#include "data_structures/queues/readerwriterqueue.h"

/**
* @class MCLockFreeQueue
* @brief single-consumer lock-free queue based on moodycamel's ReaderWriterQueue.
*
* @tparam TOrder Type of orders stored in the queue
*/
template <typename TOrder>
class MCLockFreeQueue : public IQueue<TOrder, MCLockFreeQueue<TOrder>> {

Expand Down
6 changes: 6 additions & 0 deletions include/data_structures/queues/mc_mpmc_queue.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
#include "data_structures/queues/i_queue.hpp"
#include "data_structures/queues/concurrentqueue.h"

/**
* @class MCConcurrentQueue
* @brief multi-producer, multi-consumer lock-free queue from moodycamel.
*
* @tparam TOrder Type of orders stored in the queue
*/
template <typename TOrder>
class MCConcurrentQueue : public IQueue<TOrder, MCConcurrentQueue<TOrder>> {

Expand Down
10 changes: 7 additions & 3 deletions include/exchange/matching_engine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@
#include "order_simulation/benchmark_order.hpp"

/**
* MatchingEngine struct
* Entry point for the exchange, stores books and initialises ticks for 'traded-commodity'
* @class MatchingEngine
* @brief Core financial exchange matching engine implementing price-time priority.
*
* implemented a limit order book (LOB) matching engine using two separate price books
* (buy and sell sides) with dynamic recentering
*
* @tparam TOrder User-defined order type to be matched
*/

template<typename TOrder>
struct MatchingEngine {

Expand Down
14 changes: 7 additions & 7 deletions include/exchange/price_book.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
#include "exchange/price_level.hpp"

/**
* Going for a recentering OrderBook, so that it dynamically adapts
* to price changes like in a real exchange
*
* Uses a PriceBook that stores various PriceLevels
* each pricelevel represents a different tick index
* bitmap indicates which price levels are active
* @class PriceBook
* @brief dynamic limit order book side (buy or sell) with bitmap level discovery.
*
* order book (one side) implemented using a fixed array of price levels with a
* bitmap for O(1) lookup of active levels.
* supports dynamic recentering
*
*/

struct PriceBook {
static constexpr int NUM_LEVELS = 8192;
static constexpr int BITMAP_WORDS = (NUM_LEVELS + 63) / 64;
Expand Down
11 changes: 7 additions & 4 deletions include/order_simulation/market_state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@ struct EventParams {
};

/**
* @brief each generator reads from the market state to inform its order generation
* atomic variables for thread-safe access and modification
* aligned to cache line size (prevents false sharing)
* @class MarketState
* @brief simulated market microstructure with event-driven volatility regimes
*
* maintains synthetic market state (price, volatility, bias) that drives order
* generation patterns in benchmarks
* supports multiple volatility regimes
*
*/

class MarketState {
public:
explicit MarketState(std::optional<uint32_t> seed = std::nullopt);
Expand Down
Loading