Add a venue¶
Model a JSON-over-WebSocket connector on the five that ship: Binance Spot (include/fastmm/venues/binance/), Binance USDⓈ-M (include/fastmm/venues/binance_usdm/), Bybit v5 spot and linear perpetuals (include/fastmm/venues/bybit/), OKX v5 USDT-margined swaps (include/fastmm/venues/okx/) and Deribit options and futures (include/fastmm/venues/deribit/). Bybit is the main worked example; Deribit shows JSON-RPC, request credits and options data. Binary wire formats live in codecs/ (FIX, Nasdaq, CME MDP 3.0), and a connector can build on them: nasdaq_itch is a Venue over ITCH, MoldUDP64 and OUCH (Venue connectors), and Binance SBE market data is a connector option (md_format = "sbe"). FIX and CME MDP 3.0 have no connector yet and are built only with -DFASTMM_CODEC_FIX=ON / -DFASTMM_CODEC_MDP3=ON (Optional codecs); turn the option on in the same change that adds the venue.
Throughout, foo stands for your venue.
1. Before you start¶
A connector is one control-path class that implements fastmm::venues::Venue (include/fastmm/venues/venue.hpp) and owns the hot-path objects: a market-data feed, a private (user) stream parser and an order encoder, all driven from the venue's own network thread.
The threading contract, from venue.hpp:
load_reference_data()andattach()run once on the main thread before any network thread starts; blocking REST calls are allowed there.fastmm-livealso callssubscribe()once during that setup.connect(),disconnect(),on_timer(),on_wake(),poll(),resync_books(),request_open_orders()andrequest_executions()run on the venue's reactor thread.poll()is called after every reactor iteration, and[engine] spin_mode = "busy"needs it to make progress without an epoll wake-up.send_now()is called on the engine thread with[engine] threading = "single": encode and write the batch inline instead of pushing it to the outbound ring (Architecture).cancel_all()must work from any thread, including while the reactor thread is stuck: use an independent blocking REST connection (BlockingHttp,include/fastmm/venues/blocking_http.hpp).- Two sinks carry events to the engine (
include/fastmm/venues/event_sink.hpp). The market-data sink is lossy: when its ring is full the delta is dropped and the book must resync. The order sink never drops: it spins, then calls its overflow callback, andfastmm-liveshuts down. - Nothing on the hot path allocates, throws or calls a virtual function; the virtual
Venueinterface is control path only.
Read Venue connectors for what the five connectors do today, and its section What a Binance-compatible simulator must implement as a list of venue behaviours your connector must handle.
2. File layout¶
The Bybit connector, with the file names a new venue should mirror:
| File | Contents |
|---|---|
include/fastmm/venues/bybit/bybit_auth.hpp |
Credentials and Signer: REST header and WebSocket auth signatures |
include/fastmm/venues/bybit/bybit_md_parser.hpp, src/venues/bybit/bybit_md_parser.cpp |
BybitMdParser::decode(): one public frame into one normalised message |
include/fastmm/venues/bybit/bybit_md_feed.hpp |
BybitMdFeed: the MarketDataFeed; owns the parser and one book sync per instrument |
include/fastmm/venues/bybit/bybit_book_sync.hpp |
BybitBookSync: BookSyncer<BybitSyncTraits> plus resubscribe on gaps |
include/fastmm/venues/bybit/bybit_order_encoder.hpp, src/venues/bybit/bybit_order_encoder.cpp |
BybitOrderEncoder (WebSocket and REST requests) and BybitResponseDecoder (order responses, open orders) |
include/fastmm/venues/bybit/bybit_private_parser.hpp, src/venues/bybit/bybit_private_parser.cpp |
BybitPrivateParser: order, execution and wallet topics into order events |
include/fastmm/venues/bybit/bybit_rest_decoder.hpp, src/venues/bybit/bybit_rest_decoder.cpp |
Reference data and server time |
include/fastmm/venues/bybit/bybit_error_map.hpp |
map_error(): retCode to RejectReason and VenueAction |
include/fastmm/venues/bybit/bybit_venue.hpp, src/venues/bybit/bybit_venue.cpp |
BybitVenueConfig, BybitVenue, make_bybit_config() |
src/venues/bybit/bybit_registration.cpp |
the registry entry: the kind, the capabilities, the config keys the venue owns and the factory (section 10) |
Differences in the other connectors:
- Binance names its private parser
binance_user_parser.hppand its book syncbinance_depth_sync.hpp(the snapshot comes from REST). - OKX needs a third credential (the generic
api_passphrase), names instruments byinstIdCodein WebSocket order operations, counts swaps in contracts (contract_multiplier=ctVal) and keeps each book level's text for the (now deprecated) checksum inokx_book_sync.hpp. - Deribit adds
deribit_json.hpp(exact parsing of JSON numbers with exponents) andderibit_credits.hpp(CreditBucket, the matching-engine request credits). - The two Coinbase connectors (
include/fastmm/venues/coinbase/) enter orders over REST and take their order events from a WebSocket user channel; their book streams carry no update ids, so the feeds number the messages themselves (coinbase_book_sync.hpp). Advanced Trade signs with ES256 JWTs (advanced_auth.hpp) and reads each order's executions when its cumulative quantity grows; the Exchange maps client ids to UUIDs (coinbase_wire.hpp). - Binance USDⓈ-M is the same exchange as Binance Spot, so it reuses the Spot signing, feed and depth-sync code and holds only the futures protocol itself;
include/fastmm/venues/binance_usdm/binance_usdm_venue.hpplists what is shared and what is not.
Do not copy a connector's plumbing: the pieces that are the same for every venue already exist.
| Header | What it gives you |
|---|---|
include/fastmm/venues/connector_common.hpp |
map_conn_state() / channel_state() (the transport state the engine is told about), header_int(), origin_of() / url_root(), IdText, load_published_status(), trip_venue_kill_once(), VenueExtras for the extra keys of your make_foo_config() |
include/fastmm/venues/book_sync.hpp |
StreamBookSync<Traits> for a venue that sends its snapshot on the stream; write the traits, not the syncer |
include/fastmm/venues/order_events.hpp |
emit_order_ack(), emit_order_reject(), emit_cancel_ack(), emit_cancel_reject(), emit_replayed_fill(), emit_funding(), emit_connection_state(), emit_venue_kill() |
include/fastmm/venues/order_commands.hpp |
OrderCommand, SentWatermark, is_reconcile_request() |
include/fastmm/venues/wire_latency.hpp |
BatchedOrders, drain_outbound_coalesced() |
include/fastmm/venues/dead_mans_switch.hpp |
CountdownSwitch, the refresh clock for a venue-side countdown cancel |
src/venues/CMakeLists.txt globs src/venues/**/*.cpp and the test targets glob tests/venues/*.cpp, both with CONFIGURE_DEPENDS, so new files need no CMake edits.
3. Market data¶
The feed contract¶
include/fastmm/venues/feed.hpp defines the concept every feed satisfies:
template <class F>
concept MarketDataFeed = requires(F& f, std::string_view json, std::int64_t rx_ts) {
{ f.on_message(json, rx_ts) } -> std::same_as<ParseStatus>;
{ f.on_connected() };
{ f.on_disconnected() };
{ f.subscription_payloads() } -> std::convertible_to<std::span<const std::string>>;
};
Every feed checks it with a static_assert at the end of its header, for example static_assert(MarketDataFeed<BybitMdFeed>); in bybit_md_feed.hpp. Do the same.
Underneath, the parser decodes one frame into a caller-provided buffer, for example MdDecodeResult BybitMdParser::decode(std::string_view json, Timestamp recv_ts, Cycles t0, std::span<std::byte> out) noexcept, and the feed writes the message into the sink.
Parse status¶
The feed counts Malformed into VenueStatus::md_malformed and ring overflows into md_dropped:
| Status | Use it for |
|---|---|
Ok |
A message was produced |
Ignored |
A valid frame you do not consume: subscription acks, pongs, heartbeats |
Malformed |
JSON error, or a missing or invalid field |
UnknownSymbol |
A symbol that is not in the SymbolTable |
Overflow |
The ring is full or the output buffer is too small |
Error |
A venue error payload |
Messages and parsing rules¶
- Produce
BookSnapshotMsgandBookDeltaMsg,TradeMsg,BookTickerMsgand, for options,OptionTickerMsg(include/fastmm/core/messages.hpp). Never construct aBookDeltaMsgby value; reserve it in the sink or the output buffer (CONTRIBUTING rule 8). - Parse every price and quantity string exactly with
include/fastmm/venues/decimal.hpp(Fixed::from_decimal, nodouble). If the venue sends JSON numbers, parse the raw token as Deribit does inderibit_json.hpp. - simdjson needs 64 readable bytes after the payload (
kJsonPadding). WebSocket frames have them; in tests and benchmarks wrap fixtures inPaddedJson(include/fastmm/venues/padded_json.hpp). - Look symbols up in
SymbolTable(include/fastmm/venues/symbology.hpp); lookups fold case without allocating.
4. Book synchronisation¶
BookSyncer<Traits, Sink> (include/fastmm/core/book/book_syncer.hpp) is the venue-independent snapshot and delta state machine. A venue supplies a traits struct:
| Member | Meaning |
|---|---|
kNeedsRestSnapshot |
The snapshot comes from REST (request_snapshot() is called) instead of the stream |
kBuffersDeltas |
Deltas are buffered until the snapshot arrives |
is_stale(delta, snapshot_id) |
The delta is older than the snapshot and is dropped |
first_applies(delta, snapshot_id) |
The first delta after the snapshot bridges it |
next_applies(delta, prev_id) |
The delta continues the chain; otherwise it is a gap |
is_snapshot_marker(delta) |
The venue signals a reset |
The sink receives on_snapshot(), on_delta(), on_resync(SyncReason) and request_snapshot(). The shipped rules:
| Traits | Rule |
|---|---|
BinanceSpotSyncTraits (book_syncer.hpp) |
REST snapshot with lastUpdateId; buffered deltas chained on U/u |
BinanceFuturesSyncTraits (book_syncer.hpp) |
REST snapshot; the first delta brackets lastUpdateId, later deltas chained on pu (Binance USDⓈ-M) |
BybitSyncTraits (book_syncer.hpp) |
Snapshot in the stream; u strictly increasing; u == 1 is a reset marker |
DeribitSyncTraits (include/fastmm/venues/deribit/deribit_book_sync.hpp) |
First notification is the snapshot; prev_change_id equals the previous change_id |
OkxSyncTraits (include/fastmm/venues/okx/okx_book_sync.hpp) |
Snapshot in the stream; prevSeqId equals the previous seqId (a heartbeat repeats it, a reset lowers it) |
GeminiSyncTraits (include/fastmm/venues/gemini/gemini_book_sync.hpp) |
Snapshot in the stream, not marked: the first frame after subscribing; U ≤ the last u < u |
On a gap, the connector emits ConnectionStateMsg with ConnState::Resyncing on channel 0 (emit_connection_state() in include/fastmm/venues/order_events.hpp) and fetches a new snapshot, rate limited: Bybit, Deribit and OKX resubscribe at most once every 2 s per instrument and again when no snapshot arrives within 10 s. Any state other than Live on channel 0 makes the engine clear the book and pull the quotes of that venue's instruments.
A market-data channel with no traffic for stale_ms reports ConnState::Stale; after dead_ms the connection is closed and reopened. Private and order channels are often quiet, so they report no Stale, and each connector raises dead_ms above its keepalive interval for them. The per-connector values are in Venue connectors.
5. Order entry¶
feed.hpp also defines an OrderGateway concept (encode(cmd, now_ms, out) -> std::size_t, on_message(json, rx_ts) -> ParseStatus, can_send(now_ns) -> bool), but no shipped encoder is checked against it. Binance and Bybit have encode_ws() and encode_rest(); Deribit has DeribitOrderEncoder::encode(const OrderCommand& cmd, const OrderShadow* shadow, std::string_view access_token, std::span<char> out). Follow the pattern rather than the concept:
- The engine writes
OutNewOrderMsg,OutCancelMsgandOutReplaceMsginto the outbound ring and wakes the venue.on_wake()drains the ring withdrain_outbound_coalesced()(include/fastmm/venues/wire_latency.hpp) and wraps each message in anOrderCommand(include/fastmm/venues/order_commands.hpp, kindsNew,Cancel,Replace). The order connection is corked for the drain (ConnectionSlot::cork()/uncork()), so all orders of one drain leave in one write. The ring can also carry aControlMsgwithControlCommand::Reconcile, which the engine sends when a cancel is refused more than three times: answer it withrequest_open_orders()(is_reconcile_request(h)inorder_commands.hpp). - It checks the client-side rate limiter (
include/fastmm/venues/rate_limiter.hpp; Deribit'sCreditBucket), then encodes into a fixed buffer withJsonWriter(include/fastmm/venues/json_writer.hpp), which never allocates and refuses to send a truncated request. - Request ids are
<kind><client order id>(include/fastmm/venues/request_id.hpp: kindn,c,rorafor an amend, 15 characters). - Post-only maps to the venue's flag: Binance
LIMIT_MAKER, BybittimeInForcePostOnly, Deribitpost_onlywithreject_post_only. - The quote manager replaces quotes on the venue's instruments only when
VenueCaps::supports_replace, the venue'ssupports_replaceand[engine] supports_replaceare all true; otherwise it sends cancel and new. - When a request cannot be sent (rate limit, fatal state, dry run), emit a reject to the order sink with
emit_order_reject(); otherwise the OMS waits for an answer that never comes.
Record encode and send latency in VenueStatus::order_encode and order_send (include/fastmm/venues/wire_latency.hpp). Inside a drain the send stamp of every order is the return of the drain's one write.
6. Authentication¶
Use include/fastmm/net/crypto.hpp for HMAC-SHA256, Ed25519 (Binance key_type = "ed25519") and ECDSA P-256 with base64url (ES256 JWTs, Coinbase Advanced Trade). Sign the exact bytes you send: Bybit signs timestamp, key, receive window and the query string or body. Signed requests carry a timestamp and a receive window (recv_window_ms), so measure the clock offset from the venue's server-time endpoint at startup and whenever the venue reports a timestamp error, and log it when it exceeds 1000 ms.
7. Error map¶
Map every documented venue error code to an ErrorMapping (include/fastmm/venues/error_action.hpp): a RejectReason for the engine, a VenueAction for the connector and whether the code is known. The signature to follow is Bybit's constexpr ErrorMapping map_error(int code, std::string_view msg = {}) noexcept. Cite the venue's documentation next to each code.
What each action does in the shipped connectors (BybitVenue::apply_action()):
VenueAction |
Connector behaviour |
|---|---|
None |
Report the reject only |
Backoff |
Pause sending for 1 s |
RateLimit |
Pause until the venue's reset time, count a cooldown, log rate limited (...) |
ResyncClock |
Fetch the server time and recompute the offset |
Reconcile |
request_open_orders() |
DisableInstrument |
Log venue rejected a precision/filter rule (...); the config is wrong for the symbol |
HardStop |
Stop all REST requests (IP ban), log REST hard stop (...) and ask the engine to kill this venue (KillReason::VenueHardStop) |
Fatal |
Refuse every further order on the venue, log fatal venue error (...) and ask the engine, once, to kill this venue only: trip_venue_kill_once() sends ControlCommand::TripVenueKill with KillReason::VenueFatal |
8. Private stream and reconciliation¶
- Emit acknowledgements, rejects, cancel acknowledgements and cancel rejects with the helpers in
include/fastmm/venues/order_events.hpp, andOrderFillMsg,OrderExpiredMsgandPositionUpdateMsgdirectly. - Every fill carries
feein units offee_asset(FeeAsset::Quote,BaseorOther); the engine books base-asset commission into the position (Binance charges BTC on buys). Classify the commission asset against the instrument's base and quote. - Ignore events for client ids that are not FastMM's (orders placed by hand or by other software).
- A perpetual's funding payment is a
FundingMsg(emit_funding()), carrying the venue's id for the payment; the engine books each id once, so the stream and a replay may both deliver it. - Balances: pass the instrument table to
ReconcileDriver::attach, implementReconcileHooks::fetch_balances(generation)(rows withadd_balance(), thenbalances_fetched(generation, ok, venue_ms)once), and emit the private stream's balance updates withemit_balance()(include/fastmm/venues/balances.hpp), filtered byreconcile_.assets(). Amounts are absolute; stamp the venue's time of the balance (Balance check). Without a balance stream, callrequest_balances()after fills. request_open_orders()is a reconciliation. If the venue can list the account's executions (caps.executions), first replay them since the last replay withemit_replayed_fill()(the venue's execution id,OrderFillMsg::kReplayed); then emitReconcileMsgBegin(withkSentWatermark, andkExecutionsExactwhen every instrument's replay answered in full), oneOpenOrderper live order (andPositionif the venue reports one), thenEnd. The engine pauses quoting fromBegintoEndand cancels live orders it does not know.- Reconcile on the first connect of the private channel with an empty watermark: this sweeps orders a dead process left resting, whose ids belong to an earlier session. Reconcile again after every private or order-channel reconnect (not on a return from
Stale), and runrequest_executions()once a minute while all is well, which books a fill the stream dropped without disconnecting.ReplaySchedulerdoes that; withHooks::active(backed by anOrderActivitythe connector notes orders and order events in) the minute's replay skips the instruments without order activity. - On order-channel loss with
cancel_on_order_channel_loss = true, cancel everything over REST. - If the venue offers a dead man's switch, arm it: a countdown (Binance USDⓈ-M
countdownCancelAll, refreshed withCountdownSwitch) or cancel-on-disconnect (Deribit, Bybit DCP), wired in the connector.
9. The venue class¶
Subclass Venue and implement:
load_reference_data(): fetch instruments and overwrite tick, lot, minimum and maximum quantity and minimum notional (and, for options, strike, expiry, type and contract size). Disable instruments that are not trading, and fail on a required symbol that is missing unlessallow_offline_reference_datais set.attach(): keep the symbol table, instruments, both sinks and the outbound ring.connect(),disconnect(),subscribe(): open the channels. Bybit uses one handler struct per channel (on_state,on_text,on_binary,on_connected_send_subscriptions) inside aConnectionSlot<Handler>(include/fastmm/venues/connection_slot.hpp), which picks plain TCP forws://and TLS forwss://; reconnect backoff comes fromnet::BackoffConfig(include/fastmm/net/backoff.hpp).on_timer(): keepalives and application pings, clock-offset refresh, snapshot retries. The backend calls it about once a second.on_wake(): drain the outbound ring (section 5).poll(): one non-blocking pass over the venue's sockets, forspin_mode = "busy".send_now(): encode and write a batch handed over by the engine thread, forthreading = "single".request_open_orders()andcancel_all()(sections 1 and 8);request_executions(since_venue_ms)if the venue has a trade-history query.resume_executions(since_venue_ms, known): where the first replay starts after a restart, in the venue's clock, and the execution ids the earlier session booked. A venue whose trade ids increase per instrument also implementsresume_trade_ids()for an exact start andresume_known_trade_ids()for the ids above it that are booked already (Binance;ReplayScheduler::set_cursorandset_known_ids). A connector withoutcaps.executionsignores both, and the session restores no position for it.resync_books(): start every book over from a fresh snapshot;fastmm-gatewaycalls it when its own copies are lost. The default does nothing.refused_account_settings(): true afterload_reference_data()refused an account setting a retry does not fix (hedge position mode), sofastmm-liveexits 3 instead of 4.status(): fillVenueStatus;fastmm-livelogs it every second andfastmm-topshows it.- Control-path REST on the reactor thread goes through
RestChannel(include/fastmm/venues/rest_channel.hpp). Honour--record-rawwithRawRecorder(include/fastmm/venues/raw_recorder.hpp). - In a dry run, open market data only and refuse orders (
VenueCaps::user_stream = false).
10. Registration¶
A connector is one entry in the venue registry (include/fastmm/venues/registry.hpp): the kind that selects it, what it can do, the [venues.<name>] keys it owns, and a factory. Put it in its own translation unit next to the connector, src/venues/foo/foo_registration.cpp:
#include "fastmm/venues/foo/foo_venue.hpp"
#include "fastmm/venues/registry.hpp"
namespace fastmm::venues {
namespace {
// The keys this venue owns. The registry validates them before the factory runs, and
// tools/docs_config_ref.py turns this table into the connector's table in the configuration
// reference: say what the key does, its unit and its default.
constexpr VenueKeySpec kFooKeys[] = {
{"stale_ms", KeyType::Int, false, "no traffic for this long pulls the quotes, ms (default 2000)"},
{"order_api", KeyType::String, false, "order entry: ws (default) | rest"},
};
std::unique_ptr<Venue> make(VenueId id, const VenueSection& s, const VenueFactoryOptions& opts) {
foo::FooVenueConfig c = foo::make_foo_config(s, opts.dry_run);
c.record_raw_dir = opts.record_raw_dir;
return std::make_unique<foo::FooVenue>(id, std::move(c));
}
} // namespace
void register_foo_venue(VenueRegistry& r) {
static_cast<void>(r.add({.name = "foo",
.summary = "Foo spot (testnet)",
.keys = kFooKeys,
.caps = {.credentials = true, .order_entry = true,
.replace = true, .positions = true, .polls = false,
.executions = true},
.make = &make}));
}
} // namespace fastmm::venues
Then two lines in src/venues/registry.cpp: a declaration of register_foo_venue and a call in register_builtin_venues(). Nothing self-registers: the linker drops a static library's self-registering object. No other core file changes.
What the entry says¶
| Field | Meaning |
|---|---|
name |
the [venues.<name>] kind value that selects this connector |
aliases |
other kind values that select it (binance_spot answers to binance and sim) |
summary |
one line for the connector table in Configuration |
keys |
the [venues.<name>] keys the venue owns, beyond the generic ones |
caps |
credentials, order_entry, replace, positions, polls, executions: what this connector can do |
make |
the factory, called after the section's keys have been validated |
The core reads caps instead of testing kind: credentials = false is why fastmm-live never asks nasdaq_itch for an API key, polls = true is why its sockets are driven from Venue::poll(), and executions = true lets a restart restore the venue's position. They say what the connector can do; whether a given instance does is Venue::caps(), which sees the configuration (a dry run, order_entry = "none", missing credentials).
Configuration keys¶
The central schema (include/fastmm/config/schema.hpp) knows only the generic keys (kind, the URLs, the credentials, testnet, supports_replace, insecure_tls, ca_file, recv_window_ms, fees). Everything else in the section is kept verbatim in VenueSection::extra, with its line in extra_lines, and handed to the venue that owns it. fastmm::venues::validate_venues() then checks the section against kFooKeys: a key the venue does not own, a key of the wrong type or a missing required key stops the session, naming the line. Values only the connector can judge (a URL, a range, a pair that must not cross) are make_foo_config's job; throw std::invalid_argument with a message that starts venues.<name>.<key>:.
Documentation¶
The key table and the connector table in Configuration are generated from the registration file: add a section with the two region markers and run python3 tools/docs_config_ref.py.
The rest is prose: a section in Venue connectors, a configs/foo-testnet.toml, the key variables in .env.example, an environment section in Run on a testnet and a CHANGELOG entry.
11. A venue outside FastMM¶
A connector can live outside this repository. examples/external-venue/ is a venue project built against the installed headers: a connector, its registration, a live app and a test.
// apps/live.cpp: fastmm-live plus this project's connector.
#include "echo/echo_venue.hpp"
#include <fastmm/cli/live.hpp>
int main(int argc, char** argv) {
echo::register_echo_venue();
return fastmm::cli::live(argc, argv);
}
find_package(fastmm CONFIG REQUIRED COMPONENTS live)
add_library(echo_venue STATIC src/echo_venue.cpp src/register.cpp)
target_link_libraries(echo_venue PUBLIC fastmm::venues)
add_executable(echo-live apps/live.cpp)
target_link_libraries(echo-live PRIVATE echo_venue fastmm::live)
kind = "echo" in a configuration selects it and its keys are validated by its own declaration. Build it against an install prefix, or against a source tree with -DFASTMM_SOURCE_DIR=<checkout>; ctest in this repository compiles the same two files into fastmm_venues_tests (venues.external.*), and the CI external-project job builds the project itself.
12. Fixtures¶
Once market data works, record public frames from the venue's testnet with the connector:
./build/release/bin/fastmm-live --config configs/foo-testnet.toml --dry-run --duration 60s \
--record-raw tests/fixtures/foo/raw
Each channel is appended to <dir>/<venue>-<channel>.jsonl, one frame per line prefixed with the receive timestamp and a tab. Cut single messages out into tests/fixtures/foo/*.json, remove keys, account ids and order ids that identify an account, and describe every file in tests/fixtures/foo/fixtures.meta.json as recorded, synthesised from ... or docs-example (<url>), with the source URL and recording date (see tests/fixtures/bybit/fixtures.meta.json). Private payloads you cannot record yet come from the venue's documentation examples; say so in the meta file and in the CHANGELOG.
13. Tests¶
Name the tests after the Bybit ones; they are picked up by the fastmm_venues_tests binary (labels unit and fixture):
| Test file | What it proves |
|---|---|
tests/venues/foo_md_parser_test.cpp |
Fixture in, expected messages out, including malformed, unknown-symbol and ignored frames, and a recorded session that syncs without a resync |
tests/venues/foo_book_sync_test.cpp |
Snapshot first, stale and out-of-order deltas, gaps leading to a rate-limited resync |
tests/venues/foo_order_encoder_test.cpp |
Golden request strings, signatures against an independent implementation, response decoding and the error map |
tests/venues/foo_private_parser_test.cpp |
Every order status, fills with the commission asset, positions, foreign client ids |
tests/venues/foo_venue_test.cpp |
The whole connector against FakeVenueServer (tests/venues/fake_venue_util.hpp): config mapping, reference data, subscribe, order round trip, reconciliation, channel loss and the blocking cancel_all() |
tests/venues/live_foo_test.cpp |
Opt-in testnet check with tests/venues/live_test_util.hpp: skipped unless FASTMM_LIVE_TESTS=1. Name the test cases live.foo: ...; the venues.live. prefix gives them the ctest label live instead of unit and fixture |
tests/hotpath/venues_noalloc_test.cpp |
A test case for the market-data parser, the private parser and the order encoder: after a warm-up pass over the fixtures, no allocation (binary fastmm_hotpath_tests, label noalloc) |
tests/core/book_syncer_test.cpp |
New sync traits, if you add them to book_syncer.hpp |
Run them:
Benchmarks: add the parsers to bench/bench_json.cpp and the order encoder to bench/bench_order_encoders.cpp, then a p50 budget for each in bench/ci_budget.toml.
14. Conformance checklist¶
A venue is done when each item has an equivalent test.
| Item | Proven by |
|---|---|
The feed satisfies MarketDataFeed |
static_assert in bybit_md_feed.hpp |
| Recorded snapshot and delta parse into exact messages | bybit_md_parser_test.cpp "bybit.md_parser: recorded orderbook.50 snapshot and delta" |
Malformed, unknown-symbol and control frames get the right ParseStatus |
bybit_md_parser_test.cpp "bybit.md_parser: publicTrade aggressor from S, control frames, errors" |
| A recorded session syncs with no resync | bybit_md_parser_test.cpp "bybit.md_feed: recorded session syncs the book with no resync" |
| Deltas before the snapshot are dropped | bybit_book_sync_test.cpp "bybit.book_sync: snapshot first, deltas dropped before it" |
| A gap or reset marker resyncs and resubscribes | bybit_book_sync_test.cpp "bybit.book_sync: u == 1 marker and a stale u resync and resubscribe" |
| A missing snapshot is retried | bybit_book_sync_test.cpp "bybit.book_sync: no snapshot within the timeout resubscribes" |
| A full market-data ring forces a resync | binance_depth_sync_test.cpp "binance.depth_sync: full market-data ring forces a resync" |
| Signatures match an independent implementation | bybit_order_encoder_test.cpp "bybit.auth: signatures match an independent HMAC implementation" |
| New, cancel and replace requests are byte-exact | bybit_order_encoder_test.cpp "bybit.encoder: order.create / order.cancel / order.amend WS frames" |
| REST requests sign the bytes sent | bybit_order_encoder_test.cpp "bybit.encoder: REST requests sign the exact bytes sent" |
| Every documented error code maps to a reason and an action | bybit_order_encoder_test.cpp "bybit.error_map: documented retCodes" |
| Order statuses map to order events | bybit_private_parser_test.cpp "bybit.private_parser: order topic statuses map to order events" |
| Fills and positions, with the commission asset | bybit_private_parser_test.cpp "bybit.private_parser: execution -> fill, wallet -> position, control frames"; binance_user_parser_test.cpp "binance.user: the commission asset of a fill is classified as base, quote or other" |
| Foreign client ids are ignored | binance_user_parser_test.cpp "binance.user: foreign client ids, unknown symbol, malformed, ignored events" |
| Config keys map onto the connector config | bybit_venue_test.cpp "bybit.venue: config mapping derives the private and trade URLs" |
The registry resolves the kind and builds the connector |
registry_test.cpp "venues.registry: make_venue builds the connector its kind names" |
Reference data, subscribe, orders, reconciliation and cancel_all() against a fake exchange |
bybit_venue_test.cpp "bybit.venue: scripted fake exchange end to end" |
| Private-channel loss cancels over REST, re-authenticates and reconciles | deribit_venue_test.cpp "deribit.venue: scripted fake exchange end to end" |
| Invalid credentials are fatal; the request limit refuses orders | deribit_venue_test.cpp "deribit.venue: invalid credentials are fatal and the credit limit refuses orders" |
| A dry run opens market data only and refuses orders | binance_venue_test.cpp "binance.venue: dry run opens market data only and refuses orders" |
| Client-side rate limits and cooldowns | rate_limiter_test.cpp |
| The real testnet: book sync, place and cancel | live_bybit_test.cpp "live.bybit: testnet book sync, then place and cancel a far limit order" |
| The engine quotes, fills and shuts down cleanly over the network (Binance protocol only) | e2e_test.cpp, against fastmm-sim-exchange |
Then run the connector on its testnet with tight [risk] limits (Run on a testnet).