Known gaps¶
slick-sim is early-stage. This page lists everything found in the current source that is
unimplemented, half-wired, or behaves differently from what the surrounding code implies. It exists
so the rest of the documentation can describe the intended design without having to hedge every
paragraph, and so you do not spend an afternoon debugging something that was never finished.
Each entry says what it is, where it lives, and what it means for you.
Venues¶
CME, Eurex and ICE are not implemented¶
src/exchange/exch_cme.hpp
is a zero-byte file. There is no CmeExchange, no Eurex adapter, and no ICE adapter, despite
Venue::CME and Venue::ICE existing in
types.hpp and
README.md describing exchange-adapter namespaces for all three.
What it means: only coinbase and hyperliquid are usable venue keys in the config.
An enabled venue key with no adapter starts but does nothing¶
main.cpp
dispatches on the config key: coinbase builds a CoinbaseExchange, hyperliquid builds a
HyperliquidExchange, and anything else constructs a plain Exchange. Only the two concrete
adapters populate md_publisher_ and order_gateways_; the base constructor does not.
Exchange::start() now tolerates that — md_publisher_ is null-checked, so a generic exchange no
longer crashes. What is left is that it does nothing useful:
Exchange::processRequest()
reads one request from the queue and returns. There is no loop. Both working adapters override
start() with a real spin loop; the base class does not. So a generic exchange binds no ports,
serves no clients, processes at most one queued request, and its thread exits.
The committed sample config carries such a block — a cme entry — but it is "enabled": false, so
it is constructed and then skipped with a warning:
What it means: leave the cme block disabled, or delete it. Enabling it gets you an inert
exchange, not a working one. If you add a venue, populating md_publisher_ and overriding
start() with a loop are both mandatory — see Adding an exchange.
Previously a crash
Until recently Exchange::start() dereferenced md_publisher_ unconditionally and the cme
block segfaulted the process on startup — before either working venue was constructed, since
nlohmann::json iterates object keys in sorted order and "cme" sorts before "coinbase".
Both that and the enabled flag being ignored for non-adapter venues are now fixed.
Startup aborts if logs/ does not exist¶
main.cpp calls logger.add_file_sink("logs/slick-sim.log") with a path relative to the current
working directory, and nothing creates the directory first. Run slick-sim from a directory with no
logs/ subdirectory and the process aborts with exit code 3, writing nothing at all — no console
message, no log, no clue.
The repository ships a logs/ directory, so this never bites anyone running from the repo root. It
bites immediately when running a packaged release binary from a fresh directory.
What it means: mkdir logs before the first run in any new working directory.
Order and market-data plumbing¶
Market-data unsubscribe requests are dropped¶
Both publishers send MessageType::MD_UNSUBSCRIPTION requests when a client unsubscribes or
disconnects, but the switch in
Exchange::processRequest()
handles only NEW_ORDER_SINGLE, ORDER_REPLACE_REQUEST, ORDER_CANCEL_REQUEST and
MD_SUBSCRIPTION. There is no MD_UNSUBSCRIPTION case, so handleMdUnsubscription() — implemented
on both adapters — is never called from the request loop.
What it means: Symbol::num_subscriptions_ only ever increases, and upstream feeds are never
torn down when the last client goes away. Harmless for a long-running sim with a stable symbol set;
a slow leak otherwise.
The Coinbase publisher never republishes trades¶
CoinbasePublisher::publish_market_data_update
handles BOOK, LEVEL, ORDER, SUB_RESPONSE and BOOK_SNAPSHOT. It has no case for
MDUpdateType::TRADE (nor TRADE_SUMMARY), even though
Exchange::publishMDTrades
puts them on the queue.
What it means: a client subscribed to Coinbase market_trades receives the initial (empty)
snapshot and the subscription confirmation, then nothing. Hyperliquid's publisher does handle
TRADE, so its trades channel works.
MDUpdateType::ORDER is never published¶
Symbol::onOrderUpdate fills md_order_update_cache_, but every call site clears the cache behind a
// TODO: publish MD order update comment rather than calling publishMDOrderUpdate. The function
exists and is correct; nothing invokes it.
What it means: no order-by-order (L3) market data reaches clients. Only level data does.
L3 order books are stubs¶
OrderBookImpl<OrderBookType::L3>::populateL2SubscriptionResponse, populateMDBookUpdate and
populateL2Snapshot are
empty function bodies with "stub implementation for testing" comments.
Only the L2 specialisations do real work — and both adapters call createOrderBook<OrderBookType::L2>().
What it means: OrderBookType::L3 is only useful in tests. Do not select it for a live venue.
Pro-rata matching does not exist¶
MatchingEngine::Type declares ProRata, and matching_engines_ is sized by
MatchingEngine::Type::__count__, but
pro_rata_matching_engine.hpp
is a zero-byte file.
What it means: every symbol on every venue uses FifoMatchingEngine.
The historical-data feed does not exist¶
alpaca_historical_data_feed.hpp
is a zero-byte file, and md_feed's
CMakeLists.txt
compiles only hyperliquid_live_ws_feed.cpp.
What it means: historical replay is an intended operating mode — matching client orders against recorded market data — but no feed implements it. Live WebSocket feeds are the only source of market data today.
A md_feeds entry whose type is neither coinbase_live_ws nor hyperliquid_live_ws is silently
ignored, which leaves the venue running
self-contained. That is a valid
mode, so a typo in type degrades quietly rather than erroring — worth checking the startup log if
you expected live data and got an empty book.
Exchange::processMdData() and the market-data thread are commented out¶
Exchange declares md_thread_ and md_feed_, and start() has a commented-out block that would
launch a dedicated market-data thread. stop() still contains if (md_feed_ && md_thread_.joinable())
md_thread_.join(); — dead code, since nothing ever starts that thread.
What it means: market data is processed on the same thread as order requests. See the threading model in Architecture.
Bugs found while documenting¶
These are latent defects, not design decisions. They are recorded here because the documentation would otherwise describe behaviour the code does not deliver.
onMarketTradesSnapshot loops past the end of its buffer¶
exch_coinbase_feed_callbacks.cpp:220
contains:
snapshots.size() is size_t, so i is unsigned and i >= 0 is always true — the loop runs past
zero and wraps. A second for (const auto& trade : snapshots) loop then writes another
snapshots.size() trades through the same advancing pointer, into a queue slot that was reserved for
only snapshots.size() entries.
What it means: the path is reached whenever a market_trades subscription is pending and a
trades snapshot arrives from Coinbase. Expect memory corruption or a crash.
avg_fill_price is recomputed in floating point on every fill¶
Both
OrderBook::executeOrder
and the
matching loop
round-trip the running average through double (to_price_double → arithmetic → to_price_t) on
every partial fill, rather than accumulating notional in fixed point. Errors compound across fills.
Worse, when a resting simulator order is filled by an incoming aggressor, both functions update
the same order: FifoMatchingEngine::match updates the aggressor's fields, then calls
book.executeOrder, which updates the resting order's fields. The commented-out block at
fifo_matching_engine.cpp:272-276
shows this duplication was noticed for the market-data path.
What it means: treat avg_fill_price on a heavily partially-filled order as approximate.
Configuration and packaging¶
The default config filename does not match the committed sample¶
With no command-line argument, main.cpp looks for slick-sim.json in the current directory
(main.cpp:126).
The sample that ships with the repo is config/slick_sim.json — different directory, and an
underscore rather than a hyphen.
What it means: always pass the config path explicitly: slick-sim config/slick_sim.json.
QuickFIX, SBE and the TCP gateway are built but never used¶
tcp_order_gateway.cpp,
fix_parser.cpp,
sbe_parser.cpp,
json_parser.cpp
and the ~180 generated CME iLink3 SBE headers under src/order_gateway/sbe/ all compile into the
order_gateway target, but no exchange ever constructs a TcpOrderGateway. main.cpp still carries
roughly 90 lines of commented-out scaffolding (print_protocol_info, per-client protocol selection)
from when that path was live.
What it means: find_package(quickfix CONFIG REQUIRED) in the top-level CMakeLists.txt is a
hard build dependency for code that never runs. You still need QuickFIX installed to build.
The Doxygen API reference deliberately excludes the SBE headers¶
Not a defect — a documentation decision worth knowing. Doxyfile.in sets
EXCLUDE = src/order_gateway/sbe, because ~180 machine-generated classes would bury every
hand-written slick::sim type in the class index.
What it means: generated SBE message classes will not appear in the
API reference. Read the headers directly, or
the schema at src/order_gateway/sbe/schemas/cme_official/ilinkbinary.xml.