Skip to main content

rustrade_data/exchange/binance/
channel.rs

1use super::{Binance, futures::BinanceFuturesUsdMarket, spot::BinanceSpot};
2use crate::{
3    Identifier,
4    subscription::{
5        Subscription,
6        book::{OrderBooksL1, OrderBooksL2},
7        candle::{CandleInterval, Candles},
8        liquidation::Liquidations,
9        trade::PublicTrades,
10    },
11};
12use serde::Serialize;
13
14/// Type that defines how to translate a Barter [`Subscription`] into a [`Binance`]
15/// channel to be subscribed to.
16///
17/// See docs: <https://binance-docs.github.io/apidocs/spot/en/#websocket-market-streams>
18/// See docs: <https://binance-docs.github.io/apidocs/futures/en/#websocket-market-streams>
19#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize)]
20pub struct BinanceChannel(pub &'static str);
21
22impl BinanceChannel {
23    /// [`Binance`] real-time trades channel name.
24    ///
25    /// See docs: <https://binance-docs.github.io/apidocs/spot/en/#trade-streams>
26    ///
27    /// Note:
28    /// For [`BinanceFuturesUsd`](super::futures::BinanceFuturesUsd) this real-time
29    /// stream is undocumented.
30    ///
31    /// See discord: <https://discord.com/channels/910237311332151317/923160222711812126/975712874582388757>
32    pub const TRADES: Self = Self("@trade");
33
34    /// [`Binance`] real-time OrderBook Level1 (top of books) channel name.
35    ///
36    /// See docs:<https://binance-docs.github.io/apidocs/spot/en/#individual-symbol-book-ticker-streams>
37    /// See docs:<https://binance-docs.github.io/apidocs/futures/en/#individual-symbol-book-ticker-streams>
38    pub const ORDER_BOOK_L1: Self = Self("@bookTicker");
39
40    /// [`Binance`] OrderBook Level2 channel name (100ms delta updates).
41    ///
42    /// See docs: <https://binance-docs.github.io/apidocs/spot/en/#diff-depth-stream>
43    /// See docs: <https://binance-docs.github.io/apidocs/futures/en/#diff-book-depth-streams>
44    pub const ORDER_BOOK_L2: Self = Self("@depth@100ms");
45
46    /// [`BinanceFuturesUsd`](super::futures::BinanceFuturesUsd) liquidation orders channel name.
47    ///
48    /// Routed on Binance's `/market` WS tier (see
49    /// [`BinanceFuturesUsdMarket`]).
50    ///
51    /// See docs: <https://binance-docs.github.io/apidocs/futures/en/#liquidation-order-streams>
52    pub const LIQUIDATIONS: Self = Self("@forceOrder");
53
54    /// [`BinanceSpot`] kline (candle) channel name for the given [`CandleInterval`],
55    /// e.g. `@kline_1m`.
56    ///
57    /// The interval suffix is exactly [`CandleInterval::as_str`] — pinned by the
58    /// `spot_candle_channel_suffix_matches_interval` drift-guard test.
59    ///
60    /// See docs: <https://binance-docs.github.io/apidocs/spot/en/#kline-candlestick-streams>
61    #[must_use]
62    pub const fn spot_candle(interval: CandleInterval) -> Self {
63        match interval {
64            CandleInterval::Sec1 => Self("@kline_1s"),
65            // Binance serves no sub-minute kline other than `1s`. These follow the
66            // `@kline_<interval>` convention (keeping the drift-guard invariant total)
67            // but name streams Binance does not publish, so a SUBSCRIBE using one is
68            // rejected by the exchange and surfaced by `BinanceSubResponse::validate`.
69            // `supports_candle_interval` reports which those are, but only the dynamic builder
70            // consults it — see that function's docs for which path gates and which does not.
71            CandleInterval::Sec5 => Self("@kline_5s"),
72            CandleInterval::Sec15 => Self("@kline_15s"),
73            CandleInterval::Sec30 => Self("@kline_30s"),
74            CandleInterval::Min1 => Self("@kline_1m"),
75            CandleInterval::Min3 => Self("@kline_3m"),
76            CandleInterval::Min5 => Self("@kline_5m"),
77            CandleInterval::Min15 => Self("@kline_15m"),
78            CandleInterval::Min30 => Self("@kline_30m"),
79            CandleInterval::Hour1 => Self("@kline_1h"),
80            CandleInterval::Hour2 => Self("@kline_2h"),
81            CandleInterval::Hour4 => Self("@kline_4h"),
82            CandleInterval::Hour6 => Self("@kline_6h"),
83            CandleInterval::Hour8 => Self("@kline_8h"),
84            CandleInterval::Hour12 => Self("@kline_12h"),
85            CandleInterval::Day1 => Self("@kline_1d"),
86            CandleInterval::Day3 => Self("@kline_3d"),
87            CandleInterval::Week1 => Self("@kline_1w"),
88            CandleInterval::Month1 => Self("@kline_1M"),
89        }
90    }
91
92    /// [`BinanceFuturesUsd`](super::futures::BinanceFuturesUsd) continuous-contract
93    /// (perpetual) kline channel name for the given [`CandleInterval`], e.g.
94    /// `_perpetual@continuousKline_1m`.
95    ///
96    /// Routed on Binance's `/market` WS tier (see
97    /// [`BinanceFuturesUsdMarket`]). The
98    /// `contractType` is fixed `PERPETUAL` because [`BinanceFuturesUsd`](super::futures::BinanceFuturesUsd)
99    /// is perpetual-only. The interval suffix is exactly [`CandleInterval::as_str`] —
100    /// pinned by the `futures_candle_channel_suffix_matches_interval` drift-guard test.
101    ///
102    /// See docs: <https://binance-docs.github.io/apidocs/futures/en/#continuous-contract-kline-candlestick-streams>
103    #[must_use]
104    pub const fn futures_candle(interval: CandleInterval) -> Self {
105        match interval {
106            CandleInterval::Sec1 => Self("_perpetual@continuousKline_1s"),
107            // See `spot_candle`: Binance publishes no `5s`/`15s`/`30s` kline stream.
108            CandleInterval::Sec5 => Self("_perpetual@continuousKline_5s"),
109            CandleInterval::Sec15 => Self("_perpetual@continuousKline_15s"),
110            CandleInterval::Sec30 => Self("_perpetual@continuousKline_30s"),
111            CandleInterval::Min1 => Self("_perpetual@continuousKline_1m"),
112            CandleInterval::Min3 => Self("_perpetual@continuousKline_3m"),
113            CandleInterval::Min5 => Self("_perpetual@continuousKline_5m"),
114            CandleInterval::Min15 => Self("_perpetual@continuousKline_15m"),
115            CandleInterval::Min30 => Self("_perpetual@continuousKline_30m"),
116            CandleInterval::Hour1 => Self("_perpetual@continuousKline_1h"),
117            CandleInterval::Hour2 => Self("_perpetual@continuousKline_2h"),
118            CandleInterval::Hour4 => Self("_perpetual@continuousKline_4h"),
119            CandleInterval::Hour6 => Self("_perpetual@continuousKline_6h"),
120            CandleInterval::Hour8 => Self("_perpetual@continuousKline_8h"),
121            CandleInterval::Hour12 => Self("_perpetual@continuousKline_12h"),
122            CandleInterval::Day1 => Self("_perpetual@continuousKline_1d"),
123            CandleInterval::Day3 => Self("_perpetual@continuousKline_3d"),
124            CandleInterval::Week1 => Self("_perpetual@continuousKline_1w"),
125            CandleInterval::Month1 => Self("_perpetual@continuousKline_1M"),
126        }
127    }
128}
129
130impl<Server, Instrument> Identifier<BinanceChannel>
131    for Subscription<Binance<Server>, Instrument, PublicTrades>
132{
133    fn id(&self) -> BinanceChannel {
134        BinanceChannel::TRADES
135    }
136}
137
138impl<Server, Instrument> Identifier<BinanceChannel>
139    for Subscription<Binance<Server>, Instrument, OrderBooksL1>
140{
141    fn id(&self) -> BinanceChannel {
142        BinanceChannel::ORDER_BOOK_L1
143    }
144}
145
146impl<Server, Instrument> Identifier<BinanceChannel>
147    for Subscription<Binance<Server>, Instrument, OrderBooksL2>
148{
149    fn id(&self) -> BinanceChannel {
150        BinanceChannel::ORDER_BOOK_L2
151    }
152}
153
154// `@forceOrder` is a Binance `/market`-tier stream, so liquidations route through
155// `BinanceFuturesUsdMarket` (not `BinanceFuturesUsd`/`/public`). Restricting the impl to
156// the market-tier server type makes a `/public`-tier liquidation subscription a compile error.
157impl<Instrument> Identifier<BinanceChannel>
158    for Subscription<BinanceFuturesUsdMarket, Instrument, Liquidations>
159{
160    fn id(&self) -> BinanceChannel {
161        BinanceChannel::LIQUIDATIONS
162    }
163}
164
165impl<Instrument> Identifier<BinanceChannel> for Subscription<BinanceSpot, Instrument, Candles> {
166    fn id(&self) -> BinanceChannel {
167        BinanceChannel::spot_candle(self.kind.interval)
168    }
169}
170
171// Futures klines ride the `@continuousKline_` `/market`-tier stream, so the channel impl is
172// specialised to `BinanceFuturesUsdMarket` — routing a kline sub to `/public` is a compile error.
173impl<Instrument> Identifier<BinanceChannel>
174    for Subscription<BinanceFuturesUsdMarket, Instrument, Candles>
175{
176    fn id(&self) -> BinanceChannel {
177        BinanceChannel::futures_candle(self.kind.interval)
178    }
179}
180
181impl AsRef<str> for BinanceChannel {
182    fn as_ref(&self) -> &str {
183        self.0
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    /// The spot kline channel must be `@kline_<interval>` where `<interval>` is exactly
192    /// [`CandleInterval::as_str`] for every variant — otherwise the channel string would
193    /// drift from the live Binance stream name and silently fail to match frames.
194    #[test]
195    fn spot_candle_channel_suffix_matches_interval() {
196        for interval in CandleInterval::ALL {
197            let channel = BinanceChannel::spot_candle(interval).0;
198            assert_eq!(
199                channel,
200                format!("@kline_{}", interval.as_str()),
201                "spot channel drifted for {interval:?}"
202            );
203        }
204    }
205
206    /// The futures continuous kline channel must be `_perpetual@continuousKline_<interval>`
207    /// where `<interval>` is exactly [`CandleInterval::as_str`] for every variant.
208    #[test]
209    fn futures_candle_channel_suffix_matches_interval() {
210        for interval in CandleInterval::ALL {
211            let channel = BinanceChannel::futures_candle(interval).0;
212            assert_eq!(
213                channel,
214                format!("_perpetual@continuousKline_{}", interval.as_str()),
215                "futures channel drifted for {interval:?}"
216            );
217        }
218    }
219}