Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

order_matching_engine

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.

Build & run in CLion

  1. File -> Open and select this folder. CLion detects CMakeLists.txt and loads the project automatically.
  2. Pick a run configuration (top-right dropdown) -- test_order_book, test_itch_parser, test_book_reconstructor, or test_mold -- and press Run. Each prints one line per case and a pass/fail summary.
  3. Or use CLion's CTest integration to run all four suites at once.
  4. Run mcast_demo to 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.

Command line (optional)

cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure

Layout

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

Matching engine (Phase 1)

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.

ITCH 5.0 parser (Phase 2)

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 returns nullopt for 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.

Book reconstruction (Phase 3)

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 that Replace omits.
  • One OrderBook per 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.

Multicast transport & gap recovery (Phase 4)

The ome::mold layer wraps ITCH in MoldUDP64-style sequence-numbered packets and recovers from packet loss so it cannot silently corrupt the book:

  • MoldSequencer tracks the expected sequence number, delivers messages strictly in order, and buffers anything that arrives ahead of a gap.
  • RetransmitStore is the authoritative log / rewind server: it re-frames any requested sequence range back into a packet, as a MoldUDP64 request server does.
  • MoldClient ties 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_demo

The live feed is real UDP multicast; retransmission is served from the in-process store, which stands in for a networked request server.

Transport: raw sockets or Boost.Asio

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 by run_for to honour the receive timeout). Boost stays out of the header via the pImpl idiom, so only the .cpp needs 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_demo

The core library and all 45 tests never touch the transport, so they need neither Boost nor sockets to build.

About

C++17 price-time priority matching engine with O(1) order operations, paired with a NASDAQ ITCH 5.0 / MoldUDP64 market-data pipeline that recovers from packet loss via sequence-gap detection and retransmission.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages