Skip to main content

rustrade_data/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(
3    clippy::unwrap_used,
4    clippy::expect_used,
5    clippy::cast_possible_truncation,
6    clippy::cast_sign_loss,
7    // A public item linking to a private one renders as dead, non-hyperlinked code on docs.rs — the
8    // reader is pointed at something they cannot open. Denied rather than warned because these
9    // accumulate silently: `cargo doc` is not part of the normal build or the pre-commit gate, so a
10    // warning here is one nobody sees. Fix by inlining the fact or linking a public item instead.
11    rustdoc::private_intra_doc_links
12)]
13#![warn(
14    unused,
15    clippy::cognitive_complexity,
16    unused_crate_dependencies,
17    unused_extern_crates,
18    clippy::unused_self,
19    clippy::useless_let_if_seq,
20    missing_debug_implementations,
21    rust_2018_idioms
22)]
23// Inherited from the barter-rs upstream: the generic, trait-heavy public API legitimately produces
24// complex nested types and many-argument constructors, and `type_alias_bounds` fires on
25// documentation-bearing alias bounds kept intentionally. Denying these adds churn without improving
26// the API.
27#![allow(clippy::type_complexity, clippy::too_many_arguments, type_alias_bounds)]
28
29//! # Barter-Data
30//! A high-performance WebSocket integration library for streaming public market data from leading cryptocurrency
31//! exchanges - batteries included. It is:
32//! * **Easy**: Barter-Data's simple [`StreamBuilder`](streams::builder::StreamBuilder) and [`DynamicStreams`](streams::builder::dynamic::DynamicStreams) interface allows for easy & quick setup (see example below and /examples!).
33//! * **Normalised**: Barter-Data's unified interface for consuming public WebSocket data means every Exchange returns a normalised data model.
34//! * **Real-Time**: Barter-Data utilises real-time WebSocket integrations enabling the consumption of normalised tick-by-tick data.
35
36//! * **Extensible**: Barter-Data is highly extensible, and therefore easy to contribute to with coding new integrations!
37//!
38//! ## User API
39//! - [`StreamBuilder`](streams::builder::StreamBuilder) for initialising [`MarketStream`]s of specific data kinds.
40//! - [`DynamicStreams`](streams::builder::dynamic::DynamicStreams) for initialising [`MarketStream`]s of every supported data kind at once.
41//! - Define what exchange market data you want to stream using the [`Subscription`] type.
42//! - Pass [`Subscription`]s to the [`StreamBuilder::subscribe`](streams::builder::StreamBuilder::subscribe) or [`DynamicStreams::init`](streams::builder::dynamic::DynamicStreams::init) methods.
43//! - Each call to the [`StreamBuilder::subscribe`](streams::builder::StreamBuilder::subscribe) (or each batch passed to the [`DynamicStreams::init`](streams::builder::dynamic::DynamicStreams::init))
44//!   method opens a new WebSocket connection to the exchange - giving you full control.
45//!
46//! ## Examples
47//! For a comprehensive collection of examples, see the /examples directory.
48//!
49//! ### Multi Exchange Public Trades
50//! ```rust,no_run
51//! use rustrade_data::{
52//!     exchange::{
53//!         gateio::spot::GateioSpot,
54//!         binance::{futures::BinanceFuturesUsd, spot::BinanceSpot},
55//!         coinbase::Coinbase,
56//!         okx::Okx,
57//!     },
58//!     streams::{Streams, reconnect::stream::ReconnectingStream},
59//!     subscriber::WebSocketSubscriber,
60//!     subscription::trade::PublicTrades,
61//! };
62//! use rustrade_instrument::instrument::market_data::kind::MarketDataInstrumentKind;
63//! use futures::StreamExt;
64//! use tracing::warn;
65//!
66//! #[tokio::main]
67//! async fn main() {
68//!     // Initialise PublicTrades Streams for various exchanges
69//!     // '--> each call to StreamBuilder::subscribe() initialises a separate WebSocket connection
70//!
71//!     let streams = Streams::<PublicTrades>::builder()
72//!         .subscribe(WebSocketSubscriber, [
73//!             (BinanceSpot::default(), "btc", "usdt", MarketDataInstrumentKind::Spot, PublicTrades),
74//!             (BinanceSpot::default(), "eth", "usdt", MarketDataInstrumentKind::Spot, PublicTrades),
75//!         ])
76//!         .subscribe(WebSocketSubscriber, [
77//!             (BinanceFuturesUsd::default(), "btc", "usdt", MarketDataInstrumentKind::Perpetual, PublicTrades),
78//!             (BinanceFuturesUsd::default(), "eth", "usdt", MarketDataInstrumentKind::Perpetual, PublicTrades),
79//!         ])
80//!         .subscribe(WebSocketSubscriber, [
81//!             (Coinbase, "btc", "usd", MarketDataInstrumentKind::Spot, PublicTrades),
82//!             (Coinbase, "eth", "usd", MarketDataInstrumentKind::Spot, PublicTrades),
83//!         ])
84//!         .subscribe(WebSocketSubscriber, [
85//!             (GateioSpot::default(), "btc", "usdt", MarketDataInstrumentKind::Spot, PublicTrades),
86//!             (GateioSpot::default(), "eth", "usdt", MarketDataInstrumentKind::Spot, PublicTrades),
87//!         ])
88//!         .subscribe(WebSocketSubscriber, [
89//!             (Okx, "btc", "usdt", MarketDataInstrumentKind::Spot, PublicTrades),
90//!             (Okx, "eth", "usdt", MarketDataInstrumentKind::Spot, PublicTrades),
91//!             (Okx, "btc", "usdt", MarketDataInstrumentKind::Perpetual, PublicTrades),
92//!             (Okx, "eth", "usdt", MarketDataInstrumentKind::Perpetual, PublicTrades),
93//!        ])
94//!         .init()
95//!         .await
96//!         .unwrap();
97//!
98//!     // Select and merge every exchange Stream using futures_util::stream::select_all
99//!     // Note: use `Streams.select(ExchangeId)` to interact with individual exchange streams!
100//!     let mut joined_stream = streams
101//!         .select_all()
102//!         .with_error_handler(|error| warn!(?error, "MarketStream generated error"));
103//!
104//!     while let Some(event) = joined_stream.next().await {
105//!         println!("{event:?}");
106//!     }
107//! }
108//! ```
109
110// Silence unused_crate_dependencies for dev-dependencies used only in tests
111#[cfg(test)]
112use tracing_subscriber as _;
113// serial_test is only referenced by integration tests under the `massive` feature
114// (tests/massive_integration.rs), which compile as separate units.
115#[cfg(test)]
116use serial_test as _;
117// wiremock is referenced by the pagination integration tests under the `massive` feature
118// (tests/massive_pagination.rs, a separate compilation unit) and by `exchange::http`'s unit tests,
119// which are no longer feature-gated and so reference it in every `cfg(test)` build. This stub is
120// therefore redundant today; it is kept alongside its siblings so re-gating those tests cannot
121// silently reintroduce the warning.
122#[cfg(test)]
123use wiremock as _;
124// time is only referenced by the integration tests under the `databento`/`ibkr` features
125// (tests/databento_integration.rs, tests/ibkr_integration.rs) and by a databento example, which
126// compile as separate units.
127#[cfg(test)]
128use time as _;
129// temp_env is only referenced by the in-tree unit tests under the `massive` feature
130// (exchange::massive::{live, rest}), so it is unused when that feature is off.
131#[cfg(test)]
132use temp_env as _;
133// http is only referenced by the in-tree unit tests under the `ibkr` feature
134// (exchange::ibkr::flex), which build synthetic `reqwest::Response`s without a socket.
135#[cfg(test)]
136use http as _;
137// tempfile, sha2 and hex are only referenced by the export-job integration tests under the `lse`
138// feature (tests/lse_export.rs), which compiles as a separate unit: tempfile supplies a scratch
139// directory for the download/resume cases, and sha2/hex compute the digest the synthetic job
140// records carry. (sha2 and hex are also non-dev dependencies under `lse`, so their stubs matter
141// only when that feature is off.)
142#[cfg(test)]
143use {hex as _, sha2 as _, tempfile as _};
144// tokio_tungstenite is a dev-dependency for the `lse` WebSocket handshake test
145// (tests/lse_ws_handshake.rs), which speaks the server half of the protocol in-process and compiles
146// as a separate unit. It is *also* a non-dev dependency under the `massive` feature, where the lib
147// itself uses it -- so this stub matters only when that feature is off, and the `cfg(test)` gate is
148// load-bearing: an unconditional `use` would fail to resolve in a non-test build without `massive`.
149#[cfg(test)]
150use tokio_tungstenite as _;
151
152use crate::{
153    error::DataError,
154    event::MarketEvent,
155    exchange::{Connector, PingInterval},
156    instrument::InstrumentData,
157    subscriber::{Subscribed, Subscriber},
158    subscription::{Subscription, SubscriptionKind},
159    transformer::ExchangeTransformer,
160};
161use futures::{SinkExt, Stream, StreamExt};
162use rustrade_instrument::exchange::ExchangeId;
163use rustrade_integration::{
164    Transformer,
165    error::SocketError,
166    protocol::{
167        StreamParser,
168        websocket::{WsError, WsMessage, WsSink, WsStream},
169    },
170};
171
172use rustrade_integration::stream::ExchangeStream;
173use std::{collections::VecDeque, future::Future};
174use tokio::sync::mpsc;
175use tracing::{debug, error, warn};
176
177/// All [`Error`](std::error::Error)s generated in Barter-Data.
178pub mod error;
179
180/// Defines the generic [`MarketEvent<T>`](MarketEvent) used in every [`MarketStream`].
181pub mod event;
182
183/// [`Connector`] implementations for each exchange.
184pub mod exchange;
185
186/// High-level API types used for building [`MarketStream`]s from collections
187/// of Barter [`Subscription`]s.
188pub mod streams;
189
190/// [`Subscriber`], [`SubscriptionMapper`](subscriber::mapper::SubscriptionMapper) and
191/// [`SubscriptionValidator`](subscriber::validator::SubscriptionValidator)  traits that define how a
192/// [`Connector`] will subscribe to exchange [`MarketStream`]s.
193///
194/// Standard implementations for subscribing to WebSocket [`MarketStream`]s are included.
195pub mod subscriber;
196
197/// Types that communicate the type of each [`MarketStream`] to initialise, and what normalised
198/// Barter output type the exchange will be transformed into.
199pub mod subscription;
200
201/// [`InstrumentData`] trait for instrument describing data.
202pub mod instrument;
203
204/// [`OrderBook`](books::OrderBook) related types, and utilities for initialising and maintaining
205/// a collection of sorted local Instrument [`OrderBook`](books::OrderBook)s
206pub mod books;
207
208/// Generic [`ExchangeTransformer`] implementations used by [`MarketStream`]s to translate exchange
209/// specific types to normalised Barter types.
210///
211/// A standard [`StatelessTransformer`](transformer::stateless::StatelessTransformer) implementation
212/// that works for most `Exchange`-`SubscriptionKind` combinations is included.
213///
214/// Cases that need custom logic, such as fetching initial [`OrderBooksL2`](subscription::book::OrderBooksL2)
215/// and [`OrderBooksL3`](subscription::book::OrderBooksL3) snapshots on startup, may require custom
216/// [`ExchangeTransformer`] implementations.
217/// For examples, see [`Binance`](exchange::binance::Binance) [`OrderBooksL2`](subscription::book::OrderBooksL2)
218/// [`ExchangeTransformer`] implementations for
219/// [`spot`](exchange::binance::spot::l2::BinanceSpotOrderBooksL2Transformer) and
220/// [`futures_usd`](exchange::binance::futures::l2::BinanceFuturesUsdOrderBooksL2Transformer).
221pub mod transformer;
222
223/// Convenient type alias for an [`ExchangeStream`] utilizing a tungstenite
224/// [`WebSocket`](rustrade_integration::protocol::websocket::WebSocket).
225pub type ExchangeWsStream<Parser, Transformer> = ExchangeStream<Parser, WsStream, Transformer>;
226
227/// Defines a generic identification type for the implementor.
228pub trait Identifier<T> {
229    fn id(&self) -> T;
230}
231
232/// [`Stream`] that yields [`Market<Kind>`](MarketEvent) events. The type of [`Market<Kind>`](MarketEvent)
233/// depends on the provided [`SubscriptionKind`] of the passed [`Subscription`]s.
234pub trait MarketStream<Exchange, Instrument, Kind>
235where
236    Self: Stream<Item = Result<MarketEvent<Instrument::Key, Kind::Event>, DataError>>
237        + Send
238        + Sized
239        + Unpin,
240    Exchange: Connector,
241    Instrument: InstrumentData,
242    Kind: SubscriptionKind,
243{
244    fn init<SnapFetcher>(
245        subscriber: &Exchange::Subscriber,
246        subscriptions: &[Subscription<Exchange, Instrument, Kind>],
247    ) -> impl Future<Output = Result<Self, DataError>> + Send
248    where
249        SnapFetcher: SnapshotFetcher<Exchange, Kind>,
250        Subscription<Exchange, Instrument, Kind>:
251            Identifier<Exchange::Channel> + Identifier<Exchange::Market>;
252}
253
254/// Defines how to fetch market data snapshots for a collection of [`Subscription`]s.
255///
256/// Useful when a [`MarketStream`] requires an initial snapshot on start-up.
257///
258/// See examples such as Binance OrderBooksL2: <br>
259/// - [`BinanceSpotOrderBooksL2SnapshotFetcher`](exchange::binance::spot::l2::BinanceSpotOrderBooksL2SnapshotFetcher)
260/// - [`BinanceFuturesUsdOrderBooksL2SnapshotFetcher`](exchange::binance::futures::l2::BinanceFuturesUsdOrderBooksL2SnapshotFetcher)
261pub trait SnapshotFetcher<Exchange, Kind> {
262    fn fetch_snapshots<Instrument>(
263        subscriptions: &[Subscription<Exchange, Instrument, Kind>],
264    ) -> impl Future<Output = Result<Vec<MarketEvent<Instrument::Key, Kind::Event>>, SocketError>> + Send
265    where
266        Exchange: Connector,
267        Instrument: InstrumentData,
268        Kind: SubscriptionKind,
269        Kind::Event: Send,
270        Subscription<Exchange, Instrument, Kind>: Identifier<Exchange::Market>;
271}
272
273impl<Exchange, Instrument, Kind, Transformer, Parser> MarketStream<Exchange, Instrument, Kind>
274    for ExchangeWsStream<Parser, Transformer>
275where
276    Exchange: Connector + Send + Sync,
277    Instrument: InstrumentData,
278    Kind: SubscriptionKind + Send + Sync,
279    Transformer: ExchangeTransformer<Exchange, Instrument::Key, Kind> + Send,
280    Kind::Event: Send + Sync,
281    Parser: StreamParser<Transformer::Input, Message = WsMessage, Error = WsError> + Send,
282{
283    async fn init<SnapFetcher>(
284        subscriber: &Exchange::Subscriber,
285        subscriptions: &[Subscription<Exchange, Instrument, Kind>],
286    ) -> Result<Self, DataError>
287    where
288        SnapFetcher: SnapshotFetcher<Exchange, Kind>,
289        Subscription<Exchange, Instrument, Kind>:
290            Identifier<Exchange::Channel> + Identifier<Exchange::Market>,
291    {
292        // Connect & subscribe
293        let Subscribed {
294            websocket,
295            map: instrument_map,
296            buffered_websocket_events,
297        } = subscriber.subscribe(subscriptions).await?;
298
299        // Fetch any required initial MarketEvent snapshots
300        let initial_snapshots = SnapFetcher::fetch_snapshots(subscriptions).await?;
301
302        // Split WebSocket into WsStream & WsSink components
303        let (ws_sink, ws_stream) = websocket.split();
304
305        // Spawn task to distribute Transformer messages (eg/ custom pongs) to the exchange
306        let (ws_sink_tx, ws_sink_rx) = mpsc::unbounded_channel();
307        tokio::spawn(distribute_messages_to_exchange(
308            Exchange::ID,
309            ws_sink,
310            ws_sink_rx,
311        ));
312
313        // Spawn optional task to distribute custom application-level pings to the exchange
314        if let Some(ping_interval) = Exchange::ping_interval() {
315            tokio::spawn(schedule_pings_to_exchange(
316                Exchange::ID,
317                ws_sink_tx.clone(),
318                ping_interval,
319            ));
320        }
321
322        // Initialise Transformer associated with this Exchange and SubscriptionKind
323        let mut transformer =
324            Transformer::init(instrument_map, &initial_snapshots, ws_sink_tx).await?;
325
326        // Process any buffered active subscription events received during Subscription validation
327        let mut processed = process_buffered_events::<Parser, Transformer>(
328            &mut transformer,
329            buffered_websocket_events,
330        );
331
332        // Extend buffered events with any initial snapshot events
333        processed.extend(initial_snapshots.into_iter().map(Ok));
334
335        Ok(ExchangeWsStream::new(ws_stream, transformer, processed))
336    }
337}
338
339/// Implementation of [`SnapshotFetcher`] that does not fetch any initial market data snapshots.
340/// Often used for stateless [`MarketStream`]s, such as public trades.
341#[derive(Debug)]
342pub struct NoInitialSnapshots;
343
344impl<Exchange, Kind> SnapshotFetcher<Exchange, Kind> for NoInitialSnapshots {
345    fn fetch_snapshots<Instrument>(
346        _: &[Subscription<Exchange, Instrument, Kind>],
347    ) -> impl Future<Output = Result<Vec<MarketEvent<Instrument::Key, Kind::Event>>, SocketError>> + Send
348    where
349        Exchange: Connector,
350        Instrument: InstrumentData,
351        Kind: SubscriptionKind,
352        Kind::Event: Send,
353        Subscription<Exchange, Instrument, Kind>: Identifier<Exchange::Market>,
354    {
355        std::future::ready(Ok(vec![]))
356    }
357}
358
359pub fn process_buffered_events<Parser, StreamTransformer>(
360    transformer: &mut StreamTransformer,
361    events: Vec<Parser::Message>,
362) -> VecDeque<Result<StreamTransformer::Output, StreamTransformer::Error>>
363where
364    Parser: StreamParser<StreamTransformer::Input>,
365    StreamTransformer: Transformer,
366{
367    events
368        .into_iter()
369        .filter_map(|event| {
370            Parser::parse(Ok(event))?
371                .inspect_err(|error| {
372                    warn!(
373                        ?error,
374                        "failed to parse message buffered during Subscription validation"
375                    )
376                })
377                .ok()
378        })
379        .flat_map(|parsed| transformer.transform(parsed))
380        .collect()
381}
382
383/// Transmit [`WsMessage`]s sent from the [`ExchangeTransformer`] to the exchange via
384/// the [`WsSink`].
385///
386/// **Note:**
387/// [`Transformer`] is a synchronous trait, so we use this separate task to handle
388/// async WebSocket writes without requiring the transformer to be async.
389pub async fn distribute_messages_to_exchange(
390    exchange: ExchangeId,
391    mut ws_sink: WsSink,
392    mut ws_sink_rx: mpsc::UnboundedReceiver<WsMessage>,
393) {
394    while let Some(message) = ws_sink_rx.recv().await {
395        if let Err(error) = ws_sink.send(message).await {
396            if rustrade_integration::protocol::websocket::is_websocket_disconnected(&error) {
397                break;
398            }
399
400            // Log error only if WsMessage failed to send over a connected WebSocket
401            error!(
402                %exchange,
403                %error,
404                "failed to send output message to the exchange via WsSink"
405            );
406        }
407    }
408}
409
410/// Schedule the sending of custom application-level ping [`WsMessage`]s to the exchange using
411/// the provided [`PingInterval`].
412///
413/// **Notes:**
414///  - This is only used for those exchanges that require custom application-level pings.
415///  - This is additional to the protocol-level pings already handled by `tokio_tungstenite`.
416pub async fn schedule_pings_to_exchange(
417    exchange: ExchangeId,
418    ws_sink_tx: mpsc::UnboundedSender<WsMessage>,
419    PingInterval { mut interval, ping }: PingInterval,
420) {
421    loop {
422        // Wait for next scheduled ping
423        interval.tick().await;
424
425        // Construct exchange custom application-level ping payload
426        let payload = ping();
427        debug!(%exchange, %payload, "sending custom application-level ping to exchange");
428
429        if ws_sink_tx.send(payload).is_err() {
430            break;
431        }
432    }
433}
434
435pub mod test_utils {
436    use crate::{
437        event::{DataKind, MarketEvent},
438        subscription::trade::PublicTrade,
439    };
440    use chrono::{DateTime, Utc};
441    use rust_decimal::Decimal;
442    use rustrade_instrument::{Side, exchange::ExchangeId};
443
444    pub fn market_event_trade_buy<InstrumentKey>(
445        time_exchange: DateTime<Utc>,
446        time_received: DateTime<Utc>,
447        instrument: InstrumentKey,
448        price: Decimal,
449        quantity: Decimal,
450    ) -> MarketEvent<InstrumentKey, DataKind> {
451        MarketEvent {
452            time_exchange,
453            time_received,
454            exchange: ExchangeId::BinanceSpot,
455            instrument,
456            kind: DataKind::Trade(PublicTrade {
457                id: "trade_id".into(),
458                price,
459                amount: quantity,
460                side: Some(Side::Buy),
461            }),
462        }
463    }
464}