A price-time priority matching engine in C++17, built in phases that each stand on their own.
- Phase 1 — matching core (done). An order book that adds, cancels, and matches limit, market, and IOC orders in O(1) on the hot path.
- Phase 2 — ITCH 5.0 parser (done). Decodes NASDAQ TotalView-ITCH 5.0 binary messages into typed C++ structs.
- Phase 3 — book reconstruction (done). Replays decoded ITCH events into per-symbol books and validates the result against recorded sessions.
- Phase 4 — multicast transport (done). MoldUDP64-style sequence-numbered packets over real UDP multicast, with gap detection and retransmission recovery.
- File -> Open and select this folder. CLion detects
CMakeLists.txtand loads the project automatically. - Pick a run configuration (top-right dropdown) --
test_order_book,test_itch_parser,test_book_reconstructor, ortest_mold-- and press Run. Each prints one line per case and a pass/fail summary. - Or use CLion's CTest integration to run all four suites at once.
- Run
mcast_demoto see the whole path over real localhost UDP multicast (multicast in, deliberate packet loss, gap recovery, rebuilt book).
No external libraries are required -- the tests use a tiny built-in harness, so CLion's bundled toolchain builds everything as-is.
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure| Path | What it holds |
|---|---|
include/ome/types.hpp |
Order, Side, Trade, TimeInForce, aliases |
include/ome/order_book.hpp |
the OrderBook public API |
src/order_book.cpp |
matching logic + the O(1) data structure |
include/ome/itch/messages.hpp |
typed ITCH 5.0 message structs + the Message union |
include/ome/itch/parser.hpp |
decode(), message_length(), for_each_message() |
src/itch/parser.cpp |
the ITCH 5.0 decoders |
include/ome/itch/book_reconstructor.hpp |
BookReconstructor, replay(), replay_file() |
src/itch/book_reconstructor.cpp |
applies ITCH events to per-symbol books |
include/ome/mold/, src/mold/ |
MoldUDP64 framing, gap-detecting sequencer, retransmit |
include/ome/net/, src/net/ |
UDP multicast transport: raw sockets and Boost.Asio |
apps/mcast_demo.cpp |
end-to-end demo over real localhost multicast |
tests/ |
45 tests across 4 suites + shared harness & builder |
The core is a limit order book with price-time priority: an incoming order fills against the best opposite price first, oldest-first within a price level, at the resting (maker) price. It handles limit orders (GTC or IOC), market orders, and cancels, and every hot-path operation is O(1).
| Operation | Cost | Why |
|---|---|---|
| add (resting) | O(1) | direct-indexed price ladder + append to the level's FIFO |
| cancel | O(1) | hash index id -> node, then unlink from a linked list |
| execute (per fill) | O(1) | pop the front of the best level's FIFO |
Two honest caveats, worth stating up front:
- "execute is O(1)" is per fill. One aggressive order that crosses k resting orders is O(k) -- unavoidable, since it produces k trades.
- The price ladder assumes a bounded integer price range (
[min, max]ticks, fixed at construction). Maintaining best bid/ask is O(1) on add and amortized O(1) on remove; the worst case is a scan to the next non-empty level, which a bitmap index could make truly O(1) later.
How it fits together: prices are integer ticks, so two arrays (bids_,
asks_) indexed directly by price give O(1) access to any level. Each level is an
intrusive doubly-linked FIFO of Order nodes: appending preserves time priority,
and unlinking a filled or cancelled order is a pointer splice. An
unordered_map<OrderId, Order*> turns cancel into a lookup plus a splice. Nodes
live in a std::deque pool with a free list, so pointers stay stable and there is
no allocation on the matching path.
decode(body, len) turns one message body into a typed Message (a
std::variant over the modeled types: system event, stock directory, add,
execute, cancel, delete, replace, trade). Design notes:
- Big-endian by construction. The byte readers assemble each integer from
individual bytes, so decoding is correct on any host with no byte-swap
intrinsics or
reinterpret_cast. - Fixed layouts, validated.
message_length()knows every ITCH 5.0 type;decode()refuses a body that is too short and returnsnulloptfor types it does not model -- it never over-reads or guesses. for_each_message()walks the download-file framing (each message prefixed by a 2-byte length), skipping unmodeled messages and stopping cleanly on a truncated tail. This is the seam Phase 3 will feed into the book.
Remember ITCH is a market-data feed -- it reports what NASDAQ's matching already did -- so the parser feeds book reconstruction (Phase 3), a separate path from the Phase 1 matching engine.
BookReconstructor applies decoded ITCH messages to per-symbol OrderBooks to
rebuild the exchange's book -- without matching, since ITCH already reflects
NASDAQ's matches:
- Add rests the order unconditionally (
OrderBook::insert_order); Execute and Cancel shrink it (reduce_order); Delete removes it; Replace deletes the old ref and inserts the new one. - Execute/cancel/delete messages carry only an order reference -- no symbol,
side, or price -- so the reconstructor keeps a
ref -> {symbol, side}map to route each event and to recover the side thatReplaceomits. - One
OrderBookper stock locate, so equal prices in different symbols never mix.
Replay a recorded session with replay_file(path, reconstructor) (or replay()
over an in-memory buffer). The tests build small sessions as ITCH bytes -- one even
round-trips through a file on disk -- and assert the rebuilt book's depth and top.
Known limitation (stated honestly): the O(1) ladder needs a bounded price range, but real ITCH prices span a huge fixed-point range. The reconstructor takes a configured range and drops out-of-range orders; a production build would swap the array ladder for a map-based price index.
The ome::mold layer wraps ITCH in MoldUDP64-style sequence-numbered packets and
recovers from packet loss so it cannot silently corrupt the book:
MoldSequencertracks the expected sequence number, delivers messages strictly in order, and buffers anything that arrives ahead of a gap.RetransmitStoreis the authoritative log / rewind server: it re-frames any requested sequence range back into a packet, as a MoldUDP64 request server does.MoldClientties them together -- feed it live packets and it detects gaps and initiates retransmission until each hole is filled.
The tests inject packet loss, reordering, duplicates, and even a lost retransmission; the headline test drops packets from a full ITCH session and proves the reconstructed book is identical to a loss-free reference.
ome::net::UdpMulticast{Sender,Receiver} is the real transport (Winsock / BSD
sockets). The mcast_demo app runs the whole path over real localhost
multicast -- it multicasts an ITCH session, drops packets on purpose, and the
receiver detects the gaps, recovers them, and rebuilds the correct book:
cmake --build build --target mcast_demoThe live feed is real UDP multicast; retransmission is served from the in-process store, which stands in for a networked request server.
The multicast transport has two interchangeable implementations behind the same
ok() / send() / recv() interface:
ome::net::UdpMulticast{Sender,Receiver}(src/net/udp_multicast.cpp) -- raw Winsock / BSD sockets, no dependencies. The default.ome::net::AsioMulticast{Sender,Receiver}(src/net/asio_udp_multicast.cpp) -- the same transport on Boost.Asio (io_context+async_receive_from, driven byrun_forto honour the receive timeout). Boost stays out of the header via the pImpl idiom, so only the.cppneeds Boost.
CMake selects the Asio version for mcast_demo automatically when Boost is found
(defining OME_USE_ASIO); otherwise it uses raw sockets, so the project always
builds. To build the Asio path, install Boost and point CMake at it, e.g. via vcpkg:
vcpkg install boost-asio
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE=<vcpkg>/scripts/buildsystems/vcpkg.cmake
cmake --build build --target mcast_demoThe core library and all 45 tests never touch the transport, so they need neither Boost nor sockets to build.