Skip to content

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() and attach() run once on the main thread before any network thread starts; blocking REST calls are allowed there. fastmm-live also calls subscribe() once during that setup.
  • connect(), disconnect(), on_timer(), on_wake(), poll(), resync_books(), request_open_orders() and request_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, and fastmm-live shuts down.
  • Nothing on the hot path allocates, throws or calls a virtual function; the virtual Venue interface 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.hpp and its book sync binance_depth_sync.hpp (the snapshot comes from REST).
  • OKX needs a third credential (the generic api_passphrase), names instruments by instIdCode in WebSocket order operations, counts swaps in contracts (contract_multiplier = ctVal) and keeps each book level's text for the (now deprecated) checksum in okx_book_sync.hpp.
  • Deribit adds deribit_json.hpp (exact parsing of JSON numbers with exponents) and deribit_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.hpp lists 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 BookSnapshotMsg and BookDeltaMsg, TradeMsg, BookTickerMsg and, for options, OptionTickerMsg (include/fastmm/core/messages.hpp). Never construct a BookDeltaMsg by 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, no double). If the venue sends JSON numbers, parse the raw token as Deribit does in deribit_json.hpp.
  • simdjson needs 64 readable bytes after the payload (kJsonPadding). WebSocket frames have them; in tests and benchmarks wrap fixtures in PaddedJson (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:

  1. The engine writes OutNewOrderMsg, OutCancelMsg and OutReplaceMsg into the outbound ring and wakes the venue. on_wake() drains the ring with drain_outbound_coalesced() (include/fastmm/venues/wire_latency.hpp) and wraps each message in an OrderCommand (include/fastmm/venues/order_commands.hpp, kinds New, 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 a ControlMsg with ControlCommand::Reconcile, which the engine sends when a cancel is refused more than three times: answer it with request_open_orders() (is_reconcile_request(h) in order_commands.hpp).
  2. It checks the client-side rate limiter (include/fastmm/venues/rate_limiter.hpp; Deribit's CreditBucket), then encodes into a fixed buffer with JsonWriter (include/fastmm/venues/json_writer.hpp), which never allocates and refuses to send a truncated request.
  3. Request ids are <kind><client order id> (include/fastmm/venues/request_id.hpp: kind n, c, r or a for an amend, 15 characters).
  4. Post-only maps to the venue's flag: Binance LIMIT_MAKER, Bybit timeInForce PostOnly, Deribit post_only with reject_post_only.
  5. The quote manager replaces quotes on the venue's instruments only when VenueCaps::supports_replace, the venue's supports_replace and [engine] supports_replace are all true; otherwise it sends cancel and new.
  6. 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, and OrderFillMsg, OrderExpiredMsg and PositionUpdateMsg directly.
  • Every fill carries fee in units of fee_asset (FeeAsset::Quote, Base or Other); 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, implement ReconcileHooks::fetch_balances(generation) (rows with add_balance(), then balances_fetched(generation, ok, venue_ms) once), and emit the private stream's balance updates with emit_balance() (include/fastmm/venues/balances.hpp), filtered by reconcile_.assets(). Amounts are absolute; stamp the venue's time of the balance (Balance check). Without a balance stream, call request_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 with emit_replayed_fill() (the venue's execution id, OrderFillMsg::kReplayed); then emit ReconcileMsg Begin (with kSentWatermark, and kExecutionsExact when every instrument's replay answered in full), one OpenOrder per live order (and Position if the venue reports one), then End. The engine pauses quoting from Begin to End and 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 run request_executions() once a minute while all is well, which books a fill the stream dropped without disconnecting. ReplayScheduler does that; with Hooks::active (backed by an OrderActivity the 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 with CountdownSwitch) 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 unless allow_offline_reference_data is 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 a ConnectionSlot<Handler> (include/fastmm/venues/connection_slot.hpp), which picks plain TCP for ws:// and TLS for wss://; reconnect backoff comes from net::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, for spin_mode = "busy". send_now(): encode and write a batch handed over by the engine thread, for threading = "single".
  • request_open_orders() and cancel_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 implements resume_trade_ids() for an exact start and resume_known_trade_ids() for the ids above it that are booked already (Binance; ReplayScheduler::set_cursor and set_known_ids). A connector without caps.executions ignores both, and the session restores no position for it.
  • resync_books(): start every book over from a fresh snapshot; fastmm-gateway calls it when its own copies are lost. The default does nothing.
  • refused_account_settings(): true after load_reference_data() refused an account setting a retry does not fix (hedge position mode), so fastmm-live exits 3 instead of 4.
  • status(): fill VenueStatus; fastmm-live logs it every second and fastmm-top shows it.
  • Control-path REST on the reactor thread goes through RestChannel (include/fastmm/venues/rest_channel.hpp). Honour --record-raw with RawRecorder (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.

#### `foo`

<!-- BEGIN config-keys venue:foo -->
<!-- END config-keys -->

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:

cmake --build --preset release -j --target fastmm_venues_tests
ctest --preset release -R 'foo\.'

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).