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