rustrade_data/exchange/binance/futures/mod.rs
1use self::liquidation::BinanceLiquidation;
2use super::{Binance, ExchangeServer};
3use crate::{
4 NoInitialSnapshots,
5 exchange::{
6 StreamSelector,
7 binance::{
8 BinanceWsStream,
9 futures::l2::{
10 BinanceFuturesUsdOrderBooksL2SnapshotFetcher,
11 BinanceFuturesUsdOrderBooksL2Transformer,
12 },
13 kline::BinanceContinuousKline,
14 },
15 },
16 instrument::InstrumentData,
17 subscription::{book::OrderBooksL2, candle::Candles, liquidation::Liquidations},
18 transformer::stateless::StatelessTransformer,
19};
20use rustrade_instrument::exchange::ExchangeId;
21use std::fmt::{Display, Formatter};
22
23/// Level 2 OrderBook types.
24pub mod l2;
25
26/// Liquidation types.
27pub mod liquidation;
28
29/// [`BinanceFuturesUsd`] `/public`-tier WebSocket server base url.
30///
31/// Binance routes `fstream.binance.com` into mutually-exclusive path tiers; the legacy unrouted
32/// `/ws` was deprecated and stopped delivering `/market`-tier streams (e.g. `@forceOrder`) on
33/// 2026-04-23. Tier map: `@trade`/`@bookTicker`/`@depth` are `/public`-tier (here, on `/public/ws`);
34/// `@forceOrder`/`@aggTrade`/`@continuousKline`/`@markPrice` are `/market`-tier (see
35/// [`BinanceFuturesUsdMarket`]).
36///
37/// See docs: <https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams>
38pub const WEBSOCKET_BASE_URL_BINANCE_FUTURES_USD: &str = "wss://fstream.binance.com/public/ws";
39
40/// [`BinanceFuturesUsdMarket`] `/market`-tier WebSocket server base url.
41///
42/// The `/market` tier is a **distinct connection** from `/public` — a socket is bound to exactly
43/// one tier and silently delivers zero frames for channels outside it. `@continuousKline_`
44/// (klines) and `@forceOrder` (liquidations) are `/market`-tier streams.
45pub const WEBSOCKET_BASE_URL_BINANCE_FUTURES_USD_MARKET: &str =
46 "wss://fstream.binance.com/market/ws";
47
48/// [`Binance`] perpetual usd exchange (`/public` WS tier: trade/bookTicker/depth).
49///
50/// # Caller obligation: detect a silently dead stream
51///
52/// Binance has *demonstrated* that it re-routes which WS stream is served on which path tier (the
53/// `/public` vs `/market` split — see [`BinanceFuturesUsdMarket`]), and futures `PublicTrades` here
54/// rides the **undocumented** `@trade` stream. A tier change does **not** surface as an error: the
55/// handshake still returns HTTP `101` and the socket then delivers **zero frames**, silently —
56/// there is no `Err`, no disconnect, no reconnect trigger. The library cannot distinguish "the
57/// market is quiet" from "this stream is dead" without policy that belongs in the consumer.
58///
59/// Per this library's separation-of-concerns contract, staleness detection is the **caller's**
60/// responsibility: consumers (especially trading systems acting on this data) **must** run a
61/// staleness watchdog — assert that an expected-dense stream (e.g. `btcusdt` `PublicTrades`)
62/// delivers at least one event within a sane window (seconds for a liquid symbol), and treat
63/// prolonged silence on a connected socket as a fault to alert/halt on. Do not assume a healthy
64/// socket implies live data. If `@trade` ever goes dark, the known fix on rustrade's side is
65/// migrating futures `PublicTrades` to `@aggTrade` on [`BinanceFuturesUsdMarket`] (`/market`); the
66/// `#[ignore]`d `binance_ws_tier_canary` integration test re-verifies the live tier map on demand.
67///
68/// # WebSocket connection limits
69///
70/// Existing Binance constraints a multi-symbol consumer must respect (not new — already handled by
71/// `ReconnectingStream` + tungstenite's automatic pong, but worth stating); they apply equally to
72/// the `/market`-tier [`BinanceFuturesUsdMarket`] socket:
73/// - **1024 streams per connection.**
74/// - **Incoming-message cap: 10 messages/second** (counts ping/pong + control frames) — space out
75/// bulk `SUBSCRIBE`s.
76/// - **Forced disconnect at 24h** (transparently re-established by `ReconnectingStream`).
77/// - **Keepalive:** the server pings every **3 minutes** with a **10-minute** pong deadline
78/// (tungstenite auto-pongs, so no action) — note this is far looser than spot's 20s/1-min cadence.
79pub type BinanceFuturesUsd = Binance<BinanceServerFuturesUsd>;
80
81/// [`Binance`] perpetual usd exchange on the `/market` WS tier (klines + liquidations).
82///
83/// Shares [`ExchangeId::BinanceFuturesUsd`] with [`BinanceFuturesUsd`] — the [`ExchangeId`] is the
84/// *logical* exchange (used for event tagging and dynamic dispatch); the server type is a
85/// connection-layer detail selecting the WS path. Streams that Binance routes to `/market`
86/// (klines, liquidations) are implemented only on this type so routing them to the `/public`
87/// tier is a compile error rather than a silent dead stream.
88pub type BinanceFuturesUsdMarket = Binance<BinanceServerFuturesUsdMarket>;
89
90/// [`Binance`] perpetual usd [`ExchangeServer`] (`/public` WS tier).
91#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
92pub struct BinanceServerFuturesUsd;
93
94impl ExchangeServer for BinanceServerFuturesUsd {
95 const ID: ExchangeId = ExchangeId::BinanceFuturesUsd;
96
97 fn websocket_url() -> &'static str {
98 WEBSOCKET_BASE_URL_BINANCE_FUTURES_USD
99 }
100}
101
102/// [`Binance`] perpetual usd [`ExchangeServer`] for the `/market` WS tier.
103///
104/// Same [`const ID`](ExchangeServer::ID) as [`BinanceServerFuturesUsd`]; only the
105/// [`websocket_url`](ExchangeServer::websocket_url) differs (`/market/ws`).
106#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
107pub struct BinanceServerFuturesUsdMarket;
108
109impl ExchangeServer for BinanceServerFuturesUsdMarket {
110 const ID: ExchangeId = ExchangeId::BinanceFuturesUsd;
111
112 fn websocket_url() -> &'static str {
113 WEBSOCKET_BASE_URL_BINANCE_FUTURES_USD_MARKET
114 }
115}
116
117impl<Instrument> StreamSelector<Instrument, OrderBooksL2> for BinanceFuturesUsd
118where
119 Instrument: InstrumentData,
120{
121 type SnapFetcher = BinanceFuturesUsdOrderBooksL2SnapshotFetcher;
122 type Stream = BinanceWsStream<BinanceFuturesUsdOrderBooksL2Transformer<Instrument::Key>>;
123}
124
125// `@forceOrder` is a `/market`-tier stream, so `Liquidations` is implemented on the market-tier
126// server type (not `BinanceFuturesUsd`). Routing a liquidation sub to `/public` is a compile error.
127impl<Instrument> StreamSelector<Instrument, Liquidations> for BinanceFuturesUsdMarket
128where
129 Instrument: InstrumentData,
130{
131 type SnapFetcher = NoInitialSnapshots;
132 type Stream = BinanceWsStream<
133 StatelessTransformer<Self, Instrument::Key, Liquidations, BinanceLiquidation>,
134 >;
135}
136
137// Live futures klines: `@continuousKline_<interval>` on the `/market` tier.
138impl<Instrument> StreamSelector<Instrument, Candles> for BinanceFuturesUsdMarket
139where
140 Instrument: InstrumentData,
141{
142 type SnapFetcher = NoInitialSnapshots;
143 type Stream = BinanceWsStream<
144 StatelessTransformer<Self, Instrument::Key, Candles, BinanceContinuousKline>,
145 >;
146}
147
148impl Display for BinanceFuturesUsd {
149 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
150 write!(f, "BinanceFuturesUsd")
151 }
152}
153
154impl Display for BinanceFuturesUsdMarket {
155 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
156 write!(f, "BinanceFuturesUsdMarket")
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 /// The two futures server types share one [`ExchangeId`] but must resolve distinct WS URLs:
165 /// `/public/ws` (trade/book/depth) vs `/market/ws` (klines/liquidations). A regression here
166 /// would silently dead-stream one tier.
167 #[test]
168 fn futures_server_types_resolve_distinct_tier_urls() {
169 assert_eq!(
170 BinanceServerFuturesUsd::websocket_url(),
171 "wss://fstream.binance.com/public/ws"
172 );
173 assert_eq!(
174 BinanceServerFuturesUsdMarket::websocket_url(),
175 "wss://fstream.binance.com/market/ws"
176 );
177 assert_ne!(
178 BinanceServerFuturesUsd::websocket_url(),
179 BinanceServerFuturesUsdMarket::websocket_url()
180 );
181 assert_eq!(
182 BinanceServerFuturesUsd::ID,
183 BinanceServerFuturesUsdMarket::ID
184 );
185 }
186}