├── .gitignore ├── src ├── OrderIdGenerator.cpp ├── OrderIdGenerator.h ├── NetworkInterface.h ├── Logging.h ├── EventLoop.h ├── MessageParser.h ├── Replay.h ├── Session.h ├── main.cpp ├── Order.h ├── MemoryPool.h ├── Replay.cpp ├── EngineController.h ├── SymbolConfig.h ├── MatchingEngine.h ├── Messages.h ├── NetworkInterface.cpp ├── OrderBook.h ├── EventLoop.cpp ├── EngineController.cpp ├── Session.cpp ├── MessageParser.cpp ├── MatchingEngine.cpp └── OrderBook.cpp ├── Makefile ├── client ├── main.py ├── util.py ├── client.py └── order.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | build/ 3 | 4 | __pycache__/ 5 | 6 | *.log 7 | -------------------------------------------------------------------------------- /src/OrderIdGenerator.cpp: -------------------------------------------------------------------------------- 1 | #include "OrderIdGenerator.h" 2 | 3 | std::atomic OrderIdGenerator::idCounter{1}; 4 | 5 | -------------------------------------------------------------------------------- /src/OrderIdGenerator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | class OrderIdGenerator { 5 | public: 6 | static uint64_t nextId() { 7 | return idCounter.fetch_add(1, std::memory_order_relaxed); 8 | } 9 | private: 10 | static std::atomic idCounter; 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /src/NetworkInterface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | class NetworkInterface { 6 | public: 7 | int setupListener(const std::string &host, int port); 8 | bool setNonBlocking(int fd); 9 | int acceptClient(int listenFd, std::string &clientAddrOut); 10 | }; 11 | -------------------------------------------------------------------------------- /src/Logging.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | enum class LogLevel { DEBUG, INFO, WARN, ERROR }; 7 | 8 | inline LogLevel GLOBAL_LOG_LEVEL = LogLevel::INFO; 9 | inline std::mutex LOG_MUTEX; 10 | 11 | #define LOG(level, msg) \ 12 | do { \ 13 | if (level >= GLOBAL_LOG_LEVEL) { \ 14 | std::lock_guard lock(LOG_MUTEX); \ 15 | std::cerr << #level << ": " << msg << "\n"; \ 16 | } \ 17 | } while(0) 18 | 19 | -------------------------------------------------------------------------------- /src/EventLoop.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "Session.h" 4 | #include "EngineController.h" 5 | 6 | class EventLoop { 7 | public: 8 | EventLoop(EngineController &controller); 9 | 10 | bool init(int listenFd); 11 | void run(); 12 | 13 | private: 14 | int kqfd_ = -1; 15 | int listenFd_ = -1; 16 | EngineController &controller_; 17 | std::unordered_map sessions_; 18 | 19 | bool handleNewConnection(); 20 | bool handleEvent(int fd, int16_t filter); 21 | void removeSession(int fd); 22 | }; 23 | ; 24 | -------------------------------------------------------------------------------- /src/MessageParser.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Messages.h" 3 | #include 4 | #include 5 | 6 | class MessageParser { 7 | public: 8 | void appendData(const char* data, size_t len); 9 | std::optional nextMessageHeader(); 10 | std::optional nextAddMessage(); 11 | std::optional nextCancelMessage(); 12 | std::optional nextCancelReplaceMessage(); 13 | std::optional nextSnapshotRequest(); 14 | 15 | private: 16 | std::vector buffer_; 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /src/Replay.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "Messages.h" 4 | #include "Logging.h" 5 | #include 6 | 7 | class Replay { 8 | public: 9 | Replay(); 10 | void logAddMessage(uint64_t seq, const AddMessage &msg); 11 | void logCancelMessage(uint64_t seq, const CancelMessage &msg); 12 | void logCancelReplaceMessage(uint64_t seq, const CancelReplaceMessage &msg); 13 | void logExecutionMessage(uint64_t seq, const ExecutionMessage &msg); 14 | 15 | void replayAll(); 16 | 17 | private: 18 | std::mutex mtx_; 19 | std::ofstream logfile_; 20 | }; 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Compiler and flags 2 | CXX := g++ 3 | CXXFLAGS := -std=c++20 -Wall -Wextra -pedantic -O2 -pthread 4 | 5 | # Directories 6 | SRC_DIR := src 7 | BUILD_DIR := build 8 | BIN_DIR := bin 9 | 10 | # Output executable 11 | TARGET := $(BIN_DIR)/exchange 12 | 13 | # Source files 14 | SRC_FILES := $(wildcard $(SRC_DIR)/*.cpp) 15 | OBJ_FILES := $(patsubst $(SRC_DIR)/%.cpp, $(BUILD_DIR)/%.o, $(SRC_FILES)) 16 | 17 | # Rules 18 | all: $(TARGET) 19 | 20 | $(TARGET): $(OBJ_FILES) | $(BIN_DIR) 21 | $(CXX) $(CXXFLAGS) $^ -o $@ 22 | 23 | $(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(BUILD_DIR) 24 | $(CXX) $(CXXFLAGS) -c $< -o $@ 25 | 26 | $(BUILD_DIR): 27 | mkdir -p $(BUILD_DIR) 28 | 29 | $(BIN_DIR): 30 | mkdir -p $(BIN_DIR) 31 | 32 | clean: 33 | rm -rf $(BUILD_DIR) $(BIN_DIR) 34 | 35 | run: $(TARGET) 36 | ./$(TARGET) 37 | 38 | .PHONY: all clean run 39 | 40 | -------------------------------------------------------------------------------- /src/Session.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "MessageParser.h" 5 | #include "EngineController.h" 6 | 7 | class Session { 8 | public: 9 | Session(int fd, EngineController &controller, int kqfd_); 10 | ~Session(); 11 | 12 | int getFd() const { return fd_; } 13 | 14 | bool onReadable(); 15 | bool onWritable(); 16 | void queueResponse(const std::string &msg); 17 | 18 | private: 19 | int kqfd_; 20 | int fd_; 21 | std::string clientAddr_; 22 | EngineController &controller_; 23 | 24 | std::vector writeQueue_; 25 | MessageParser parser_; 26 | 27 | bool handleAdd(const AddMessage &msg); 28 | bool handleCancel(const CancelMessage &msg); 29 | bool handleCancelReplace(const CancelReplaceMessage &msg); 30 | bool handleSnapshotRequest(const SnapshotRequest &msg); 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /client/main.py: -------------------------------------------------------------------------------- 1 | from client import Client 2 | from order import Order, OrderManager 3 | 4 | manager = OrderManager("orders.json") 5 | 6 | client = Client("127.0.0.1", 9999, manager) 7 | client.connect() 8 | 9 | # Add a LIMIT order 10 | order1 = Order(order_id = 1, symbol = "AAPL", side = "BUY", price = 110.0, quantity = 10, order_type = "LIMIT", tif = "GTC") 11 | print(f"Placing order: {order1}") 12 | client.add_order(order1) 13 | 14 | # Request a snapshot for AAPL 15 | print("Requesting snapshot for AAPL...") 16 | client.request_snapshot(1, "AAPL") 17 | 18 | # Replace the order 19 | print(f"Replacing order {order1.order_id}") 20 | client.cancel_replace_order(order1.order_id, 115.0, 15, 1) 21 | 22 | # View current order state 23 | print("\nOrder State:") 24 | for order in manager.orders.values(): 25 | print(order) 26 | 27 | # Cancel the order 28 | print(f"Canceling order {order1.order_id}") 29 | client.cancel_order(order1.order_id, 1) 30 | -------------------------------------------------------------------------------- /client/util.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | def build_add_order_message(order_id: int, symbol: str, price: float, quantity: int, side: str, order_type: str, tif: str) -> str: 4 | timestamp = int(time.time()) 5 | return (f"ADD|{order_id}|{timestamp}|{order_id}|{symbol}|{price}|{quantity}|{side}|{tif}|{order_type}|" 6 | f"{order_id}|0.0|{quantity}\n") 7 | 8 | def build_snapshot_request_message(seq: int, symbol: str) -> str: 9 | timestamp = int(time.time()) 10 | return f"SNAPSHOT_REQUEST|{seq}|{timestamp}|{symbol}\n" 11 | 12 | def build_cancel_order_message(order_id: int, participant_id: int) -> str: 13 | timestamp = int(time.time()) 14 | return f"CANCEL|{order_id}|{timestamp}|{order_id}|{participant_id}\n" 15 | 16 | def build_cancel_replace_message(order_id: int, new_price: float, new_quantity: int, participant_id: int) -> str: 17 | timestamp = int(time.time()) 18 | return f"CANCEL_REPLACE|{order_id}|{timestamp}|{order_id}|{new_price}|{new_quantity}|{participant_id}\n" 19 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "EngineController.h" 2 | #include "Replay.h" 3 | #include "Logging.h" 4 | #include "NetworkInterface.h" 5 | #include "EventLoop.h" 6 | #include "SymbolConfig.h" 7 | 8 | int main() { 9 | GLOBAL_LOG_LEVEL = LogLevel::INFO; 10 | Replay replayLog; 11 | SymbolConfigManager configManager; 12 | EngineController controller(replayLog, configManager); 13 | 14 | // Add some symbols 15 | controller.addEngineForSymbol("AAPL", 0.01, 1, 1.00, 10000.00, 0.5, 150.00); 16 | controller.addEngineForSymbol("BTCUSD", 0.01, 1, 1000.00, 100000.00, 0.3, 20000.00); 17 | 18 | NetworkInterface net; 19 | int listenFd = net.setupListener("", 9999); 20 | if (listenFd < 0) { 21 | LOG(LogLevel::ERROR, "Failed to setup listener"); 22 | return 1; 23 | } 24 | 25 | EventLoop loop(controller); 26 | if (!loop.init(listenFd)) { 27 | LOG(LogLevel::ERROR, "Failed to init event loop"); 28 | return 1; 29 | } 30 | 31 | LOG(LogLevel::INFO, "Server running on port 9999"); 32 | loop.run(); 33 | 34 | return 0; 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/Order.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include "Messages.h" 5 | 6 | struct Order { 7 | uint64_t orderId; 8 | Side side; 9 | char symbol[8]; 10 | double price; 11 | uint64_t quantity; 12 | uint64_t timestamp; 13 | uint64_t participantId; 14 | TimeInForce tif; 15 | OrderType orderType; 16 | double triggerPrice; 17 | uint64_t visibleQuantity; 18 | uint64_t totalQuantity; // For iceberg: total initial qty 19 | 20 | Order(uint64_t id, Side s, const std::string &sym, double p, uint64_t q, uint64_t ts, 21 | uint64_t partId, TimeInForce t, OrderType otype, double trigP, uint64_t visQty) 22 | : orderId(id), side(s), price(p), quantity(q), timestamp(ts), 23 | participantId(partId), tif(t), orderType(otype), triggerPrice(trigP), 24 | visibleQuantity(visQty), totalQuantity(q) { 25 | for (size_t i = 0; i<7 && i(sym.size(),7)] = '\0'; 29 | } 30 | 31 | Order() = default; 32 | }; 33 | 34 | -------------------------------------------------------------------------------- /src/MemoryPool.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | template 7 | class MemoryPool { 8 | public: 9 | MemoryPool() { 10 | allocateBlock(); 11 | } 12 | 13 | T* allocate() { 14 | std::lock_guard lock(mutex_); 15 | if (freeList_.empty()) { 16 | allocateBlock(); 17 | } 18 | T* obj = freeList_.back(); 19 | freeList_.pop_back(); 20 | return obj; 21 | } 22 | 23 | void deallocate(T* obj) { 24 | std::lock_guard lock(mutex_); 25 | freeList_.push_back(obj); 26 | } 27 | 28 | private: 29 | void allocateBlock() { 30 | blockStorage_.emplace_back(new char[sizeof(T) * BLOCK_SIZE]); 31 | char* block = blockStorage_.back(); 32 | for (size_t i = 0; i < BLOCK_SIZE; ++i) { 33 | freeList_.push_back(reinterpret_cast(block + i * sizeof(T))); 34 | } 35 | } 36 | 37 | std::vector blockStorage_; 38 | std::vector freeList_; 39 | std::mutex mutex_; 40 | }; 41 | 42 | -------------------------------------------------------------------------------- /src/Replay.cpp: -------------------------------------------------------------------------------- 1 | #include "Replay.h" 2 | 3 | Replay::Replay() { 4 | logfile_.open("replay.log", std::ios::app); 5 | } 6 | 7 | void Replay::logAddMessage(uint64_t seq, const AddMessage &msg) { 8 | std::lock_guard lock(mtx_); 9 | logfile_ << "ADD|" << seq << "|" << msg.orderId << "|" << msg.symbol << "|" << msg.price << "|" << msg.quantity << "\n"; 10 | } 11 | 12 | void Replay::logCancelMessage(uint64_t seq, const CancelMessage &msg) { 13 | std::lock_guard lock(mtx_); 14 | logfile_ << "CANCEL|" << seq << "|" << msg.orderId << "\n"; 15 | } 16 | 17 | void Replay::logCancelReplaceMessage(uint64_t seq, const CancelReplaceMessage &msg) { 18 | std::lock_guard lock(mtx_); 19 | logfile_ << "CANCEL_REPLACE|" << seq << "|" << msg.orderId << "|" << msg.newPrice << "|" << msg.newQuantity << "\n"; 20 | } 21 | 22 | void Replay::logExecutionMessage(uint64_t seq, const ExecutionMessage &msg) { 23 | std::lock_guard lock(mtx_); 24 | logfile_ << "EXEC|" << seq << "|" << msg.symbol << "|" << msg.price << "|" << msg.quantity << "\n"; 25 | } 26 | 27 | void Replay::replayAll() { 28 | // Read replay.log and re-apply messages to rebuild state but not required yet. 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/EngineController.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include "MatchingEngine.h" 6 | #include "Replay.h" 7 | #include "MemoryPool.h" 8 | #include "SymbolConfig.h" 9 | 10 | class EngineController { 11 | public: 12 | EngineController(Replay &replay, SymbolConfigManager &configManager); 13 | 14 | bool dispatchAdd(const AddMessage &msg); 15 | bool dispatchCancel(const CancelMessage &msg); 16 | bool dispatchCancelReplace(const CancelReplaceMessage &msg); 17 | void dispatchSnapshotRequest(const SnapshotRequest &msg); 18 | double getLastTradePrice(const std::string &symbol) const; 19 | void getTopOfBook(const std::string &symbol, double &bestBid, double &bestAsk); 20 | 21 | void addEngineForSymbol(const std::string &symbol, double tickSize, uint64_t minQty, double minP, double maxP, double volThreshold, double refPrice); 22 | 23 | private: 24 | std::unordered_map engines; 25 | mutable std::shared_mutex enginesMutex; 26 | Replay &replayLog; 27 | MemoryPool orderPool; 28 | SymbolConfigManager &configManager; 29 | 30 | // We'll need a map orderId->symbol for cancel 31 | std::unordered_map orderSymbolMap; 32 | std::mutex orderSymbolMapMutex; 33 | 34 | void recordOrderSymbol(uint64_t orderId, const std::string &symbol); 35 | bool findOrderSymbol(uint64_t orderId, std::string &symbol); 36 | }; 37 | 38 | -------------------------------------------------------------------------------- /src/SymbolConfig.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | struct SymbolConfig { 7 | double tickSize; 8 | uint64_t minQuantity; 9 | double minPrice; 10 | double maxPrice; 11 | double volatilityThreshold; // e.g. max percent change from reference 12 | double referencePrice; // base price for volatility checks 13 | bool tradingHalted; 14 | }; 15 | 16 | class SymbolConfigManager { 17 | public: 18 | void setConfig(const std::string &symbol, const SymbolConfig &config) { 19 | std::lock_guard lock(mtx_); 20 | configs_[symbol] = config; 21 | } 22 | 23 | bool getConfig(const std::string &symbol, SymbolConfig &out) { 24 | std::lock_guard lock(mtx_); 25 | auto it = configs_.find(symbol); 26 | if (it == configs_.end()) return false; 27 | out = it->second; 28 | return true; 29 | } 30 | 31 | void haltTrading(const std::string &symbol) { 32 | std::lock_guard lock(mtx_); 33 | if (configs_.find(symbol) != configs_.end()) { 34 | configs_[symbol].tradingHalted = true; 35 | } 36 | } 37 | 38 | void resumeTrading(const std::string &symbol) { 39 | std::lock_guard lock(mtx_); 40 | if (configs_.find(symbol) != configs_.end()) { 41 | configs_[symbol].tradingHalted = false; 42 | } 43 | } 44 | 45 | private: 46 | std::unordered_map configs_; 47 | std::mutex mtx_; 48 | }; 49 | 50 | -------------------------------------------------------------------------------- /src/MatchingEngine.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "OrderBook.h" 3 | #include "Replay.h" 4 | #include "MemoryPool.h" 5 | #include "Logging.h" 6 | #include "SymbolConfig.h" 7 | #include 8 | 9 | class MatchingEngine { 10 | public: 11 | MatchingEngine(const std::string& symbol, Replay& replay, MemoryPool& pool, SymbolConfigManager &configManager); 12 | 13 | double getLastTradePrice() const; 14 | bool processAdd(const AddMessage &msg); 15 | bool processCancel(const CancelMessage &msg); 16 | bool processCancelReplace(const CancelReplaceMessage &msg); 17 | void processSnapshotRequest(const SnapshotRequest &msg); 18 | void sendExecution(const ExecutionMessage &exec); 19 | 20 | void step(); 21 | 22 | OrderBook orderBook; 23 | 24 | private: 25 | std::string symbol_; 26 | Replay& replayLog; 27 | MemoryPool& orderPool; 28 | SymbolConfigManager &configManager; 29 | 30 | std::atomic nextSequence{1}; 31 | 32 | bool validateAdd(const AddMessage &msg); 33 | bool validateCancel(const CancelMessage &msg); 34 | bool validateCancelReplace(const CancelReplaceMessage &msg); 35 | 36 | bool checkVolatilityHalt(const AddMessage &msg); 37 | bool priceValidForSymbol(const std::string &symbol, double price); 38 | bool tickSizeValid(const std::string &symbol, double price); 39 | bool quantityValid(const std::string &symbol, uint64_t qty); 40 | bool checkTimeInForce(Order* o, std::vector &trades, uint64_t timestamp); 41 | 42 | void handleMarketOrder(Order* o, std::vector &trades, uint64_t timestamp); 43 | void handleIocFok(Order* o, std::vector &trades, uint64_t timestamp); 44 | }; 45 | 46 | -------------------------------------------------------------------------------- /src/Messages.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | enum class MessageType { 6 | ADD, 7 | CANCEL, 8 | CANCEL_REPLACE, 9 | EXECUTION, 10 | SNAPSHOT_REQUEST, 11 | SNAPSHOT_RESPONSE, 12 | HEARTBEAT 13 | }; 14 | 15 | enum class Side : uint8_t { BUY, SELL }; 16 | 17 | enum class TimeInForce : uint8_t { 18 | GTC, // Good Till Cancel 19 | IOC, // Immediate Or Cancel 20 | FOK // Fill Or Kill 21 | }; 22 | 23 | enum class OrderType : uint8_t { 24 | LIMIT, 25 | MARKET, 26 | STOP_LOSS, 27 | ICEBERG 28 | }; 29 | 30 | struct MessageHeader { 31 | MessageType type; 32 | uint64_t sequence; 33 | uint64_t timestamp; 34 | }; 35 | 36 | struct AddMessage { 37 | MessageHeader header; 38 | uint64_t orderId; 39 | std::string symbol; 40 | double price; 41 | uint64_t quantity; 42 | Side side; 43 | TimeInForce tif; 44 | OrderType orderType; 45 | uint64_t participantId; 46 | double triggerPrice; // For STOP_LOSS 47 | uint64_t visibleQuantity; // For ICEBERG 48 | }; 49 | 50 | struct CancelMessage { 51 | MessageHeader header; 52 | uint64_t orderId; 53 | uint64_t participantId; // for validation 54 | }; 55 | 56 | struct CancelReplaceMessage { 57 | MessageHeader header; 58 | uint64_t orderId; 59 | double newPrice; 60 | uint64_t newQuantity; 61 | uint64_t participantId; 62 | }; 63 | 64 | struct ExecutionMessage { 65 | MessageHeader header; 66 | uint64_t buyOrderId; 67 | uint64_t sellOrderId; 68 | char symbol[8]; 69 | double price; 70 | uint64_t quantity; 71 | uint64_t buyParticipantId; 72 | uint64_t sellParticipantId; 73 | }; 74 | 75 | struct SnapshotRequest { 76 | MessageHeader header; 77 | std::string symbol; 78 | }; 79 | 80 | struct SnapshotResponse { 81 | MessageHeader header; 82 | std::string symbol; 83 | double bestBid; 84 | double bestAsk; 85 | double lastTradePrice; 86 | // Could add more levels later but not required for now 87 | }; 88 | -------------------------------------------------------------------------------- /src/NetworkInterface.cpp: -------------------------------------------------------------------------------- 1 | #include "NetworkInterface.h" 2 | #include "Logging.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | int NetworkInterface::setupListener(const std::string &host, int port) { 11 | int fd = socket(AF_INET, SOCK_STREAM, 0); 12 | if (fd < 0) { 13 | LOG(LogLevel::ERROR, "socket creation failed"); 14 | return -1; 15 | } 16 | 17 | int optval = 1; 18 | setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); 19 | 20 | sockaddr_in addr; 21 | std::memset(&addr, 0, sizeof(addr)); 22 | addr.sin_family = AF_INET; 23 | addr.sin_port = htons(port); 24 | if (!host.empty()) { 25 | inet_pton(AF_INET, host.c_str(), &addr.sin_addr); 26 | } else { 27 | addr.sin_addr.s_addr = INADDR_ANY; 28 | } 29 | 30 | if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { 31 | LOG(LogLevel::ERROR, "bind failed"); 32 | close(fd); 33 | return -1; 34 | } 35 | 36 | if (listen(fd, 128) < 0) { 37 | LOG(LogLevel::ERROR, "listen failed"); 38 | close(fd); 39 | return -1; 40 | } 41 | 42 | if (!setNonBlocking(fd)) { 43 | close(fd); 44 | return -1; 45 | } 46 | 47 | return fd; 48 | } 49 | 50 | bool NetworkInterface::setNonBlocking(int fd) { 51 | int flags = fcntl(fd, F_GETFL, 0); 52 | if (flags < 0) flags = 0; 53 | return fcntl(fd, F_SETFL, flags | O_NONBLOCK) >= 0; 54 | } 55 | 56 | int NetworkInterface::acceptClient(int listenFd, std::string &clientAddrOut) { 57 | sockaddr_in clientAddr; 58 | socklen_t len = sizeof(clientAddr); 59 | int clientFd = accept(listenFd, (struct sockaddr*)&clientAddr, &len); 60 | if (clientFd >= 0) { 61 | char addrStr[INET_ADDRSTRLEN]; 62 | inet_ntop(AF_INET, &clientAddr.sin_addr, addrStr, sizeof(addrStr)); 63 | clientAddrOut = addrStr; 64 | setNonBlocking(clientFd); 65 | } 66 | return clientFd; 67 | } 68 | 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plutus 2 | advanced high-performance matching engine for financial trading systems 3 | 4 | plutus, named after the [greek god of wealth](https://en.wikipedia.org/wiki/Plutus), is a robust high-performance matching engine, 5 | similar to those found in stock exchanges today. it's loosely inspired by the [Jane Street](https://www.youtube.com/watch?v=b1e4t2k2KJY) 6 | talk on building exchanges, and extends it with a more modern and advanced architecture for real-time order 7 | matching, risk management, and trade execution. 8 | 9 | plutus supports a wide range of trading scenarios and incorporates advanced order types (limit, market, stop-loss, 10 | iceberg), time-in-force (GTC, IOC, FOK) options, and symbol-specific configurations. i tried to emphasize 11 | low-latency and high-throughput processing through efficient data structures and a multithreaded API to mimic 12 | how production systems work from what I could gather from various blog posts, articles, papers, and discussions. 13 | 14 | at its core, plutus is a matching engine that processes, validates, and matches incoming orders 15 | against the order book, with support for partial fills, self-trade prevention, and other advanced handling. 16 | the order book maintains structures for bids and asks and the event loop manages the API with non-blocking I/O 17 | for connections and sockets. it uses a volume-weighted average price algorithm to calculate the average price of 18 | a symbol over a period of time. 19 | 20 | the client communicates with the server with a rudimentary text-based protocol. eg: 21 | ``` 22 | ADD|1|1640995200000|1001|AAPL|150.25|10|BUY|GTC|LIMIT|123|0|0 23 | ``` 24 | the library in `client/` is a simple order management system wrapper over plutus that serves as a lightweight demo and 25 | validation for changes. 26 | 27 | i tried to comment parts i found more difficult and write self-documenting code to make it easier to navigate 28 | and learn, especially to simplify the design choices i made. `make` builds the entire project with c++20 with 29 | only a `kqueue` dependency for macos. the build outputs a binary which launches a server. 30 | -------------------------------------------------------------------------------- /client/client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | import order 4 | from util import * 5 | 6 | class Client: 7 | def __init__(self, host: str, port: int, order_manager: order.OrderManager) -> None: 8 | self.host = host 9 | self.port = port 10 | self.sock = None 11 | self.order_manager = order_manager 12 | 13 | def connect(self) -> None: 14 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 15 | self.sock.connect((self.host, self.port)) 16 | print(f"Connected to {self.host}:{self.port}") 17 | 18 | def disconnect(self) -> None: 19 | if self.sock: 20 | self.sock.close() 21 | print("Disconnected from server") 22 | self.order_manager.save_orders() 23 | 24 | def send_and_receive(self, message: str) -> str: 25 | print(f"Sending: {message.strip()}") 26 | self.sock.sendall(message.encode('utf-8')) 27 | response = self.sock.recv(4096).decode('utf-8') 28 | print(f"Response: {response.strip()}") 29 | return response 30 | 31 | def add_order(self, order_: order.Order) -> None: 32 | message = build_add_order_message( 33 | order_.order_id, order_.symbol, order_.price, order_.quantity, 34 | order_.side, order_.order_type, order_.tif 35 | ) 36 | response = self.send_and_receive(message) 37 | if "ADD_ACK" in response: 38 | self.order_manager.update_order_status(order_.order_id, "ACKED") 39 | self.order_manager.add_order(order_) 40 | 41 | def cancel_order(self, order_id: int, participant_id: int) -> None: 42 | message = build_cancel_order_message(order_id, participant_id) 43 | response = self.send_and_receive(message) 44 | if "CANCEL_ACK" in response: 45 | self.order_manager.update_order_status(order_id, "CANCELED") 46 | 47 | def cancel_replace_order(self, order_id: int, new_price: float, new_quantity: int, participant_id: int) -> None: 48 | message = build_cancel_replace_message(order_id, new_price, new_quantity, participant_id) 49 | response = self.send_and_receive(message) 50 | if "CANCEL_REPLACE_ACK" in response: 51 | self.order_manager.update_order_status(order_id, "REPLACED") 52 | 53 | def request_snapshot(self, seq: int, symbol: str) -> str: 54 | message = build_snapshot_request_message(seq, symbol) 55 | return self.send_and_receive(message) 56 | -------------------------------------------------------------------------------- /client/order.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pprint 3 | 4 | class Order: 5 | def __init__( 6 | self, 7 | order_id: int, 8 | symbol: str, 9 | side: str, 10 | price: float, 11 | quantity: int, 12 | order_type: str, 13 | tif: str 14 | ) -> None: 15 | self.order_id = order_id 16 | self.symbol = symbol 17 | self.side = side 18 | self.price = price 19 | self.quantity = quantity 20 | self.order_type = order_type 21 | self.tif = tif 22 | self.status = "PENDING" # PENDING, ACKED, REPLACED, CANCELED, FILLED 23 | 24 | def to_dict(self) -> dict: 25 | return { 26 | "order_id": self.order_id, 27 | "symbol": self.symbol, 28 | "side": self.side, 29 | "price": self.price, 30 | "quantity": self.quantity, 31 | "order_type": self.order_type, 32 | "tif": self.tif, 33 | "status": self.status, 34 | } 35 | 36 | @staticmethod 37 | def from_dict(data: dict) -> "Order": 38 | order = Order( 39 | data["order_id"], 40 | data["symbol"], 41 | data["side"], 42 | data["price"], 43 | data["quantity"], 44 | data["order_type"], 45 | data["tif"], 46 | ) 47 | order.status = data["status"] 48 | return order 49 | 50 | def __str__(self) -> str: 51 | return f"Order({pprint.pformat(self.to_dict())})\n" 52 | 53 | class OrderManager: 54 | def __init__(self, persistence_file: str) -> None: 55 | self.orders: Dict[int, Order] = {} 56 | self.persistence_file = persistence_file 57 | self.load_orders() 58 | 59 | def add_order(self, order: Order) -> None: 60 | self.orders[order.order_id] = order 61 | 62 | def update_order_status(self, order_id: int, status: str) -> None: 63 | if order_id in self.orders: 64 | self.orders[order_id].status = status 65 | 66 | def get_order(self, order_id: int) -> Order: 67 | return self.orders.get(order_id) 68 | 69 | def save_orders(self) -> None: 70 | with open(self.persistence_file, "w") as file: 71 | json.dump([order.to_dict() for order in self.orders.values()], file) 72 | 73 | def load_orders(self) -> None: 74 | try: 75 | with open(self.persistence_file, "r") as file: 76 | data = json.load(file) 77 | self.orders = {item["order_id"]: Order.from_dict(item) for item in data} 78 | except (FileNotFoundError, json.JSONDecodeError): 79 | self.orders = {} 80 | 81 | -------------------------------------------------------------------------------- /src/OrderBook.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "Messages.h" 8 | #include "MemoryPool.h" 9 | #include "Order.h" 10 | #include "Logging.h" 11 | 12 | // OrderBook now also maintains stop and iceberg orders. 13 | // Stop-loss orders are stored in a separate structure and activated when price triggers. 14 | // Iceberg orders are stored like normal orders but manage visibleQuantity internally. 15 | 16 | class OrderBook { 17 | public: 18 | OrderBook(); 19 | 20 | bool addOrder(Order* o); 21 | bool cancelOrder(uint64_t orderId, uint64_t participantId); 22 | bool modifyOrder(uint64_t orderId, double newPrice, uint64_t newQty, uint64_t participantId); 23 | 24 | std::vector match(uint64_t seqBase, uint64_t timestamp); 25 | 26 | void getTopOfBook(double &bestBid, double &bestAsk); 27 | void setMemoryPool(MemoryPool* pool) { orderPool_ = pool; } 28 | 29 | // Add a trade price to track volatility 30 | double getLastTradePrice() const; 31 | void recordTradePrice(double price, uint64_t quantity); 32 | 33 | // Trigger stop-loss orders if conditions are met 34 | void triggerStopOrders(uint64_t timestamp, uint64_t &seqBase); 35 | 36 | private: 37 | // For simplicity: bids: descending price, asks: ascending price 38 | std::map> bids; 39 | std::map> asks; 40 | 41 | std::unordered_map orderLookup; 42 | 43 | // Stop-loss orders: store separately keyed by trigger price and side 44 | // On trigger, convert them into market orders 45 | // We use a multimap keyed by triggerPrice, to quickly find triggers 46 | std::multimap stopOrdersBuy; // trigger when price <= triggerPrice 47 | std::multimap stopOrdersSell; // trigger when price >= triggerPrice 48 | 49 | // Access control 50 | mutable std::shared_mutex bookMutex; 51 | MemoryPool* orderPool_ = nullptr; 52 | 53 | // Internal utilities 54 | bool removeOrderFromBook(Order* o); 55 | void insertStopOrder(Order* o); 56 | void activateStopOrder(Order* o, uint64_t timestamp, uint64_t &seqBase, std::vector &trades); 57 | 58 | std::vector matchBook(uint64_t seqBase, uint64_t timestamp); 59 | 60 | // For volatility tracking 61 | double lastTradePrice = 0.0; 62 | bool haveLastTrade = false; 63 | std::deque> recentTrades; // Price, Quantity 64 | size_t maxRecentTrades = 100; // Maintain last 100 trades 65 | 66 | // Helper for iceberg orders: refresh visible qty after partial fills 67 | void refreshIceberg(Order* o); 68 | 69 | friend class MatchingEngine; 70 | }; 71 | -------------------------------------------------------------------------------- /src/EventLoop.cpp: -------------------------------------------------------------------------------- 1 | #include "EventLoop.h" 2 | #include "Logging.h" 3 | #include "NetworkInterface.h" 4 | #include 5 | #include 6 | #include 7 | 8 | EventLoop::EventLoop(EngineController &controller) 9 | : controller_(controller) {} 10 | 11 | bool EventLoop::init(int listenFd) { 12 | listenFd_ = listenFd; 13 | kqfd_ = kqueue(); 14 | if (kqfd_ < 0) { 15 | LOG(LogLevel::ERROR, "kqueue creation failed"); 16 | return false; 17 | } 18 | 19 | struct kevent ev; 20 | EV_SET(&ev, listenFd_, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, nullptr); 21 | if (kevent(kqfd_, &ev, 1, nullptr, 0, nullptr) < 0) { 22 | LOG(LogLevel::ERROR, "kevent ADD listenFd failed"); 23 | return false; 24 | } 25 | return true; 26 | } 27 | 28 | void EventLoop::run() { 29 | const int MAX_EVENTS = 64; 30 | struct kevent events[MAX_EVENTS]; 31 | 32 | while (true) { 33 | int n = kevent(kqfd_, nullptr, 0, events, MAX_EVENTS, nullptr); 34 | if (n < 0) { 35 | if (errno == EINTR) continue; 36 | LOG(LogLevel::ERROR, "kevent wait error"); 37 | break; 38 | } 39 | 40 | for (int i = 0; i < n; ++i) { 41 | int fd = (int)events[i].ident; 42 | int16_t filter = events[i].filter; 43 | 44 | if (fd == listenFd_) { 45 | if (!handleNewConnection()) { 46 | LOG(LogLevel::ERROR, "handleNewConnection failed"); 47 | } 48 | } else { 49 | if (!handleEvent(fd, filter)) { 50 | removeSession(fd); 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | bool EventLoop::handleNewConnection() { 58 | NetworkInterface net; 59 | std::string addr; 60 | int clientFd = net.acceptClient(listenFd_, addr); 61 | if (clientFd < 0) { 62 | return true; 63 | } 64 | 65 | Session *sess = new Session(clientFd, controller_, kqfd_); 66 | 67 | struct kevent ev; 68 | EV_SET(&ev, clientFd, EVFILT_READ, EV_ADD | EV_ENABLE | EV_CLEAR, 0, 0, nullptr); 69 | if (kevent(kqfd_, &ev, 1, nullptr, 0, nullptr) < 0) { 70 | LOG(LogLevel::ERROR, "kevent ADD clientFd failed"); 71 | delete sess; 72 | return false; 73 | } 74 | 75 | sessions_[clientFd] = sess; 76 | LOG(LogLevel::INFO, "New client connected fd=" << clientFd); 77 | return true; 78 | } 79 | 80 | bool EventLoop::handleEvent(int fd, int16_t filter) { 81 | auto it = sessions_.find(fd); 82 | if (it == sessions_.end()) { 83 | LOG(LogLevel::WARN, "Event for unknown fd"); 84 | return false; 85 | } 86 | Session* sess = it->second; 87 | 88 | if (filter == EVFILT_READ) { 89 | if (!sess->onReadable()) return false; 90 | } else if (filter == EVFILT_WRITE) { 91 | if (!sess->onWritable()) return false; 92 | } 93 | 94 | return true; 95 | } 96 | 97 | void EventLoop::removeSession(int fd) { 98 | struct kevent ev; 99 | EV_SET(&ev, fd, EVFILT_READ, EV_DELETE, 0, 0, nullptr); 100 | kevent(kqfd_, &ev, 1, nullptr, 0, nullptr); 101 | 102 | auto it = sessions_.find(fd); 103 | if (it != sessions_.end()) { 104 | delete it->second; 105 | sessions_.erase(it); 106 | } 107 | close(fd); 108 | LOG(LogLevel::INFO, "Client disconnected fd=" << fd); 109 | } 110 | 111 | -------------------------------------------------------------------------------- /src/EngineController.cpp: -------------------------------------------------------------------------------- 1 | #include "EngineController.h" 2 | #include "Logging.h" 3 | 4 | EngineController::EngineController(Replay &replay, SymbolConfigManager &cfg) 5 | : replayLog(replay), configManager(cfg) {} 6 | 7 | void EngineController::addEngineForSymbol(const std::string &symbol, double tickSize, uint64_t minQty, double minP, double maxP, double volThreshold, double refPrice) { 8 | std::unique_lock lock(enginesMutex); 9 | if (engines.find(symbol) != engines.end()) { 10 | LOG(LogLevel::WARN, "addEngineForSymbol: already have engine for symbol"); 11 | return; 12 | } 13 | 14 | SymbolConfig sc; 15 | sc.tickSize = tickSize; 16 | sc.minQuantity = minQty; 17 | sc.minPrice = minP; 18 | sc.maxPrice = maxP; 19 | sc.volatilityThreshold = volThreshold; 20 | sc.referencePrice = refPrice; 21 | sc.tradingHalted = false; 22 | configManager.setConfig(symbol, sc); 23 | 24 | MatchingEngine* engine = new MatchingEngine(symbol, replayLog, orderPool, configManager); 25 | engines[symbol] = engine; 26 | } 27 | 28 | bool EngineController::dispatchAdd(const AddMessage &msg) { 29 | std::shared_lock lock(enginesMutex); 30 | auto it = engines.find(msg.symbol); 31 | if (it == engines.end()) { 32 | LOG(LogLevel::ERROR, "dispatchAdd: No engine for symbol"); 33 | return false; 34 | } 35 | bool success = it->second->processAdd(msg); 36 | if (success) { 37 | std::lock_guard l(orderSymbolMapMutex); 38 | orderSymbolMap[msg.orderId] = msg.symbol; 39 | } 40 | return success; 41 | } 42 | 43 | bool EngineController::dispatchCancel(const CancelMessage &msg) { 44 | std::string sym; 45 | if (!findOrderSymbol(msg.orderId, sym)) { 46 | LOG(LogLevel::ERROR, "dispatchCancel: Unknown orderId"); 47 | return false; 48 | } 49 | 50 | std::shared_lock lock(enginesMutex); 51 | auto it = engines.find(sym); 52 | if (it == engines.end()) { 53 | LOG(LogLevel::ERROR, "dispatchCancel: No engine for symbol"); 54 | return false; 55 | } 56 | return it->second->processCancel(msg); 57 | } 58 | 59 | bool EngineController::dispatchCancelReplace(const CancelReplaceMessage &msg) { 60 | std::string sym; 61 | if (!findOrderSymbol(msg.orderId, sym)) { 62 | LOG(LogLevel::ERROR, "dispatchCancelReplace: Unknown orderId"); 63 | return false; 64 | } 65 | 66 | std::shared_lock lock(enginesMutex); 67 | auto it = engines.find(sym); 68 | if (it == engines.end()) { 69 | LOG(LogLevel::ERROR, "dispatchCancelReplace: No engine for symbol"); 70 | return false; 71 | } 72 | return it->second->processCancelReplace(msg); 73 | } 74 | 75 | void EngineController::dispatchSnapshotRequest(const SnapshotRequest &msg) { 76 | std::shared_lock lock(enginesMutex); 77 | auto it = engines.find(msg.symbol); 78 | if (it == engines.end()) { 79 | LOG(LogLevel::ERROR, "dispatchSnapshotRequest: No engine for symbol"); 80 | return; 81 | } 82 | it->second->processSnapshotRequest(msg); 83 | } 84 | 85 | double EngineController::getLastTradePrice(const std::string &symbol) const { 86 | std::shared_lock lock(enginesMutex); 87 | auto it = engines.find(symbol); 88 | if (it == engines.end()) { 89 | throw std::runtime_error("getLastTradePrice: No machine engine for symbol"); 90 | } 91 | return it->second->getLastTradePrice(); 92 | } 93 | 94 | void EngineController::getTopOfBook(const std::string &symbol, double &bestBid, double &bestAsk) { 95 | std::shared_lock lock(enginesMutex); 96 | auto it = engines.find(symbol); 97 | if (it == engines.end()) { 98 | throw std::runtime_error("getTopOfBook: No engine for symbol " + symbol); 99 | } 100 | it->second->orderBook.getTopOfBook(bestBid, bestAsk); 101 | } 102 | 103 | void EngineController::recordOrderSymbol(uint64_t orderId, const std::string &symbol) { 104 | std::lock_guard lock(orderSymbolMapMutex); 105 | orderSymbolMap[orderId] = symbol; 106 | } 107 | 108 | bool EngineController::findOrderSymbol(uint64_t orderId, std::string &symbol) { 109 | std::lock_guard lock(orderSymbolMapMutex); 110 | auto it = orderSymbolMap.find(orderId); 111 | if (it == orderSymbolMap.end()) return false; 112 | symbol = it->second; 113 | return true; 114 | } 115 | 116 | -------------------------------------------------------------------------------- /src/Session.cpp: -------------------------------------------------------------------------------- 1 | #include "Session.h" 2 | #include "Logging.h" 3 | #include 4 | #include 5 | #include 6 | 7 | Session::Session(int fd, EngineController &controller, int kqfd) 8 | : fd_(fd), kqfd_(kqfd), controller_(controller) { } 9 | 10 | Session::~Session() { 11 | close(fd_); 12 | } 13 | 14 | bool Session::onReadable() { 15 | char buf[4096]; 16 | while (true) { 17 | ssize_t n = read(fd_, buf, sizeof(buf)); 18 | if (n > 0) { 19 | parser_.appendData(buf, n); 20 | while (true) { 21 | auto hdr = parser_.nextMessageHeader(); 22 | if (!hdr.has_value()) break; // need more data 23 | MessageType mt = hdr->type; 24 | 25 | bool handled = true; 26 | if (mt == MessageType::ADD) { 27 | auto m = parser_.nextAddMessage(); 28 | if (!m.has_value()) break; 29 | handled = handleAdd(*m); 30 | } else if (mt == MessageType::CANCEL) { 31 | auto m = parser_.nextCancelMessage(); 32 | if (!m.has_value()) break; 33 | handled = handleCancel(*m); 34 | } else if (mt == MessageType::CANCEL_REPLACE) { 35 | auto m = parser_.nextCancelReplaceMessage(); 36 | if (!m.has_value()) break; 37 | handled = handleCancelReplace(*m); 38 | } else if (mt == MessageType::SNAPSHOT_REQUEST) { 39 | auto m = parser_.nextSnapshotRequest(); 40 | if (!m.has_value()) break; 41 | handled = handleSnapshotRequest(*m); 42 | } else { 43 | LOG(LogLevel::WARN, "Unknown message type"); 44 | handled = false; 45 | break; 46 | } 47 | 48 | if (!handled) { 49 | LOG(LogLevel::ERROR, "Failed to handle message"); 50 | } 51 | } 52 | } else if (n == 0) { 53 | // client disconnected 54 | return false; 55 | } else { 56 | if (errno == EAGAIN || errno == EWOULDBLOCK) { 57 | // no more data 58 | break; 59 | } else { 60 | LOG(LogLevel::ERROR, "read error on client fd"); 61 | return false; 62 | } 63 | } 64 | } 65 | 66 | return true; 67 | } 68 | 69 | bool Session::onWritable() { 70 | while (!writeQueue_.empty()) { 71 | const std::string &msg = writeQueue_.front(); 72 | ssize_t n = write(fd_, msg.data(), msg.size()); 73 | if (n < 0) { 74 | if (errno == EAGAIN || errno == EWOULDBLOCK) { 75 | return true; 76 | } else { 77 | LOG(LogLevel::ERROR, "write error"); 78 | return false; 79 | } 80 | } else if ((size_t)n < msg.size()) { 81 | writeQueue_.front() = msg.substr(n); 82 | return true; 83 | } else { 84 | writeQueue_.erase(writeQueue_.begin()); 85 | } 86 | } 87 | return true; 88 | } 89 | 90 | void Session::queueResponse(const std::string &msg) { 91 | writeQueue_.push_back(msg); 92 | 93 | // Register for writable events 94 | struct kevent ev; 95 | EV_SET(&ev, fd_, EVFILT_WRITE, EV_ADD | EV_ENABLE, 0, 0, this); 96 | if (kevent(kqfd_, &ev, 1, nullptr, 0, nullptr) < 0) { 97 | LOG(LogLevel::ERROR, "Failed to register writable event for fd=" << fd_); 98 | } 99 | } 100 | 101 | bool Session::handleAdd(const AddMessage &msg) { 102 | bool success = controller_.dispatchAdd(msg); 103 | if (success) queueResponse("ADD_ACK\n"); 104 | else queueResponse("ADD_NACK\n"); 105 | return success; 106 | } 107 | 108 | bool Session::handleCancel(const CancelMessage &msg) { 109 | bool success = controller_.dispatchCancel(msg); 110 | if (success) queueResponse("CANCEL_ACK\n"); 111 | else queueResponse("CANCEL_NACK\n"); 112 | return success; 113 | } 114 | 115 | bool Session::handleCancelReplace(const CancelReplaceMessage &msg) { 116 | bool success = controller_.dispatchCancelReplace(msg); 117 | if (success) queueResponse("CANCEL_REPLACE_ACK\n"); 118 | else queueResponse("CANCEL_REPLACE_NACK\n"); 119 | return success; 120 | } 121 | 122 | bool Session::handleSnapshotRequest(const SnapshotRequest &msg) { 123 | controller_.dispatchSnapshotRequest(msg); 124 | 125 | // Prepare detailed snapshot response 126 | double bestBid = 0.0, bestAsk = 0.0, lastTradePrice = 0.0; 127 | controller_.getTopOfBook(msg.symbol, bestBid, bestAsk); 128 | lastTradePrice = controller_.getLastTradePrice(msg.symbol); 129 | 130 | std::ostringstream response; 131 | response << "SNAPSHOT|symbol=" << msg.symbol 132 | << "|bestBid=" << bestBid 133 | << "|bestAsk=" << bestAsk 134 | << "|lastTradePrice=" << lastTradePrice << "\n"; 135 | 136 | queueResponse(response.str()); 137 | return true; 138 | } 139 | 140 | -------------------------------------------------------------------------------- /src/MessageParser.cpp: -------------------------------------------------------------------------------- 1 | #include "MessageParser.h" 2 | #include "Logging.h" 3 | #include 4 | #include 5 | #include 6 | 7 | void MessageParser::appendData(const char* data, size_t len) { 8 | buffer_.insert(buffer_.end(), data, data + len); 9 | } 10 | 11 | std::optional MessageParser::nextMessageHeader() { 12 | auto it = std::find(buffer_.begin(), buffer_.end(), '\n'); 13 | if (it == buffer_.end()) { 14 | return std::nullopt; 15 | } 16 | // Peek line 17 | std::string line(buffer_.begin(), it); 18 | // Format: TYPE|seq|timestamp|... 19 | // Extract first three fields 20 | std::istringstream iss(line); 21 | std::string typeStr; 22 | std::getline(iss, typeStr, '|'); 23 | if (typeStr.empty()) { 24 | // consume line 25 | buffer_.erase(buffer_.begin(), it+1); 26 | return std::nullopt; 27 | } 28 | 29 | std::string seqStr; 30 | std::getline(iss, seqStr, '|'); 31 | if (seqStr.empty()) { 32 | buffer_.erase(buffer_.begin(), it+1); 33 | return std::nullopt; 34 | } 35 | 36 | std::string tsStr; 37 | std::getline(iss, tsStr, '|'); 38 | if (tsStr.empty()) { 39 | // Some messages might not have more fields 40 | // Just handle minimal 41 | tsStr = "0"; 42 | } 43 | 44 | MessageType mt; 45 | if (typeStr == "ADD") mt = MessageType::ADD; 46 | else if (typeStr == "CANCEL") mt = MessageType::CANCEL; 47 | else if (typeStr == "CANCEL_REPLACE") mt = MessageType::CANCEL_REPLACE; 48 | else if (typeStr == "SNAPSHOT_REQUEST") mt = MessageType::SNAPSHOT_REQUEST; 49 | else { 50 | // unknown 51 | LOG(LogLevel::WARN, "Unknown message type: " << typeStr); 52 | buffer_.erase(buffer_.begin(), it+1); 53 | return std::nullopt; 54 | } 55 | 56 | uint64_t seq = std::stoull(seqStr); 57 | uint64_t ts = std::stoull(tsStr); 58 | 59 | MessageHeader hdr{mt, seq, ts}; 60 | return hdr; 61 | } 62 | 63 | static std::vector splitLine(const std::string &line) { 64 | std::vector parts; 65 | size_t start = 0; 66 | while (true) { 67 | auto pos = line.find('|', start); 68 | if (pos == std::string::npos) { 69 | parts.push_back(line.substr(start)); 70 | break; 71 | } else { 72 | parts.push_back(line.substr(start, pos - start)); 73 | start = pos + 1; 74 | } 75 | } 76 | return parts; 77 | } 78 | 79 | std::optional MessageParser::nextAddMessage() { 80 | auto it = std::find(buffer_.begin(), buffer_.end(), '\n'); 81 | if (it == buffer_.end()) return std::nullopt; 82 | std::string line(buffer_.begin(), it); 83 | 84 | auto parts = splitLine(line); 85 | // ADD|seq|ts|orderId|symbol|price|qty|side|tif|ordertype|participantId|triggerPrice|visibleQty 86 | // Side=BUY/SELL, tif=GTC/IOC/FOK, ordertype=LIMIT/MARKET/STOP_LOSS/ICEBERG 87 | if (parts.size() < 8) { 88 | // consume line 89 | buffer_.erase(buffer_.begin(), it+1); 90 | return std::nullopt; 91 | } 92 | if (parts[0] != "ADD") { 93 | buffer_.erase(buffer_.begin(), it+1); 94 | return std::nullopt; 95 | } 96 | AddMessage msg; 97 | msg.header.type = MessageType::ADD; 98 | msg.header.sequence = std::stoull(parts[1]); 99 | msg.header.timestamp = std::stoull(parts[2]); 100 | msg.orderId = std::stoull(parts[3]); 101 | msg.symbol = parts[4]; 102 | msg.price = std::stod(parts[5]); 103 | msg.quantity = std::stoull(parts[6]); 104 | msg.side = (parts[7] == "BUY") ? Side::BUY : Side::SELL; 105 | 106 | // Default 107 | msg.tif = TimeInForce::GTC; 108 | msg.orderType = OrderType::LIMIT; 109 | msg.participantId = 0; 110 | msg.triggerPrice = 0.0; 111 | msg.visibleQuantity = msg.quantity; 112 | 113 | if (parts.size() >= 9) { 114 | std::string tifStr = parts[8]; 115 | if (tifStr == "GTC") msg.tif = TimeInForce::GTC; 116 | else if (tifStr == "IOC") msg.tif = TimeInForce::IOC; 117 | else if (tifStr == "FOK") msg.tif = TimeInForce::FOK; 118 | } 119 | 120 | if (parts.size() >= 10) { 121 | std::string otype = parts[9]; 122 | if (otype == "LIMIT") msg.orderType = OrderType::LIMIT; 123 | else if (otype == "MARKET") msg.orderType = OrderType::MARKET; 124 | else if (otype == "STOP_LOSS") msg.orderType = OrderType::STOP_LOSS; 125 | else if (otype == "ICEBERG") msg.orderType = OrderType::ICEBERG; 126 | } 127 | 128 | if (parts.size() >= 11) { 129 | msg.participantId = std::stoull(parts[10]); 130 | } 131 | 132 | if (parts.size() >= 12) { 133 | msg.triggerPrice = std::stod(parts[11]); 134 | } 135 | 136 | if (parts.size() >= 13) { 137 | msg.visibleQuantity = std::stoull(parts[12]); 138 | } 139 | 140 | buffer_.erase(buffer_.begin(), it+1); 141 | return msg; 142 | } 143 | 144 | std::optional MessageParser::nextCancelMessage() { 145 | auto it = std::find(buffer_.begin(), buffer_.end(), '\n'); 146 | if (it == buffer_.end()) return std::nullopt; 147 | std::string line(buffer_.begin(), it); 148 | auto parts = splitLine(line); 149 | // CANCEL|seq|ts|orderId|participantId 150 | if (parts.size() < 4 || parts[0] != "CANCEL") { 151 | buffer_.erase(buffer_.begin(), it+1); 152 | return std::nullopt; 153 | } 154 | 155 | CancelMessage msg; 156 | msg.header.type = MessageType::CANCEL; 157 | msg.header.sequence = std::stoull(parts[1]); 158 | msg.header.timestamp = std::stoull(parts[2]); 159 | msg.orderId = std::stoull(parts[3]); 160 | msg.participantId = 0; 161 | if (parts.size() >= 5) { 162 | msg.participantId = std::stoull(parts[4]); 163 | } 164 | 165 | buffer_.erase(buffer_.begin(), it+1); 166 | return msg; 167 | } 168 | 169 | std::optional MessageParser::nextCancelReplaceMessage() { 170 | auto it = std::find(buffer_.begin(), buffer_.end(), '\n'); 171 | if (it == buffer_.end()) return std::nullopt; 172 | std::string line(buffer_.begin(), it); 173 | auto parts = splitLine(line); 174 | // CANCEL_REPLACE|seq|ts|orderId|newPrice|newQty|participantId 175 | if (parts.size() < 6 || parts[0] != "CANCEL_REPLACE") { 176 | buffer_.erase(buffer_.begin(), it+1); 177 | return std::nullopt; 178 | } 179 | CancelReplaceMessage msg; 180 | msg.header.type = MessageType::CANCEL_REPLACE; 181 | msg.header.sequence = std::stoull(parts[1]); 182 | msg.header.timestamp = std::stoull(parts[2]); 183 | msg.orderId = std::stoull(parts[3]); 184 | msg.newPrice = std::stod(parts[4]); 185 | msg.newQuantity = std::stoull(parts[5]); 186 | msg.participantId = 0; 187 | if (parts.size() >= 7) { 188 | msg.participantId = std::stoull(parts[6]); 189 | } 190 | 191 | buffer_.erase(buffer_.begin(), it+1); 192 | return msg; 193 | } 194 | 195 | std::optional MessageParser::nextSnapshotRequest() { 196 | auto it = std::find(buffer_.begin(), buffer_.end(), '\n'); 197 | if (it == buffer_.end()) return std::nullopt; 198 | std::string line(buffer_.begin(), it); 199 | auto parts = splitLine(line); 200 | // SNAPSHOT_REQUEST|seq|ts|symbol 201 | if (parts.size() < 4 || parts[0] != "SNAPSHOT_REQUEST") { 202 | buffer_.erase(buffer_.begin(), it+1); 203 | return std::nullopt; 204 | } 205 | 206 | SnapshotRequest msg; 207 | msg.header.type = MessageType::SNAPSHOT_REQUEST; 208 | msg.header.sequence = std::stoull(parts[1]); 209 | msg.header.timestamp = std::stoull(parts[2]); 210 | msg.symbol = parts[3]; 211 | 212 | buffer_.erase(buffer_.begin(), it+1); 213 | return msg; 214 | } 215 | 216 | -------------------------------------------------------------------------------- /src/MatchingEngine.cpp: -------------------------------------------------------------------------------- 1 | #include "MatchingEngine.h" 2 | #include 3 | #include 4 | 5 | MatchingEngine::MatchingEngine(const std::string& sym, Replay& replay, MemoryPool& pool, SymbolConfigManager &cfg) 6 | : symbol_(sym), replayLog(replay), orderPool(pool), configManager(cfg) { 7 | orderBook.setMemoryPool(&orderPool); 8 | } 9 | 10 | bool MatchingEngine::validateAdd(const AddMessage &msg) { 11 | if (msg.symbol.size() > 7 || msg.quantity == 0) { 12 | LOG(LogLevel::ERROR, "Invalid AddMessage basic checks"); 13 | return false; 14 | } 15 | 16 | if (!quantityValid(msg.symbol, msg.quantity)) { 17 | LOG(LogLevel::WARN, "validateAdd: Quantity below min"); 18 | return false; 19 | } 20 | 21 | if (msg.orderType == OrderType::LIMIT || msg.orderType == OrderType::ICEBERG) { 22 | if (msg.price <= 0) { 23 | LOG(LogLevel::ERROR, "Invalid AddMessage price <=0 for limit/iceberg"); 24 | return false; 25 | } 26 | if (!tickSizeValid(msg.symbol, msg.price)) { 27 | LOG(LogLevel::WARN, "validateAdd: price not aligned to tickSize"); 28 | return false; 29 | } 30 | if (!priceValidForSymbol(msg.symbol, msg.price)) { 31 | LOG(LogLevel::WARN, "validateAdd: price out of allowed range"); 32 | return false; 33 | } 34 | } 35 | 36 | if (msg.orderType == OrderType::STOP_LOSS) { 37 | if (msg.triggerPrice <= 0) { 38 | LOG(LogLevel::ERROR, "Stop order invalid triggerPrice"); 39 | return false; 40 | } 41 | } 42 | 43 | if (checkVolatilityHalt(msg)) { 44 | LOG(LogLevel::WARN, "Trading halted for symbol due to volatility"); 45 | return false; 46 | } 47 | 48 | return true; 49 | } 50 | 51 | bool MatchingEngine::validateCancel(const CancelMessage &msg) { 52 | if (msg.orderId == 0) { 53 | LOG(LogLevel::ERROR, "Invalid CancelMessage orderId=0"); 54 | return false; 55 | } 56 | return true; 57 | } 58 | 59 | bool MatchingEngine::validateCancelReplace(const CancelReplaceMessage &msg) { 60 | if (msg.orderId == 0 || msg.newPrice <= 0 || msg.newQuantity == 0) { 61 | LOG(LogLevel::ERROR, "Invalid CancelReplaceMessage"); 62 | return false; 63 | } 64 | if (!tickSizeValid(symbol_, msg.newPrice)) { 65 | LOG(LogLevel::WARN, "cancelReplace: new price not aligned to tickSize"); 66 | return false; 67 | } 68 | if (!quantityValid(symbol_, msg.newQuantity)) { 69 | LOG(LogLevel::WARN, "cancelReplace: quantity below min"); 70 | return false; 71 | } 72 | if (!priceValidForSymbol(symbol_, msg.newPrice)) { 73 | LOG(LogLevel::WARN, "cancelReplace: price out of allowed range"); 74 | return false; 75 | } 76 | return true; 77 | } 78 | 79 | double MatchingEngine::getLastTradePrice() const { 80 | return orderBook.getLastTradePrice(); 81 | } 82 | 83 | bool MatchingEngine::processAdd(const AddMessage &msg) { 84 | if (!validateAdd(msg)) return false; 85 | 86 | SymbolConfig cfg; 87 | if (!configManager.getConfig(msg.symbol, cfg)) { 88 | LOG(LogLevel::ERROR, "No config for symbol"); 89 | return false; 90 | } 91 | if (cfg.tradingHalted) { 92 | LOG(LogLevel::WARN, "Trading halted for symbol"); 93 | return false; 94 | } 95 | 96 | auto timestamp = (uint64_t)std::chrono::system_clock::now().time_since_epoch().count(); 97 | Order* o = orderPool.allocate(); 98 | new(o) Order(msg.orderId, msg.side, msg.symbol, msg.price, msg.quantity, timestamp, 99 | msg.participantId, msg.tif, msg.orderType, msg.triggerPrice, msg.visibleQuantity); 100 | 101 | // Write-ahead log 102 | replayLog.logAddMessage(msg.header.sequence, msg); 103 | 104 | std::vector trades; 105 | 106 | if (o->orderType == OrderType::MARKET) { 107 | // Market orders try to match immediately 108 | handleMarketOrder(o, trades, timestamp); 109 | } else if (o->tif != TimeInForce::GTC) { 110 | // IOC/FOK logic 111 | handleIocFok(o, trades, timestamp); 112 | } else { 113 | // GTC limit/iceberg/stop 114 | std::unique_lock lock(orderBook.bookMutex); 115 | if (!orderBook.addOrder(o)) { 116 | orderPool.deallocate(o); 117 | return false; 118 | } 119 | // If limit order just placed, try match 120 | if (o->orderType == OrderType::LIMIT || o->orderType == OrderType::ICEBERG) { 121 | auto res = orderBook.matchBook(nextSequence.load(), timestamp); 122 | for (auto &t : res) { 123 | nextSequence.store(t.header.sequence + 1); 124 | sendExecution(t); 125 | } 126 | } 127 | } 128 | 129 | // Send executions 130 | for (auto &t : trades) { 131 | nextSequence.store(t.header.sequence + 1); 132 | sendExecution(t); 133 | } 134 | 135 | return true; 136 | } 137 | 138 | bool MatchingEngine::processCancel(const CancelMessage &msg) { 139 | if (!validateCancel(msg)) return false; 140 | replayLog.logCancelMessage(msg.header.sequence, msg); 141 | 142 | std::unique_lock lock(orderBook.bookMutex); 143 | bool success = orderBook.cancelOrder(msg.orderId, msg.participantId); 144 | return success; 145 | } 146 | 147 | bool MatchingEngine::processCancelReplace(const CancelReplaceMessage &msg) { 148 | if (!validateCancelReplace(msg)) return false; 149 | 150 | replayLog.logCancelReplaceMessage(msg.header.sequence, msg); 151 | 152 | std::unique_lock lock(orderBook.bookMutex); 153 | bool success = orderBook.modifyOrder(msg.orderId, msg.newPrice, msg.newQuantity, msg.participantId); 154 | if (success) { 155 | uint64_t timestamp = (uint64_t)std::chrono::system_clock::now().time_since_epoch().count(); 156 | auto trades = orderBook.matchBook(nextSequence.load(), timestamp); 157 | for (auto &t : trades) { 158 | nextSequence.store(t.header.sequence + 1); 159 | sendExecution(t); 160 | } 161 | } 162 | return success; 163 | } 164 | 165 | void MatchingEngine::processSnapshotRequest(const SnapshotRequest &msg) { 166 | std::shared_lock lock(orderBook.bookMutex); 167 | SnapshotResponse resp; 168 | resp.header.type = MessageType::SNAPSHOT_RESPONSE; 169 | resp.header.sequence = msg.header.sequence; 170 | resp.header.timestamp = (uint64_t)std::chrono::system_clock::now().time_since_epoch().count(); 171 | resp.symbol = msg.symbol; 172 | orderBook.getTopOfBook(resp.bestBid, resp.bestAsk); 173 | resp.lastTradePrice = orderBook.getLastTradePrice(); 174 | LOG(LogLevel::INFO, "Snapshot for " << msg.symbol << ": bestBid=" << resp.bestBid << ", bestAsk=" << resp.bestAsk); 175 | } 176 | 177 | void MatchingEngine::sendExecution(const ExecutionMessage &exec) { 178 | // Multicast execution 179 | replayLog.logExecutionMessage(exec.header.sequence, exec); 180 | LOG(LogLevel::INFO, "Execution: seq=" << exec.header.sequence << " symbol=" << exec.symbol << " qty=" << exec.quantity << " price=" << exec.price); 181 | } 182 | 183 | void MatchingEngine::step() { 184 | // All ops triggered by client request so no delayed orders 185 | } 186 | 187 | bool MatchingEngine::priceValidForSymbol(const std::string &symbol, double price) { 188 | SymbolConfig cfg; 189 | if (!configManager.getConfig(symbol, cfg)) return false; 190 | return (price >= cfg.minPrice && price <= cfg.maxPrice); 191 | } 192 | 193 | bool MatchingEngine::tickSizeValid(const std::string &symbol, double price) { 194 | SymbolConfig cfg; 195 | if (!configManager.getConfig(symbol, cfg)) return false; 196 | double ticks = price / cfg.tickSize; 197 | double rounded = std::floor(ticks); 198 | double diff = std::abs(ticks - rounded); 199 | return diff < 1e-9; // close enough for floating point 200 | } 201 | 202 | bool MatchingEngine::quantityValid(const std::string &symbol, uint64_t qty) { 203 | SymbolConfig cfg; 204 | if (!configManager.getConfig(symbol, cfg)) return false; 205 | return qty >= cfg.minQuantity; 206 | } 207 | 208 | bool MatchingEngine::checkVolatilityHalt(const AddMessage &msg) { 209 | SymbolConfig cfg; 210 | if (!configManager.getConfig(msg.symbol, cfg)) return false; 211 | if (cfg.tradingHalted) return true; 212 | // Simple check: if price is way off reference 213 | if (msg.orderType == OrderType::LIMIT || msg.orderType == OrderType::ICEBERG) { 214 | double pctChange = std::abs((msg.price - cfg.referencePrice)/cfg.referencePrice); 215 | if (pctChange > cfg.volatilityThreshold) { 216 | // Trigger halt 217 | configManager.haltTrading(msg.symbol); 218 | return true; 219 | } 220 | } 221 | return false; 222 | } 223 | 224 | bool MatchingEngine::checkTimeInForce(Order* o, std::vector &trades, uint64_t timestamp) { 225 | // If FOK and not fully matched, cancel: 226 | if (o->tif == TimeInForce::FOK) { 227 | if (o->quantity > 0) { 228 | // not fully matched 229 | orderBook.cancelOrder(o->orderId, o->participantId); 230 | } 231 | return false; // order done 232 | } else if (o->tif == TimeInForce::IOC) { 233 | // IOC: cancel remainder 234 | if (o->quantity > 0) { 235 | orderBook.cancelOrder(o->orderId, o->participantId); 236 | } 237 | return false; 238 | } 239 | return true; // GTC continues 240 | } 241 | 242 | void MatchingEngine::handleMarketOrder(Order* o, std::vector &trades, uint64_t timestamp) { 243 | std::unique_lock lock(orderBook.bookMutex); 244 | orderBook.addOrder(o); // Add to lookup to allow cancel if needed 245 | // Immediately match 246 | auto res = orderBook.matchBook(nextSequence.load(), timestamp); 247 | 248 | // Market orders are either fully matched or partial 249 | // If partial remains, and TIF=FOK or IOC, handle 250 | // If GTC (which doesn't make sense for market in this design), we would just discard remainder. 251 | 252 | for (auto &t : res) { 253 | trades.push_back(t); 254 | } 255 | 256 | // For a market order without FOK/IOC explicitly set, treat as FOK? 257 | // Let's assume market orders always IOC. 258 | // Cancel remainder if any 259 | if (o->quantity > 0) { 260 | orderBook.cancelOrder(o->orderId, o->participantId); 261 | } 262 | } 263 | 264 | void MatchingEngine::handleIocFok(Order* o, std::vector &trades, uint64_t timestamp) { 265 | std::unique_lock lock(orderBook.bookMutex); 266 | orderBook.addOrder(o); 267 | auto res = orderBook.matchBook(nextSequence.load(), timestamp); 268 | for (auto &t : res) { 269 | trades.push_back(t); 270 | } 271 | checkTimeInForce(o, trades, timestamp); 272 | } 273 | 274 | -------------------------------------------------------------------------------- /src/OrderBook.cpp: -------------------------------------------------------------------------------- 1 | #include "OrderBook.h" 2 | #include "Logging.h" 3 | #include 4 | #include 5 | 6 | OrderBook::OrderBook() {} 7 | 8 | bool OrderBook::addOrder(Order* o) { 9 | if (!o) { LOG(LogLevel::ERROR, "addOrder: Null order pointer"); return false; } 10 | if (orderLookup.find(o->orderId) != orderLookup.end()) { 11 | LOG(LogLevel::WARN, "addOrder: orderId already exists"); 12 | return false; 13 | } 14 | 15 | if (o->orderType == OrderType::STOP_LOSS) { 16 | insertStopOrder(o); 17 | orderLookup[o->orderId] = o; 18 | return true; 19 | } 20 | 21 | if (o->orderType == OrderType::MARKET) { 22 | // Market orders won't rest in the book. 23 | // They will be matched immediately by the caller. 24 | // Just add to lookup so we can cancel if needed quickly (FOK scenario) 25 | orderLookup[o->orderId] = o; 26 | return true; 27 | } 28 | 29 | auto &book = (o->side == Side::BUY) ? bids : asks; 30 | book[o->price].push(o); 31 | orderLookup[o->orderId] = o; 32 | return true; 33 | } 34 | 35 | bool OrderBook::cancelOrder(uint64_t orderId, uint64_t participantId) { 36 | auto it = orderLookup.find(orderId); 37 | if (it == orderLookup.end()) { 38 | LOG(LogLevel::INFO, "cancelOrder: orderId not found"); 39 | return false; 40 | } 41 | Order* o = it->second; 42 | if (o->participantId != participantId) { 43 | LOG(LogLevel::WARN, "cancelOrder: participant mismatch"); 44 | return false; // not allowed to cancel others' orders 45 | } 46 | 47 | if (o->orderType == OrderType::STOP_LOSS) { 48 | // Remove from stopOrders maps 49 | bool found = false; 50 | if (o->side == Side::BUY) { 51 | auto range = stopOrdersBuy.equal_range(o->triggerPrice); 52 | for (auto sit = range.first; sit != range.second; ++sit) { 53 | if (sit->second == o) { 54 | stopOrdersBuy.erase(sit); 55 | found = true; 56 | break; 57 | } 58 | } 59 | } else { 60 | auto range = stopOrdersSell.equal_range(o->triggerPrice); 61 | for (auto sit = range.first; sit != range.second; ++sit) { 62 | if (sit->second == o) { 63 | stopOrdersSell.erase(sit); 64 | found = true; 65 | break; 66 | } 67 | } 68 | } 69 | if (!found) { 70 | LOG(LogLevel::WARN, "cancelOrder: stop order not found in stopOrders map"); 71 | } 72 | orderLookup.erase(orderId); 73 | orderPool_->deallocate(o); 74 | return true; 75 | } else if (o->orderType == OrderType::MARKET) { 76 | // If market order is still here, means FOK/IOC scenario 77 | orderLookup.erase(orderId); 78 | orderPool_->deallocate(o); 79 | return true; 80 | } 81 | 82 | bool removed = removeOrderFromBook(o); 83 | if (removed && orderPool_) { 84 | orderPool_->deallocate(o); 85 | } 86 | return removed; 87 | } 88 | 89 | bool OrderBook::modifyOrder(uint64_t orderId, double newPrice, uint64_t newQty, uint64_t participantId) { 90 | auto it = orderLookup.find(orderId); 91 | if (it == orderLookup.end()) { 92 | LOG(LogLevel::INFO, "modifyOrder: orderId not found"); 93 | return false; 94 | } 95 | Order* oldOrder = it->second; 96 | if (oldOrder->participantId != participantId) { 97 | LOG(LogLevel::WARN, "modifyOrder: participant mismatch"); 98 | return false; 99 | } 100 | 101 | // Can't modify stop loss trigger conditions or order type easily here; we keep it simple 102 | // We'll allow price/qty modifications for limit orders. For stop or market, disallow. 103 | if (oldOrder->orderType != OrderType::LIMIT && oldOrder->orderType != OrderType::ICEBERG) { 104 | LOG(LogLevel::WARN, "modifyOrder: can only modify limit/iceberg"); 105 | return false; 106 | } 107 | 108 | if (!removeOrderFromBook(oldOrder)) { 109 | return false; 110 | } 111 | 112 | oldOrder->price = newPrice; 113 | oldOrder->quantity = newQty; 114 | oldOrder->visibleQuantity = (oldOrder->orderType == OrderType::ICEBERG && oldOrder->visibleQuantity > newQty) ? newQty : oldOrder->visibleQuantity; 115 | oldOrder->totalQuantity = newQty; 116 | 117 | auto &book = (oldOrder->side == Side::BUY) ? bids : asks; 118 | book[newPrice].push(oldOrder); 119 | orderLookup[oldOrder->orderId] = oldOrder; 120 | return true; 121 | } 122 | 123 | bool OrderBook::removeOrderFromBook(Order* o) { 124 | auto &book = (o->side == Side::BUY) ? bids : asks; 125 | auto it = book.find(o->price); 126 | if (it == book.end()) return false; 127 | 128 | std::queue &q = it->second; 129 | std::queue newQ; 130 | bool removed = false; 131 | while(!q.empty()) { 132 | Order* front = q.front(); q.pop(); 133 | if (front == o) { 134 | removed = true; 135 | } else { 136 | newQ.push(front); 137 | } 138 | } 139 | if (newQ.empty()) { 140 | book.erase(it); 141 | } else { 142 | it->second = std::move(newQ); 143 | } 144 | if (removed) { 145 | orderLookup.erase(o->orderId); 146 | } 147 | return removed; 148 | } 149 | 150 | void OrderBook::getTopOfBook(double &bestBid, double &bestAsk) { 151 | std::shared_lock lock(bookMutex); 152 | bestBid = (bids.empty()) ? 0.0 : bids.rbegin()->first; 153 | bestAsk = (asks.empty()) ? 0.0 : asks.begin()->first; 154 | } 155 | 156 | std::vector OrderBook::match(uint64_t seqBase, uint64_t timestamp) { 157 | std::unique_lock lock(bookMutex); 158 | // Trigger stop orders if needed 159 | triggerStopOrders(timestamp, seqBase); 160 | return matchBook(seqBase, timestamp); 161 | } 162 | 163 | std::vector OrderBook::matchBook(uint64_t seqBase, uint64_t timestamp) { 164 | std::vector trades; 165 | while(!bids.empty() && !asks.empty()) { 166 | double bestBid = bids.rbegin()->first; 167 | double bestAsk = asks.begin()->first; 168 | if (bestBid < bestAsk) break; 169 | 170 | auto &bidQueue = bids.rbegin()->second; 171 | auto &askQueue = asks.begin()->second; 172 | Order* bidOrder = bidQueue.front(); 173 | Order* askOrder = askQueue.front(); 174 | 175 | // Prevent self-trade 176 | if (bidOrder->participantId == askOrder->participantId) { 177 | // If same participant, we cannot match these two. 178 | // Move on by removing or skipping one side. Typically you'd just break. 179 | // We'll just break as no trade is possible at this price level. 180 | break; 181 | } 182 | 183 | uint64_t tradeQty = std::min(bidOrder->quantity, askOrder->quantity); 184 | double tradePrice = askOrder->price; // trades at passive order price 185 | 186 | ExecutionMessage exec; 187 | exec.header.type = MessageType::EXECUTION; 188 | exec.header.sequence = seqBase++; 189 | exec.header.timestamp = timestamp; 190 | exec.buyOrderId = (bidOrder->side == Side::BUY) ? bidOrder->orderId : askOrder->orderId; 191 | exec.sellOrderId = (askOrder->side == Side::SELL) ? askOrder->orderId : bidOrder->orderId; 192 | for (int i=0; i<8; i++) exec.symbol[i] = bidOrder->symbol[i]; 193 | exec.price = tradePrice; 194 | exec.quantity = tradeQty; 195 | exec.buyParticipantId = bidOrder->participantId; 196 | exec.sellParticipantId = askOrder->participantId; 197 | trades.push_back(exec); 198 | 199 | bidOrder->quantity -= tradeQty; 200 | askOrder->quantity -= tradeQty; 201 | 202 | recordTradePrice(tradePrice, tradeQty); 203 | if (bidOrder->orderType == OrderType::ICEBERG) refreshIceberg(bidOrder); 204 | if (askOrder->orderType == OrderType::ICEBERG) refreshIceberg(askOrder); 205 | 206 | if (bidOrder->quantity == 0) { 207 | bidQueue.pop(); 208 | orderLookup.erase(bidOrder->orderId); 209 | if (orderPool_) orderPool_->deallocate(bidOrder); 210 | } 211 | 212 | if (askOrder->quantity == 0) { 213 | askQueue.pop(); 214 | orderLookup.erase(askOrder->orderId); 215 | if (orderPool_) orderPool_->deallocate(askOrder); 216 | } 217 | 218 | if (bidQueue.empty()) { 219 | auto bidIter = bids.end(); 220 | --bidIter; 221 | bids.erase(bidIter->first); 222 | } 223 | 224 | if (askQueue.empty()) { 225 | asks.erase(asks.begin()->first); 226 | } 227 | } 228 | return trades; 229 | } 230 | 231 | double OrderBook::getLastTradePrice() const { 232 | std::shared_lock lock(bookMutex); 233 | // Use a Volume-Weighted Average Price 234 | double totalValue = 0.0; 235 | uint64_t totalVolume = 0; 236 | for (const auto &[price, quantity] : recentTrades) { 237 | totalValue += price * quantity; 238 | totalVolume += quantity; 239 | } 240 | return (totalVolume > 0) ? (totalValue / totalVolume) : 0.0; 241 | } 242 | 243 | void OrderBook::recordTradePrice(double price, uint64_t quantity) { 244 | std::unique_lock lock(bookMutex); 245 | recentTrades.emplace_back(price, quantity); 246 | if (recentTrades.size() > maxRecentTrades) { 247 | recentTrades.pop_front(); 248 | } 249 | } 250 | 251 | void OrderBook::triggerStopOrders(uint64_t timestamp, uint64_t &seqBase) { 252 | // If we have lastTradePrice, trigger STOP_LOSS orders 253 | if (!haveLastTrade) return; 254 | 255 | std::vector toActivate; 256 | // Buy stop: trigger when lastTradePrice <= triggerPrice 257 | // We'll find all buy stops with triggerPrice >= lastTradePrice 258 | { 259 | auto it = stopOrdersBuy.lower_bound(lastTradePrice); 260 | // Need to activate all orders with triggerPrice >= lastTradePrice 261 | // Actually for buy stop-loss orders, usually they trigger if price breaks below a threshold. 262 | // Here we interpret stop-loss buy: triggers if lastPrice >= triggerPrice or lastPrice <= triggerPrice? 263 | // Typically a stop BUY triggers if market goes above trigger (for a breakout). 264 | // A stop SELL triggers if market goes below trigger. 265 | // Let's define: 266 | // Stop BUY triggers if lastTradePrice >= triggerPrice 267 | // Stop SELL triggers if lastTradePrice <= triggerPrice 268 | // We'll adjust logic: 269 | } 270 | 271 | // Re-interpret: 272 | // STOP_LOSS BUY: Activate if lastTradePrice >= triggerPrice 273 | // STOP_LOSS SELL: Activate if lastTradePrice <= triggerPrice 274 | 275 | // Activate buy stops where triggerPrice <= lastTradePrice 276 | { 277 | // For buy stops: triggers if lastTradePrice >= triggerPrice 278 | // So we want all orders in stopOrdersBuy with triggerPrice <= lastTradePrice 279 | for (auto it = stopOrdersBuy.begin(); it != stopOrdersBuy.end();) { 280 | if (it->first <= lastTradePrice) { 281 | toActivate.push_back(it->second); 282 | it = stopOrdersBuy.erase(it); 283 | } else { 284 | ++it; 285 | } 286 | } 287 | } 288 | 289 | // Activate sell stops where triggerPrice >= lastTradePrice 290 | { 291 | // Sell stops trigger if lastTradePrice <= triggerPrice 292 | // Means if their triggerPrice >= lastTradePrice, we activate them 293 | for (auto it = stopOrdersSell.begin(); it != stopOrdersSell.end();) { 294 | if (it->first >= lastTradePrice) { 295 | toActivate.push_back(it->second); 296 | it = stopOrdersSell.erase(it); 297 | } else { 298 | ++it; 299 | } 300 | } 301 | } 302 | 303 | std::vector dummyTrades; 304 | for (auto* o : toActivate) { 305 | activateStopOrder(o, timestamp, seqBase, dummyTrades); 306 | } 307 | 308 | // Now that stop orders are activated as market orders, they will match on the next match call 309 | // Actually, we can immediately attempt to match them by calling matchBook again 310 | // but that is done by the caller after triggerStopOrders. 311 | } 312 | 313 | void OrderBook::activateStopOrder(Order* o, uint64_t timestamp, uint64_t &seqBase, std::vector &trades) { 314 | // Turn stop-loss order into a market order (or limit if we prefer) 315 | // For simplicity, stop orders become market orders when triggered 316 | o->orderType = OrderType::MARKET; 317 | // Insert into lookup if not present 318 | orderLookup[o->orderId] = o; 319 | // They will be matched later (caller will run matchBook) 320 | // Market orders do not rest in book, matching engine handles them immediately. 321 | // This function just changes their type. The engine call after this should handle them. 322 | } 323 | 324 | void OrderBook::insertStopOrder(Order* o) { 325 | if (o->side == Side::BUY) { 326 | stopOrdersBuy.insert({o->triggerPrice, o}); 327 | } else { 328 | stopOrdersSell.insert({o->triggerPrice, o}); 329 | } 330 | } 331 | 332 | void OrderBook::refreshIceberg(Order* o) { 333 | if (o->orderType != OrderType::ICEBERG) return; 334 | 335 | // If visibleQuantity is depleted but totalQuantity still > quantity, we can refresh visible. 336 | uint64_t alreadyVisible = o->totalQuantity - o->quantity; 337 | if (o->quantity < o->visibleQuantity && o->quantity > 0) { 338 | // visibleQuantity can't exceed current quantity 339 | // Actually, iceberg logic: visibleQuantity sets how much is shown. If partially filled, 340 | // once visible is depleted, we show again up to visibleQuantity, but not exceeding total left. 341 | // If currently quantity < visibleQuantity, no need to do anything, it's already less visible. 342 | return; 343 | } else if (o->quantity > o->visibleQuantity) { 344 | // It's possible that after trade, full visible was taken. We still have more hidden qty. 345 | // Actually, we only refresh iceberg after a fill reduces visible to 0. 346 | // If quantity still >= visibleQuantity, we do nothing special. 347 | return; 348 | } 349 | // If the order was partially filled and now quantity < visibleQuantity, 350 | // that means no refresh needed. If quantity =0, order is gone anyway. 351 | } 352 | 353 | --------------------------------------------------------------------------------