Venue connectors¶
FastMM ships nine connectors behind the control-path fastmm::venues::Venue interface (include/fastmm/venues/venue.hpp). Eight are crypto venues:
| Venue | Markets | Orders | Environment |
|---|---|---|---|
| Binance Spot | spot | WebSocket API | Binance Demo, Spot testnet |
| Binance USDⓈ-M | perpetuals | WebSocket API | Binance Demo |
| Bybit v5 | spot, linear perpetuals | WebSocket | Bybit testnet |
| OKX v5 | spot, USDT swaps | WebSocket | OKX Demo |
| Deribit | options, futures | WebSocket | Deribit testnet |
| Gemini | spot, perpetuals | WebSocket | Gemini sandbox |
| Coinbase Advanced Trade | spot | REST | production |
| Coinbase Exchange | spot | REST | Coinbase Exchange sandbox |
The ninth reads Nasdaq TotalView-ITCH market data and enters orders over OUCH 5.0 to fastmm-sim-itch (below); Binance Spot also runs against the local simulator fastmm-sim-exchange.
Each connector registers itself in the venue registry (include/fastmm/venues/registry.hpp) under the kind a [venues.<name>] section names, and make_venue() resolves through it:
| kind | connector |
|---|---|
binance_spot, binance, sim |
binance::BinanceVenue |
binance_usdm |
binance_usdm::BinanceUsdmVenue |
bybit, bybit_spot |
bybit::BybitVenue |
okx |
okx::OkxVenue |
gemini |
gemini::GeminiVenue |
coinbase_advanced |
coinbase::CoinbaseAdvancedVenue |
coinbase_exchange |
coinbase::CoinbaseExchangeVenue |
deribit |
deribit::DeribitVenue |
nasdaq_itch |
nasdaq::NasdaqItchVenue |
A project can register its own connector, with its own configuration keys, without changing FastMM (Add a venue, examples/external-venue/). Operating them: Run in production.
Every connector runs on its own net::Reactor thread and writes normalised messages into two rings per venue: market data (lossy: a full ring drops the delta and forces a resync) and order events (never dropped: bounded spin, then an overflow the venue treats as fatal). Behind fastmm-gateway the same code runs in the gateway and the rings are shared memory the strategy process reads. cancel_all() uses an independent blocking REST connection, so it works from any thread even if the reactor is wedged; Binance Spot retries a 418/429 refusal three times before reporting failure (nasdaq_itch shuts the OUCH connection down instead, see below). Venue::poll() runs after every reactor iteration; nasdaq_itch polls its sockets there in spin_mode = "busy".
Account pools¶
[venues.<member>] pool_of = "<primary>" runs a second account of the same exchange behind one venue (configuration, strategy API). The member's connector is a slot of its own: its own network thread, keys, order-count windows and budget publication, order and user sessions, start-up open-order and balance snapshot, execution replay, cancel_on_order_channel_loss and kill-switch cancel_all, all over the primary's instruments, and no market data: it opens no stream, requests no depth snapshot and holds no books, so its status line reads md=down books=0/0 by design while order and user are live. Symbols are resolved on the primary's venue id and every event the member emits carries its own id, which is how the engine keeps each order with the account it went to. A member's position report (USDⓈ-M) is its account's; the engine sums the accounts' reports into the instrument's position. Binance Spot and Binance USDⓈ-M run as members (VenueCapabilities::account_pools); a connector that does not is refused at configuration time. Binance counts request weight per IP: a pool's accounts that leave from one address spend one weight window (RateLimiter::share_ip, keyed by the REST URL and the source address, ip_weight_key), a header's used weight only raises the shared count (an answer to one account predates the others' requests in flight), and a 429 pause or a 418 stop answered to one account holds them all.
Internal transfers¶
A pool treasury ([venues.<primary>.treasury]) moves an asset between a pool's accounts through Venue::transfer(TransferRequest) and asks for its state with Venue::transfer_status (core/transfer.hpp): blocking calls on a REST connection of their own, made from the treasury's thread, never the reactor's. transfer answers Pending or Done (taken), Failed (refused, nothing moved) or Unknown (no usable answer: ask for the state); transfer_status also answers NotFound. Venue::transfer_account() is the account's name in a request, Venue::request_balances() asks for a balance snapshot on the reactor thread. A connector says it can with VenueCapabilities::internal_transfer and, once it has the credentials, VenueCaps::internal_transfer; the default refuses.
Binance Spot and USDⓈ-M use the master account's universal transfer (POST /sapi/v1/sub-account/universalTransfer, IP weight 1; binance/binance_transfer.hpp) with clientTranId set to the transfer's client id, and look a transfer up with GET /sapi/v1/sub-account/universalTransfer?clientTranId=…&fromEmail=… (status SUCCESS: done; FAILURE: failed; otherwise pending; no row: not found). A 4xx answer is a refusal, a 5xx or a transport error is Unknown. The endpoint needs a key of the master account with "internal transfer" enabled, which the connector of the pool's primary reads from the environment variables transfer_api_key_env and transfer_api_secret_env name (HMAC); it is used for nothing else, and the accounts' trading keys need no transfer permission. Each account names itself with sub_account_email (empty: the master account). The sub-account keys' own endpoints (sub-account/transfer/subToSub, subToMaster) move spot assets only and take no client id, so an answer lost on the way could not be looked up. Spot moves SPOT to SPOT in one request. The endpoint has no futures-to-futures scenario, so a USDⓈ-M transfer is two: the sender's USDT_FUTURE to the receiver's SPOT (<id>a), then the receiver's SPOT to its USDT_FUTURE (<id>b); a second step the venue refused is sent again from transfer_status, and one it refuses again leaves the amount in the receiver's spot wallet (Failed, said in the log).
Venue-side dead man's switch¶
cancel_on_order_channel_loss is the connector cancelling over REST when its order channel drops, which needs a live process. What survives a SIGKILL, an OOM kill or a dead host is the venue's own switch:
| venue | request | shape | window | default |
|---|---|---|---|---|
| Binance USDⓈ-M | POST /fapi/v1/countdownCancelAll (IP weight 10, per symbol) |
countdown the connector refreshes every window/3 | dead_mans_switch_ms, ms; countdownTime=0 stops it |
60000 |
| Deribit | private/enable_cancel_on_disconnect, scope connection |
the venue watches its own socket | detection is the public/set_heartbeat interval; without heartbeats a socket that dies without a FIN takes the 10-minute inactivity timeout |
on |
| Bybit v5 | POST /v5/order/disconnected-cancel-all (product SPOT, or DERIVATIVES for linear) + the dcp.spot / dcp.future private topic |
account setting; Bybit starts the clock when every private connection subscribing dcp.* is gone |
dead_mans_switch_s, seconds, clamped to [3, 300] |
off |
| OKX | POST /api/v5/trade/cancel-all-after (1 request/s) |
countdown the connector refreshes every window/3, for every pending order of the account | dead_mans_switch_s, seconds, clamped to [10, 120]; timeOut "0" stops it |
60 |
| Gemini | cancelOnDisconnect=true on the order connection; with heartbeat, POST /v1/heartbeat |
the venue watches its own socket; the heartbeat is a countdown the connector refreshes every 15 s, for the API key's orders | disconnect; 30 s without an authenticated request | on; heartbeat off |
| Binance Spot | — | none exists | — | — |
| Coinbase Advanced Trade | — | none exists for spot | — | — |
| Coinbase Exchange | — | cancel-on-disconnect only for a FIX order-entry session (Logon tag 8013); the connector enters orders over REST | — | — |
Bybit's is off by default because Bybit grants it only to institutional accounts, configured by an account manager ("DCP feature is only available for Ins clients"); the connector arms it and subscribes dcp.spot (dcp.future for linear) only when dead_mans_switch_s is set, and logs a refusal without stopping. Binance Spot has none: rest-api.md, web-socket-api.md and fix-api.md have no countdown, session auto-cancel or cancel-on-disconnect as of the 2026-09 docs, and the FIX News "countdown" counts down to a maintenance logout without touching orders.
Only a countdown can lapse while the process lives. If Binance USDⓈ-M, OKX or the Gemini heartbeat refuses the refresh for a whole window, the venue has cancelled those orders, so the connector sets its fatal flag and trips the venue kill with DeadMansSwitchLost instead of requoting (Kill switch and shutdown). CountdownSwitch (include/fastmm/venues/dead_mans_switch.hpp) keeps two clocks apart: when to retry, and when the venue last confirmed the countdown.
Batch order entry¶
Only the Hyperliquid connector batches order entry (an action carries many orders, and the venue's limits count posts per IP but orders per address; see Hyperliquid). The others send single orders:
- Binance Spot has no batch endpoint. The order lists (OCO/OTO/OTOCO/OPO/OPOCO) are contingency structures, not a way to send two independent quotes.
- Binance USDⓈ-M has
POST /fapi/v1/batchOrders(5 orders, IP weight 5, 1 against the 1-minute order limit instead of 5) andDELETE /fapi/v1/batchOrders(10 orders, IP weight 1 instead of 1 each). Both are REST-only, so using them would move quotes and cancels off the WS API onto an HTTP round trip. The connector already coalesces an engine batch into one TCP write betweencork()anduncork(), so a batch request would not save a round trip on the WS path either. - Bybit v5 has
order.create-batch/order.amend-batch/order.cancel-batchon the WebSocket trade endpoint, with their own rate-limit pool. They are asynchronous ("please use the websocket to confirm the order status") and report per-order outcomes positionally inretExtInfo.list[]while the top-levelretCodestays 0, so a two-sided quote can come back half-accepted. Two single orders each get their own response under their ownreqIdand the per-order error mapping. Spot batches are charged per order, so there is no rate-limit saving. - OKX has
batch-orders/batch-amend-orders/batch-cancel-orders(20 orders) on the same WebSocket, reported per order like Bybit's, andmass-cancelonly for options in portfolio margin mode. The connector sends single orders; cancel-all uses RESTcancel-batch-orders. - Lighter has
jsonapi/sendtxbatch(15 transactions), but each transaction in it still counts against the sendTx limit and the volume quota, and what the venue does when one of them fails is not documented. The connector sends onejsonapi/sendtxframe per transaction, a drain's frames in one write. - Gemini has no batch order entry;
order.cancel_allandorder.cancel_sessionexist on the WebSocket, and the connector cancels over RESTorder/cancel/session. - Coinbase Advanced Trade has no batch order entry;
batch_canceltakes several order ids and is what the kill switch uses. Coinbase Exchange batches only on FIX (15 orders a message); its RESTDELETE /orders?product_id=cancels a product's orders. - Deribit has only
private/mass_quote, which is a separate pathway: it needs administrator approval per user and currency (13902 mass_quotes_disabled), cancel-on-disconnect (13042 cod_not_enabled) and an MMP group that reserves initial margin; it allows one quote per instrument per side per group, is not available for spot, and its quotes are not orders (private/buy,private/sellandprivate/editdo not apply; they are cancelled withprivate/cancel_quotes). Supporting it means a second order lifecycle beside the OMS's. Its bulk cancels (private/cancel_all_by_instrument,private/cancel_all_by_currency) are what the kill switch and the channel-loss path use.
Requests are charged what their endpoint charges. On Binance USDⓈ-M's WebSocket order path a place and a modify cost 0 IP weight and a cancel 1, with the order limits taking the other side (charging 1 per request would use half the 2400/minute IP budget at the venue's 1200 orders/minute). Binance Spot's order.amend.keepPriority costs IP weight 4 and nothing against the ORDERS bucket ("Unfilled Order Count: 0").
A venue-fatal error and a REST hard stop stop new orders and replaces; Cancel and cancel-all are always admitted, on whatever transport is still usable. Between cork() and uncork() a frame is only encoded, so a failed uncork() rejects every order of the batch (RejectReason::TransportFull) rather than counting them as sent. A reconciliation snapshot reaches the engine only when the whole reply parsed: Oms::reconcile_end() cancels every order the snapshot does not name, so a rejected or truncated reply is dropped rather than emitted as an empty snapshot. What it may cancel is bounded by the sent_watermark its Begin carries: orders the engine sent after the snapshot was asked for cannot be in it. An order the snapshot drops while it still had working quantity is reported (OmsStats::reconcile_unresolved, EngineStats::unresolved_orders) and logged, because the venue ended it without saying whether it filled or was cancelled.
A connector reconciles after a private or order channel reconnects, and when the engine asks: after more than three cancel rejects the engine sends ControlCommand::Reconcile on the venue's outbound ring and the connector calls request_open_orders(). The first snapshot of a session is a sweep (Binance Spot: when the order channel first goes Live; USDⓈ-M: the user stream; Bybit, OKX, Deribit: the private channel), with an empty watermark, so the snapshot says nothing about the engine's own orders: it finds only orders the engine does not know, which a session that died without cancelling left resting, and the engine cancels them. Their client order ids carry an earlier session epoch, so they cannot be confused with this session's.
A warm standby (Hand over a running session) calls defer_private() before connect(): a connector that takes it opens only its public market data (a pool member opens nothing), and enable_private(), on its network thread, opens the rest: order entry, the private stream, the execution replay and the start-up sweep. Until then it sends, cancels and queries nothing of the account; on a shared account a sweep would find the running session's orders and cancel them as unknown. Binance Spot takes it; the other connectors refuse, and the standby waits for the lock before it connects at all.
Every connector runs its snapshot through one ReconcileDriver (venues/reconcile_driver.hpp): one snapshot at a time, a request made meanwhile served by one more straight after it; the execution replay first; the watermark taken when the snapshot is asked for, after the replay; a snapshot that failed asked again after 5 s and one that got no answer abandoned after 60 s; nothing asked once disconnect() has begun. After each snapshot the connector drops the order shadows it proves over: an order answered before the snapshot was asked for that the venue no longer holds ended with its terminal event lost (VenueStatus::shadows_swept). "Before" is by send sequence: every New and Replace takes the connector's next number, stamped on its shadow, and the watermark carries the number of the id it names, so behind fastmm-gateway every strategy's shadows are judged (their ids are in send order within one session epoch only).
The shadow table holds 7168 orders (8192 slots, kShadowSlots): the orders working at the venue and the replaces in flight, over every strategy behind the connector. An order or replace it has no room for is not sent; it goes back to the engine as OrderTableFull, counted in VenueStatus::shadows_refused, with one error line when the table fills and one warning when it has room again. Sent untracked, its replies would carry no instrument and it could not be amended; a USD-ⓈM modify would leave the venue's later fills booked to the order it replaced.
Executions the private stream never delivered¶
An open-order snapshot cannot report a fill that finished an order: the venue no longer holds the order, so its quantity, price and fee are absent. Inferring the quantity from a cum_qty the venue reports later (Oms::absorb_cum → Engine::book_missed_fill) works only while the order is still open, and is an estimate at the order's own price with no fee.
So a reconciliation asks what the account executed before it asks what is open. Venue::request_executions(since_venue_ms) runs the venue's trade-history query for every subscribed instrument and emits each execution into the order sink as an ordinary OrderFillMsg carrying the venue's execution id and OrderFillMsg::kReplayed. Oms::on_fill deduplicates on the venue execution id, instrument and side over the last 4096 executions, so only the ones the engine never saw are booked, with their real price and fee. An order the snapshot then drops has been proved cancelled. The snapshot's Begin carries ReconcileMsg::kExecutionsExact when every instrument answered in full.
Every connector runs its replays (and USDⓈ-M's and OKX's funding queries) through one ReplayScheduler (venues/replay_scheduler.hpp). A replay reads each stream (a symbol on Binance and Gemini, a currency on Deribit, the account on Bybit and OKX) from its watermark to now. On Binance Spot and USDⓈ-M the watermark is the trade id after the last one read for that symbol (fromId) once there is one; otherwise, and on the other venues, it is a venue time, inclusive, and the rows read at or after it are remembered by id and not forwarded again. It moves to the end of what a replay read in full, but never past the replay's start less a settle margin (60 s; OKX 5 min), whether or not the replay found anything: an execution the venue shows in its history only after the query ran, with a time before it, is still inside the next query, and a quiet account does not keep asking from its connect time. It starts at connect(): a connector replays nothing from before it connected, because the engine's position starts at zero, unless a restarted session that restores its position says where to start with resume_executions(since_venue_ms, known) (venue time, the ids the store already holds) and, on Binance, resume_trade_ids (the next trade id per symbol) (Recovery at start-up).
A range longer than the venue takes in one query is walked in windows (Binance Spot 24 h, USDⓈ-M and Bybit 7 days), and a start older than the venue keeps is moved up to what it keeps (USDⓈ-M and OKX 3 months, Bybit 2 years), which makes that replay not exact. A page is full when the venue returned as many rows as asked for, whether or not they were forwarded; the next page is asked for in the same replay, from the newest row (Binance: the id after the highest; Bybit and OKX, which answer newest first: the venue's page token, the window emitted oldest first once read). A window needing more than 20 pages is read again up to its oldest row seen; more pages than a replay allows (Binance 10 per symbol, 20 for USDⓈ-M funding, 100 otherwise), or a full page that cannot move past its start, leaves the replay not exact.
VenueCapabilities::executions says whether a connector can ask at all. When a reconciliation was not preceded by a complete replay:
- the snapshot's
Begincarries nokExecutionsExact; the engine counts it (EngineStats::estimated_reconciles) and logs that a quantity no fill covered will be booked at the order's own price with no fee; - an order the snapshot drops with quantity still working is reported (
EngineStats::unresolved_orders), and the message says which case it is: the executions were replayed first, so the order was cancelled, or the venue could not be asked and the position may be short; - a query that failed (a rate limit, a ban, a reply that did not parse) leaves the watermark where it was, and the replay is asked again 5 s after it ended. A query the connector cannot send yet (its rate limiter's bulk share of the weight is spent, or the venue asked for a pause) waits for a later housekeeping tick instead: a start with many symbols sends what fits and the rest tick by tick, and the retry after a 429 waits for
Retry-Afterto pass; one that waited 180 s fails its stream. A reply to a query adisconnect()(or a lost Deribit connection) abandoned is ignored.
The replay also runs once a minute on every connector with executions. The watermark moves only when a replay runs, and the OMS deduplicates over a bounded window, so without it a reconnect after hours of streaming would replay more fills than it can deduplicate; it also books a fill the private stream dropped without disconnecting. VenueStatus::execution_queries, executions_fetched and execution_query_errors count the replays.
On Binance Spot and USDⓈ-M that minute's replay asks only for the symbols the account had order activity on (an order sent, an order event received on the user stream) since the symbol's watermark less the settle margin, once the connection has read the symbol in full; a quiet symbol's watermark moves as if it had answered empty. At 20 weight per myTrades query, 100 symbols cost 2000 a minute, more than a pool of accounts sharing an IP has for its bulk requests. A symbol active later is read from its watermark, which is before the activity. The replays before a reconciliation, after a reconnect, a restart or a failure read every symbol, as does every replay in the first two minutes after the connector starts. A fill the user stream dropped on a resting order with no other activity is not looked for within the minute: once the symbol has a fromId watermark the next replay that reads the symbol books it; before that, only a reconciliation's snapshot shows it. Other connectors read every stream; ReplaySchedulerBase::Hooks::active is the switch, OrderActivity the per-instrument record.
A fill the venue reported as cumulative quantity before any execution named it stays remembered (Oms's synthetic-fill pool). When the replay later names it, the execution does not add its quantity again: it corrects the price and the fee (OmsUpdate::corrected_qty, PositionTracker::correct_fill, EngineStats::corrected_fills). Correcting qty from est_px to px moves the position's PnL by qty * (px - est_px), given up on a buy and received on a sell; the average entry price keeps the estimate.
What each venue can answer, from their documentation (2026-09-24):
| venue | query | scope | window | identity | implemented |
|---|---|---|---|---|---|
| Binance Spot | GET /api/v3/myTrades |
per symbol (symbol required) |
fromId (ascending, inclusive) or startTime/endTime no more than 24 h apart; the two cannot be combined |
id (int64, per symbol); the order only as orderId (no clientOrderId), and side as isBuyer |
yes; weight 20, limit ≤ 1000 |
| Binance USDⓈ-M | GET /fapi/v1/userTrades |
per symbol | fromId or a range up to 7 days, within the last 3 months; neither given returns 7 days |
id (int64, per symbol, the user stream's t); the order only as orderId, and side |
yes; weight 5, limit ≤ 1000 |
| Bybit v5 | GET /v5/execution/list |
per account (only category is required) |
a range up to 7 days, 2 years of history, opaque nextPageCursor, newest first |
execId (string), and orderLinkId is the client id |
yes; one account-wide query, limit 100 |
| OKX | GET /api/v5/trade/fills (3 days), fills-history (3 months) |
per account and instType |
begin/end (ms, on ts), paged newest first with after = billId, limit 100 |
tradeId (per instrument), clOrdId, billId |
yes; one stream per instType traded (SWAP, SPOT; fills-history requires one), fills-history for a watermark older than 3 days |
| Gemini | POST /v1/mytrades |
per symbol | timestamp (ms, trades at or after it), limit_trades ≤ 500, listed newest first |
tid (int64), order_id, client_order_id |
yes; the next page from the newest row |
| Coinbase Advanced Trade | GET /api/v3/brokerage/orders/historical/fills |
per account (product_ids, order_ids optional) |
start_sequence_timestamp / end_sequence_timestamp (RFC 3339), opaque cursor |
trade_id (string); the order only as order_id |
yes; limit 100 |
| Coinbase Exchange | GET /fills |
per product (product_id or order_id required) |
start_date / end_date (RFC 3339, inclusive), after cursor (CB-AFTER), newest first by trade_id |
trade_id (per product); the order only as order_id |
yes; limit 100 |
| Deribit | private/get_user_trades_by_currency_and_time (WebSocket) |
per currency (kind optional), or ..._by_instrument_and_time per instrument |
start_timestamp/end_timestamp (ms), count ≤ 1000, sorting, has_more; historical: false covers the last 24 h only and true older records (indexed after a short delay, recent ones excluded); a single call cannot span both |
trade_id (string, unique per currency), order_id, label (the client id); direction is documented as the taker's, see below |
yes; per currency, kind: any |
On Binance, Spot's and USDⓈ-M's history names the order by orderId only. The connector maps it through the order ids its acks and snapshots carried; one it has not seen (an order whose acknowledgement never came, or any order when the process started since) it asks for with GET /api/v3/order?orderId= (weight 4) or GET /fapi/v1/order?orderId= (weight 1) before the window's fills go out, so the fill names the order's clientOrderId, and with it the session and strategy. At most 16 lookups per replay, one per order, and none while the rate limiter has no room. An order that is not FastMM's is not asked for again. A failed or skipped lookup sends the fill naming no order with OrderFillMsg::kUnresolved, and the next replay (5 s later, 5 times at most) asks again and sends the fill once more, naming its order; after the last attempt, or when the order is not FastMM's, the last copy goes without the flag and still names none. An engine keeps one of the copies (same trade id); fastmm-gateway gives a flagged fill to no strategy until the copy that settles it. Bybit, OKX, Gemini and Deribit rows carry the client id (orderLinkId, clOrdId, client_order_id, label) and need no lookup.
On Deribit, a page with has_more is followed from its last row's timestamp; the replay is exact when every currency ends with has_more: false. A watermark older than 23 h is first read with historical: true up to 23 h ago, then with historical: false from there (the recent endpoint covers 24 h). The open-ended query ends 60 s past the replay's start, the venue's clock not being ours.
The schema (checked 2026-09-25) describes a user trade's direction as "Trade direction of the taker", the same text as for public trades, while the same row's liquidity (M/T) and fee are the account's own. The connector takes the side from the order it still holds and otherwise reads direction as the account's side, as it does for user.trades; neither reading has been checked on testnet.
Balances¶
Every connector reports the account's balances as BalanceMsg events on the order ring (Balance check): after each reconciliation snapshot ReconcileDriver runs a balance leg (ReconcileHooks::fetch_balances) and sends its rows as one snapshot (kSnapshot, the last one kSnapshotEnd: an asset it does not name holds nothing), and the private stream's balance updates follow as single messages. Only the assets the engine keeps are forwarded (VenueAssets, venues/balances.hpp: the base and quote of spot instruments, the settlement asset of derivatives). A failed balance fetch is asked again after 5 s and does not hold up the order snapshot. A venue without a balance stream asks for the leg again after a fill (request_balances(), at most once a second).
| venue | snapshot | stream | mapping (free / locked / total / equity / maintenance) | venue time |
|---|---|---|---|---|
| Binance Spot | WS API account.status, else GET /api/v3/account, omitZeroBalances=true (weight 20) |
outboundAccountPosition |
free / locked / sum / sum / 0 |
stream: u; snapshot: the connector's venue clock (updateTime is the last change) |
| Binance USDⓈ-M | GET /fapi/v3/account (weight 5); the mode from /fapi/v1/multiAssetsMargin and ACCOUNT_CONFIG_UPDATE ai.j |
ACCOUNT_UPDATE asks for the leg again: its B[] has the wallet balance only |
availableBalance / initialMargin / walletBalance / marginBalance / maintMargin; multi-assets mode: per asset maxWithdrawAmount as free, and an account row in USD from the total* fields |
venue clock |
| Bybit | GET /v5/account/wallet-balance?accountType=UNIFIED |
wallet topic (spot and linear) |
coin: walletBalance - locked - totalOrderIM - totalPositionIM - bonus / locked + totalOrderIM + totalPositionIM / walletBalance / equity / totalPositionMM; linear, unless /v5/account/info says ISOLATED_MARGIN: an account row in USD from totalAvailableBalance, totalInitialMargin, totalWalletBalance, totalEquity, totalMaintenanceMargin |
reply time; push creationTime |
| OKX | GET /api/v5/account/balance; the mode from acctLv (/api/v5/account/config) |
account channel |
spot mode: availBal / frozenBal / sum / sum / 0; futures mode: availEq / frozenBal / cashBal / eq / mmr; multi-currency and portfolio margin: availBal / frozenBal / cashBal / eq / 0 per currency, and an account row in USD: availEq - imr / imr / totalEq / adjEq / mmr |
account uTime; push: the currency's uTime |
| Gemini | POST /v1/balances; with a perpetual, /v1/margin as the account row (an exchange account answers AccountNotOfTypeRequired: no row) |
balances@account on the order connection; a perpetual fill asks for the leg again |
available / amount - available / amount / amount / 0; margin: available_margin / initial_margin / margin_assets_value / same / margin_maintenance_limit |
the reply's latest _timestamp; push u |
| Coinbase Advanced Trade | GET /api/v3/brokerage/accounts, paged by cursor (vault, futures and international accounts left out) |
none for spot; every fill asks for the leg again | available_balance / hold / sum / sum / 0 |
venue clock |
| Coinbase Exchange | GET /accounts |
authenticated balance channel, subscribed with the account ids of the kept assets after each fetch; a fill also asks for the leg (the channel "does not track every update") |
available / hold / sum / sum / 0, truncated to 8 decimals |
venue clock; push updated |
| Deribit | private/get_account_summaries |
user.portfolio.{currency} |
available_funds / initial_margin / balance / equity / maintenance_margin; with cross_collateral_enabled, an account row in USD from the total_*_usd fields |
reply usOut; push: venue clock |
| Nasdaq ITCH | none | none |
Sources, read 2026-09-30: Binance user-data-stream.md "Account Update", rest-api.md and web-socket-api.md "Account information"; Binance USDⓈ-M Account Information V3 and "Event: Balance and Position Update"; Bybit Get Wallet Balance and WebSocket "Wallet"; OKX Get balance, Account channel; Gemini websocket/streams.md "Balance Updates" and specs/openapi/rest.yaml at developer.gemini.com; Coinbase Advanced Trade List Accounts, Exchange accounts and the Balance channel; Deribit user.portfolio, get_account_summaries.
Mark, index and funding¶
Connectors of derivatives send the venue's mark price, index price, funding and open interest as PerpStateMsg events on the market-data ring (Perpetuals); the engine values positions at the mark (Valuation). fields names what a message carries: a venue that publishes on separate channels sends one message per channel, and one whose updates carry only the changed fields (Bybit) keeps the rest and sends them all. funding_rate is the rate the venue will apply at next_funding, per funding_interval; positive, longs pay.
| venue | source | mark / index | funding (rate, interval, next) | open interest | measured on production, 2026-09-30 |
|---|---|---|---|---|---|
| Binance USDⓈ-M | <sym>@markPrice@1s on /market, perpetuals |
p / i |
r, T; the interval from GET /fapi/v1/fundingInfo at start (fundingIntervalHours; a symbol it does not list: 8 h), corrected when T steps by whole hours after a funding |
none | 1.0/s per symbol; r changed once in 2 min; 803 fundingInfo rows: 469 at 4 h, 333 at 8 h, 1 at 1 h |
| OKX | mark-price, index-tickers (the swap's index, BTC-USDT for BTC-USDT-SWAP), funding-rate, open-interest on /ws/v5/public, swaps, in their own subscribe request |
markPx / idxPx, one message each |
fundingRate ("predicted funding rate for the upcoming settlement"), fundingTime (the next settlement), interval nextFundingTime - fundingTime (8 h, some 4 h) |
oi, contracts |
mark 5.1/s (documented: 200 ms on change, else 10 s), index 0.9 to 4.8/s (documented: once a minute without change, longest gap 2.1 s), funding every 20 to 73 s (documented 30 to 90 s), open interest every 5 to 10 s |
| Bybit | tickers.<sym>, linear perpetuals; a snapshot, then deltas of the changed fields, cached per symbol and cleared on reconnect |
markPrice / indexPrice |
fundingRate, nextFundingTime, fundingIntervalHour (instruments-info fundingInterval if absent) |
singleOpenInterest (one side; openInterest is both) |
7.5 to 8.1 pushes/s per symbol; markPrice in 9 % of deltas (longest gap 9 s), fundingRate changes once a minute |
| Deribit | the ticker.<name>.100ms notification already subscribed, perpetuals and futures (options: OptionTickerMsg only) |
mark_price / index_price |
perpetuals: current_funding per 8 h, next_funding 0 (funding accrues continuously; funding_8h is the past 8 h) |
open_interest in contracts (USD for inverse contracts, divided by the contract size) |
2.4/s per perpetual, longest gap 3.4 s |
| Gemini | {sym}@markPrice and {sym}@fundingAmount on wss://ws.gemini.com, perpetuals, in their own subscribe request; neither is in the published docs or AsyncAPI spec |
p / none (i is the index times an undocumented factor per symbol) |
f / p per i minutes (60), next funding T (f: the estimated funding of one long contract over the hour, in quote); no snapshot on subscribe, so up to a minute after connect |
none | markPrice every 5 s, fundingAmount on each minute |
| Coinbase, Binance Spot, Nasdaq | none (spot) |
Sources, read 2026-09-30: Binance USDⓈ-M Mark Price Stream, Get Funding Rate Info and the Important WebSocket Change Notice (markPrice under /market); OKX Mark price, Index tickers, Funding rate and Open interest channels, Get funding rate; Bybit Ticker and Get Instruments Info; Deribit ticker.{instrument_name}.{interval} and the support article "Inverse Perpetual" (funding "expressed as an 8-hour interest rate", "calculated every millisecond"); Gemini websocket/streams.md and specs/asyncapi/websocket.yaml at developer.gemini.com (the streams are named only in the revision history, "Mark Price WebSocket API", 2025-10-31) and the support article "How is the Funding Amount calculated?".
Binance Spot¶
| channel | endpoint | purpose |
|---|---|---|
| md | <ws_url>?streams=<sym>@depth@100ms/<sym>@bookTicker/<sym>@trade |
combined stream, dispatch by stream suffix |
md (md_format = "sbe") |
<sbe_ws_url>?streams=<sym>@depth/<sym>@bestBidAsk/<sym>@trade |
binary SBE frames, dispatch by template id |
| user | <ws_api_url> + userDataStream.subscribe.signature (HMAC) or session.logon + userDataStream.subscribe (Ed25519) |
executionReport, outboundAccountPosition |
| order | <ws_api_url>: order.place / order.cancel / order.amend.keepPriority / order.cancelReplace / openOrders.status / openOrders.cancelAll |
order entry; REST fallback |
| rest | <rest_url> |
exchangeInfo, depth, time, openOrders, account/commission (fetch_fees), REST order entry, kill-switch cancel-all |
The listenKey user stream (POST/PUT /api/v3/userDataStream + /ws/<listenKey>) remains as user_stream = "listen_key" for the simulator only; Binance removed it on 2026-02-20.
Keys and session logon¶
- HMAC keys (
key_type = "hmac", the default) sign every request: apiKey + hex HMAC-SHA256. The key pads are hashed once (net::HmacSha256Key), so a signature costs about 170 ns. - Ed25519 keys (
key_type = "ed25519",private_key_fileorprivate_key_env) log on once per WS API connection withsession.logon(the only signed request) on the order and user connections. Later requests carry neitherapiKeynorsignature, onlytimestampandrecvWindow. The order channel is Live only after the logon reply. A failed logon is fatal for bad key, signature or permission errors; timestamp, rate-limit and server errors retry the logon after 2 s. A revoked session ({"id":null,"status":401,...}, key deleted or IP not whitelisted) is logged and acted on through the error map (-2015 is fatal). REST requests (fallback, reconciliation, kill switch) are signed with Ed25519 per request, about 30 µs each, so useorder_api = "ws"with Ed25519 keys. - Binance supports
session.logonwith Ed25519 keys only, on production, the Spot testnet (wss://ws-api.testnet.binance.vision/ws-api/v3) and Demo Mode (wss://demo-ws-api.binance.com/ws-api/v3); RSA keys are not supported by FastMM.
Order encode with HMAC signing: 523 ns (bench/bench_order_encoders.cpp, bench/README.md, release-native, one pinned core, 2026-09-23). BM_Encode_BinanceOrderPlace_Session and _Ed25519 are not in bench/results/latest.
Fee rates¶
fetch_fees = true makes fastmm-live ask for the account's rates on each configured symbol at start-up, after exchangeInfo: GET /api/v3/account/commission?symbol=S (signed, weight 20 per symbol; HMAC and Ed25519 keys). /sapi/v1/asset/tradeFee, the bulk query, is not on the Spot testnet or in Demo Mode. The rate of a fill is standardCommission + specialCommission + taxCommission, maker or taker, plus the larger of each group's buyer and seller rate (a warning when they differ); the BNB discount is left out, because a commission paid in BNB is not booked (FeeAsset::Other). Rates are rounded to 1 cbps (1e-6).
The fetched rates replace [venues.<x>.fees] and [[instruments]] maker_bps / taker_bps for those symbols: ctx.fees returns them, and a line per symbol logs them, as a warning when they differ from the configuration. They are written as maker_bps / taker_bps into the configuration the journal and the store embed, so a replay and a backtest over the journal use them. A refused request stops the start (exit code 4). Fills are still booked with the commission each executionReport carries. fastmm-gateway does not fetch them; a strategy attached to it reads its own configuration.
SBE market data¶
md_format = "sbe" reads the SBE market-data streams (https://developers.binance.com/docs/binance-spot-api-docs/sbe-market-data-streams): wss://stream-sbe.binance.com[:9443], Demo Mode wss://demo-stream-sbe.binance.com, testnet wss://stream-sbe.testnet.binance.vision. sbe_ws_url defaults to ws_url with stream. / demo-stream. replaced by stream-sbe. / demo-stream-sbe.. The connection needs an Ed25519 API key in the X-MBX-APIKEY upgrade header (no signature; the server answers 400 No X-MBX-APIKEY header without it), so the setting requires key_type = "ed25519" and works in a dry run without the private key.
| stream | template | message |
|---|---|---|
<sym>@depth |
DepthDiffStreamEvent 10003 |
BookDelta (firstBookUpdateId / lastBookUpdateId as U / u) |
<sym>@bestBidAsk |
BestBidAskStreamEvent 10001 |
BookTicker (bookUpdateId as sequence) |
<sym>@trade |
TradesStreamEvent 10000 |
one Trade per group entry |
<sym>@depth20 |
DepthSnapshotStreamEvent 10002 |
decoded, not subscribed |
The schema is tools/sbe/binance_spot_stream_1_0.xml (schema id 1, version 0, from the binance-spot-api-docs repository); tools/sbe_gen.py generates include/fastmm/venues/binance/generated/binance_stream_sbe.hpp (CI checks it is current). Timestamps are microseconds; prices and quantities are int64 mantissas with a per-message exponent, converted exactly to 1e-8 fixed point (a value finer than 1e-8 makes the frame malformed). The depth snapshot still comes from REST (JSON), and depth sync is unchanged. JSON decode per message (bench/README.md, release-native, 2026-09-23): BM_Json_BinanceDepth20 608 ns, BM_Json_BinanceDepth100 2.73 µs, BM_Json_BinanceBookTicker 111 ns, BM_Json_BinanceTrade 109 ns. The BM_Sbe_* benchmarks are in bench/bench_json.cpp (--benchmark_filter=Sbe_Binance) but not in bench/results/latest.
WS API responses stay JSON: responseFormat=sbe would move order acks and execution reports to SBE schema 3 (retired every few months, currently version 5), for a saving of about 300 ns per order event.
Depth sync follows "How to manage a local order book correctly": buffer deltas, fetch GET /api/v3/depth?symbol=S&limit=L once a new stream's first delta is buffered (0.5 s after it opens if none has come); L is depth_limit, or without it 1000 (weight 50) with up to 10 subscribed symbols and 100 (weight 5) above, because every book is snapshotted at a start and the minute's 6000 weight is also the orders'. A snapshot goes out when it fits the rate limiter's bulk share (half the weight), else the depth sync asks again from the housekeeping timer. Then drop u <= lastUpdateId, first applied delta must have U <= lastUpdateId+1 <= u, then U == prev_u + 1. A gap emits ConnectionState{Resyncing} and re-snapshots (at most once per min_snapshot_interval, 2 s by default). A delta that changes more than 1024 levels on a side (production USDⓈ-M BTCUSDT does in a fast market; see level_spill.hpp) keeps the 1024 nearest the touch instead of being dropped, which would break the chain. The update that reveals a gap starts the buffer for the next snapshot. Every resync is logged with its reason and update ids, and the book's return with the time it was away.
What a Binance-compatible simulator must implement¶
BinanceVenue uses only the following.
REST¶
Parameters go in the query string. Signed requests carry timestamp, recvWindow, signature = lowercase hex HMAC-SHA256(secret, query without &signature=...) and the X-MBX-APIKEY header. The REST connection carries one request at a time; a signed request is stamped and signed when it is written, not when it is queued, so a wait behind other requests does not count against recvWindow. An execution replay keeps at most 4 requests on the connection.
| method + path | request | response the connector reads |
|---|---|---|
GET /api/v3/exchangeInfo?symbols=["S"] (percent-encoded) |
public | serverTime, rateLimits[]{rateLimitType,interval,intervalNum,limit}, symbols[]{symbol,status,baseAsset,quoteAsset,orderTypes[],filters[]}; filters PRICE_FILTER.tickSize, LOT_SIZE.{stepSize,minQty,maxQty}, NOTIONAL.{minNotional,maxNotional} or MIN_NOTIONAL.minNotional |
GET /api/v3/time |
public | {"serverTime": ms} |
GET /api/v3/depth?symbol=S&limit=<depth_limit> |
public | {"lastUpdateId":L,"bids":[["px","qty"]...],"asks":[...]} |
GET /api/v3/account/commission?symbol=S |
signed, with fetch_fees |
standardCommission, specialCommission, taxCommission {maker,taker,buyer,seller} |
GET /api/v3/openOrders[?symbol=S] |
signed | array of {symbol,orderId,clientOrderId,price,origQty,executedQty,status,timeInForce,type,side} |
DELETE /api/v3/openOrders?symbol=S |
signed | 200 with an array; 400 {"code":-2011,...} is treated as "nothing open" |
GET /api/v3/order?symbol=S&orderId=N |
signed | {orderId,clientOrderId}, also for an order that has ended and traded |
POST /api/v3/order, DELETE /api/v3/order, POST /api/v3/order/cancelReplace, PUT /api/v3/order/amend/keepPriority |
signed; same parameters as the WS API methods below | same bodies as the WS API result / error objects |
POST/PUT /api/v3/userDataStream |
simulator listenKey mode only | {"listenKey":"..."} |
Headers read: X-MBX-USED-WEIGHT-1M, X-MBX-ORDER-COUNT-10S, Retry-After (429/418). Error envelope: {"code":-NNNN,"msg":"..."}. A 429 pauses every send for Retry-After (10 s without the header); another 429 within a minute of the last pause's end waits 2, 4, 8 ... s on top, up to 120 s (RateLimiter::cooldown), since a client back at once earns the next 429 and then the 418 ban. The myTrades replay and order lookups wait for the limiter's bulk share and a pause the same way, and read 429 and 418 by their status. The limiter's windows roll on the venue's clock (the server time offset), as Binance's do on the UTC minute and 10 s; a count the venue reports raises the local estimate to it and lowers it to it plus what was sent in the 2 s before (the count may not include those yet), except in a window's first 2 s, where it may be the last window's. The bulk share is paced over the minute: what the process itself sent may fill a fifth of it at the minute's start and all of it at its end, so a backlog of history queries does not spend it the moment the window opens; the IP's count, other processes' requests included, is held to the whole share. Orders the limiter refuses (VenueRateLimit, never sent) are counted by the limit that refused them, in the status line's rate_refused(weight/10s/1m/1d/paused). account/commission at a start (fetch_fees) waits for its weight to fit before each request.
Market-data WebSocket¶
/stream?streams=... sends {"stream":"<name>","data":{...}}:
<sym>@depth@100ms:{"e":"depthUpdate","E":ms,"s":"SYM","U":first,"u":last,"b":[["px","qty"]],"a":[...]}(qty"0"deletes)<sym>@bookTicker:{"u":id,"s":"SYM","b":"px","B":"qty","a":"px","A":"qty"}<sym>@trade:{"e":"trade","E":ms,"s":"SYM","t":id,"p":"px","q":"qty","T":ms,"m":bool}(m= buyer is maker, so the seller aggressed)
A raw /ws/<stream> payload without the wrapper is also accepted. Server pings are answered; the connector treats any frame or ping as liveness.
WebSocket API¶
<ws_api_url> (e.g. /ws-api/v3). Requests are {"id":"<string>","method":"...","params":{...}} with params in alphabetical order, apiKey, timestamp, recvWindow and signature (HMAC over the sorted k=v&... string). Responses: {"id":..,"status":HTTP,"result":..|"error":{"code","msg","data"?},"rateLimits":[{rateLimitType,interval,intervalNum,limit,count}]}.
| method | params used | result read |
|---|---|---|
userDataStream.subscribe.signature |
apiKey, recvWindow, timestamp, signature | {"subscriptionId":N} (request id "uds") |
session.logon |
apiKey, recvWindow, timestamp, signature (Ed25519 keys only; request ids "logon-o" / "logon-u") |
status 200 |
userDataStream.subscribe |
none, after session.logon |
{"subscriptionId":N} |
order.place |
newClientOrderId, newOrderRespType=ACK, price (not MARKET), quantity, side BUY/SELL, symbol, timeInForce (LIMIT only), type LIMIT/LIMIT_MAKER/MARKET | {symbol,orderId,clientOrderId,transactTime} |
order.cancel |
orderId or origClientOrderId, symbol | {symbol,origClientOrderId,orderId,executedQty,status} |
order.cancelReplace |
cancelOrderId or cancelOrigClientOrderId, cancelReplaceMode=STOP_ON_FAILURE, newClientOrderId, newOrderRespType=ACK, price, quantity, side, symbol, timeInForce, type | {cancelResult,newOrderResult,cancelResponse{orderId,origClientOrderId,executedQty},newOrderResponse{orderId,clientOrderId}}; on failure error.data carries the same keys plus newOrderResponse.code/msg |
order.amend.keepPriority |
newClientOrderId, newQty, orderId or origClientOrderId, symbol | {transactTime,executionId,amendedOrder{symbol,orderId,origClientOrderId,clientOrderId,price,qty,executedQty,status}} |
openOrders.status |
recvWindow, timestamp (no symbol = all) | array as in REST openOrders (request id "oo") |
openOrders.cancelAll |
symbol | array (request id "ca") |
Request ids for orders are n|c|r|a + the 14-character client id (fm + 12 hex), so the simulator only has to echo id. a is an amend, because its response has a different shape from a cancel-replace's.
User data events¶
User data events arrive on the subscribed WS API connection as {"subscriptionId":0,"event":{...}} (legacy /ws/<listenKey> raw events are also accepted):
executionReport:E,s,c,S,q,p,C,x,X,r,i,l,z,L,n,N,T,t,m, withxinNEW,CANCELED(the cancelled id is inC),REPLACED,REJECTED,TRADE,EXPIRED,TRADE_PREVENTIONoutboundAccountPosition:E,B[]{a,f,l}(forwarded only withposition_from_balance = true)
Replaces: amend where the venue allows it, cancel-replace otherwise¶
order.cancelReplace cancels and places again, so the order goes to the back of the queue at its price. order.amend.keepPriority keeps the order, its orderId and its time priority, and only reduces the remaining quantity; it has no price parameter. The connector sends an amend when the replace is a size-down at the shadow's price (cmd.qty < shadow.qty; newQty at or above the current quantity is refused with -2038), and a cancelReplace for anything else: a price change, a size-up, a replace whose original has no shadow.
- The amended order gets the engine's new client id (
newClientOrderId) and keeps its venue order id and itsexecutedQty, where a cancel-replace produces a different venue order whose fills start at zero. The connector marks the ack withOrderAckMsg::kAmendedInPlace(from the response, and from the user stream, wherexisREPLACED), andOms::on_ackrekeys the order without resettingcum_qty.QuoteManagerholds a handle, not an id, and needs nothing. exchangeInfo'sMAX_NUM_ORDER_AMENDSfilter caps amendments per order (10 where published) and returns -2038 past it. The connector counts them in the order's shadow and falls back tocancelReplaceatmax_order_amends.
amend_keep_priority = false puts every replace back on cancelReplace.
Error codes the connector acts on: -1003/-1015 (cool down), -1021 (resync clock), -1022/-2014/-2015 (fatal), -2010/-2011 message texts (Order would immediately match and take., Unknown order sent., Account has insufficient balance for requested action., Duplicate order sent.), -2013, -2021/-2022, -2038 (amend refused: the order is gone, the quantity did not go down, or MAX_NUM_ORDER_AMENDS is used up: nothing changed on the venue, so the original keeps working and the replacement id is rejected), HTTP 418/429.
Binance USDⓈ-M futures¶
Perpetual contracts in one-way position mode, with HMAC or Ed25519 keys. Sources: the USDⓈ-M documentation at https://developers.binance.com/docs/derivatives/usds-margined-futures/general-info (read 2026-09-15), the field names of the official connector's generated models (binance-connector-python, derivatives_trading_usds_futures) and recorded Demo Trading market data (tests/fixtures/binance_usdm/fixtures.meta.json).
| channel | endpoint | purpose |
|---|---|---|
| md | <ws_url>/public/stream?streams=<sym>@depth@100ms/<sym>@bookTicker |
depth diffs, best bid and offer |
| trades | <ws_url>/market/stream?streams=<sym>@aggTrade |
aggregate trades |
| user | <ws_private_url>/ws/<listenKey>, default <ws_url>/private |
ORDER_TRADE_UPDATE, ACCOUNT_UPDATE, listenKeyExpired |
| order | <ws_api_url>: order.place / order.cancel / order.modify |
order entry; REST fallback POST / DELETE / PUT /fapi/v1/order |
| rest | <rest_url> |
exchangeInfo, depth, time, listenKey, userTrades, openOrders, positionRisk, account checks, kill-switch DELETE /fapi/v1/allOpenOrders, dead man's switch POST /fapi/v1/countdownCancelAll |
The /public, /market and /private paths come from the 2026-03-05 URL split; the unrouted /ws and /stream URLs were decommissioned on 2026-04-23 ("Important WebSocket Change Notice"). Demo Trading hosts: REST https://demo-fapi.binance.com, streams wss://demo-fstream.binance.com, WebSocket API wss://testnet.binancefuture.com/ws-fapi/v1.
Ed25519 keys (key_type = "ed25519") log on to the WS API order connection with session.logon (request id "logon"; the USDⓈ-M WS API documents it for Ed25519 keys only) and send order.place / order.cancel / order.modify without apiKey and signature after that, as on Spot. REST requests are signed with Ed25519. There are no SBE streams for USDⓈ-M.
The listenKey comes from POST /fapi/v1/listenKey and is kept alive with PUT every 30 minutes (valid for 60). On listenKeyExpired or a failed keepalive the connector requests a key, reopens the user connection and reconciles.
Book sync¶
GET /fapi/v1/depth?symbol=S&limit=L (depth_limit; without it 1000, weight 20, with up to 10 subscribed symbols and 100, weight 5, above), requested once a new stream's first delta is buffered and when it fits the rate limiter's bulk share of the minute's 2400 weight, as on Spot: requested as the stream opened, the snapshot often came back before the first delta and older than it, and that delta resynced the book. Deltas with u below lastUpdateId are dropped, the first applied delta has U <= lastUpdateId <= u, and each later delta's pu must equal the previous u (BinanceFuturesSyncTraits). A mismatch emits ConnectionState{Resyncing} and fetches a new snapshot, at most once per 2 s. As on Spot, a delta longer than 1024 levels a side keeps the levels nearest the touch (production BTCUSDT: up to 2144 on a side, 17 frames in 15 minutes on 2026-09-28). The trades connection does not report its state to the engine, so a quiet aggTrade stream never clears the book. Mark price is not subscribed: the engine has no message for it.
Orders¶
- LIMIT maps
timeInForceGTC, IOC and FOK directly; post-only is LIMIT withGTX; MARKET has no price and notimeInForce.reduceOnly=trueis sent when the engine sets it;positionSideis omitted (BOTH). A crossing GTX order is rejected with -5022 or expires (OrderExpired). post_only_rpi = truesends post-only orders withtimeInForceRPIinstead: a retail price improvement order rests post-only, is matched only against orders entered in Binance's app or web page, is hidden from the API depth, and queues behind every other order at its price. The venue does not modify an RPI order, so pair it withsupports_replace = false.exchangeInfomust list the symbol as aPERPETUALorTRADIFI_PERPETUALcontract (the tokenised equity and commodity perpetuals); a delivery contract is refused at start.order.modifykeeps the venue'sclientOrderIdand takes the total quantity. The connector sends the new remaining quantity plus the filled quantity, reports later events for that order under the engine's new client id, and counts their fills from the modify. A modify answered with statusCANCELEDorEXPIREDbecomes a cancel of the original and a reject of the replacement. A modified order loses its queue position, soconfigs/binance-usdm-demo.tomluses cancel and new (supports_replace = false).- Rate limits come from
exchangeInfo.rateLimits, the WS APIrateLimitsand the REST headersX-MBX-USED-WEIGHT-1MandX-MBX-ORDER-COUNT-10S. Requests are charged what their endpoint charges, over the WS API as over REST: a place and a modify cost 0 IP weight and one against the 10 s and 1-minute order limits, a cancel costs 1 IP weight and no order. TheuserTradesandincomereplay queries and the order lookups wait while the bulk share (half) of the minute's weight is spent or the venue asked for a pause; a 429 answer to one waitsRetry-After(and longer for a second 429 within a minute, as on Spot), a 418 stops REST. The start'ssymbolConfigqueries, one per enabled symbol, wait for their weight to fit the share before each request. The windows, the venue's counts, the paced bulk share and the refusal counts work as on Spot.
Dead man's switch¶
POST /fapi/v1/countdownCancelAll?symbol=S&countdownTime=<ms> (signed, IP weight 10). "If this endpoint is not called within 120 seconds, all your orders of the specified symbol will be automatically canceled". The timer is per symbol, sending it again replaces the running one, and countdownTime=0 stops it. Binance publishes no minimum or maximum for /fapi (the 5000 ms floor belongs to the options endpoint /eapi/v1/countdownCancelAll) and notes only that it checks countdowns about every 10 ms. There is no WebSocket API method and no way to read the armed state back.
The connector arms it on every subscribed symbol from the housekeeping timer and refreshes at dead_mans_switch_ms/3, at most every 500 ms. The default 60 s window costs 10 IP weight per symbol per 20 s: 120 a minute for four symbols, 5 % of the 2400/minute budget; a shorter window raises that in proportion. disconnect() sends countdownTime=0, because a clean shutdown cancels its own orders.
The refresh clock advances only when the venue answers. A refused refresh is logged and retried; if none succeeds for a whole window, the connector trips DeadMansSwitchLost.
Positions and reconciliation¶
- On every user-stream connect and order-channel reconnect,
GET /fapi/v1/userTradesper symbol replays the executions since the last one forwarded (above), thenGET /fapi/v1/openOrders(weight 40) andGET /fapi/v3/positionRisk(weight 5) become oneReconcileMsgsequence: Begin, oneOpenOrderper order, onePositionper configured instrument (flat when absent), End. userTradesnames the order byorderIdonly; the connector maps it back through the order ids its acks and snapshots carried (to the engine id a modify moved it to), keeping the last 8192 pairings (RecentMap, oldest evicted; Binance Spot does the same); an order not mapped is looked up (Binance: the order a trade names). The replay asksfromId= the last trade id + 1 once it has one, elsestartTime= connect time or the restored watermark. A start older than 7 days is walked forward a week per query (bounded byendTime) and older than 3 months is clamped; either way that replay is not exact.- The engine books every fill, including liquidations and ADL (
autoclose-*client ids). The connector compares eachACCOUNT_UPDATEposition with the fills it forwarded once no fill has arrived for 1 s, and sends aPositionUpdateonly when they differ (position_from_account_update). The two event types are not ordered against each other, so forwarding every position would count fills twice. load_reference_data()refuses an account in hedge mode (fastmm-liveandfastmm-gatewayexit 3) and logs the position mode, the leverage and margin type of each symbol and the margin balance. It changes no account setting unless asked:one_way_mode = trueswitches a hedge-mode account to one-way mode (POST /fapi/v1/positionSide/dual) whenpositionRiskshows no position andopenOrdersno order, and refuses it otherwise;leverage = <n>sets every enabled symbol whose leverage differs (POST /fapi/v1/leverage). A refused change stops the start.- Funding:
GET /fapi/v1/income?incomeType=FUNDING_FEE(weight 30, every symbol in one query) is the source. Each row on a subscribed symbol becomes aFundingevent:income(negative paid) inasset,time,tranIdas its id. The query runs with every reconciliation's execution replay and once a minute, from its own watermark (as the executions), in windows of at most 7 days and no further back than 3 months; a full page is followed by the next, a failed query asked again 5 s later. AnACCOUNT_UPDATEwith reasonFUNDING_FEEnames the symbol (a.S, since 2026-08-07) and the balance change (B[].bc) but carries no id, so it only triggers the query, a second later. User Data Streams and Get Income History pages read 2026-09-26.
Errors¶
binance_usdm_error_map.hpp follows the USDⓈ-M error code page: -5022 post-only reject, -2018/-2019/-2027 insufficient balance or margin, -2022/-4118 reduce-only rejects, -4164 minimum notional, -4014/-4023 tick and lot, -1003/-1015 rate limit, -1021/-5028 clock resync, -1022/-2015/-4109 and -4061 (hedge mode) fatal, -2011/-2013/-4116 reconcile, HTTP 418 (stop REST), 429 (cool down) and 503 (execution status unknown, reconcile).
Bybit v5 spot and linear perpetuals¶
One connector, one category per [venues.<name>] section: spot (the default) or linear, the USDT- and USDC-margined perpetuals. Every request that takes a category gets it, and the private topics are filtered by it. The table and the paragraphs below describe spot; Linear perpetuals lists what changes.
| channel | endpoint | purpose |
|---|---|---|
| md | ws_url (/v5/public/spot) |
orderbook.<depth>.SYM snapshot/delta, orderbook.1.SYM to BookTicker, publicTrade.SYM |
| private | ws_private_url (default: ws_url host + /v5/private) |
op: auth, then order, execution, wallet, and dcp.spot when dead_mans_switch_s is set |
| trade | ws_api_url (/v5/trade) |
op: auth, then order.create / order.amend / order.cancel with reqId and X-BAPI-TIMESTAMP / X-BAPI-RECV-WINDOW headers |
| rest | rest_url |
/v5/market/instruments-info, /v5/market/time, /v5/order/realtime, /v5/execution/list, /v5/order/create|amend|cancel|cancel-all, /v5/order/disconnected-cancel-all |
Signing: REST X-BAPI-SIGN = hex HMAC-SHA256(secret, timestamp + api_key + recv_window + payload), where the payload is the exact query string (GET) or JSON body (POST) sent. WS auth: {"op":"auth","args":[key, expires_ms, hex HMAC("GET/realtime" + expires)]}. Every channel sends {"op":"ping"} every 20 s.
Book sync: the stream's snapshot resets the book; each delta must have a larger u; u == 1 or a missing snapshot (10 s) re-subscribes the depth topic to get a fresh snapshot. Amend keeps the venue's orderLinkId: the engine receives an ack for its new client id and later order/execution events for the old link id are translated to it.
Disconnect-Cancel-All: POST /v5/order/disconnected-cancel-all with {"product":"SPOT","timeWindow":<3..300>}, sent once per private connect when dead_mans_switch_s is set. The window is an account setting, not a countdown the client refreshes: Bybit starts the clock once every private connection that subscribed a dcp.* topic is gone and resets it when one reconnects ("your private websocket connection must subscribe 'dcp' topic in order to trigger DCP successfully"), so the connector subscribes dcp.spot only when the switch is on. Nothing is disarmed on shutdown; the session cancels its own orders.
Reconciliation: GET /v5/order/realtime?category=spot&limit=50 is paged with cursor = result.nextPageCursor until the cursor is empty (at most 40 pages). Bybit answers a rate limit (10006/10018) and a clock or signature error (10002/10004) with HTTP 200 and a non-zero retCode, so the snapshot is gated on retCode == 0 as well as the HTTP status.
Execution replay: GET /v5/execution/list?category=spot&startTime=<watermark>&limit=100, one query for the account, rows for unsubscribed symbols dropped. A range may span at most 7 days, so a watermark older than that is walked in 7-day windows (endTime = startTime + 7 days - 1 ms); the last window leaves endTime off. Each window is paged with cursor to its last page and then emitted oldest first, since the rows come newest first. A window that needs more than 20 pages is read again up to its oldest row seen; a replay that needs more than 100 requests stops, reports itself incomplete and continues from the housekeeping retry. Only execType Trade is a fill; Funding is a funding payment on linear (below); AdlTrade, BustTrade, Settle and Delivery are skipped, as on the private stream. The fee asset is feeCurrency, or the spot rule when it is absent (buy pays base, sell pays quote, reversed for a negative maker rate). On the first private connect the replay runs before the start-up sweep's snapshot, from the resumed watermark when there is one.
Linear perpetuals¶
category = "linear", with ws_url on /v5/public/linear (configs/bybit-linear-testnet.toml). Sources: the v5 pages for instruments-info, place/amend/cancel order, open orders, cancel-all, trade history, position info, switch position mode, set DCP, and the private order, execution, position and dcp topics (read 2026-09-26).
- Reference data:
GET /v5/market/instruments-info?category=linear&symbol=S. OnlycontractTypeLinearPerpetualis accepted (LinearFuturesis refused). Tick ispriceFilter.tickSize, lotlotSizeFilter.qtyStep, minimum quantityminOrderQty, minimum notionalminNotionalValue.qtyis in the base coin, so the multiplier is 1. The instrument becomes a perpetual that supports reduce-only, with base =baseCoinand quote =settleCoin(USDT forBTCUSDT, USDC forBTCPERP): accounting settles a linear instrument in its quote, so a configured quote that differs is replaced, with a warning. - Position mode: one-way only. Bybit has no call that reads the mode; it is set per symbol (symbol > coin > default).
GET /v5/position/list?category=linear&symbol=Sanswers whether or not there is a position, with one row (positionIdx0) in one-way mode and one per side (1 and 2) in hedge mode. With keys,load_reference_data()reads it for each symbol and refuses a symbol in hedge mode, or one whose mode it cannot read.fastmm-liveandfastmm-gatewayexit 3 for this (Venue::refused_account_settings()), not 4: a retry does not fix it. A hedge-mode row that appears later, on the position topic or in a reconciliation, is fatal for the venue. - Orders:
order.createadds"positionIdx":0and"reduceOnly":truewhen the engine sets reduce-only. A market order has nomarketUnit("Perps, Futures & Option: always order by qty"). Amend and cancel takecategoryand nothing else. The defaultorders_per_second = 10is the per-UID limit for linear create, amend and cancel. - Private topics:
order,executionandposition(nowallet;position_from_walletis spot only), anddcp.futurewhendead_mans_switch_sis set. DCP arms{"product":"DERIVATIVES","timeWindow":N}, which covers USDT and USDC perpetuals and futures and inverse contracts, with the same semantics and refusal handling as spot. - Fees: a linear
execFeeis in the settlement coin, and a maker rebate is negative. WithfeeCurrencyempty or equal to the quote, the fill's fee asset isQuote; another coin isOther. The spot base/quote rule does not apply. - Reconciliation: after the execution replay (
GET /v5/execution/list?category=linear, which needs no symbol),GET /v5/order/realtime?category=linear&settleCoin=C&limit=50andGET /v5/position/list?category=linear&settleCoin=C&limit=200, paged, for each settle coin of the subscribed instruments, become oneReconcileMsgsequence: Begin,OpenOrder*, onePositionper subscribed instrument, End. Listed by settle coin, Bybit returns only non-zero positions, so an absent symbol is flat. The start-up sweep does the same, so a restart converges to the venue's position. A failed or unparsable position list emits nothing, as for open orders. - Positions between reconciliations: Bybit publishes the position "every time when you create/amend/cancel an order ... regardless if there's any actual change". The connector compares its value with the sum of the fills it forwarded once neither has changed for 1 s, and sends a
PositionUpdateonly when they differ (a liquidation, ADL, another client), as on Binance USDⓈ-M;position_from_stream = falseturns this off. The two topics are not ordered against each other, so forwarding every position would count fills twice. - Cancel-all:
POST /v5/order/cancel-allwith{"category":"linear","symbol":S}per subscribed symbol (linear requires a symbol, base coin or settle coin). - Funding: an execution with
execTypeFunding, on the privateexecutiontopic or fromexecution/list, becomes aFundingevent under itsexecId, with the amount-execFeeinfeeCurrency(the settle coin when empty). The sign is inferred: the transaction log page says itsfundingfield ("positive fee value means receive funding") "is opposite to the execFee from Get Trade History"; the execution pages say nothing about funding. Whether theexecutiontopic pushes Funding rows is not documented (execution.fastdoes not), so the replay's one-minute sweep books what the stream does not. The transaction log (/v5/account/transaction-log,typeSETTLEMENT) carries the same payments with an explicit sign, but only for unified accounts and 50 rows a page. Pages read 2026-09-26. - Not handled: the
tickerstopic (mark price, funding rate) is not subscribed, because no engine message carries it;AdlTradeandBustTraderows are not fills of our orders and reach the engine only through the position correction or the next reconciliation. Leverage and margin mode are not read or set.
OKX v5 USDT-margined swaps and spot¶
kind = "okx": USDT-margined perpetual swaps (instType SWAP, ctType linear, e.g. BTC-USDT-SWAP) in net position mode (configs/okx-demo.toml), and spot pairs (instType SPOT, e.g. BTC-USDT; configs/okx-spot-demo.toml), in one instance or apart. An instrument id ending in -SWAP is a swap, any other a spot pair. Sources: https://www.okx.com/docs-v5/en/ and the changelog https://www.okx.com/docs-v5/log_en/ (entries to 2026-09-15), read 2026-09-26; the regional pages https://my.okx.com/docs-v5/en/ and https://app.okx.com/docs-v5/en/, read 2026-09-30; public data recorded from the demo and production hosts (tests/fixtures/okx/fixtures.meta.json). Spot ran on the demo of a my.okx.com account on 2026-09-30.
Hosts. Keys work only on the domain of the site the account was registered on ("Regional API Domain Requirement"); region sets ws_url and rest_url when the section leaves them empty:
region |
site | REST | WebSocket, production | WebSocket, demo |
|---|---|---|---|---|
global (default) |
www.okx.com | https://openapi.okx.com (www.okx.com answers too) |
wss://ws.okx.com:8443 |
wss://wspap.okx.com:8443 |
eea, my |
my.okx.com | https://eea.okx.com (my.okx.com answers too) |
wss://wseea.okx.com:8443 |
wss://wseeapap.okx.com:8443 |
us, app |
app.okx.com | https://us.okx.com |
wss://wsus.okx.com:8443 |
wss://wsuspap.okx.com:8443 |
Demo trading uses the production REST host with x-simulated-trading: 1. A my.okx.com demo key is refused by www.okx.com and openapi.okx.com (401, 50119 "API key doesn't exist").
| channel | endpoint | purpose |
|---|---|---|
| md | ws_url (/ws/v5/public) |
books (or books50-l2-tbt / books-l2-tbt, which need VIP4 and a login), bbo-tbt to BookTicker, trades |
| private | ws_private_url (default: ws_url host + /ws/v5/private) |
op: login, then orders, positions (updateInterval 0: events only), balance_and_position |
| order | ws_api_url (default: the private URL, a second connection) |
op: login, then order / amend-order / cancel-order |
| rest | rest_url |
the requests below |
| request | use | documented limit |
|---|---|---|
GET /api/v5/public/instruments?instType=SWAP\|SPOT&instId=I |
reference data, instIdCode |
20 / 2 s per IP and instType |
GET /api/v5/public/time |
clock offset (30 s tolerance) | 10 / 2 s per IP |
GET /api/v5/account/config |
position and account mode, at start-up | 5 / 2 s per user |
GET /api/v5/trade/orders-pending (instType SWAP or SPOT, none with both) |
reconciliation, cancel-all | 60 / 2 s per user |
GET /api/v5/account/positions?instType=SWAP (swaps only: it takes no SPOT) |
reconciliation | 10 / 2 s per user |
GET /api/v5/trade/fills, fills-history |
fill replay: 3 days, 3 months | 60 / 2 s, 10 / 2 s per user |
GET /api/v5/account/bills, bills-archive (type=8) |
funding: 7 days, 3 months | 5 / s, 5 / 2 s per user |
POST /api/v5/trade/cancel-batch-orders |
kill switch, order-channel loss (20 a request) | 300 orders / 2 s per user and instrument |
POST /api/v5/trade/cancel-all-after |
dead man's switch | 1 / s per user |
WebSocket order, amend-order, cancel-order (REST trade/order etc. as fallback) |
order entry | 60 / 2 s each, per user and instrument; 1000 new + amend / 2 s per sub-account |
- Signing: REST
OK-ACCESS-SIGN= base64 HMAC-SHA256(secret, ISO timestamp + method + path with query + body), withOK-ACCESS-KEY,OK-ACCESS-TIMESTAMP,OK-ACCESS-PASSPHRASE. WebSocketlogin: the same HMAC over Unix seconds +GET/users/self/verify. Three credentials:api_key,api_secret,api_passphrase.testnet = true(the default) is demo trading: every REST request, public ones included, carriesx-simulated-trading: 1, because demo instruments differ (BTC-USDT-SWAP: tick 0.01 against 0.1,instIdCode2021032601102993 against 10459). A demo host withtestnet = false, or the reverse, is refused. - Units: OKX sizes swaps in contracts of
ctVal×ctMultofctValCcy(0.01 BTC for BTC-USDT-SWAP), and so does FastMM: book, trade, order, fill and position quantities are contracts,contract_multiplier=ctVal×ctMult, so notional is price × contracts × multiplier in USDT and PnL is linear in it. Base =ctValCcy, quote =settleCcy, which accounting settles in. TicktickSz, lotlotSz, minimumminSz, maximummaxLmtSz; no minimum notional. Only linear SWAP is accepted; astateother thanliveorpost_onlydisables the instrument. - Account: with keys,
load_reference_data()readsaccount/configand, when swaps are configured, refusesposModelong_short_modeandacctLv1 (spot mode trades no swaps); a spot-only instance takes every account mode. A key the venue refuses is refused;fastmm-liveandfastmm-gatewayexit 3. Alongorshortposition seen later is fatal for the venue.td_mode(cross by default, or isolated) goes on every order; leverage and margin are not read or set. - Spot: sized in the base coin (
baseCcy), priced inquoteCcy, multiplier 1,minSzthe floor (demo BTC-USDT: tick 0.1, lot 0.00000001, minimum 0.00002,instIdCode3). Orders go withtdModecash(the non-margin mode, which every account mode takes), neverreduceOnly("only applicable to MARGIN orders, and FUTURES/SWAP orders in net mode"), and a market order withtgtCcybase_ccy(a market buy is sized in the quote coin otherwise). The private stream subscribesordersonly (instTypeSPOT, or ANY next to swaps);positionsand funding are swaps'. - Spot holdings: OKX has no position for a spot pair; what the account holds is a balance, of which the strategy's inventory is a part. As on Binance Spot, the engine's position is the strategy's: zero at start, or what the store carries over (
[engine] restore_position, with the execution replay from the store's last fill), moved by the fills. A buy pays its commission in the base coin (fillFeeCcyBTC), which the engine takes off what the fill adds; a sell pays in the quote coin. Reconciliation reports no position for a spot pair, so the engine's stands. The balance is not read into the engine. - Fees:
fillFee/feeis negative when charged. It is the fill times the rate and has more than 8 decimals as often as not (a 0.00002 BTC sell at 84079.9 paid 0.001681598 USDT; the docs' spot example is -0.00000192834 BTC): it is rounded half away from zero to 8 decimals (parse_fee). - Book sync:
bookssends a 400-level snapshot (prevSeqId-1), then updates every 100 ms. An update applies when itsprevSeqIdequals the lastseqId: the documented heartbeat (no change for 60 s, empty sides,seqId=prevSeqId) and the reset after maintenance (seqIdbelowprevSeqId) both do. Anything else emitsResyncingand unsubscribes and subscribes the channel for a new snapshot (OkxSyncTraits,okx_book_sync.hpp). An update can change more levels than the book holds (up to 800 asks on BTC-USDT-SWAP); past 512 a side the parser keeps the levels nearest the touch (level_spill.hpp). The CRC-32checksumof the top 25 levels was deprecated on 2026-06-23 and is 0 on both hosts; the connector keeps each level's text and checks a non-zero checksum (a mismatch resyncs,SyncReason::ChecksumMismatch), and costs nothing when it is 0. - Orders:
instIdCodenames the instrument (the WebSocket ignoresinstIdsince 2026-03-26 fororderand since 2026-04-07 for amend and cancel); REST still takesinstId, and an instrument without aninstIdCodesends its orders over REST.ordTypelimit,post_only,ioc,fokormarket;reduceOnlywhen the engine sets it; noposSide. The requestid(<kind><id>, 15 characters) andclOrdId(FastMM's 14) fit the "up to 32 alphanumerics" rule. A post-only order that would take is accepted and then pushedcanceledwithcancelSource31 (since 2026-08-20): it becomesOrderExpired, as do IOC and FOK remainders (13, 14); other cancels are cancel acks. - Amend:
newSzis the total including the filled quantity, as the engine's replace quantity is.sCode0 means accepted, not applied: theorderspush withamendResultunder thereqId(the engine's new client id) is the ack (kAmendedInPlace, so the fills stay) or the reject. Later events for the originalclOrdIdare translated to the new id. The config uses cancel and new until amend has been seen on the demo. - Private stream:
orderspushes carry the fill (tradeId,fillPx,fillSz,fillFeenegative when charged,fillFeeCcy,execTypeM/T) and the state; the fee is booked with the sign turned. The channel may repeat a message (2025-07-08): the OMS drops a repeatedtradeId, and a repeated cancel or amend result finds nothing pending.positionsis compared with the fills the connector forwarded, as on Bybit and Binance USDⓈ-M, and corrects the engine only when they differ (position_from_stream). - Reconciliation: fills, then
orders-pending(paged byordId) andaccount/positions, as one Begin / OpenOrder* / Position* / End; an instrument absent from the positions is flat. The start-up sweep has an empty watermark. The fill replay asks from one millisecond before its watermark (the newesttsforwarded; whetherbeginis inclusive is not documented), drops rows below it and skips the ids forwarded at it; with no rows it moves to 5 minutes before the query. A window of more than 20 pages is read again up to its oldest row. - Funding:
account/billswithtype=8(subType 173 expense, 174 income) is the source: oneFundingevent per bill on a subscribed instrument,balChg(positive received) inccy,billIdas its id. It runs with every reconciliation's fill replay, once a minute, and a second after abalance_and_positionpush witheventTypefunding_fee, which names no bill. - Dead man's switch:
cancel-all-afterwithtimeOut=dead_mans_switch_s(default 60, [10, 120]), refreshed from the housekeeping timer every window/3; it cancels every pending order of the account, not only FastMM's, spot included ("all trading symbols through order book"; on the demo a resting spot order was cancelled withcancelSource20, "Cancel all after triggered").triggerTimeis in milliseconds.disconnect()sends"0"on its own blocking connection (a request queued on the REST channel would be dropped by its reset). A lapsed countdown tripsDeadMansSwitchLost. - Rate limits: client-side
orders_per_second(default 25) on new orders and amends; a 50011 or 50061 reply pauses sending for 2 s (OKX counts over 2 s windows). - Errors (
okx_error_map.hpp): 50011/50061/60014 rate limit; 50102/50112/60004/60006/50036 clock resync; authentication and account blocks (501xx, 60005/60007/60009/60024, 50007, 50009, 50027, 50029, 51024, 51010, 59113) fatal; 50121 hard stop; 51400/51503/51603/50004 reconcile; 51008 balance, 51121/51020 lot, 51006/51137 price limit, 51016 duplicate id, 51511 post-only amend. - Not handled:
books-rpi,trades-all(one trade a message, on/business;tradesaggregates per taker order and price), SBE market data,expTime, batch operations, the mark price and the funding rate.
Gate USDT perpetual futures (gate_usdt)¶
kind = "gate_usdt" (aliases gate, gate_futures): Gate APIv4 USDT-settled perpetual futures (BTC_USDT, ETH_USDT, ... ; settle = "usd1" or "btc" selects the other settle currencies' endpoints), single position mode (configs/gate-usdt.toml). Sources: the futures WebSocket docs https://www.gate.com/docs/developers/futures/ws/en/ (channels, the WebSocket API under "Futures Account Trade", changelog to 2026-08-25) and the APIv4 REST docs https://www.gate.com/docs/developers/apiv4/en/ (signing, rate limits, labels), read 2026-10-03; public data recorded from production (tests/fixtures/gate/fixtures.meta.json); the private path ran against production with a funded-with-nothing account the same day (login, subscriptions, two orders refused INSUFFICIENT_AVAILABLE, the countdown stopped at shutdown).
| channel | endpoint | purpose |
|---|---|---|
| md | ws_url (wss://fx-ws.gateio.ws/v4/ws/usdt) |
futures.obu (ob.SYM.50 or .400), futures.book_ticker to BookTicker, futures.trades, futures.tickers to PerpState |
| private | the same URL, a second connection | futures.login (WebSocket API), then futures.orders, futures.usertrades, futures.positions, futures.balances, each subscribed with the auth block and the account's user id |
| trade | the same URL, a third connection | futures.login, then futures.order_place / futures.order_cancel / futures.order_amend with req_id = <kind><client id>; REST fallback |
| rest | rest_url (https://fx-api.gateio.ws) |
/api/v4/futures/{settle}/contracts/{name}, order_book?limit=1 (the clock: the host has no time endpoint), accounts, positions?holding=true, orders?status=open, orders (POST / DELETE / PUT), orders?contract= (DELETE, cancel-all), my_trades_timerange, fee, countdown_cancel_all |
- Signing (
gate_auth.hpp): REST headersKEY,Timestamp(Unix seconds, within 60 s of the venue) andSIGN= hex HMAC-SHA512(secret,METHOD\npath\nquery\nhex SHA512(body)\nTimestamp) over the exact bytes sent (net::hmac_sha512_hex,net::sha512_hex). A subscription'sauthblock signschannel=<c>&event=subscribe&time=<t>; the WebSocket API'spayload.signaturesignsapi\n<channel>\n<req_param>\n<timestamp>,req_paramempty for the login. Every connection sendsfutures.pingeveryping_interval_msand upgrades withX-Gate-Size-Decimal: 1, so sizes arrive as strings (contracts may be fractional); the parsers read numbers too. - Units: contracts.
contract_multiplier=quanto_multiplier(base units a contract), tick =order_price_round, lot 1 contract,min_qty=order_size_min,max_qty=order_size_max, no minimum notional; base = the contract's prefix, quote = the settle currency, which accounting settles in. Onlytypedirectis accepted; a contract nottradingorin_delistingis disabled. - Account: with keys,
load_reference_data()readsGET /accounts(theuserid the private channels need, the balances) and refuses an account in dual position mode (fastmm-liveexits 3,refused_account_settings()); adual_long/dual_shortposition seen later is fatal for the venue. Leverage and margin mode are not read or set.fetch_fees = truereadsGET /feeinto the account's rates. - Book sync (
gate_book_sync.hpp):futures.obusends a full snapshot (full: true, depth idu) after subscribing and increments withU/u; an increment applies whenU= the local id + 1, anything else unsubscribes and subscribes the stream again for a new snapshot (rate limited to one per 2 s, again after 10 s without one). The 50-level stream is pushed every 20 ms, the 400-level one every 100 ms (book_level). - Orders:
sizeis signed contracts (a bid positive),pricea string ("0"withtifiocfor a market order),tifgtc/ioc/poc(post-only) /fok,text=t-+ FastMM's 14-character id (Gate requires thet-prefix),reduce_onlywhen the engine sets it. The venue answers an order request twice:{"ack":true}when it took it, then the result ordata.errs{label, message}; the ack from the result (emit_ack_from_response) or from theorderschannel's firstopen, which the connector sends once (the channel repeatsopenafter an amend). Cancel and amend name the order by the venue's id once a reply gave it, else by its text. Amend keeps the order (kAmendedInPlace); a size increase moves it to the back of the queue;supports_replace = falsein the shipped configs. - Private channels:
ordersfinishedwithfinish_ascancelledis a cancel ack withsize - leftfilled;ioc,stp,reduce_only,position_closed,reduce_out,liquidatedandauto_deleveragedareOrderExpired;filledand anopenwith a part filled produce nothing, the fills come onusertrades(idthe execution id,feein the settle currency, positive paid, a rebate negative,rolethe liquidity); the connector completes a fill's cumulative and remaining quantity from its order shadow (the trade does not say).positionsis compared with the fills the connector forwarded, as on Bybit linear and Binance USDⓈ-M, and corrects the engine only when they differ once neither has changed for 1 s (position_from_stream). Abalancesitem asks for the balances again (at most 1/s; the push carries one change, not the account); an item oftypefundwhosetextstarts with a subscribed contract becomes aFundingevent under<time_ms>:<contract>(the shape of that text is not documented: a payment the connector cannot place is not booked, and the venue's account book is not replayed). - Reconciliation: the fill replay (
GET /my_trades_timerange?from=&to=&limit=1000&offset=, one stream for the account, a day a window, 30 days back, rows newest first paged byoffset), thenGET /orders?status=open&contract=C&limit=100&offset=per subscribed contract andGET /positions?holding=true, as one Begin / OpenOrder* / Position* / End; a contract the positions do not list is flat. The start-up sweep has an empty watermark. - Balances: the driver's balance leg reads
GET /accountsafter every snapshot: free =available, locked =order_margin+position_margin, total =total, equity =total+unrealised_pnl, maintenance =maintenance_margin, one row for the settle currency. - Dead man's switch:
POST /countdown_cancel_all {"timeout":N}withdead_mans_switch_s, refreshed at a third of the window (CountdownDriver); a lapse kills the venue (DeadMansSwitchLost);disconnect()sendstimeout0 on a blocking connection. The countdown is per account, every contract. - Rate limits (REST docs "Frequency limit rule"): futures order placement and amend 100 r/s per UID (WebSocket API and REST together), cancels 200 r/s, other private endpoints 200 r/10 s each, public 200 r/10 s per endpoint and IP. Client-side
orders_per_second(default 50) on new orders and amends;X-Gate-RateLimit-Requests-Remain/-Limit(REST, reset in seconds) andx_gate_ratelimit_*(WebSocket API, reset in ms) feed the limiter; a 429 orTOO_MANY_REQUESTScools down until the reset. - Errors (
gate_error_map.hpp), by label:INVALID_KEY/INVALID_SIGNATURE/IP_FORBIDDEN/READ_ONLY/FORBIDDEN/ACCOUNT_LOCKEDfatal;REQUEST_EXPIREDclock resync;TOO_MANY_REQUESTS/TOO_BUSYrate limit;SERVER_ERROR/INTERNALbackoff;ORDER_NOT_FOUND/ORDER_NOT_OWNEDreconcile;INSUFFICIENT_AVAILABLEbalance;ORDER_POC_IMMEDIATEpost-only would cross;SIZE_TOO_SMALL/SIZE_TOO_LARGE/PRICE_TOO_DEVIATED/INVALID_PRECISIONthe lot, size and price reasons;POSITION_DUAL_MODEfatal. - Measured from AWS Tokyo (ap-northeast-1a) on 2026-10-03:
futures.order_placeover the WebSocket API answers in 1.6 ms (p50; the ack in 1.0 ms),order_cancel1.8 ms,futures.login10.5 ms; RESTPOST /orders15 ms including a new TCP + TLS connection a request; the public stream's book was 2 ms behind the venue's clock. - Not handled: the
futures.order_book_updatechannel (the REST-snapshot variant of the book;obureplaces it), batch orders (futures.order_batch_place, 20 a request),futures.order_cancel_ids, the fractional lot step of a contract withenable_decimal(the lot stays 1 contract), the account book as a funding source, USD1- and BTC-settled contracts beyond the endpoint names (settle), SBE market data (/v4/ws/usdt/sbe).
Hyperliquid perpetuals (hyperliquid)¶
kind = "hyperliquid" (alias hl): perpetuals on the main dex (BTC) and on builder-deployed (HIP-3) dexes (xyz:NVDA), one-way positions. api_key is the account's address, api_secret the signing key: an agent wallet the account approved (load_reference_data() checks extraAgents and refuses another key, exit 3) or the account's own; vault_address trades for a vault or sub-account. testnet = false selects mainnet. Sources: the API docs https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api (exchange and info endpoints, WebSocket subscriptions and post requests, asset ids, tick and lot size, rate limits), read 2026-10-12, and hyperliquid-python-sdk 0.24.0 for the signing; public data and the account streams of a public address recorded from production (tests/fixtures/hyperliquid/fixtures.meta.json).
| channel | endpoint | purpose |
|---|---|---|
| md | ws_url (wss://api.hyperliquid.xyz/ws) |
l2Book (20 levels a side, every push a whole book), bbo, trades per coin |
| private | the same URL, a second connection | orderUpdates, userFills, userFundings of the account address (public: no auth) |
| trade | the same URL, a third connection | {"method":"post"} actions: order, cancel, cancelByCloid, batchModify; REST fallback |
| rest | rest_url (https://api.hyperliquid.xyz) |
POST /info (perpDexs, metaAndAssetCtxs, extraAgents, userRateLimit, frontendOpenOrders, clearinghouseState, spotClearinghouseState, userFillsByTime), POST /exchange (the fallback, scheduleCancel, cancel-all) |
- Signing (
hl_crypto.hpp): an action is hashed as msgpack in the SDK's field order, followed by the nonce (8 bytes big endian) and the vault flag; keccak256 of that is theconnectionIdof an EIP-712Agent{source "a" mainnet / "b" testnet, connectionId}under the domainExchange/1/ chain 1337. The signature is secp256k1 ECDSA with RFC 6979 nonces and low s (libsecp256k1, built from the pinned 0.6.0 release: the system OpenSSL 3.0 has neither deterministic nonces nor key recovery). Golden vectors from the SDK pin every byte (hyperliquid_signing_test.cpp). Nonces are the wall clock's milliseconds, strictly increasing per signer. - Instruments: the symbol is the coin with its dex prefix, or without it when
dexnames the dex. Asset id = the index in the main dex's meta, or 100000 + the dex's index inperpDexsx 10000 + the index in its meta. Lot = 10^-szDecimals, min notional 10 USDC, quote USDC; tick = the step at the current mark (5 significant figures, at most 6 - szDecimals decimals, an integer price always allowed). The encoder puts every price on that grid, rounding a bid down and an ask up, so a price that crosses a power of ten stays valid. - Orders: post-only is
Alo, IOC and marketIoc(a market order needs a price), the restGtc; FOK is refused. The cloid is0x666d000000000000followed by the client order id. The orders of one engine drain go out as one action per kind, at most 40 orders each, and the reply answers them by position:resting/filledis the ack (emit_ack_from_response), an error text the reject (hl_error_map.hpp),successa cancel's acceptance.orderUpdatessays the same again (open,canceled,*Rejected,*Canceled,scheduledCancel); the connector forwards whichever comes first, once. A cancel names the order by its oid once a reply or the stream gave it, else by cloid. Withsupports_replacea replace is abatchModify, which the venue carries out as a new order: the new id's ack has nokAmendedInPlaceand the old order'scanceledis not forwarded. - Account streams:
canceledis a cancel ack withorigSz - szfilled; the other*Canceledstatuses andscheduledCancelareOrderExpired;*Rejectedis a reject (badAloPxRejectedpost-only would cross);filledproduces nothing, the fills come onuserFills(exec idtid,crossedthe taker,feein USDC, a rebate negative), completed from the order shadow. The snapshotuserFillsanduserFundingssend on subscribing is skipped: the fill replay books what the stream missed. A funding payment becomes aFundingevent under<time>:<coin>. - Reconciliation: the fill replay (
userFillsByTimefrom the watermark, oldest first, 2000 a reply, 7 days back), thenfrontendOpenOrdersandclearinghouseStateof each dex the instruments use. Balances:spotClearinghouseState, one row per token (free = total - hold, locked = hold). A unified account keeps the collateral of every dex in its spot USDC; the dexes'clearinghouseStatethen showaccountValue0. - Limits: each new order and modify spends one request of the address budget (1 a USDC traded since the address's inception plus 10000; growth-mode volume counts 10 %; cancels have a larger limit), the IP 2000 WebSocket messages a minute and 100 posts in flight. The connector reads
userRateLimitat start and everyrate_limit_refresh_s(the cap includesnRequestsSurplus, the requests the account reserved withreserveRequestWeight), counts what it sends in between, and refuses a new order or modify when no more thanaddress_reserverequests would remain (VenueRateLimit"local rate limit: address budget", counted with the day-long refusals and published as the day window of the order budget).posts_per_minuteandorders_per_secondcap the rest. A rate-limit error cools down 10 s (an address over its budget may send one request every 10 s). - Dead man's switch:
scheduleCancelat now +dead_mans_switch_sover REST, refreshed at a third of the window (CountdownDriver; each refresh is a request of the budget, and the venue allows 10 triggers a day);disconnect()removes it. The venue refuses it until the address has traded enough: the connector logs the reason once and runs the session without it. Cancel-all (cancel_all(), the kill switch) readsfrontendOpenOrdersof each dex and cancels them by oid in signed batches of 40. - Latency: the connector logs every
latency_log_sthe post reply's round trip and, for each order, the block time of itsopenon the stream minus its send time (the venue's clock against the host's), with the address budget. - Not handled: spot, TWAP and trigger orders,
expiresAfter, the priority fee, updating leverage or isolated margin, the user-signed actions (transfers, withdrawals).
Lighter perpetuals (lighter)¶
kind = "lighter" (alias zklighter): Lighter's perpetual markets (crypto and equity perps such as NVDA), one-way cross-margin positions. api_key is the account index (a sub-account's works the same), api_secret the private key of an API key the account registered, api_key_index its slot (4 to 254; 0 to 3 belong to the web and mobile apps). load_reference_data() checks that the venue holds this key's public key in that slot (apikeys) and refuses another key, exit 3. testnet = true selects testnet.zklighter.elliot.ai and chain id 300 (mainnet 304). Sources: the API docs https://apidocs.lighter.xyz (WebSocket reference, API keys, nonces and expiries, rate limits, the volume quota), read 2026-10-13; lighter-go and poseidon_crypto v0.0.15 for the signing; lighter-python 1.1.6 for the REST shapes. Public data, the account trades of a public account and refused transactions were recorded from production (tests/fixtures/lighter/fixtures.meta.json).
| channel | endpoint | purpose |
|---|---|---|
| md | ws_url (wss://mainnet.zklighter.elliot.ai/stream) |
order_book, ticker, trade per market |
| private | the same URL, a second connection | account_all_orders (with the auth token), account_all_trades of the account |
| trade | the same URL, a third connection | jsonapi/sendtx, one frame per transaction, answered under its id; REST fallback |
| rest | rest_url (https://mainnet.zklighter.elliot.ai) |
orderBookDetails, apikeys, nextNonce, account, accountActiveOrders (auth), sendTx |
- Signing (
lighter_crypto.hpp,lighter_tx.hpp): a transaction's fields (chain id, type, nonce, expiry, account, key slot, then the fields of its type) are Goldilocks field elements (p = 2^64 - 2^32 + 1). They are hashed with Poseidon2 (width 12, rate 8) into an element of the quintic extension; attributes such as SkipNonce add a second hash. The API key signs the result with Schnorr over the ECgFp5 curve (k random, e = H(r || m), s = k - e sk; 80 bytes). The transaction goes out as the JSON of the SDK's Go struct, with the signature in base64. This is a native port with no Go runtime in the process; a fixed-base table makes the k G of each signature 80 additions. Golden vectors from the official Go signer pin, byte for byte, the hash, the public key, every transaction's signature and JSON, and the auth token (lighter_signing_test.cpp). A verifier checks signatures made with random nonces. - Auth token:
<deadline>:<account>:<slot>:<signature over its hash>, valid for at most 8 hours. The account channel subscribes with a token and reconnects with a new one at 90 % ofauth_ttl_s. REST calls send it as theAuthorizationheader. - Instruments: the symbol is the market's (
orderBookDetails, perps only). That gives the market index, the tick 10^-price_decimals, the lot 10^-size_decimals, the minimum quantitymin_base_amount, the minimum notionalmin_quote_amountand quote USDC. Prices go out as the integer price x 10^price_decimals (uint32), a bid rounded down and an ask up; sizes are rounded down. - Orders: one CreateOrder per new order. Post-only is TimeInForce 2, and IOC and market orders are TimeInForce 0 with no expiry (a market order needs a price); everything else is good-till-time. Resting orders expire after
order_expiry_s, and FOK is refused. The client order index is the engine's client order id itself (48 bits, the venue's range), so the account should trade through FastMM only. A cancel names the client order index. Withsupports_replacea replace is a ModifyOrder of the same order, applied in place. The new id's ack carrieskAmendedInPlace, and the order's later events, which still name the first index, go out under the new id. The docs do not say what ModifyOrder's base amount means after a partial fill; the connector sends the new total. - Nonces: each transaction takes the slot's next nonce (
nextNonceat start-up and after a reconnect) and, by default, the SkipNonce attribute. With it the venue accepts any larger nonce, so a transaction it refuses does not fail the ones behind it. A refusal that names the nonce makes the connector read it again. Orders, cancels and the scheduled cancel share the trade connection so that their nonces arrive in order. - Acknowledgement: a reply with code 200 means accepted, not executed. The ack is the order's
openonaccount_all_orders, or the reply withemit_ack_from_response. An order the venue ends before it rests (canceled-post-only,canceled-margin-not-allowed, an IOC with nothing to take) is a reject.canceledis the cancel ack, and any othercanceled-*after the order rested isOrderExpired. A refused transaction is a reject with the venue's code and message. - Fills come from
account_all_trades: the account's side of each trade (both legs of a trade with itself), with the fee from the trade's maker or taker rate (millionths of the notional) and the trade id as exec id. Cum and leaves come from the order shadow. A cancel or expiry waits until the fills it counts have arrived, at mostterminal_wait_ms. The history sent on subscribing is skipped. - Reconciliation:
accountActiveOrders(authenticated) and the positions ofaccount. Balances are one USDC row fromaccount(free = available balance, total = collateral, equity = total asset value). There is no execution replay: fills the stream missed come through the position check. - Limits:
tx_per_minuteandorders_per_second, both local. The venue allows 4000 sendTx a minute on a Premium account with no LIT staked, up to 57600 with LIT, and 60 on Standard. On Premium each CreateOrder, ModifyOrder and CancelAll spends one unit of the volume quota (1000 to start, one per 2 USDC traded, one free transaction every 15 s). The replies carry what is left, and the connector refuses new orders and modifies atquota_reserve(VenueRateLimit"local rate limit: volume quota", published as the day window of the order budget). - Dead man's switch: a scheduled CancelAll at now +
dead_mans_switch_s(310 s to 15 days; the venue wants the time at least 5 minutes ahead), refreshed at a third of the window on the trade connection.disconnect()aborts it. Cancel-all (the kill switch) is one CancelAll over REST. - Latency: every
latency_log_sthe log gives the sendtx reply round trip, the open lag of each order (the venue time of itsopenminus its send time) and the volume quota left. - Checked against production: the signing (golden vectors), the market data,
account_all_trades, the REST reads and the shape of a refused transaction. Taken from the docs only: an accepted sendtx reply (read at the top level or underdata),account_all_orders(it needs the account's token), the order statuses, ModifyOrder's base amount and the quota numbers. - Not handled: spot markets, trigger and TWAP orders, grouped orders, sendTxBatch, leverage and margin mode, isolated margin, funding events, transfers and withdrawals.
Gemini perpetuals and spot¶
kind = "gemini": linear perpetuals (product_type swap, e.g. btcgusdperp) and spot (btcusd), one API for both (configs/gemini-sandbox.toml). Sources: https://developer.gemini.com (formerly docs.gemini.com), its OpenAPI spec specs/openapi/rest.yaml and AsyncAPI spec specs/asyncapi/websocket.yaml (0.10.7), read 2026-09-30; public data recorded from production the same day (tests/fixtures/gemini/fixtures.meta.json); order entry run against the sandbox (spot btcusd) the same day. The WebSocket API at wss://ws.gemini.com replaces the archived v2/marketdata and v1/order/events streams.
| channel | endpoint | purpose |
|---|---|---|
| md | ws_url + ?snapshot=-1 |
{sym}@depth@100ms, {sym}@bookTicker to BookTicker, {sym}@trade; time for the clock offset |
| order | ws_api_url (default ws_url) + ?cancelOnDisconnect=true |
authenticated at the upgrade; orders@account, order.place, order.cancel |
| rest | rest_url |
the requests below, all POST except the first |
| request | use |
|---|---|
GET /v1/symbols/details/{symbol} |
reference data |
POST /v1/orders |
key check at start-up, reconciliation |
POST /v1/positions |
reconciliation, with a perpetual subscribed |
POST /v1/mytrades |
trade replay, per symbol |
POST /v1/perpetuals/fundingPayment?since=&to= |
funding |
POST /v1/order/cancel |
a cancel while the order connection is down |
POST /v1/order/cancel/session |
kill switch, order-connection loss |
POST /v1/heartbeat |
with heartbeat |
- Hosts: production
api.gemini.comandws.gemini.com, sandboxapi.sandbox.gemini.comandws.sandbox.gemini.com. The URLs default to the sandbox;testnetmust match the hosts. The sandbox lists more perpetuals (53 against 13 on 2026-09-30) and returns nothing from the publicfundingamount. - Signing: REST requests are
POSTs with an empty body; the JSON payload (request= the path,nonce, parameters) goes base64-encoded inX-GEMINI-PAYLOAD, withX-GEMINI-SIGNATURE= hex HMAC-SHA384 of it andX-GEMINI-APIKEY. The order connection sendsX-GEMINI-NONCE,X-GEMINI-PAYLOAD= base64 of the nonce and its signature on the upgrade request, with a new nonce for every connection. The WebSocket takes only account-scoped keys (account-...) with a time-based nonce; amaster-key is refused at start-up. The documentation gives the time-based nonce in seconds within 30 s of the venue, but the sandbox also refuses one that has not increased ("Nonce ... has not increased since your last call") and takes milliseconds, so the connector sends venue-time milliseconds, each above the last, on REST and the upgrade. One key serves one process: two clients on a key refuse each other's nonces. - Units: a perpetual is 1 base unit per contract, so quantities are base units and
contract_multiplieris 1. Tick isquote_increment, lot istick_size(Gemini's name for the quantity step), minimummin_order_size; basebase_currency, quotequote_currency(GUSD or USDC for perpetuals, the currency PnL is booked in). An inverse perpetual is refused;statusother thanopen,post_onlyorlimit_onlydisables the instrument. - Book sync: with
snapshot=-1the firstdepthUpdateafter subscribing is the full book (U=u), the rest are diffs. A diff applies whenU≤ the lastu<u(consecutive frames overlap atU= previousu), one withu≤ the last is dropped, and one whoseUis past the lastuis a gap:Resyncing, then an unsubscribe and subscribe of the depth stream (GeminiSyncTraits,gemini_book_sync.hpp). A snapshot looks like any diff, so the feed takes the first frame after the connection opens, or after the unsubscribe's reply, as the snapshot. Levels past 512 a side are cut to those nearest the touch (level_spill.hpp); the production spot book lists asks at 1e10 and above, past the 8-decimal fixed point, which are left out. - Orders:
timeInForceGTC,IOC,FOK, orMOCfor post-only;typeLIMITorMARKET. There is no reduce-only (areduce_onlyorder goes out plain) and no amend, sosupports_replaceis off.order.canceltakes the venue's id only, as a string (a number is refused with-1013): a cancel of an order not yet named, by theorder.placeresult or itsNEWevent, waits for it. With the order connection down, a new order is refused (TransportFull) and a cancel goes over REST. - Order events:
orders@accountorderUpdate:NEWis the ack;PARTIALLY_FILLEDandFILLEDcarry the execution (t,L,Zthis execution,zleft, feenonFILLEDonly,mmaker);CANCELEDcarries the cumulativeZand isOrderExpiredfor the venue's own reasons (MakerOrCancelWouldTake,ImmediateOrCancelWouldPost,FillOrKillWouldNotFill,SelfCrossPrevented,ExceedsPriceLimits);REJECTEDnames its reason inr; a refused placement is answered by both a400reply and aREJECTEDevent, and one reject reaches the engine. The stream has no sequence number and the WebSocket sends no heartbeat: the connector sendspingeveryping_interval_msand reconciles after every reconnect. - Accounts: perpetuals trade in a derivatives account, opened after Gemini's Derivatives Knowledge Assessment and funded by a transfer from the exchange account, with a key scoped to it. With a perpetual configured, start-up asks
/v1/positions; an exchange account answersAccountNotOfTypeRequired("Account is not of required type: derivatives") andfastmm-liveexits 3. Orders for a perpetual from an exchange account are refused withInsufficientFunds. - Reconciliation: trades, then
/v1/ordersand/v1/positionsas one Begin / OpenOrder* / Position* / End; a perpetual absent from the positions is flat, and spot has no position rows. The start-up sweep has an empty watermark. A broken trade (break=full) is not booked. - Funding:
perpetuals/fundingPayment(hourly transfers,Debitpaid,Creditreceived, inassetCode), with every reconciliation's replay and once a minute; a transfer has no id, so its symbol and time are its id. - Dead man's switch:
cancelOnDisconnect=true(cancel_on_disconnect, on by default) has the venue cancel the orders placed on the order connection when it closes. A key with "Requires Heartbeat" cancels its orders after 30 s without an authenticated request;heartbeat = truesendsPOST /v1/heartbeatevery 15 s, and a lapse tripsDeadMansSwitchLost. The kill switch and an order-connection loss sendorder/cancel/session, the key's orders on every symbol (Gemini has no per-symbol cancel-all), a 429 waited out (BlockingControl). - Rate limits: REST 600 requests a minute for private endpoints, 5 a second recommended, a burst of 5 queued, then 429; the WebSocket's
conninforeports 3500 orders and 7000 request weight per 10 s. Client-sideorders_per_second(default 20); a 429 or-1003pauses sending for 1 s. - Errors (
gemini_error_map.hpp):RateLimit, 429 and-1003rate limit;InvalidNonceclock resync;InvalidSignature,InvalidApiKey,MissingRole,EndpointMismatch, 401/403 and-1002fatal;RemoteAddressForbiddenhard stop;InsufficientFunds,InvalidPrice/LimitPriceOffTick,InvalidQuantity,DuplicateOrder,MakerOrCancelWouldTakeandExceedsPriceLimitsas rejects. - Not handled: the archived v1/v2 streams, stop orders, the mark price and funding-rate streams (v2 only),
positions@account(documented for prediction markets),balances@account, OAuth, master keys and theiraccountfield.
Coinbase Advanced Trade spot¶
kind = "coinbase_advanced": spot products (BTC-USD, ...) on the Coinbase Advanced Trade API, the API of individual Coinbase accounts, with a CDP API key (configs/coinbase-advanced.toml). Production only: the Advanced Trade sandbox answers with fixed replies and matches nothing, so testnet = true with api.coinbase.com is refused. Sources: the OpenAPI spec https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/advanced-trade-spec.yaml, the AsyncAPI spec https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/advanced-trade-asyncapi.json and the authentication page, read 2026-09-30; public data recorded from production the same day, and the read-only private calls (tests/venues/live_coinbase_test.cpp) run against a live account.
| channel | endpoint | purpose |
|---|---|---|
| md | ws_url (wss://advanced-trade-ws.coinbase.com) |
level2, market_trades, heartbeats; no key |
| user | ws_private_url (default wss://advanced-trade-ws-user.coinbase.com) |
user (a JWT in the subscribe), heartbeats |
| rest | rest_url (https://api.coinbase.com), paths under /api/v3/brokerage |
the requests below; order entry |
| request | use |
|---|---|
GET /market/products/<id>, GET /time |
reference data, clock offset (public) |
POST /orders |
new order |
POST /orders/batch_cancel {"order_ids":[..]} |
cancel, kill switch, user-channel loss |
GET /orders/historical/batch?order_status=OPEN&product_ids= |
reconciliation, kill switch, paged by cursor |
GET /orders/historical/fills?order_ids= |
the executions of an order whose cumulative quantity grew |
GET /orders/historical/fills?product_ids=&start_sequence_timestamp= |
fill replay, paged by cursor |
GET /orders/historical/<order_id> |
the client id of an order a replayed fill names |
- Signing: every private request carries
Authorization: Bearer <JWT>, a new JWT each time: ES256 over the header{"alg":"ES256","kid":<key name>,"nonce":<random hex>,"typ":"JWT"}and the payload{"sub":<key name>,"iss":"cdp","nbf":<now>,"exp":<now + 120 s>,"uri":"<METHOD> api.coinbase.com<path>"}(no query string inuri). The user subscribe carries a JWT withouturi.api_keyis the key name (organizations/<org>/apiKeys/<id>),api_secretthe EC P-256 private key PEM, whose\nescapes are read as newlines; Ed25519 CDP keys, which Advanced Trade does not accept, are refused at start-up. ECDSA and base64url arenet::EcdsaP256Keyandnet::base64url_encode(include/fastmm/net/crypto.hpp). Every request carriesUser-Agent. - Reference data: tick
quote_increment, lotbase_increment, minimum sizebase_min_size, minimum notionalquote_min_size, base and quotebase_currency_id/quote_currency_id; aproduct_typeother thanSPOTis refused, and astatusother thanonline,trading_disabled,is_disabled,view_onlyorcancel_onlydisables the product. - Book sync: a
level2snapshot is the whole book (22677 bids and 18963 offers, 4.6 MB for BTC-USD on 2026-09-30), each side best first; the connector keeps each side's first 1024 levels and refuses a side out of price order (resync). Updates carry each level's new size; a side longer than 1024 keeps the levels nearest the touch (level_spill.hpp). Every message of the connection carriessequence_num, one more than the last: a gap resyncs every book of the connection (SequenceGap) and resubscribeslevel2for each product, at most every 2 s per product; the feed numbers the book messages itself (coinbase_book_sync.hpp).market_tradessideis the maker's, so the aggressor is the other side; its snapshot (the trades before subscribing) is dropped. A market-data connection that goes quiet while a snapshot is still arriving leaves the books waiting for it instead of resubscribing. - Orders: REST only.
client_order_idis FastMM's client order id. Post-only islimit_limit_gtcwithpost_only: true; IOCsor_limit_ioc, FOKlimit_limit_fok, marketmarket_market_ioc, all sized inbase_size. There is no replace;supports_replace = trueis refused. A refusal is"success": falsewithnew_order_failure_reason(or the deprecatederror):INVALID_LIMIT_PRICE_POST_ONLYisPostOnlyWouldCross. A cancel names the venue'sorder_id; one sent before the order's id is known (the reply or the user channel brings it) waits for it. - User channel: every message carries an order's whole state and no execution. The first message of an order is its acknowledgement;
CANCELLEDis a cancel acknowledgement (an IOC's or FOK's isOrderExpired),EXPIREDOrderExpired,FAILEDa reject; the cancel and expiry carrycumulative_quantity. A largercumulative_quantitymakes the connector read the order's executions (fills?order_ids=) and send the new ones, each once bytrade_id, as fills with their price and commission (in the quote currency); fills the endpoint does not list yet are read again every 500 ms, ten times, and then left to the replay. The fill reaches the engine a REST round trip after the user channel reported it. Asequence_numgap on the user channel reconciles. Orders with other client ids are ignored. - Reconciliation: the fills of the subscribed products since the watermark, then the open orders, as one Begin / OpenOrder* / End; spot has no positions. A fill names its order by
order_idonly: the connector maps the ones it has seen and asksGET /orders/historical/<id>for the rest (as Binance does). The start-up sweep has an empty watermark. On user-channel loss the connector lists and cancels the products' open orders over REST (cancel_on_order_channel_loss) and reconciles when the channel is back. - Dead man's switch: none. Nothing in the Advanced Trade API cancels spot orders when a client disconnects (the cancel-on-disconnect Coinbase documents belongs to the international derivatives gateway). Orders a process left resting stay until the next session's start-up sweep cancels them.
- Kill switch: the open orders of the subscribed products, then
batch_cancel(cancel_batch, 50, a request), over its own blocking connection, repeated until the list is empty, at most 3 rounds; a 429 is waited out as for the other connectors. - Rate limits: client-side
orders_per_second(default 8) on new orders; a 429 pauses sending for 1 s. The current documentation gives no REST figures for Advanced Trade; WebSocket connections and unauthenticated messages are limited to 8 a second per IP. - Errors (
advanced_error_map.hpp): the order failure reasons (insufficient funds, post-only, price and size precision, duplicate client id, untradable product, geofencing: fatal), the cancel failure reasons (UNKNOWN_CANCEL_ORDERandORDER_IS_FULLY_FILLED: unknown order), and HTTP 401/403 fatal, 429 rate limit, 5xx back-off. - Not handled: order edits (
/orders/edit), perpetuals (Coinbase moves international derivatives to the Deribit-baseddrb.coinbase.comgateway on 2026-10-01; US futures), stop and bracket orders,quote_size, portfolios other than the key's, thetickerchannels.
Coinbase Exchange spot¶
kind = "coinbase_exchange": spot products (BTC-USD, ...) on the Coinbase Exchange API, the sandbox by default (configs/coinbase-sandbox.toml). The Exchange API is for institutional accounts; its sandbox (an account of its own, with fake funds) matches orders. Sources: https://docs.cdp.coinbase.com/exchange/ (REST reference, WebSocket channels, authentication, rate limits, sandbox) and its changelog (entries to 2026-02-02), read 2026-09-30; public data recorded from production the same day (tests/fixtures/coinbase/fixtures.meta.json).
| channel | endpoint | purpose |
|---|---|---|
| md | ws_url (production wss://ws-feed.exchange.coinbase.com, sandbox wss://ws-feed-public.sandbox.exchange.coinbase.com) |
level2_batch (or level2, signed), matches, heartbeat |
| user | ws_private_url (default: ws_url, a second connection) |
signed subscribe to user and heartbeat |
| rest | rest_url (production https://api.exchange.coinbase.com, sandbox https://api-public.sandbox.exchange.coinbase.com) |
the requests below; order entry |
| request | use | documented limit |
|---|---|---|
GET /products/<id>, GET /time |
reference data, clock offset (30 s tolerance) | 10 / s per IP, bursts of 15 |
POST /orders |
new order | 15 / s per profile, bursts of 30, for every private request |
DELETE /orders/client:<client_oid>?product_id= |
cancel | as above |
DELETE /orders?product_id= |
kill switch, user-channel loss | as above |
GET /orders?status=open&status=pending&status=active&limit=1000 |
reconciliation, paged by CB-AFTER |
as above |
GET /fills?product_id=&start_date= |
fill replay, newest first, paged by CB-AFTER |
10 / s per profile, bursts of 20 |
GET /orders/<order_id> |
the client id of an order a replayed fill names | 15 / s per profile |
- Signing:
CB-ACCESS-SIGN= base64 HMAC-SHA256(base64-decoded secret, timestamp + method + path with query + body), withCB-ACCESS-KEY,CB-ACCESS-TIMESTAMP(seconds, three decimals) andCB-ACCESS-PASSPHRASE. A WebSocket subscribe carriessignature,key,passphraseandtimestamp, signed over timestamp +GET/users/self/verify. Every REST request carriesUser-Agent(the venue answers 400 without one). A secret that is not base64 stops the session at start-up.testnet = truewith a production host, orfalsewith a sandbox host, is refused. - Reference data: tick
quote_increment, lot and minimum sizebase_increment(base_min_sizeis no longer sent), minimum notionalmin_market_funds, base and quotebase_currency/quote_currency. Astatusother thanonline,trading_disabledorcancel_onlydisables the product. - Book sync: both depth channels send the whole book as a snapshot (22662 bids and 18909 asks for BTC-USD, about 1.3 MB), best level first, then updates of a level's new size. The connector keeps each snapshot side's first 1024 levels, refuses a side out of price order (resync), and cuts an update side longer than 1024 levels to the ones nearest the touch (
level_spill.hpp). The level2 channels carry no sequence number: the feed numbers each product's book messages itself (CoinbaseSyncTraits,coinbase_md_feed.hpp) and treats a lost trade on the same connection as a lost update. Amatchwhosetrade_idis not the previous one plus one, or a heartbeat'slast_trade_idstill unseen at the next heartbeat (one a second), emitsResyncing(SequenceGap) and unsubscribes and subscribes the depth channel for a new snapshot, at most every 2 s per product.matchesgives the trades; itssideis the maker's, so the aggressor is the other side. - Orders: REST only; the Exchange's WebSocket feed takes no orders, and the connector does not use FIX.
client_oidmust be a lowercase variant-1 UUIDv4: FastMM's 48-bit client order id is the last 12 hex digits of666d0000-0000-4000-8000-+ id. Post-only ispost_only: truewithtime_in_forceGTC; IOC and FOK aretime_in_force;stpis thestpkey (defaultdc). There is no replace:supports_replace = trueis refused, and the quote manager sends cancel and new. A post-only order that would take comes back 200 withstatus: rejectedandreject_reason(PostOnlyWouldCross). New orders are acknowledged from the reply (emit_ack_from_response) and fromreceived. - User channel:
received(the only message namingclient_oid) acknowledges and starts tracking the order by itsorder_id;matchis a fill for the maker or taker leg the connector tracks, with fee = price × size ×maker_fee_rateortaker_fee_ratein the quote currency andtrade_idas the execution id;donewithreason: canceledis a cancel acknowledgement, orOrderExpiredwhencancel_reasonis 101 (time in force);changesets the order's remaining size. Orders of other software on the same profile are ignored. - Reconciliation: the fills of each subscribed product since the watermark, then the open orders, as one Begin / OpenOrder* / End; spot has no positions. A fill names its order by
order_idonly: the connector maps the ones it has seen and asksGET /orders/<order_id>for the rest (as Binance does, at most 16 a replay). The start-up sweep has an empty watermark. On user-channel loss the connector cancels each product's orders over REST (cancel_on_order_channel_loss) and reconciles when the channel is back. - Dead man's switch: none. The Exchange cancels on disconnect only for orders entered on a FIX session that asked for it at logon (tag 8013
CancelOrdersOnDisconnect); nothing on REST or the WebSocket feed arms one. Orders a process left resting stay until the next session's start-up sweep cancels them. - Kill switch:
DELETE /orders?product_id=per product over its own blocking connection, repeated until a round cancels nothing ("may require you to make the request multiple times"), at mostcancel_all_rounds(3) rounds; a 429 is waited out as for the other connectors. - Rate limits: client-side
orders_per_second(default 8) on new orders; a 429 pauses sending for 1 s. - Errors (
coinbase_error_map.hpp): by HTTP status (401 and 403 fatal, 429 rate limit, 404 unknown order, 5xx back-off), refined by themessagetext (timestamp: clock resync; insufficient funds; price or size precision: disable). - Not handled:
level3andfull,ticker, FIX order entry and market data, stop orders, thebalancechannel, profiles other than the key's.
Deribit (options and futures)¶
Deribit speaks JSON-RPC 2.0 over one WebSocket endpoint (wss://test.deribit.com/ws/api/v2), and the same methods are available over REST (https://test.deribit.com/api/v2/<method>). The sources are the official documentation at https://docs.deribit.com (articles, OpenAPI and AsyncAPI specs) and recorded testnet responses (tests/fixtures/deribit/fixtures.meta.json).
| channel | endpoint | purpose |
|---|---|---|
| md | ws_url, unauthenticated |
public/set_heartbeat, public/subscribe book.NAME.100ms, ticker.NAME.100ms, trades.NAME.100ms |
| private | ws_private_url (default ws_url), a second connection |
public/auth (client_credentials, refresh_token), public/set_heartbeat, private/enable_cancel_on_disconnect, private/subscribe user.orders.KIND.CURRENCY.raw + user.trades.KIND.CURRENCY.raw; order entry private/buy, private/sell, private/edit, private/cancel, private/cancel_by_label; reconciliation private/get_user_trades_by_currency_and_time, then private/get_open_orders_by_currency and private/get_positions |
| rest | rest_url |
public/get_time, public/get_instruments?currency=C&kind=option|future; kill switch private/cancel_all_by_instrument with Authorization: Basic base64(client_id:client_secret) |
Messages¶
Requests are {"jsonrpc":"2.0","id":..,"method":..,"params":{..}}. Order requests use the string id n|c|r + the 14-character client id (the testnet echoes string ids); control requests use small integers. Notifications are {"method":"subscription","params":{"channel","data"}}, and responses carry result or error {code, message, data}. Prices and amounts are JSON numbers, often with exponents (1.0002e6), and are parsed exactly from the raw token.
Units¶
Engine quantities are contracts: contract_multiplier = Deribit contract_size (1 BTC for BTC options, 10 USD for BTC-PERPETUAL). Fills carry fee_currency: a fee in the quote coin is FeeAsset::Quote (BTC options are quoted in BTC, so base == quote), a base-coin fee on a linear instrument is FeeAsset::Base, and the base-coin fee of an inverse future is FeeAsset::Other: it is neither a quote amount nor a number of contracts, so the engine counts it (unconverted_fees) instead of booking it. Orders are sent with contracts, and book, trade and fill amounts are divided by the contract size. The label is the FastMM client id, and post_only is always sent (Deribit defaults it to true). Post-only orders set reject_post_only (config reject_post_only), so a crossing order is rejected (11054) rather than repriced. Prices are rounded passively onto tick_size_steps (BTC options: 0.0001, and 0.0005 from 0.005).
Book sync¶
The first book.NAME.interval notification is a full snapshot. Every later change must have prev_change_id equal to the previous change_id (the ids are not consecutive integers). A mismatch emits ConnectionState{Resyncing} and sends public/unsubscribe then public/subscribe for that book channel, which yields a new snapshot. A missing snapshot (10 s) does the same, at most once per 2 s per instrument. A snapshot or change longer than 1024 levels on a side keeps the 1024 nearest the touch (level_spill.hpp); production BTC-PERPETUAL snapshots have about 1200 bids. Deribit lists levels best first in both (every one of 2114 snapshot sides and 104726 change sides with two or more levels, 108 books recorded on 2026-09-28; the docs do not say), but the parser selects by price and does not rely on it.
Options data¶
Each option ticker yields a BookTicker and an OptionTicker (mark, IVs, greeks, underlying price; see options.md).
Session¶
Both connections enable heartbeats (interval >= 10 s; smaller values are refused with -32602) and answer every test_request with public/test. The access token goes into params.access_token of every private request. It is refreshed with grant_type=refresh_token at 80 % of expires_in, and an order answered with 13009 re-authenticates. After the first private connect (the start-up sweep) and after every reconnect, the connector replays the executions and then reconciles open orders and positions across all configured currencies (one ReconcileMsg sequence: Begin, OpenOrder*, one Position per subscribed instrument, flat when get_positions does not list it, End). Nothing on the private stream reports a position, so a delivery, a liquidation or another client's trade reaches the engine only this way. When the private connection drops it cancels every subscribed instrument over REST (cancel_on_order_channel_loss), in addition to the venue-side cancel-on-disconnect.
Cancel-on-disconnect fires "when the TCP connection is properly terminated, when the connection is closed due to 10 minutes of inactivity, or when a heartbeat detects a disconnection", and not after private/logout. A host that dies without sending a FIN triggers neither of the first two quickly, which is why the connector always enables public/set_heartbeat. Scope is connection, so it cancels only that socket's orders.
Edits¶
private/edit keeps the order id and label, and its contracts is the new total including fills. The engine receives an ack for its new client id. While the edit is in flight, a user.orders "open" update for the old label is not acked; later updates and fills for the old label map to the new id. Fills get cum_qty/leaves_qty from the venue's order shadow, because user.trades has no cumulative quantity.
Rate limits¶
The credit model is from the rate-limits article: order requests use a leaky bucket sized by matching_engine_rate / matching_engine_burst (Tier 4 default 5/s, burst 20); check private/get_account_summary limits for the account's tier. New orders and edits are refused locally when the bucket is empty; cancels are always sent. A 10028 too_many_requests (after which the venue drops the session) drains the bucket.
private/mass_quote has its own buckets, outside this model entirely ("Rate limits described in this article do not apply to Mass Quotes"): limits.matching_engine.maximum_quotes, maximum_mass_quotes and guaranteed_mass_quotes. The connector does not use it (Batch order entry).
Funding¶
Perpetual funding is not booked. Deribit has no per-payment event: funding accrues continuously into the session PnL and is realized at the 08:00 UTC settlement. What reports it is aggregate or per interval: realized_funding on a user.changes position (the session so far), interest_pl on private/get_transaction_log entries of type settlement and on each trade or position change (1 request/s), funding in private/get_settlement_history_by_instrument. Booking it would mean deriving payments from session totals. Pages read 2026-09-26.
Errors¶
deribit_error_map.hpp follows the "Complete RPC Error Codes Reference" table. Among others: 11054 post_only_reject, 10009/10039 insufficient funds, 10004/11044 unknown order (10004 reconciles), 10043/10026 tick, 10005-10007 price bands, 10028 rate limit, 13004/13021 fatal, 13009 re-authenticate, and 10040/10041/10047/11051/13028 back off.
Nasdaq TotalView-ITCH (nasdaq_itch)¶
Market data from TotalView-ITCH 5.0 over MoldUDP64 (lines A and B, multicast or unicast), the initial book from GLIMPSE 5.0, and order entry to fastmm-sim-itch only. No REST, no API keys, no reference data from the venue: tick and lot come from [[instruments]]. Code: include/fastmm/venues/nasdaq/. Setup: Receive a multicast feed.
KernelDatagramSource | XdpDatagramSource rx_backend, lines A and B
─► moldudp::Receiver A/B, reorder, re-requests
─► RecoveryBuffer during a snapshot
─► ItchL2Bridge an L3Book per instrument
─► BookSnapshot / BookDelta / Trade ─► md ring
Startup and recovery¶
connect()joins both lines and buffers every message (up torecovery_buffer_packetsdatagrams) while it logs in to GLIMPSE.- The snapshot's Stock Directory, Trading Action and Add Order messages build the L3 books; End of Snapshot gives the sequence number to continue from.
- The buffered messages from that number on are applied. When the buffer starts after it, the receiver is moved back to it and the missing messages are re-requested; without
rerequestanother snapshot is taken. - Each book emits a
BookSnapshotMsg(topdepthlevels per side) and the venue aConnectionStateMsg{Live}.
The same procedure, after a ConnectionStateMsg{Resyncing} that clears the engine's books, follows:
| Trigger | Why |
|---|---|
a gap the receiver gives up (no rerequest, or max_request_attempts unanswered requests) |
the book misses messages |
any L3 book error (an unknown or duplicate order reference, a book at max_orders) |
later messages cannot repair it |
| the recovery buffer filling during a snapshot | the snapshot is taken again |
The buffer filling during two snapshots in a row stops the feed: the venue's kill switch trips with KillReason::FeedLost, and the feed state is lost. A GLIMPSE connection that fails or closes before End of Snapshot is retried after 1 s. Without glimpse_url the books are complete (empty) before sequence 1 and the receiver starts there; a gap given up stops the feed the same way.
Events¶
Each datagram yields at most one BookDeltaMsg per instrument, with absolute quantities of the top depth levels that changed (0 deletes), and a TradeMsg per printable execution (E, C with Printable Y, P, Q). T0 (t0_cycles) is the rdtscp of the receive batch that carried the datagram, also for messages delivered later from the reorder buffer; recv_ts is the kernel receive time (the NIC time with hw_clock = "phc_synced", the T0 wall clock on af_xdp); venue_seq is the MoldUDP64 sequence number of the last message applied; T1 is taken after decoding and the L3 update.
Orders¶
order_entry |
Orders |
|---|---|
none |
refused at once: OrderRejectMsg with RejectReason::VenueReject, text order_entry = none |
sim_ouch |
OUCH 5.0 over SoupBinTCP to ouch_url: Enter, Replace, Cancel; Accepted, Replaced, Canceled, Executed and Rejected become order events |
With sim_ouch, an Enter or Replace Order triggered by market data carries the sequence token of the first ITCH message of its receive batch as ClOrdID (T + 13 digits); the simulator times it wire to wire. The engine copies only t0_cycles into outbound messages, so the venue maps each batch's t0_cycles to that sequence number (4096 recent batches). The simulator cancels a connection's orders when it closes: a lost OUCH connection is reported as ConnectionStateMsg{Disconnected} on channel 1 and a reconciliation without open orders, and the connection is retried after 1 s. cancel_all() shuts the connection down from the calling thread.
Status¶
The venue fills the feed block of its status entry (Status file): per-line packets, duplicates and A/B skew, gaps, recovered and given-up sequences, snapshots, the reorder high-water mark, requests, malformed datagrams, L3 book errors, the kernel-to-T0 histogram, and on af_xdp the XDP statistics and mode.
Configuration keys¶
Generic, parsed by FastMM for every venue: kind, ws_url, ws_api_url, rest_url, api_key, api_secret, api_passphrase (secrets), testnet, supports_replace, insecure_tls, ca_file, recv_window_ms, fees.
Everything else in a [venues.<name>] section belongs to its connector, which declares, validates and documents it: the tables under Connectors are generated from those declarations, one per kind. A key the connector does not own, or one of the wrong type, stops the session with its line.
Not in the tables:
- Deribit's
api_key/api_secretare the client id and client secret (${FASTMM_DERIBIT_CLIENT_ID}/${FASTMM_DERIBIT_CLIENT_SECRET}).rawbook intervals need an authenticated connection. - OKX needs a third credential,
api_passphrase(${FASTMM_OKX_API_PASSPHRASE}), a generic key likeapi_key. - Coinbase Advanced Trade:
api_keyis the CDP key name andapi_secretits EC private key (${COINBASE_API_KEY_NAME}/${COINBASE_API_PRIVATE_KEY}). Coinbase Exchange: key, secret andapi_passphrase(${FASTMM_COINBASE_API_KEY},${FASTMM_COINBASE_API_SECRET},${FASTMM_COINBASE_API_PASSPHRASE}). - Nasdaq TotalView-ITCH declares that it needs no credentials, so
fastmm-livenever asks forapi_key/api_secret; OUCH order entry logs in withouch_username/ouch_password.
Example configurations: configs/binance-usdm-demo.toml, configs/bybit-linear-testnet.toml, configs/gate-usdt.toml, configs/okx-demo.toml, configs/gemini-sandbox.toml, configs/coinbase-advanced.toml, configs/coinbase-sandbox.toml, configs/deribit-testnet.toml, configs/nasdaq-itch-sim.toml.
stale_ms defaults to 2000 ms for Binance, Bybit, OKX, Gate, Gemini and Coinbase and 10000 ms for Deribit. dead_ms is raised to at least 45000 ms on Binance (the venue pings every 20 s), to 45000 ms for market data and 240000 ms for the other connections on Binance USDⓈ-M (pings every 3 minutes), twice ping_interval_ms plus 5000 ms on Bybit, OKX, Gate and Gemini (45000 ms by default on Bybit and OKX; 35000 ms on Gate; 30000 ms on Gemini) and three heartbeat intervals on Deribit (30000 ms by default). A market-data connection without traffic for stale_ms is reported Stale: the engine pulls the venue's quotes and clears its books, and the connector fetches a new snapshot when data returns. Testnet BTCUSDT is often silent for more than 2 s, so the testnet and Demo configs set stale_ms = 10000 (configs/deribit-testnet.toml: 15000).
Open questions (VERIFY: in the code)¶
- Bybit: whether
uincrements by exactly one per delta (recorded testnet deltas did; the sync only requires increase). - Bybit: whether amend
qtyincludes the filled quantity (the testnet config therefore uses cancel + new,supports_replace = false). - Bybit: whether
walletBalanceincludeslocked; balance-relatedrejectReasonstrings. - Binance USDⓈ-M: the private payloads (
ORDER_TRADE_UPDATE,ACCOUNT_UPDATE, WS API order responses) follow the documentation and the official connector's models but were not recorded, because the Demo account had no futures margin balance. Whether Demo rejects a crossing GTX order with -5022 or accepts and expires it is not confirmed; both are handled.order.modifyis covered by the scripted fake exchange only. - Binance Ed25519 and SBE:
session.logon, unsigned orders and the SBE streams were tested against fastmm-sim-exchange, a scripted fake exchange and frames built fromstream_1_0.xml, not against Binance (no Ed25519 key yet). Unconfirmed: whether Demo Mode accepts Ed25519 keys for the SBE streams, the error Binance returns when an HMAC key callssession.logon(the simulator answers -4056), and whether a combined SBE stream sends one event per binary frame. - Binance USDⓈ-M
countdownCancelAll: the documentation gives no minimum or maximum forcountdownTimeon/fapiand no way to read the armed state back, sodead_mans_switch_msis only clamped to be non-negative. Nor does it say what, if anything, the user stream carries when the countdown fires: futuresORDER_TRADE_UPDATEhas no field for a cancel reason, so a countdown cancel is expected to look like any otherx: "CANCELED". Covered by the scripted fake exchange only. - Binance Spot
order.amend.keepPriority: the documentation does not say what happens whennewQtyis at or below what the order has already filled. The connector never sends it (the quote manager's target is a leaves quantity and the engine's replace quantity is a total abovecum_qty), and the sim exchange refuses it with -1013. Also unstated, though strongly implied by the fields: thatCon aREPLACEDexecutionReportcarries the pre-amend client id, which is what the connector reads. - Bybit linear: implemented from the documentation (2026-09-26) and tested against the scripted fake exchange only. Unconfirmed: the private
order/execution/positionpayloads for linear on testnet, whetherfeeCurrencyis filled for linear executions (the documented example hasUSDT; empty is treated as the settle coin), and whether a hedge-mode symbol with no position returns both rows by symbol as the documentation implies. - Bybit Disconnect-Cancel-All: no test account has it enabled, so the arming request and the
dcp.spot/dcp.futuresubscriptions are covered by the scripted fake exchange only. Bybit documents no way to turn the window off again other than asking an account manager. - OKX: spot ran on the demo of a spot-mode my.okx.com account (2026-09-30): login, the
orderspayloads, order and cancel replies,account/config, fills and fills-history,cancel-batch-ordersandcancel-all-after. Swaps are still implemented from the documentation only: thepositionsandbalance_and_positionpayloads, amend replies, bills, whether a position closed to zero is pushed withpos0; also unconfirmed: whetherfillsbeginis inclusive, whether a WebSocket rate-limit error comes ascodeorsCode, and the fee tier the demo needs for the tbt depth channels. - Gemini: the sandbox's
/v1/mytradesand/v1/orders/historyreturned[]for its account on 2026-09-30, also for trades hours old and for an order placed over REST, while/v1/order/statuswithinclude_tradeslisted them: on the sandbox the replay finds nothing, and a fill the order stream missed is booked only as the cumulative quantity a snapshot reports. Whether production'smytradespages forward fromtimestampas its walk-through says (the list is newest first) is not confirmed. The stream's feenwas 0 on every sandbox fill over the WebSocket API, maker and taker, while a REST IOC was charged 0.033 USD. Also unconfirmed: whetherfundingPaymenttakessince/toin the query withrequest= the path, perpetual order entry (no derivatives account yet), how sooncancelOnDisconnectacts on a connection that dies without a FIN (after a SIGKILL, which closes the socket, the sandbox had cancelled within 2 s), whetherorders@accountcovers orders placed over REST, and whetherbtcusdcperpandbtcgusdperpare one book (their public data was identical on 2026-09-30). - Coinbase Advanced Trade: implemented from the OpenAPI and AsyncAPI specs (2026-09-30); the public feed ran against production, and authentication, the accounts, open-orders and fills queries and the user-channel subscription against a live account (read only). Unconfirmed, with no order placed: the create, cancel and user-channel payloads of real orders, whether the fills endpoint lists newest first, how soon it lists an execution the user channel reported, the
batch_cancelmaximum, the REST rate limits, andcancel_reasonvalues. - Coinbase Exchange: implemented from the documentation (2026-09-30); the public feed ran against production and the sandbox. Unconfirmed, with no sandbox key yet: every private request and the user channel, the
messagetexts of refused requests, the form ofcancel_reason, whether a post-only order that would take comes backrejectedor asdonewith reason 101, and whetherchange.new_sizeis the remaining size. - Binance:
GET /api/v3/timeweight (1 vs 2) and listenKey validity/keepalive figures (the documentation was removed; simulator mode only). - Deribit: the private payloads (
user.orders,user.trades, order and auth responses, open orders) follow the OpenAPI/AsyncAPI schemas but were not recorded, because that needs testnet keys; the live test covers them when the keys are exported. Whetherreject_post_onlyrejections arrive as error 11054 (complete reference) or 11006 (the error page's summary table) is not confirmed; the error map also matches on the message text. The publictradesdirectionis taken as the taker side, which the current documentation does not state. The account's matching-engine tier is not queried.