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