- project has been compiled using g++12.1.0 and tested on Ubuntu 22.04.2 LTS
- compilation requires C++23 support
- CppOrderBook is intended to run on a x86_64 platform
Limited scope project demonstrating reading an order-message from a socket, constructing a message
object, possibly handing it off to another thread through a seqlock-queue and inserting it into an
order book.
The project employs compile time checks using both classic template metaprogramming
and C++20 concepts. Cutting edge C++ features were incorporated if possible, such as monadic operations
on std::optional
(hence the project requiring C++23 for compilation) and interfacing with heap allocated memory via std::span
.
Being the de facto industry standard, the byte-layout of messages in the socket buffer complies
with the FIX-protocol, more specifically little-endian FIX Simple-Binary-Encoding (SBE) and the FIX
Simple-Open-Framing-Header. A precise XML specification of the message types used can be found in the misc folder.
For simplicity reasons (i.e. requiring only a single header file include), doctest was
chosen as a unit testing framework.
Special emphasis was put on techniques enabling low-latency. Those include:
- avoiding branches (or, more realistically, keeping them short) even at the expense of some unnecessary computation in each run or a less favorable theoretical algorithmic complexity
- avoiding system calls outside the set-up and tear-down phases of the project, primarily by objects allocating all their required heap memory during construction and holding on to it until destruction and using std::atomic_flag as a lock instead of some flavor of mutex
- memory-aligning objects in accordance with a 64-byte cacheline
- the result of a cachegrind run can be found in the
misc
directory - results indicate negligible rates of cache misses and branch mispredictions with the possible exception of indirection braches
- generated on Intel Core i5-1135G7
- generated running on both one and two threads and dedicated pysical cores (see section on profiling for more details)
- findings:
- regularly recurring spikes in latency by 1 or 2 orders of magnitude for unknown reasons
- when using the seq-lock queue, spikes seem to occur at an interval of roughly 2mb
- this quantity does not appear to coincide with the size of any cache level (or TLB with 4096kb memory pages)
- context switches seem an unlikely explanation as test runs were assigned isolated physical cores
- results for single-threaded run (in nanoseconds, n = 1000000):
- mean: 81.4
- standard deviation: 20.4
- max: 3684
- 0.99 quantile: 126
- number of outcomes > 1 microsecond: 26
- results for multithreaded run (in nanoseconds, n = 1000000):
- mean: 182.1
- standard deviation: 65.4
- max: 12125
- 0.99 quantile: 340
- number of outcomes > 1 microsecond: 218
the components listed below have the sole purpose of enabling unit testing and/or profiling
- mimics the behaviour of a user-space networking stack with a POSIX-socket like interface
- dispenses FIX messages as they might come in via network
std::int32_t fixMockSocket::recv(void* dest, std::uint32_t len)
represents a blocking call ofrecv
on a POSIX socket, copyinglen
bytes todest
- fixMockSocket is constructed from
std::vector<std::tuple<std::uint8_t, std::int32_t, std::uint32_t>>
- each
std::tuple
represents a single FIX message with its entries representing the message's type, volume and price respectively
template <typename LineTuple> std::vector<LineTuple> fileToTuples(const std::string& filePath, char delimiter = ',')
- reads lines from a file (comma seperated by default) to create a vector of tuples
- mock socket can then be constructed from the resulting vector
- build using
CppOrderBook/tests_and_demos/unittests/makefile
- must be run from
CppOrderBook/tests_and_demos/unittests
directory as that's where the CSVs with test data located RandTestDataErratic.csv
can be reproduced by runningCppOrderBook/misc/generate_test_data.py
- build using
CppOrderBook/tests_and_demos/profiling/makefile
- builds executables
profiling_single_threaded
andprofiling_multithreaded
- both executables read FIX messages from a mock socket, construct message objects and insert messages into an order book
profiling_multithreaded
hands message off to be inserted by a second thread viaQueue::SeqLockQueue
- both executables require the path to a CSV file with test data as first command line argument
- to achieve CPU-affinity, give index of the core (or indices of two cores for
profiling_multithreaded
) you wish the executable to run on as additonal command line arguments - both executables print out the volume contained in the order book at a random index to prevent the compiler from optimizing away the logic
- both executables generate a CSV cotaining the latency (time between starting read from socket and completing processing by order book) for every single message that was processed
- results of cachegring run can be found in
misc
directory
template <typename msgClassVariant_, typename socketType_> struct fixSocketHandler
msgClassVariant_
: std::variant of supported message type classes, subject to compile time checks regarding constructors and static members variablessocketType_
: class of socket that messages will be read from, needs to satisfy concept Auxil::readableAsSocket
- constructs FIX message objects from bytes read out of an object of a type providing a socket like interface
- reads FIX messages in little-endian Simple Binary Encoding(SBE) and the Simple Open Framing Header(SOFH)
- holds pointer to a socket-type object
std::optional<MsgClassVar> readNextMessage()
: returns an optional including the a message object within a variant when a message of a type contained in MSGClassVar is read an validated via checksum or an emptystd::optional
if message with a valid header of a type not included or a message is invalidated via checksum
- not able to correctly handle an incoming message with valid header and checksum if the message is shorter than the shortest message contained in msgClassVar_
- three classes representint different types of orders/messages for the order book:
- adding liquidity
- withdrawing liquidity added previously
- filling a market order
- all classes contain static data specifying the byte layout of incoming messages to be used by the socketHandler class
- all classes can be constructed from a std::span of 8-bit integers(for use by socketHandler), default constructed without arguments or constructed from arguments representing their respective non-static members (volume and possibly price)
- roughly analogous to
std::lock_guard
forstd::atomic_flag
instead of mutex - holds pointer to
std::atomic_flag
given as argument to constructor (can be nullptr) - has to be locked and can be unlocked by calls to member instead construction and destruction
- supports moving but not copying
bool lock()
: returns false if pointer to flag is nullptr or flag is already set by object, sets flag and returns true otherwise, will block/spin until flag is released by other threadbool unlock()
: clears flag and returns true, returns false if pointer to flag is nullptr or flag wasn't set in the first placevoid rebind(std::atomic_flag* otherflag_ptr)
: callsunlock()
and sets pointer tootherflag_ptr
template <typename EntryType, std::uint32_t alignment> struct alignas(alignment) orderBookBucket
EntryType
: represents volume in bucket, must be signed integralaligment
: alignment of bucket in order book, use 0 for default alignment
- encapsulates volume in for single price point in an order book as well as the logic to change and query volume
- negative volume represents supply, positive volume represents demand
EntryType consumeLiquidity(EntryType fillVolume)
: removes liquidity of an amount specified byfillVolume
and returns the amount of liquidity removed. Always reduces the absolute value of volume contained in bucket. Returns 0 if volume contained does not match the sign offillVolume
void addLiquidity(EntryType addVolume)
: simply adds addVolume to volume already contained in bucketEntryType getVolume()
: returns volume currently contained in bucket
template <typename msgClassVariant_, typename entryType_, std::uint32_t bookLength_, bool exclusiveCacheline_, size_t newLimitOrderIndex_, size_t withdrawLimitOrderIndex_, size_t marketOrderIndex_> struct orderBook
msgClassVariant_
: variant containing supported message classes (same as in socketHandler class template)entryType_
: same as in orderBookBucketbookLength_
: number of price buckets contained in order bookexclusiveCacheline_
: if true, each bucket will be aligned to a full cacheline (64 bytes)newLimitOrderIndex_
,withdrawLimitOrderIndex_
,marketOrderIndex_
: indices of respective message class inmsgClassVariant_
- contains the bid/ask volume for some asset in a range of price buckets relative to some base price
- provides functionality for querying volume for specific price and adusting volume by processing order objects
- negative volume represents demand, positive volume represents supply
- price is represented by an unsigned integer and must therefore always be positive
- contained price range is determined by base price (argument of constructor) book length
- supports adding volume for specific price, withdrawing/cancelling volume for specific price and filling market orders using volume contained in book
- keeps track of best bid, lowest bid, best offer and highest offer at all times
- supports lock-free queries for volume, relies on
std::atomic_flag
as lock when processing orders
- represent conditions that need to hold to avoid a crossed book and maintain the integrity of stats kept
- if there is a best bid, there is a lowest bid and vice versa
- if there is a best offer, there is a highest offer and vice versa
- best offer > best bid, highest offer >= best offer, lowest bid <= best bid
- no positive volume below best offer, no negative volume above best bid
- no non-zero volume above highest offer or below lowest bid
entryType volumeAtPrice(std::uint32_t price)
: returns volume contained in order book at price specified by argumentstd::tuple<std::optional<std::uint32_t>, std::optional<std::uint32_t>>bestBidAsk()
: returns tuple of two std::optionals containing the prices of best bid and best offer if book contains demand and supply respectively and empty std:optionals otherwisebool shiftBook(std::int32_t shiftBy)
: changes base price of book byshiftBy
and shifts all volume within memory. Not possible if shifting would turn base price negative or result in non-zero volume buckets falling out of bounds. Returns true if successful, false otherwisestd::tuple<std::uint32_t, entryType_, entryType_, std::uint8_t> processOrder(msgClassVariant_ order)
: processes order contained in argument, returns tuple containing:- marginal executions price/ last price at which any volume contained in order was filled
- filled volume: volume filled right when order was processed
- order revenue: price * volume for any volume contained in order that was filled while processing
- 0 if order price was within bounds, 1 if it wasn't (other three entries must be 0 in this case)
std::uint64_t __invariantsCheck(int nIt)
: intended for debugging purposes, checks invariants layed out above aren't violated, prints an error message containing argument nIt (most likely an iteration index) for any violation and returns a sum of error codes of all violations