Skip to main content

rustrade_data/exchange/binance/
mod.rs

1use self::{
2    book::l1::BinanceOrderBookL1, channel::BinanceChannel, futures::BinanceFuturesUsd,
3    kline::BinanceKline, market::BinanceMarket, spot::BinanceSpot,
4    subscription::BinanceSubResponse, trade::BinanceTrade,
5};
6use crate::{
7    ExchangeWsStream, NoInitialSnapshots,
8    exchange::{Connector, ExchangeServer, ExchangeSub, StreamSelector},
9    instrument::InstrumentData,
10    subscriber::{WebSocketSubscriber, validator::WebSocketSubValidator},
11    subscription::{
12        Map,
13        book::OrderBooksL1,
14        candle::{CandleInterval, Candles},
15        trade::PublicTrades,
16    },
17    transformer::stateless::StatelessTransformer,
18};
19use rustrade_instrument::exchange::ExchangeId;
20use rustrade_integration::protocol::websocket::{WebSocketSerdeParser, WsMessage};
21use std::{fmt::Debug, marker::PhantomData};
22use url::Url;
23
24/// OrderBook types common to both [`BinanceSpot`] and
25/// [`BinanceFuturesUsd`].
26pub mod book;
27
28/// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription)
29/// into an exchange [`Connector`] specific channel used for generating [`Connector::requests`].
30pub mod channel;
31
32/// Dedicated error type for the Binance historical klines REST client.
33mod error;
34pub use error::BinanceDataError;
35
36pub mod historical;
37pub use historical::BinanceHistoricalClient;
38
39/// [`ExchangeServer`] and [`StreamSelector`] implementations for
40/// [`BinanceFuturesUsd`].
41pub mod futures;
42
43pub mod kline;
44
45/// Defines the type that translates a Barter [`Subscription`](crate::subscription::Subscription)
46/// into an exchange [`Connector`] specific market used for generating [`Connector::requests`].
47pub mod market;
48
49/// [`ExchangeServer`] and [`StreamSelector`] implementations for
50/// [`BinanceSpot`].
51pub mod spot;
52
53/// [`Subscription`](crate::subscription::Subscription) response type and response
54/// [`Validator`](rustrade_integration::Validator) common to both [`BinanceSpot`]
55/// and [`BinanceFuturesUsd`].
56pub mod subscription;
57
58/// Public trade types common to both [`BinanceSpot`] and
59/// [`BinanceFuturesUsd`].
60pub mod trade;
61
62/// Convenient type alias for a Binance [`ExchangeWsStream`] using [`WebSocketSerdeParser`].
63pub type BinanceWsStream<Transformer> = ExchangeWsStream<WebSocketSerdeParser, Transformer>;
64
65/// Whether Binance publishes klines at the given [`CandleInterval`].
66///
67/// [`CandleInterval`] is the venue-agnostic union of every resolution any connector
68/// supports, so it is a **superset** of Binance's kline menu: Binance's only
69/// sub-minute kline is `1s`, and it serves no `5s`/`15s`/`30s` stream (nor the
70/// matching REST `interval`, which 400s).
71///
72/// # Where this is applied
73///
74/// Only the **dynamic** builder gates on this:
75/// [`exchange_supports_instrument_kind_sub_kind`](crate::subscription::exchange_supports_instrument_kind_sub_kind)
76/// consults it for [`SubKind::Candles`](crate::subscription::SubKind::Candles), so an unsupported
77/// interval is rejected before a socket is opened.
78///
79/// The **typed** builder does not. Its validation goes through
80/// [`exchange_supports_instrument_kind`](crate::subscription::exchange_supports_instrument_kind),
81/// which never inspects the resolution, and
82/// [`BinanceChannel::spot_candle`](channel::BinanceChannel::spot_candle) /
83/// [`futures_candle`](channel::BinanceChannel::futures_candle) are infallible by the
84/// [`Identifier`](crate::Identifier) contract — so a typed `5s` subscription builds a well-formed
85/// `@kline_5s` channel name for a stream Binance does not publish, and is rejected only once
86/// Binance answers the SUBSCRIBE (surfaced by
87/// [`BinanceSubResponse`]'s
88/// [`Validator`](rustrade_integration::Validator)).
89///
90/// The failure is observable either way; call this yourself before a typed subscription if you
91/// want it before the socket rather than after.
92#[must_use]
93pub fn supports_candle_interval(interval: CandleInterval) -> bool {
94    match interval {
95        CandleInterval::Sec5 | CandleInterval::Sec15 | CandleInterval::Sec30 => false,
96        CandleInterval::Sec1
97        | CandleInterval::Min1
98        | CandleInterval::Min3
99        | CandleInterval::Min5
100        | CandleInterval::Min15
101        | CandleInterval::Min30
102        | CandleInterval::Hour1
103        | CandleInterval::Hour2
104        | CandleInterval::Hour4
105        | CandleInterval::Hour6
106        | CandleInterval::Hour8
107        | CandleInterval::Hour12
108        | CandleInterval::Day1
109        | CandleInterval::Day3
110        | CandleInterval::Week1
111        | CandleInterval::Month1 => true,
112    }
113}
114
115/// Generic [`Binance<Server>`](Binance) exchange.
116///
117/// ### Notes
118/// A `Server` [`ExchangeServer`] implementations exists for
119/// [`BinanceSpot`] and [`BinanceFuturesUsd`].
120#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
121pub struct Binance<Server> {
122    server: PhantomData<Server>,
123}
124
125impl<Server> Connector for Binance<Server>
126where
127    Server: ExchangeServer,
128{
129    const ID: ExchangeId = Server::ID;
130    type Channel = BinanceChannel;
131    type Market = BinanceMarket;
132    type Subscriber = WebSocketSubscriber;
133    type SubValidator = WebSocketSubValidator;
134    type SubResponse = BinanceSubResponse;
135
136    fn url() -> Result<Url, url::ParseError> {
137        Url::parse(Server::websocket_url())
138    }
139
140    fn requests(exchange_subs: Vec<ExchangeSub<Self::Channel, Self::Market>>) -> Vec<WsMessage> {
141        let stream_names = exchange_subs
142            .into_iter()
143            .map(|sub| {
144                // Note:
145                // Market must be lowercase when subscribing, but lowercase in general since
146                // Binance sends message with uppercase MARKET (eg/ BTCUSDT).
147                format!(
148                    "{}{}",
149                    sub.market.as_ref().to_lowercase(),
150                    sub.channel.as_ref()
151                )
152            })
153            .collect::<Vec<String>>();
154
155        vec![WsMessage::text(
156            serde_json::json!({
157                "method": "SUBSCRIBE",
158                "params": stream_names,
159                "id": 1
160            })
161            .to_string(),
162        )]
163    }
164
165    fn expected_responses<InstrumentKey>(_: &Map<InstrumentKey>) -> usize {
166        1
167    }
168}
169
170// `PublicTrades` and `OrderBooksL1` are implemented per-server (NOT blanket over
171// `Binance<Server>`) so they ride only the `/public`-tier server types — `BinanceSpot` (`/ws`)
172// and `BinanceFuturesUsd` (`/public/ws`). The market-tier `BinanceFuturesUsdMarket` (`/market/ws`)
173// deliberately has no such impl, making a trade/L1 subscription on that tier — which Binance
174// would silently dead-stream — a compile error instead. This mirrors `OrderBooksL2`, which is
175// already per-server.
176impl<Instrument> StreamSelector<Instrument, PublicTrades> for BinanceSpot
177where
178    Instrument: InstrumentData,
179{
180    type SnapFetcher = NoInitialSnapshots;
181    type Stream =
182        BinanceWsStream<StatelessTransformer<Self, Instrument::Key, PublicTrades, BinanceTrade>>;
183}
184
185impl<Instrument> StreamSelector<Instrument, PublicTrades> for BinanceFuturesUsd
186where
187    Instrument: InstrumentData,
188{
189    type SnapFetcher = NoInitialSnapshots;
190    type Stream =
191        BinanceWsStream<StatelessTransformer<Self, Instrument::Key, PublicTrades, BinanceTrade>>;
192}
193
194impl<Instrument> StreamSelector<Instrument, OrderBooksL1> for BinanceSpot
195where
196    Instrument: InstrumentData,
197{
198    type SnapFetcher = NoInitialSnapshots;
199    type Stream = BinanceWsStream<
200        StatelessTransformer<Self, Instrument::Key, OrderBooksL1, BinanceOrderBookL1>,
201    >;
202}
203
204impl<Instrument> StreamSelector<Instrument, OrderBooksL1> for BinanceFuturesUsd
205where
206    Instrument: InstrumentData,
207{
208    type SnapFetcher = NoInitialSnapshots;
209    type Stream = BinanceWsStream<
210        StatelessTransformer<Self, Instrument::Key, OrderBooksL1, BinanceOrderBookL1>,
211    >;
212}
213
214// Live spot klines: `@kline_<interval>` on `BinanceSpot`'s `/ws` tier.
215impl<Instrument> StreamSelector<Instrument, Candles> for BinanceSpot
216where
217    Instrument: InstrumentData,
218{
219    type SnapFetcher = NoInitialSnapshots;
220    type Stream =
221        BinanceWsStream<StatelessTransformer<Self, Instrument::Key, Candles, BinanceKline>>;
222}
223
224impl<'de, Server> serde::Deserialize<'de> for Binance<Server>
225where
226    Server: ExchangeServer,
227{
228    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
229    where
230        D: serde::de::Deserializer<'de>,
231    {
232        let input = <String as serde::Deserialize>::deserialize(deserializer)?;
233
234        if input.as_str() == Self::ID.as_str() {
235            Ok(Self::default())
236        } else {
237            Err(serde::de::Error::invalid_value(
238                serde::de::Unexpected::Str(input.as_str()),
239                &Self::ID.as_str(),
240            ))
241        }
242    }
243}
244
245impl<Server> serde::Serialize for Binance<Server>
246where
247    Server: ExchangeServer,
248{
249    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
250    where
251        S: serde::ser::Serializer,
252    {
253        serializer.serialize_str(Self::ID.as_str())
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    /// `CandleInterval` is a union across venues, so this guard must be re-reviewed
262    /// whenever a variant is added — the exhaustive `match` inside
263    /// `supports_candle_interval` is the compile gate, and this test pins the answer.
264    #[test]
265    fn supports_candle_interval_rejects_only_the_sub_minute_intervals_binance_lacks() {
266        for interval in CandleInterval::ALL {
267            let expected = !matches!(
268                interval,
269                CandleInterval::Sec5 | CandleInterval::Sec15 | CandleInterval::Sec30
270            );
271            assert_eq!(
272                supports_candle_interval(interval),
273                expected,
274                "interval {interval}"
275            );
276        }
277    }
278}