diff --git a/CMakeLists.txt b/CMakeLists.txt index 07a9f51..e35fc41 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,8 +8,6 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2") enable_testing() -add_subdirectory(googletest) - set(Headers ./Limit_Order_Book/Book.hpp ./Limit_Order_Book/Limit.hpp @@ -27,11 +25,19 @@ set(Sources # Define the library target add_library(${PROJECT_NAME}_lib STATIC ${Sources} ${Headers}) +add_library(${PROJECT_NAME}_gen_lib STATIC ${Sources} ${Headers}) # Define the executable target add_executable(${PROJECT_NAME} main.cpp) +add_executable(GenerateOrders generate.cpp) + +# Add the definition to the executable's own compilation of the library sources +# Use a separate library target if the code logic actually changes +target_compile_definitions(${PROJECT_NAME}_gen_lib PRIVATE GENERATE) +# Link the generator to this specific "gen" version # Link the executable with the library +target_link_libraries(GenerateOrders PRIVATE ${PROJECT_NAME}_gen_lib) target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}_lib) add_subdirectory(test) diff --git a/Generate_Orders/GenerateOrders.cpp b/Generate_Orders/GenerateOrders.cpp index e3f3567..e260cb4 100644 --- a/Generate_Orders/GenerateOrders.cpp +++ b/Generate_Orders/GenerateOrders.cpp @@ -275,7 +275,7 @@ void GenerateOrders::modifyStopLimit() void GenerateOrders::createOrders(int numberOfOrders) { // Open a file named "orders.txt" for writing - file.open("C:/Users/benja/Documents/Limit_order_book/orders.txt"); + file.open("./Generate_Orders/Orders.txt"); if (!file.is_open()) { std::cerr << "Error opening file for writing!" << std::endl; @@ -285,8 +285,7 @@ void GenerateOrders::createOrders(int numberOfOrders) std::uniform_real_distribution<> dis(0.0, 1.0); // Define the probabilities and actions - std::vector probabilities = {0.025, 0, 0.195, 0.295, 0.025, 0, 0.12, 0.12, 0, 0.12, 0.12}; - // std::vector probabilities = {0.05, 0, 0.2, 0.3, 0, 0, 0.15, 0.15, 0, 0, 0.15}; + std::vector probabilities = {0.025, 0.192, 0.119, 0.179, 0.025, 0.093, 0.073, 0.073, 0.09, 0.071, 0.06}; std::vector> actions = { std::bind(&GenerateOrders::market, this), std::bind(&GenerateOrders::addLimit, this), @@ -318,16 +317,13 @@ void GenerateOrders::createOrders(int numberOfOrders) // Perform the selected action if (selectedAction < probabilities.size()) { actions[selectedAction](); + //std::cout << "Selected Action: " << selectedAction << std::endl; - // if (i%100000 == 0) - // { - // std::cout << "-------------------------------------" << std::endl; - // std::cout << "Number of orders done: " << i << std::endl; - - // std::cout << "Highest Stop Sell: " << book->getHighestStopSell()->getLimitPrice() << ", Lowest Stop Buy: " << book->getLowestStopBuy()->getLimitPrice() << std::endl; - // std::cout << "Lowest Sell: " << book->getLowestSell()->getLimitPrice() << ", Highest Buy: " << book->getHighestBuy()->getLimitPrice() << std::endl; - // book->printOrderBook(); - // } + if (i%100000 == 0) + { + std::cout << "-------------------------------------" << std::endl; + std::cout << "Number of orders done: " << i << std::endl; + } } else { std::cerr << "Error: No action selected!" << std::endl; @@ -340,7 +336,7 @@ void GenerateOrders::createOrders(int numberOfOrders) void GenerateOrders::createInitialOrders(int numberOfOrders, int centreOfBook) { // Open a file named "initialOrders.txt" for writing - std::ofstream file("C:/Users/benja/Documents/Limit_order_book/initialOrders.txt"); + std::ofstream file("./Generate_Orders/initialOrders.txt"); if (!file.is_open()) { std::cerr << "Error opening file for writing!" << std::endl; @@ -394,4 +390,4 @@ void GenerateOrders::createInitialOrders(int numberOfOrders, int centreOfBook) file.close(); std::cout << "Orders written to initialOrders.txt successfully!" << std::endl; -} \ No newline at end of file +} diff --git a/Limit_Order_Book/Book.cpp b/Limit_Order_Book/Book.cpp index 0e1e3c1..c66ce02 100644 --- a/Limit_Order_Book/Book.cpp +++ b/Limit_Order_Book/Book.cpp @@ -102,7 +102,9 @@ void Book::addLimitOrder(int orderId, bool buyOrSell, int shares, int limitPrice addLimit(limitPrice, newOrder->getBuyOrSell()); } limitMap.at(limitPrice)->append(newOrder); - // limitOrders.insert(newOrder); + #ifdef GENERATE + limitOrders.insert(newOrder); + #endif } else { executeStopOrders(buyOrSell); } @@ -123,7 +125,9 @@ void Book::cancelLimitOrder(int orderId) deleteLimit(order->getParentLimit()); } deleteFromOrderMap(orderId); - // limitOrders.erase(order); + #ifdef GENERATE + limitOrders.erase(order); + #endif delete order; } } @@ -171,7 +175,9 @@ void Book::addStopOrder(int orderId, bool buyOrSell, int shares, int stopPrice) addStop(stopPrice, newOrder->getBuyOrSell()); } stopMap.at(stopPrice)->append(newOrder); - // stopOrders.insert(newOrder); + #ifdef GENERATE + stopOrders.insert(newOrder); + #endif } } @@ -190,7 +196,9 @@ void Book::cancelStopOrder(int orderId) deleteStopLevel(order->getParentLimit()); } deleteFromOrderMap(orderId); - // stopOrders.erase(order); + #ifdef GENERATE + stopOrders.erase(order); + #endif delete order; } } @@ -237,7 +245,9 @@ void Book::addStopLimitOrder(int orderId, bool buyOrSell, int shares, int limitP addStop(stopPrice, newOrder->getBuyOrSell()); } stopMap.at(stopPrice)->append(newOrder); - // stopLimitOrders.insert(newOrder); + #ifdef GENERATE + stopLimitOrders.insert(newOrder); + #endif } } @@ -255,7 +265,9 @@ void Book::cancelStopLimitOrder(int orderId) deleteStopLevel(order->getParentLimit()); } deleteFromOrderMap(orderId); - // stopLimitOrders.erase(order); + #ifdef GENERATE + stopLimitOrders.erase(order); + #endif delete order; } } @@ -936,11 +948,15 @@ void Book::executeStopOrders(bool buyOrSell) deleteStopLevel(lowestStopBuy); } deleteFromOrderMap(headOrder->getOrderId()); - // stopOrders.erase(headOrder); + #ifdef GENERATE + stopOrders.erase(headOrder); + #endif delete headOrder; marketOrderHelper(0, true, shares); } else { - // stopLimitOrders.erase(headOrder); + #ifdef GENERATE + stopLimitOrders.erase(headOrder); + #endif stopLimitOrderToLimitOrder(headOrder, buyOrSell); } } @@ -959,11 +975,15 @@ void Book::executeStopOrders(bool buyOrSell) deleteStopLevel(highestStopSell); } deleteFromOrderMap(headOrder->getOrderId()); - // stopOrders.erase(headOrder); + #ifdef GENERATE + stopOrders.erase(headOrder); + #endif delete headOrder; marketOrderHelper(0, false, shares); } else { - // stopLimitOrders.erase(headOrder); + #ifdef GENERATE + stopLimitOrders.erase(headOrder); + #endif stopLimitOrderToLimitOrder(headOrder, buyOrSell); } } @@ -993,7 +1013,9 @@ void Book::stopLimitOrderToLimitOrder(Order* headOrder, bool buyOrSell) addLimit(headOrder->getLimit(), buyOrSell); } limitMap.at(headOrder->getLimit())->append(headOrder); - // limitOrders.insert(headOrder); + #ifdef GENERATE + limitOrders.insert(headOrder); + #endif } } @@ -1013,7 +1035,9 @@ void Book::marketOrderHelper(int orderId, bool buyOrSell, int shares) deleteLimit(bookEdge); } deleteFromOrderMap(headOrder->getOrderId()); - // limitOrders.erase(headOrder); + #ifdef GENERATE + limitOrders.erase(headOrder); + #endif delete headOrder; executedOrdersCount += 1; } diff --git a/Process_Orders/data_visualisation.py b/Process_Orders/data_visualisation.py index 8d606e3..32cb8e9 100644 --- a/Process_Orders/data_visualisation.py +++ b/Process_Orders/data_visualisation.py @@ -1,8 +1,9 @@ import pandas as pd import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D +import sys -def create_bar_chart_from_csv(csv_filename): +def create_bar_chart_from_csv(csv_filename, save_plots): # Read the CSV file into a DataFrame df = pd.read_csv(csv_filename, header=None, names=['Order Type', 'Times', 'Executed Orders', 'AVL Tree Balances']) @@ -16,13 +17,6 @@ def create_bar_chart_from_csv(csv_filename): average_time = times.mean() trades_count = df['Executed Orders'].sum() balances_count = df['AVL Tree Balances'].sum() - # print(f'{trades_count} {balances_count}') - # print(f"Average time: {average_time}") - - # Print the 20 rows with the highest times - # top_20_times = df.nlargest(20, 'Times') - # print("\nTop 20 rows with the highest times:") - # print(top_20_times) # Create a pie chart of the different order types order_type_counts = orderTypes.value_counts() @@ -30,8 +24,9 @@ def create_bar_chart_from_csv(csv_filename): plt.figure(figsize=(8, 8)) plt.pie(order_type_counts, labels=order_type_counts.index, autopct='%1.1f%%', startangle=0, colors=plt.cm.tab20.colors) plt.title('Distribution of Order Types') - # plt.show() - # plt.savefig('../figures/OrderTypes.png') + if save_plots: + plt.savefig('./figures/OrderTypes.png') + plt.show() # Create a histogram of the order latencies filtered_times = times[times <= 4000] @@ -40,8 +35,9 @@ def create_bar_chart_from_csv(csv_filename): plt.title(f'Orders by Latency Histogram - mean={average_time:.1f}ns ({1000000000/average_time:,.0f} orders/s)') plt.xlabel('Latency (ns)') plt.ylabel('Number of Orders') - # plt.show() - # plt.savefig('../figures/LatencyHistogram.png') + if save_plots: + plt.savefig('./figures/LatencyHistogram.png') + plt.show() # Exclude 'Market' and 'AddMarketLimit' order types excluded_order_types = ['Market', 'AddMarketLimit'] @@ -49,11 +45,12 @@ def create_bar_chart_from_csv(csv_filename): # Calculate mean, 15th, and 85th percentiles for each order type stats = filtered_df.groupby('Order Type')['Times'].agg(['mean', lambda x: x.quantile(0.15), lambda x: x.quantile(0.85)]).reset_index() - stats.columns = ['Order Type', 'mean', '25th', '75th'] - stats['error_lower'] = stats['mean'] - stats['25th'] - stats['error_upper'] = stats['75th'] - stats['mean'] + stats.columns = ['Order Type', 'mean', '15th', '85th'] + stats['error_lower'] = stats['15th']# - stats['25th'] + stats['error_upper'] = stats['85th']# - stats['mean'] stats = stats.sort_values(by='mean') + # Create a bar chart with error bars for latency for each order type plt.figure(figsize=(12, 6)) plt.bar(stats['Order Type'], stats['mean'], yerr=[stats['error_lower'], stats['error_upper']], capsize=5, color='skyblue', edgecolor='black') @@ -61,8 +58,9 @@ def create_bar_chart_from_csv(csv_filename): plt.xlabel('Order Type') plt.ylabel('Latency (ns)') plt.xticks(rotation=45) - # plt.show() - # plt.savefig('../figures/OrderTypeLatencies.png', bbox_inches='tight') + if save_plots: + plt.savefig('./figures/OrderTypeLatencies.png', bbox_inches='tight') + plt.show() # Filter for Market and AddMarketLimit order types market_df = df[df['Order Type'].isin(['Market', 'AddMarketLimit'])] @@ -78,8 +76,9 @@ def create_bar_chart_from_csv(csv_filename): plt.title('Latency by Number of Trades') plt.xlabel('Number of Trades per Order') plt.ylabel('Latency (ns)') - # plt.show() - # plt.savefig('../figures/ExecutedOrders.png') + if save_plots: + plt.savefig('./figures/ExecutedOrders.png') + plt.show() balance_df = df[df['AVL Tree Balances'] != 0] @@ -95,8 +94,9 @@ def create_bar_chart_from_csv(csv_filename): plt.title('Latency by Number of AVL Tree Balances') plt.xlabel('Number of AVL Tree Balances') plt.ylabel('Latency (ns)') - # plt.show() - # plt.savefig('../figures/AVLTreeBalances.png') + if save_plots: + plt.savefig('./figures/AVLTreeBalances.png') + plt.show() # Create figure and 3D axis fig = plt.figure(figsize=(10, 6)) @@ -120,9 +120,12 @@ def create_bar_chart_from_csv(csv_filename): ax.set_xticks([3, 13, 23, 33, 43, 53, 63, 73]) ax.set_xticklabels([70, 60, 50, 40, 30, 20, 10, 0]) plt.title('Latency by Number of Trades and AVL Tree Balances') - # plt.show() - # plt.savefig('../figures/3D.png') + if save_plots: + plt.savefig('./figures/3D.png') + plt.show() csv_filename = './order_processing_times.csv' -create_bar_chart_from_csv(csv_filename) +# Check if '--save' was passed in the terminal +save_plots = '--save' in sys.argv +create_bar_chart_from_csv(csv_filename, save_plots) diff --git a/README.md b/README.md index 6d58e7d..10068fd 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,40 @@ This Limit Order Book is developed in `C++` from scratch and able to handle over Performance testing of the order book was also quite a challenging task as it required getting order data for testing, performing the testing to collect latency statistics, and finally analysing and visualising the collected data. All functionality testing was completed thorough a set of unit tests and integration tests using `GoogleTest`. +## Build + +To build the targets `LimitOrderBook`, `LimitOrderBookTests`, and `GenerateOrders`, simply run + +```bash +./build.sh +``` + +## Generate Orders + +To generate `Orders.txt` and `initialOrders.txt`, just run + +```bash +./GenerateOrders +``` + +## Process Orders + +To process `Orders.txt`, run + +```bash +./LimitOrderBook +``` + +## Visualize Results + +To produce the plots in this readme, run + +```bash +python3 Process_Orders/data_visualization.py --save +``` + +The `--save` option is optional to save the figures in `./figures`. + ## Background ### Matching Engine diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..3649439 --- /dev/null +++ b/build.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +mkdir build +cd build +cmake .. +make +cp LimitOrderBook ../ +cp GenerateOrders ../ +cp ./test/LimitOrderBookTests ../ diff --git a/generate.cpp b/generate.cpp new file mode 100644 index 0000000..1c3234d --- /dev/null +++ b/generate.cpp @@ -0,0 +1,21 @@ +#include "./Generate_Orders/GenerateOrders.hpp" +#include "./Process_Orders/OrderPipeline.hpp" +#include "./Limit_Order_Book/Book.hpp" +#include "./Limit_Order_Book/Limit.hpp" +#include "./Limit_Order_Book/Order.hpp" +#include +#include +#include + +int main() { + Book* book = new Book(); + OrderPipeline orderPipeline(book); + + GenerateOrders generateOrders(book); + generateOrders.createInitialOrders(10'000, 300); + orderPipeline.processOrdersFromFile("./Generate_Orders/initialOrders.txt"); + generateOrders.createOrders(5'000'000); + + delete book; + return 0; +} diff --git a/main.cpp b/main.cpp index a71f0cf..20cd559 100644 --- a/main.cpp +++ b/main.cpp @@ -9,31 +9,19 @@ int main() { Book* book = new Book(); - OrderPipeline orderPipeline(book); - // GenerateOrders generateOrders(book); - - // generateOrders.createInitialOrders(10000, 300); - - orderPipeline.processOrdersFromFile("./initialOrders.txt"); - - // generateOrders.createOrders(5000000); - + orderPipeline.processOrdersFromFile("./Generate_Orders/initialOrders.txt"); // Start measuring time auto start = std::chrono::high_resolution_clock::now(); - - orderPipeline.processOrdersFromFile("./Orders.txt"); - + orderPipeline.processOrdersFromFile("./Generate_Orders/Orders.txt"); // Stop measuring time auto stop = std::chrono::high_resolution_clock::now(); - // Calculate the duration auto duration = std::chrono::duration_cast(stop - start); - std::cout << "Time taken to process orders: " << duration.count() << " milliseconds" << std::endl; delete book; return 0; -} \ No newline at end of file +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 95bb4cc..db18f89 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,19 +1,29 @@ cmake_minimum_required(VERSION 3.29.0) -set(This LimitOrderBookTests) +# 1. Find GTest +find_package(GTest REQUIRED) -set(Sources - LimitOrderBookTests.cpp +# 2. Define the executable and its source files +set(This LimitOrderBookTests) +set(Sources + LimitOrderBookTests.cpp ExampleOrdersTests.cpp ) add_executable(${This} ${Sources}) -target_link_libraries(${This} PUBLIC - gtest_main + +# 3. Link libraries (Must come AFTER add_executable) +# Use GTest::gtest_main (the namespaced version) for better compatibility +target_link_libraries(${This} + PRIVATE + GTest::gtest_main LimitOrderBook_lib ) +# 4. Enable testing and add the test +enable_testing() add_test( - NAME ${This} + NAME ${This} COMMAND ${This} -) \ No newline at end of file +) + diff --git a/test/ExampleOrdersTests.cpp b/test/ExampleOrdersTests.cpp index 4942beb..f24fc2b 100644 --- a/test/ExampleOrdersTests.cpp +++ b/test/ExampleOrdersTests.cpp @@ -30,10 +30,10 @@ TEST_F(ExampleOrdersTests, CreateInitialOrdersTest) { } TEST_F(ExampleOrdersTests, ProcessInitialOrdersTest) { - orderPipeline->processOrdersFromFile("C:/Users/benja/Documents/Limit_order_book/initialOrders.txt"); + orderPipeline->processOrdersFromFile("./Generate_Orders/initialOrders.txt"); } TEST_F(ExampleOrdersTests, CreateOrdersTest) { - orderPipeline->processOrdersFromFile("C:/Users/benja/Documents/Limit_order_book/initialOrders.txt"); + orderPipeline->processOrdersFromFile("./Generate_Orders/initialOrders.txt"); generateOrders->createOrders(100000); -} \ No newline at end of file +}