Overview
The difference between a production-ready trading system and a failing script isn't the strategy logic. It is how you handle the asynchronous chaos of real-time exchange data. REST snapshots racing WebSocket updates, silent socket deaths, and home-grown reconnect loops are where most DIY clients fall apart. Event-driven trading system design patterns keep market data ingestion away from reactive execution. This guide is a practical blueprint for that split in Node.js and TypeScript, using Siebly.io JavaScript SDKs as the networking layer.
Those libraries take care of authentication, request signing, and payload shaping. Several of them also expose an awaitable WebSocket API for order placement. They do not throttle you. Rate limits, buffering, and event-queue backpressure stay in your process. The rest of this article walks from architecture down to real client calls.
Key Takeaways
- Move off high-latency REST polling and use event-driven trading system design patterns. Exchange WebSockets typically land in the 10ms to 100ms range, not sub-millisecond.
- Use the State Ingestion Pattern to keep a local order book. The exact snapshot plus delta workflow is exchange-specific. Binance incremental depth needs a REST snapshot and
lastUpdateIdchecks. Bybit and OKX usually send a snapshot on the stream, then deltas. - Use Siebly SDKs to skip signing boilerplate. Awaitable WebSocket order calls exist on
WebsocketAPIClientwhere the venue supports a WebSocket API. Coinbase and BitMart do not ship that client. Those venues are REST for orders plus WebSocket subscriptions for events. - Run a Reconciliation Loop. Streams can drop events. REST is still the source of truth when sequence IDs jump.
- Heartbeats and automatic reconnect-then-resubscribe are built in. Default reconnect delay is a fixed 500ms (
reconnectTimeout), not exponential backoff. You still own rate-limiting.
The Shift from Polling to Event-Driven Trading Systems
In systematic trading, an event-driven architecture reacts to state changes as they happen. The exchange pushes. You do not ask every few hundred milliseconds. Node.js fits this because the event loop can ingest a high volume of messages without blocking, as long as your handlers stay short. The goal is a local copy of exchange state: books, balances, open orders, positions.
The Latency Cost of REST Polling
REST polling is slow for live books. Each call pays HTTP overhead. Connection reuse via keep-alive helps, but you still wait for a request/response cycle and you are blind between polls. If you poll once a second, you miss everything in that second. WebSockets keep one connection and stream matching-engine updates as they happen. High-volume venues such as Binance typically push those updates in about 10ms to 100ms. That is the real gain: less chatter, and no artificial poll interval.
Decoupling Ingestion from Execution
A common early-stage mistake is one big loop: ingest, compute, send orders, repeat. If the strategy blocks, the socket buffer backs up and you disconnect. Split collectors (they own local state) from order managers (they own execution). Ingestion should keep running even if a cancel takes 200ms.
Siebly SDKs give you that split without writing raw sockets. Packages such as bybit-api and okx-api handle auth, signing, heartbeats, reconnect, and resubscribe. You still stay inside each venue's rate limits. An IP ban is on you, not the client.
Public Bybit streams look like this. The client opens connections when you subscribe, routes spot vs linear vs inverse to the right endpoint, and emits update for every event:
Imported example
import { WebsocketClient } from "bybit-api";
const wsClient = new WebsocketClient();
wsClient.on("update", (data) => {
console.log("market event", data.topic, data.type);
});
wsClient.on("reconnect", ({ wsKey }) => {
console.log("reconnect starting", wsKey);
});
wsClient.on("reconnected", (data) => {
console.log("reconnect done", data?.wsKey);
});
wsClient.subscribeV5(["orderbook.50.BTCUSDT", "kline.5.BTCUSDT"], "linear");
That is the ingestion side. Execution lives in another module that reads the book you maintain, not inside this handler.
The State Ingestion Pattern for Market Data
The State Ingestion Pattern is the core of event-driven trading system design patterns. You keep an in-memory replica of the matching engine without polling REST on every tick. Crypto venues implement "snapshot plus delta" in different ways. Do not copy Binance's procedure onto Bybit or OKX without reading that venue's book docs.
Building a Reliable Local Order Book
You cannot subscribe and treat the first message as truth on every exchange.
Binance incremental depth (btcusdt@depth or btcusdt@depth@100ms) is the classic three-step flow:
- Subscribe to the diff stream and buffer events.
- Fetch a REST snapshot with
MainClient.getOrderBook. - Drop buffered events whose
lastUpdateIdis less than or equal to the snapshot'slastUpdateId. The first event you apply must coversnapshot.lastUpdateId + 1. Then apply later events in order.
Binance also has a different stream, btcusdt@depth20@1000ms. That one is a rolling top-N snapshot, not a delta stream. Do not mix the two.
Imported example
import {
MainClient,
WebsocketClient,
isWsDiffBookDepthEventFormatted,
} from "binance";
const rest = new MainClient();
const wsClient = new WebsocketClient({ beautify: true });
const buffer: Array = [];
let snapshotLastUpdateId: number | null = null;
wsClient.on("formattedMessage", (data) => {
if (!isWsDiffBookDepthEventFormatted(data)) {
return;
}
if (snapshotLastUpdateId === null) {
buffer.push(data);
return;
}
if (data.lastUpdateId <= snapshotLastUpdateId) {
return;
}
applyDelta(data);
});
wsClient.subscribe(["btcusdt@depth@100ms"], "main");
const snapshot = await rest.getOrderBook({ symbol: "BTCUSDT", limit: 1000 });
snapshotLastUpdateId = snapshot.lastUpdateId;
for (const event of buffer) {
if (event.lastUpdateId <= snapshotLastUpdateId) {
continue;
}
applyDelta(event);
}
buffer.length = 0;
applyDelta is your code. The SDK delivers typed events. It does not maintain the book for you.
Bybit is different. orderbook.50.BTCUSDT starts with a snapshot event, then deltas. You still check update IDs. REST RestClientV5.getOrderbook is for startup checks and the reconciliation loop, not always for the first paint.
Imported example
import { RestClientV5, WebsocketClient } from "bybit-api";
const rest = new RestClientV5();
const wsClient = new WebsocketClient();
wsClient.on("update", (data) => {
if (!data.topic?.startsWith("orderbook")) {
return;
}
if (data.type === "snapshot") {
replaceBook(data.data);
return;
}
applyBybitDelta(data.data);
});
wsClient.subscribeV5("orderbook.50.BTCUSDT", "linear");
const snapshot = await rest.getOrderbook({
category: "linear",
symbol: "BTCUSDT",
});
OKX books works the same idea: snapshot on the channel, then updates. Subscribe first, then use REST when you need to prove the book is still aligned.
Imported example
import { WebsocketClient } from "okx-api";
const wsClient = new WebsocketClient();
wsClient.on("update", (data) => {
console.log("okx book", data.arg, data.action);
});
wsClient.subscribe({
channel: "books",
instId: "BTC-USDT",
});
For a longer Bybit walkthrough, see the Bybit JavaScript tutorial. The SDKs handle the socket. You still implement rate limits around the REST snapshot call.
Handling Public Market Data Streams
Tickers, trades, and klines share one Node.js thread. A slow handler on trades delays the book. Keep handlers cheap. Push work to a queue if you must.
Same WebsocketClient pattern, different topic shapes:
Imported example
import { WebsocketClient } from "bitmart-api";
const bitmart = new WebsocketClient();
bitmart.on("update", (data) => console.log("bitmart", data));
bitmart.subscribe("spot/depth20:BTC_USDT", "spot");
bitmart.subscribe("futures/depth20:BTCUSDT", "futures");
Imported example
import { WebsocketClient, WsTopicRequest } from "gateio-api";
const gate = new WebsocketClient();
gate.on("update", (data) => console.log("gate", data));
const tickers: WsTopicRequest = {
topic: "spot.tickers",
payload: ["BTC_USDT", "ETH_USDT"],
};
gate.subscribe(tickers);
Imported example
import { WebsocketClient } from "coinbase-api";
const coinbase = new WebsocketClient();
coinbase.on("update", (data) => console.log("coinbase", data));
coinbase.subscribe(
{
topic: "ticker",
payload: { product_ids: ["BTC-USD", "ETH-USD"] },
},
"advTradeMarketData",
);
Siebly AI patterns have extra workflow notes if you want a starting layout. The SDKs already cover BitMart, Gate, Coinbase, Bitget, KuCoin, Kraken, and HTX with the same event names on most clients: update, response, exception, reconnected.
Reactive Execution: Managing Private Account Events
Once an order is out, track New, Partially Filled, Filled, and Canceled from the private stream. Do not poll order status on the hot path. That is the execution half of event-driven trading system design patterns. Miss a fill and your local balance is fiction. This applies on BitMart, Gate, Bybit, OKX, Binance, and the rest.
Bybit private V5 topics share one private endpoint. Category is ignored for those:
Imported example
import { WebsocketClient } from "bybit-api";
const wsClient = new WebsocketClient({
key: process.env.API_KEY_COM,
secret: process.env.API_SECRET_COM,
});
wsClient.on("update", (data) => {
if (data.topic === "order") {
onOrderReport(data.data);
}
if (data.topic === "wallet") {
onBalance(data.data);
}
});
wsClient.subscribeV5("position", "linear");
wsClient.subscribeV5(["order", "wallet", "greeks"], "linear");
wsClient.subscribeV5("execution", "linear");
OKX needs three credentials, not two. Pass them as accounts:
Imported example
import { WebsocketClient } from "okx-api";
const wsClient = new WebsocketClient({
accounts: [
{
apiKey: process.env.API_KEY_COM || "",
apiSecret: process.env.API_SECRET_COM || "",
apiPass: process.env.API_PASSPHRASE_COM || "",
},
],
});
wsClient.subscribe({ channel: "account" });
wsClient.subscribe({ channel: "orders", instType: "ANY" });
BitMart uses apiMemo instead of a passphrase. Orders stay on REST. The socket only reports what already happened:
Imported example
import { WebsocketClient } from "bitmart-api";
const wsClient = new WebsocketClient({
apiKey: process.env.API_KEY,
apiSecret: process.env.API_SECRET,
apiMemo: process.env.API_MEMO,
});
wsClient.subscribe("spot/user/order:BTC_USDT", "spot");
wsClient.subscribe("spot/user/balance:USDT", "spot");
Binance user data on the WebSocket API (the current path for several product groups) looks like this. resubscribeUserDataStreamAfterReconnect defaults to true:
Imported example
import {
WebsocketAPIClient,
WS_KEY_MAP,
isWsFormattedSpotUserDataEvent,
} from "binance";
const wsApi = new WebsocketAPIClient({
api_key: process.env.API_KEY_COM,
api_secret: process.env.API_SECRET_COM,
beautify: true,
});
wsApi.getWSClient().on("formattedMessage", (data) => {
if (isWsFormattedSpotUserDataEvent(data)) {
onBinanceUserEvent(data);
}
});
await wsApi.subscribeUserDataStream(WS_KEY_MAP.mainWSAPI);
The Order Intent Chaser is the safety net around those streams. You send a cancel. No report arrives. The chaser hits REST and asks what actually exists. That is operational risk control, not optional polish.
The Awaitable WebSocket Pattern
REST order placement repeats TLS and HTTP headers. Where a venue offers a WebSocket API, you send a request with a correlation id and await the matching response. Siebly wraps that as WebsocketAPIClient. It feels like a REST client. Traffic stays on the persisted socket.
This is not on every SDK.
Has WebsocketAPIClient for trading: binance, bybit-api, okx-api, gateio-api, bitget-api (V3/UTA), kucoin-api, @siebly/kraken-api (spot only), @siebly/htx-api.
Does not: coinbase-api and bitmart-api. Those keep orders on REST. Their WebsocketClient is for market and account streams.
Bybit:
Imported example
import { WebsocketAPIClient } from "bybit-api";
const wsClient = new WebsocketAPIClient({
key: process.env.API_KEY_COM,
secret: process.env.API_SECRET_COM,
});
const response = await wsClient.submitNewOrder({
category: "linear",
symbol: "BTCUSDT",
orderType: "Limit",
qty: "0.001",
side: "Buy",
price: "50000",
orderLinkId: "bot-2026-09-23-001",
});
Bybit demoTrading can consume events. It does not support the WebSocket API. Use testnet: true if you want WS API against testnet.
OKX:
Imported example
import { WebsocketAPIClient } from "okx-api";
const wsClient = new WebsocketAPIClient({
accounts: [
{
apiKey: process.env.API_KEY_COM || "",
apiSecret: process.env.API_SECRET_COM || "",
apiPass: process.env.API_PASSPHRASE_COM || "",
},
],
});
const res = await wsClient.submitNewOrder({
instId: "BTC-USDT",
tdMode: "cash",
side: "buy",
ordType: "limit",
sz: "0.001",
px: "50000",
clOrdId: "bot20260923001",
});
Gate (gateio-api on npm, docs under Gate JavaScript):
Imported example
import { WebsocketAPIClient } from "gateio-api";
const client = new WebsocketAPIClient({
apiKey: process.env.API_KEY,
apiSecret: process.env.API_SECRET,
reauthWSAPIOnReconnect: true,
});
const newOrder = await client.submitNewSpotOrder({
text: "t-bot-20260923-001",
currency_pair: "BTC_USDT",
type: "limit",
account: "spot",
side: "buy",
amount: "0.001",
price: "50000",
});
Kraken spot only:
Imported example
import { WebsocketAPIClient } from "@siebly/kraken-api";
const wsApiClient = new WebsocketAPIClient({
apiKey: process.env.API_KEY,
apiSecret: process.env.API_SECRET,
});
const orderResponse = await wsApiClient.submitSpotOrder({
order_type: "limit",
side: "buy",
limit_price: 26500.4,
order_qty: 0.001,
symbol: "BTC/USD",
});
Lower-level path on the same sockets: sendWSAPIRequest(...) returns a promise too. Use WebsocketAPIClient unless you need raw routing.
Securing Private Account Streams
Least privilege: trading keys, withdrawals off. OKX, Bitget, and KuCoin also need a passphrase. BitMart needs apiMemo. Coinbase Advanced Trade uses CDP keys (ECDSA or Ed25519), not HMAC key plus secret.
OKX JavaScript signs requests and keeps timestamps in range. It does not rate-limit you. Put a token bucket in front of both REST and WS API calls.
Resilience Patterns: State Reconciliation and Error Handling
Streams lose messages. Sequence holes happen. The Reconciliation Loop is a slow REST check against the local book and the open-order set. Startup snapshot is one thing. This loop runs later, at a low frequency, so you do not blow the rate limit.
Idempotent orders matter when the socket dies after you sent a command. Retry without a client order id and you can double up. Field names are not standard:
- Binance:
newClientOrderId - Bybit:
orderLinkId - OKX:
clOrdId - Gate:
text(spot ids use at-prefix)
The SDK signs the request. It does not invent that id for you.
WebSocket Reconnection and Stability
DIY reconnect often misses silent deaths: the socket looks open, data stops. The clients watch heartbeats (pingInterval default 10000ms, pongTimeout default 5000ms). No pong, they tear down, wait reconnectTimeout (default 500ms), connect again, and resubscribe. That delay is fixed, not exponential backoff. Raise reconnectTimeout if a venue hates fast reconnects.
coinbase-api and @siebly/kraken-api use the same heartbeat plus reconnect-then-resubscribe path as the others.
Event name watch-out: most SDKs emit reconnect then reconnected. Binance emits reconnecting then reconnected.
Imported example
wsClient.on("reconnect", ({ wsKey }) => {
console.log("ws automatically reconnecting", wsKey);
});
wsClient.on("reconnected", (data) => {
console.log("ws has reconnected", data?.wsKey);
scheduleRestReconcile();
});
On Binance, listen for reconnecting instead of reconnect. After reconnected, refetch the book and open orders. Resubscribe of topics is automatic. Your in-memory book is not.
More architecture notes: algorithmic trading system architecture in Node.js.
Idempotent Order Management
After a partition, do not resume blind. List open orders on REST, compare to local state, then start sending again. Cap position size in your process. Validate on Binance testnet (testnet: true on binance clients) or the venue's demo/paper environment before live keys.
Bybit: testnet: true for testnet. demoTrading: true for demo. Demo does not do WS API orders.
Bitget V3: demoTrading: true on WebsocketAPIClient.
BitMart: demoTrading: true is V2 futures WebSocket only.
Implementing Event-Driven Patterns with Siebly SDKs
Event-driven trading system design patterns get easier when you stop maintaining signature code. Siebly SDKs are TypeScript-first, one npm package per venue. They share a shape (WebsocketClient, events, optional WebsocketAPIClient). They are not one unified API. Credential field names and method names differ on purpose because the exchanges differ.
What each package actually does
- binance:
MainClient,USDMClient,CoinMClient. HMAC, RSA, or Ed25519. Ed25519 is the fast path for WS API login. Awaitable WS orders: yes. - bybit-api:
RestClientV5. Credentials arekey/secret. OptionalparseAPIRateLimitsreads headers. It does not throttle. Awaitable WS orders: yes. - okx-api:
RestClient.accounts[]withapiKey,apiSecret,apiPass.marketcan be GLOBAL, EEA, or US. Awaitable WS orders: yes. - gateio-api:
RestClient.apiKey/apiSecret.reauthWSAPIOnReconnectexists. Awaitable WS orders: yes, spot and futures. - bitget-api: V2 and V3 clients.
apiKey,apiSecret,apiPass. WS API is V3/UTA. Awaitable WS orders: yes on V3/UTA. - kucoin-api: spot and futures clients.
apiKey,apiSecret,apiPassphrase. Awaitable WS orders: yes. - @siebly/kraken-api:
SpotClient.apiKey/apiSecret. Awaitable WS orders: yes, spot only. - @siebly/htx-api:
SpotClient,FuturesClient.apiKey/apiSecret. Awaitable WS orders: yes. - coinbase-api: Advanced Trade, Exchange, Prime, International. CDP
apiKey/apiSecretorcdpApiKeyJSON. No order WS API client. bitmart-api:RestClient.apiKey,apiSecret,apiMemo. No WS order API. REST orders, WS fills.
Install the one you need:
Imported example
npm install binance
npm install bybit-api
npm install okx-api
npm install gateio-api
npm install bitget-api
npm install kucoin-api
npm install @siebly/kraken-api
npm install @siebly/htx-api
npm install coinbase-api
npm install bitmart-api
Simplified Exchange Integration
Public ingest and private account flows follow the same subscribe plus event pattern. Request shapes stay typed per venue. AI-oriented notes for Bybit and Binance sit next to the Siebly AI prompt framework. You still do not get a single client.placeOrder() that hides every exchange.
Next Steps for Systematic Engineers
Drop raw ws plus hand-rolled HMAC. Keep signature and reconnect maintenance in the SDK. Read the Siebly glossary if the terms here are new. For microstructure, see crypto order flow.
Start with public data. Then private streams on testnet or paper. Then one live market with withdrawal-disabled keys. Add your own limiter. The SDK will happily send the 201st request in a window if you ask it to.
Scaling Systematic Infrastructure with Node.js
Reactive architecture is how you keep latency down without stalling ingest. Event-driven trading system design patterns keep the book updater off the order sender. Snapshot plus delta (or snapshot-on-stream plus delta) beats polling for a live book. REST stays for truth checks.
Siebly SDKs cover auth and sockets on Binance, Bybit, OKX, Gate, Bitget, KuCoin, Kraken, HTX, Coinbase, and BitMart. They do not cover throttling. TypeScript types and awaitable WS APIs (where the venue has one) keep the execution path readable. Explore the JavaScript SDKs. Prove the design on testnet before production keys.
Frequently Asked Questions
What is the primary advantage of event-driven trading over REST polling?
You drop the poll interval and most of the HTTP overhead. REST can easily sit in the hundreds of milliseconds per round trip. Venue WebSockets usually land in the 10ms to 100ms range after the match. You get the event when it happens, and you stop burning the request bucket on "any change yet?" calls.
Do Siebly SDKs handle rate-limiting and throttling automatically?
No. They sign, connect, heartbeat, reconnect, and resubscribe. They do not pace outbound calls. You add a token bucket or a queue. bybit-api can set parseAPIRateLimits: true to attach header-derived limits to REST responses. That is visibility, not a brake. Ignore limits and you still get HTTP 429 or an IP ban.
How do I handle WebSocket reconnections without losing market data deltas?
On reconnected (and after reconnect / reconnecting), treat the local book as dirty. For Binance incremental depth: buffer the new diff stream, fetch getOrderBook, drop events with lastUpdateId at or below the snapshot, then apply. For Bybit and OKX, wait for the stream snapshot or refetch REST and apply later deltas. The SDK resubscribes topics. It does not rebuild your maps.
Can I place orders over WebSockets using JavaScript SDKs?
Yes, on venues that expose a WebSocket API and an SDK WebsocketAPIClient: Binance, Bybit, OKX, Gate, Bitget V3, KuCoin, Kraken spot, HTX. You await submitNewOrder (or the venue's name for that method). Coinbase and BitMart: REST for the order, WebSocket for the fill. Bybit demo trading does not accept WS API orders.
Is it safer to use testnets for event-driven system development?
Yes. Testnet, demo, and paper environments are where you break reconnect and reconcile logic. Binance and Bybit take testnet: true. Several venues also have demoTrading. Check that flag per product. Demo is not always a full copy of production WS API behavior.
What are the best practices for handling private account streams in Node.js?
Withdrawal-disabled keys. Env-based secrets. Short handlers. Process execution reports in order. OKX, Bitget, and KuCoin need a passphrase. BitMart needs apiMemo. Coinbase needs a CDP key. Do not do strategy math inside the update callback.
How does the Order Intent Chaser pattern improve execution reliability?
It is a timeout plus REST check. You sent an order or a cancel. The private stream is quiet. The chaser asks REST what exists and converges local state. One lost WS frame should not leave you unsure whether you are in the market.
Why should I use specialized SDKs instead of raw exchange API wrappers?
bybit-api, okx-api, and the rest hide venue-specific signing, listen-key refresh (Binance), 24h disconnect handling, and heartbeat timing. You get typed request objects and a reconnect path that resubscribes. You do not get a shared order schema or automatic throttling. That is still your system.
Related articles
Continue from here