exchange-apiws
Async Rust client for exchange REST APIs and WebSocket feeds.
Six exchanges supported, three signing schemes, four envelope variants. The crate is architected to be exchange-agnostic: adding a new exchange means implementing one trait and the shared runner handles connection lifecycle, reconnect, rate limiting, heartbeats, and supervised token refresh.
| Exchange | Public REST | Private REST | Public WS | Private WS | Notes |
|---|---|---|---|---|---|
| KuCoin (Futures + Spot + UTA) | ✓ | ✓ | ✓ | ✓ | + WsOrderClient for low-latency order placement |
| Binance | ✓ | ✓ | ✓ | ✓ | spot + USDT-M futures; executionReport user-data stream |
| Bybit | ✓ | ✓ | ✓ | ✓ | v5 unified API (spot / linear / inverse); fills on a separate execution topic |
| Kraken | ✓ | ✓ | ✓ | ✓ | HMAC-SHA512 signing; executions / balances v2 channels |
| Crypto.com | ✓ | ✓ | ✓ | ✓ | HMAC-SHA256 body-sig signing; fills on a separate user.trade channel |
The disconnection-hardening track lands run_feed_supervised (token-refresh
on cascade), connect / idle timeouts, cascade-start WARN logging, and a
RunnerEvent observability hook for metrics. See the supervised-feed
example below.
Table of Contents
- Status
- Installation
- Quick Start
- Placing Orders
- Rate Limits
- Error Handling
- Authentication
- KuCoin-Specific Notes
- Adding a new exchange
- License
Status
Everything in the original roadmap is implemented. The offline test suite covers signing, envelope unwrapping, WS connector parsers, and the runner lifecycle (reconnect, supervised token refresh, idle timeout, event observability).
Exchange-by-exchange surface
KuCoin
| Layer | What's covered |
|---|---|
| REST (Futures) | balance, positions, orders, stop orders, fills, klines, ticker, mark price, funding rate/history, contracts, transfers |
| REST (UTA) | account summary, cross/isolated margin accounts |
| REST (Spot margin) | place / get / cancel margin order, fills, account |
| WS (Public) | trade, ticker, orderbook (depth + L2 delta) |
| WS (Private) | order fills, position changes, balance updates, advanced (stop) orders |
| WS (Order placement) | WsOrderClient — low-latency place/cancel with clientOid routing |
Binance
| Layer | What's covered |
|---|---|
| REST (Spot) | klines, depth, recent trades, book ticker, 24h ticker, exchange info |
| REST (Futures USDT-M) | klines, funding rate history, premium index / mark price, open interest |
| REST (Private) | user-data listen-key lifecycle (create / keep-alive / close) |
| WS (Public) | <sym>@aggTrade, @bookTicker, @kline_<i>, @depth@100ms, @depth{5|10|20}@100ms, @markPrice@1s |
| WS (Private) | executionReport user-data stream → OrderUpdate (fill price/size on is-trade events), outboundAccountPosition → balances |
Bybit (v5 API)
| Layer | What's covered |
|---|---|
| REST (Public) | kline, orderbook, tickers, recent trades, instruments info, funding history, open interest, long/short ratio |
| REST (Private, HMAC-SHA256) | place / cancel order, open orders, positions, wallet balance |
| WS (Public) | publicTrade.<sym>, tickers.<sym>, kline.<i>.<sym>, orderbook.<d>.<sym> |
| WS (Private) | order (state) + execution (fills → match_price/size/trade_id) + position + wallet |
Kraken (signed)
| Layer | What's covered |
|---|---|
| REST (Public) | system status, assets, asset pairs, ticker, depth, OHLC, recent trades, spread |
| REST (Private, HMAC-SHA512) | balance, open/closed orders, place/cancel/cancel-all order, trades history, ledger, withdraw, withdrawal status, WS token |
| WS (Public v2) | trade, ticker, ohlc, book |
| WS (Private v2) | executions → OrderUpdate (fill price/size on trade executions), balances → balances |
Crypto.com (signed)
| Layer | What's covered |
|---|---|
| REST (Public) | instruments, book, candlestick, ticker, trades, valuations (mark/funding/index) |
REST (Private, HMAC-SHA256 body-sig) |
account summary, create/cancel order, cancel-all, open orders, order detail, trades, deposit address, create/list withdrawal |
| WS (Public) | trade.<inst>, ticker.<inst>, candlestick.<tf>.<inst>, book.<inst>.<d> |
| WS (Private) | user.order (state) + user.trade (fills → match_price/size/trade_id) + user.balance |
Disconnection hardening
The shared run_feed runner ships with:
run_feed_supervised— wrapsrun_feedin a token-refresh loop; on cascade exhaustion it calls a caller-supplied closure for a fresh endpoint instead of returningWsDisconnected. Drops typical stale-token blackout from ~9 min to ~10 s.connect_timeout_secs+idle_timeout_secs— bound stalled handshakes and half-closed TCP.- Cascade-start WARN — first session-end of a new reconnect chain (attempt 0, sub-5 s uptime) logs at WARN with the close-frame reason, so production logs filtered at WARN show the root cause.
RunnerEventobservability hook —SessionEnded,ReconnectsExhausted,TokenRefresh,RefreshExhaustedcallbacks for metrics without log scraping.
Default WsRunnerConfig is tuned for the futures-bot use case: 5
attempts × 30 s ceiling ≈ 95 s worst-case before the supervisor steps
in.
Installation
[]
= "0.10"
= { = "1", = ["rt-multi-thread", "macros"] }
Per-exchange Cargo features
The non-KuCoin exchanges are opt-out via Cargo features
(binance, bybit, kraken, cryptocom, coinbase, okx — all in
default). Trim the dependency footprint by disabling unused exchanges:
# KuCoin-only
= { = "0.10", = false }
# KuCoin + Binance
= { = "0.10", = false, = ["binance"] }
KuCoin and the shared runtime (actors, client, auth, http,
rest, ws) stay always-on — they're the runtime infrastructure
the other exchanges build on.
Set credentials per the exchange you're using:
# KuCoin
KC_KEY=...
KC_SECRET=...
KC_PASSPHRASE=...
# Kraken (private REST)
KRAKEN_API_KEY=...
KRAKEN_API_SECRET=...
# Crypto.com (private REST)
CRYPTOCOM_API_KEY=...
CRYPTOCOM_API_SECRET=...
Public-only exchanges (Binance, Bybit) need no credentials.
Runnable examples
The examples/ directory has a runnable binary per exchange plus
two cross-cutting demos. Run any one with cargo run --example <name>:
| Example | What it does |
|---|---|
binance_public_market |
Spot klines + 24h ticker + futures mark price for BTCUSDT |
bybit_public_market |
Linear-perp ticker + 5-level orderbook + recent funding history |
kraken_public_market |
System status + XBT/USD ticker + recent 1m OHLC bars |
cryptocom_public_market |
BTC_USDT ticker + 10-level orderbook + BTCUSD-PERP mark price |
multi_exchange_aggregator |
Drives Binance + Bybit BTCUSDT trade feeds into one channel; demonstrates the unified DataMessage cross-exchange pattern |
kucoin_supervised_feed |
Recommended production pattern — run_feed_supervised for token re-negotiation on cascade, with a RunnerEvent listener wired to a metrics counter and Ctrl-C shutdown |
Quick Start
Tip:
use exchange_apiws::prelude::*;brings the error types, the unifiedDataMessagemodel +ExchangeConnectortrait, the WS runner entry points (run_feed,run_feed_supervised), and every enabled exchange's client + connector into scope in one line. The examples below import explicit paths for clarity.
Public market data — Binance
use BinanceRestClient;
# async
Public market data — Bybit
use ;
# async
KuCoin futures REST
use ;
async
Public WebSocket feed
KuCoin Futures public feed:
use Arc;
use ;
use ;
use ;
async
Private WebSocket feed (order fills + positions)
// Use get_ws_token_private() and add private subscriptions:
let kucoin = futures;
let client = kucoin.rest_client?;
let token = client.get_ws_token_private.await?;
let conn = new;
let subs = vec!;
// ... same run_feed setup as above
⚠️ Order fills arrive differently per venue
The per-execution fill fields on OrderUpdate — match_price, match_size,
trade_id — are not populated the same way on every exchange, so anything
computing realized PnL or an average fill price must account for it:
- KuCoin / Binance / Kraken deliver fills on a single order/execution
event — the match fields are
Someon the fill (KuCointype:"match", Binance / Kraken is-trade events) andNoneon pure state changes. - Bybit splits the stream: the
ordertopic carries order state (match_price: None) and a separateexecutiontopic carries the fills (withmatch_price/size/trade_id, and a synthesizedstatus: "partialFilled"). - Crypto.com splits the same way:
user.order(state) vsuser.trade(fills).
The per-exchange private connectors already subscribe to both streams, so
all you do as a consumer is: accumulate from events where match_price is
Some, de-duplicate on trade_id, and take order lifecycle (status,
filled_size) from the events where it's None. See the
OrderUpdate rustdoc
for the full per-venue table and a venue-agnostic accumulator example.
Supervised WebSocket feed (token re-negotiation on cascade)
run_feed retries inside one token. If the disconnect cause is a stale or
invalidated token (KuCoin's gateway closing freshly subscribed sessions, for
example), retrying the dead endpoint can burn the full reconnect budget —
up to ~9 minutes of blackout with default settings. run_feed_supervised
wraps run_feed in an outer loop that calls a caller-supplied closure to
re-negotiate a fresh token whenever a cycle exhausts, typically restoring
the feed in seconds.
use Arc;
use ;
use ;
use ;
async
SupervisedConfig::default() sets runner.max_reconnect_attempts = 3 so
cascades are detected in ~35 s rather than ~9 min, and
max_refresh_cycles = u32::MAX so the supervisor keeps refreshing until you
trigger shutdown_tx.send(true). For a bounded version that surfaces
WsDisconnected after N refresh cycles, override max_refresh_cycles.
Contract sizing
calc_contracts is an async method on KuCoinClient — it calls GET /api/v1/contracts/{symbol} to retrieve the contract multiplier at runtime, so it returns a Result.
let client = futures.rest_client?;
let contracts = client.calc_contracts.await?;
println!;
Multi-exchange WS via the unified DataMessage types
Every connector implements ExchangeConnector and emits the same
DataMessage enum (Trade, Ticker, Candle, OrderBook,
FundingRate, …). The same downstream handler works for KuCoin,
Binance, Bybit, Kraken, and Crypto.com feeds.
use Arc;
use ;
use ;
use BinanceConnector;
use ;
use ;
# async
Placing Orders
use ;
// Market order
client.place_order.await?;
// Limit order with IOC + STP
client.place_order.await?;
// Stop-market order (close on breach)
client.place_stop_order.await?;
Rate Limits
REST
KuCoin enforces per-UID rate limits per resource pool. VIP0 Futures quota is 2,000 requests / 30 seconds. The client automatically:
- Retries transient failures with exponential backoff (3 attempts, 1.5× factor)
- Reads the
gw-ratelimit-resetheader on HTTP 429 and sleeps for the exact reset window - Returns
ExchangeError::Apiwith the KuCoin error code on non-200000 responses
The signed private REST clients for the other venues (BybitPrivateClient, KrakenPrivateClient, CryptocomPrivateClient) and PublicRestClient share the same hardening via http::send_with_retry: bounded retries on transient network errors plus HTTP 429 handling that honours the standard Retry-After header (falling back to jittered exponential backoff when it's absent). The 429 sleeps are capped separately so a persistent rate limit still surfaces an explicit error rather than looping. Each retry is re-signed, so venues with replay-protected nonces (Kraken, Crypto.com) never resend a stale signature.
WebSocket
KuCoin allows 100 client→server messages per 10 seconds per connection (subscribe, unsubscribe, ping). The runner enforces this with a sliding-window guard before every outbound send — subscriptions sent at startup are rate-limited too, so large subscription batches at connect time will be transparently throttled.
Error Handling
All fallible functions return Result<T> where the error type is ExchangeError:
use ExchangeError;
match client.get_position.await
Authentication
KuCoin API v2 HMAC-SHA256 signing is implemented in auth::build_headers. The prehash string is {timestamp}{METHOD}{endpoint}{body}. The passphrase is itself HMAC-signed (not sent raw), which is the v2 requirement.
Credentials are loaded from environment variables with Credentials::from_env():
| Variable | Description |
|---|---|
KC_KEY |
API key |
KC_SECRET |
API secret |
KC_PASSPHRASE |
API passphrase |
KuCoin-Specific Notes
Leverage is a per-order field in KuCoin Futures, not an account setting. Pass leverage in place_order and close_position. Use set_risk_limit_level to change the max position size tier.
Inverse vs. linear contracts — calc_contracts fetches the contract multiplier live via get_contract. Inverse (USD-margined) contracts like XBTUSDM have a multiplier of 1 USD. Linear (USDT-margined) contracts like XBTUSDTM express a base-coin multiplier (0.001 BTC per contract).
Private WS token expiry — WS tokens are valid for the lifetime of the connection. The runner reconnects automatically; call get_ws_token_private() again inside the reconnect flow if you need long-lived private feeds.
Adding a new exchange
The plumbing is reusable. A new exchange typically needs three pieces:
- REST client wrapping
PublicRestClient(and adding a signing layer for authenticated calls). Seesrc/binance/rest.rsfor an unauthenticated example orsrc/kraken/private.rsfor a signed one. - Envelope unwrap — a free function per exchange that strips the
{"code":N,"result":...}(or equivalent) wrapper. Pattern isunwrap_<exchange>_envelope<T>(raw: Value) -> Result<T>; surface non-zero codes asExchangeError::Api. ExchangeConnectorimplementation for WebSocket. The trait provides three optional hooks with default impls so most connectors only override what they need:subscription_message(symbol)— for one-frame-per-subscribe protocolsping_message()— application-level ping format (Noneif the exchange uses protocol-level Ping or server-initiated heartbeats)response_for(raw)— inbound-driven outbound (Crypto.com's heartbeat-echo pattern is the canonical user)
Once the connector is implemented, the shared runner (run_feed,
run_feed_supervised) handles reconnect, idle timeout, rate
limiting, observability events, and supervised token refresh.
KrakenConnector (subscribe-after-connect, multi-subscribe) and
CryptocomConnector (server-initiated heartbeat) are the two
worked examples of how the trait extensions cover non-trivial
exchange protocols.
Changelog
Version history with grouped Added / Changed / Fixed entries: CHANGELOG.md.
License
MIT — see LICENSE.