rustrade_data/streams/builder/dynamic/mod.rs
1use crate::{
2 Identifier,
3 error::DataError,
4 exchange::{
5 binance::{
6 futures::{BinanceFuturesUsd, BinanceFuturesUsdMarket},
7 market::BinanceMarket,
8 spot::BinanceSpot,
9 },
10 bitfinex::{Bitfinex, market::BitfinexMarket},
11 bitmex::{Bitmex, market::BitmexMarket},
12 bybit::{futures::BybitPerpetualsUsd, market::BybitMarket, spot::BybitSpot},
13 coinbase::{Coinbase, market::CoinbaseMarket},
14 gateio::{
15 future::{GateioFuturesBtc, GateioFuturesUsd},
16 market::GateioMarket,
17 option::GateioOptions,
18 perpetual::{GateioPerpetualsBtc, GateioPerpetualsUsd},
19 spot::GateioSpot,
20 },
21 kraken::{Kraken, market::KrakenMarket},
22 okx::{Okx, market::OkxMarket},
23 },
24 instrument::InstrumentData,
25 streams::{
26 consumer::{MarketStreamResult, STREAM_RECONNECTION_POLICY, init_market_stream},
27 reconnect::stream::ReconnectingStream,
28 },
29 subscriber::WebSocketSubscriber,
30 subscription::{
31 SubKind, Subscription,
32 book::{OrderBookEvent, OrderBookL1, OrderBooksL1, OrderBooksL2},
33 candle::{Candle, Candles},
34 liquidation::{Liquidation, Liquidations},
35 trade::{PublicTrade, PublicTrades},
36 },
37};
38use fnv::FnvHashMap;
39use futures::{Stream, stream::SelectAll};
40use futures_util::{StreamExt, future::try_join_all};
41use itertools::Itertools;
42use rustrade_instrument::exchange::ExchangeId;
43use rustrade_integration::{
44 Validator,
45 channel::{UnboundedRx, UnboundedTx, mpsc_unbounded},
46 error::SocketError,
47};
48use std::{
49 fmt::{Debug, Display},
50 sync::Arc,
51};
52use tokio_stream::wrappers::UnboundedReceiverStream;
53use vecmap::VecMap;
54
55pub mod indexed;
56
57#[derive(Debug)]
58pub struct DynamicStreams<InstrumentKey> {
59 pub trades:
60 VecMap<ExchangeId, UnboundedReceiverStream<MarketStreamResult<InstrumentKey, PublicTrade>>>,
61 pub l1s:
62 VecMap<ExchangeId, UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookL1>>>,
63 pub l2s: VecMap<
64 ExchangeId,
65 UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookEvent>>,
66 >,
67 pub liquidations:
68 VecMap<ExchangeId, UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Liquidation>>>,
69 pub candles:
70 VecMap<ExchangeId, UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Candle>>>,
71}
72
73impl<InstrumentKey> DynamicStreams<InstrumentKey> {
74 /// Initialise a set of `Streams` by providing one or more [`Subscription`] batches.
75 ///
76 /// Each batch (ie/ `impl Iterator<Item = Subscription>`) will initialise at-least-one
77 /// WebSocket `Stream` under the hood. If the batch contains more-than-one [`ExchangeId`] and/or
78 /// [`SubKind`], it will be further split under the hood for compile-time reasons.
79 ///
80 /// ## Examples
81 /// Please see rustrade-data-rs/examples/dynamic_multi_stream_multi_exchange.rs for a
82 /// comprehensive example of how to use this market data stream initialiser.
83 #[allow(clippy::unwrap_used)] // Invariant: Channels::try_from creates entries for all exchanges in batches; lookups iterate over the same exchanges
84 pub async fn init<SubBatchIter, SubIter, Sub, Instrument>(
85 subscription_batches: SubBatchIter,
86 ) -> Result<Self, DataError>
87 where
88 SubBatchIter: IntoIterator<Item = SubIter>,
89 SubIter: IntoIterator<Item = Sub>,
90 Sub: Into<Subscription<ExchangeId, Instrument, SubKind>>,
91 Instrument: InstrumentData<Key = InstrumentKey> + Ord + Display + 'static,
92 InstrumentKey: Debug + Clone + PartialEq + Send + Sync + 'static,
93 Subscription<BinanceSpot, Instrument, PublicTrades>: Identifier<BinanceMarket>,
94 Subscription<BinanceSpot, Instrument, OrderBooksL1>: Identifier<BinanceMarket>,
95 Subscription<BinanceSpot, Instrument, OrderBooksL2>: Identifier<BinanceMarket>,
96 Subscription<BinanceFuturesUsd, Instrument, PublicTrades>: Identifier<BinanceMarket>,
97 Subscription<BinanceFuturesUsd, Instrument, OrderBooksL1>: Identifier<BinanceMarket>,
98 Subscription<BinanceFuturesUsd, Instrument, OrderBooksL2>: Identifier<BinanceMarket>,
99 Subscription<BinanceFuturesUsdMarket, Instrument, Liquidations>: Identifier<BinanceMarket>,
100 Subscription<BinanceSpot, Instrument, Candles>: Identifier<BinanceMarket>,
101 Subscription<BinanceFuturesUsdMarket, Instrument, Candles>: Identifier<BinanceMarket>,
102 Subscription<Bitfinex, Instrument, PublicTrades>: Identifier<BitfinexMarket>,
103 Subscription<Bitmex, Instrument, PublicTrades>: Identifier<BitmexMarket>,
104 Subscription<BybitSpot, Instrument, PublicTrades>: Identifier<BybitMarket>,
105 Subscription<BybitSpot, Instrument, OrderBooksL1>: Identifier<BybitMarket>,
106 Subscription<BybitSpot, Instrument, OrderBooksL2>: Identifier<BybitMarket>,
107 Subscription<BybitPerpetualsUsd, Instrument, PublicTrades>: Identifier<BybitMarket>,
108 Subscription<BybitPerpetualsUsd, Instrument, OrderBooksL1>: Identifier<BybitMarket>,
109 Subscription<BybitPerpetualsUsd, Instrument, OrderBooksL2>: Identifier<BybitMarket>,
110 Subscription<Coinbase, Instrument, PublicTrades>: Identifier<CoinbaseMarket>,
111 Subscription<GateioSpot, Instrument, PublicTrades>: Identifier<GateioMarket>,
112 Subscription<GateioFuturesUsd, Instrument, PublicTrades>: Identifier<GateioMarket>,
113 Subscription<GateioFuturesBtc, Instrument, PublicTrades>: Identifier<GateioMarket>,
114 Subscription<GateioPerpetualsUsd, Instrument, PublicTrades>: Identifier<GateioMarket>,
115 Subscription<GateioPerpetualsBtc, Instrument, PublicTrades>: Identifier<GateioMarket>,
116 Subscription<GateioOptions, Instrument, PublicTrades>: Identifier<GateioMarket>,
117 Subscription<Kraken, Instrument, PublicTrades>: Identifier<KrakenMarket>,
118 Subscription<Kraken, Instrument, OrderBooksL1>: Identifier<KrakenMarket>,
119 Subscription<Okx, Instrument, PublicTrades>: Identifier<OkxMarket>,
120 {
121 // Validate & dedup Subscription batches
122 let batches = validate_batches(subscription_batches)?;
123
124 // Generate required Channels from Subscription batches
125 let channels = Channels::try_from(&batches)?;
126
127 let futures =
128 batches.into_iter().map(|mut batch| {
129 batch.sort_unstable_by_key(|sub| (sub.exchange, sub.kind));
130 let by_exchange_by_sub_kind =
131 batch.into_iter().chunk_by(|sub| (sub.exchange, sub.kind));
132
133 let batch_futures =
134 by_exchange_by_sub_kind
135 .into_iter()
136 .map(|((exchange, sub_kind), subs)| {
137 let subs = subs.into_iter().collect::<Vec<_>>();
138 let txs = Arc::clone(&channels.txs);
139 async move {
140 match (exchange, sub_kind) {
141 (ExchangeId::BinanceSpot, SubKind::PublicTrades) => {
142 init_market_stream(
143 STREAM_RECONNECTION_POLICY,
144 WebSocketSubscriber,
145 subs.into_iter()
146 .map(|sub| {
147 Subscription::new(
148 BinanceSpot::default(),
149 sub.instrument,
150 PublicTrades,
151 )
152 })
153 .collect(),
154 )
155 .await
156 .map(|stream| {
157 tokio::spawn(stream.forward_to(
158 txs.trades.get(&exchange).unwrap().clone(),
159 ))
160 })
161 }
162 (ExchangeId::BinanceSpot, SubKind::OrderBooksL1) => {
163 init_market_stream(
164 STREAM_RECONNECTION_POLICY,
165 WebSocketSubscriber,
166 subs.into_iter()
167 .map(|sub| {
168 Subscription::new(
169 BinanceSpot::default(),
170 sub.instrument,
171 OrderBooksL1,
172 )
173 })
174 .collect(),
175 )
176 .await
177 .map(|stream| {
178 tokio::spawn(stream.forward_to(
179 txs.l1s.get(&exchange).unwrap().clone(),
180 ))
181 })
182 }
183 (ExchangeId::BinanceSpot, SubKind::OrderBooksL2) => {
184 init_market_stream(
185 STREAM_RECONNECTION_POLICY,
186 WebSocketSubscriber,
187 subs.into_iter()
188 .map(|sub| {
189 Subscription::new(
190 BinanceSpot::default(),
191 sub.instrument,
192 OrderBooksL2,
193 )
194 })
195 .collect(),
196 )
197 .await
198 .map(|stream| {
199 tokio::spawn(stream.forward_to(
200 txs.l2s.get(&exchange).unwrap().clone(),
201 ))
202 })
203 }
204 (ExchangeId::BinanceFuturesUsd, SubKind::PublicTrades) => {
205 init_market_stream(
206 STREAM_RECONNECTION_POLICY,
207 WebSocketSubscriber,
208 subs.into_iter()
209 .map(|sub| {
210 Subscription::new(
211 BinanceFuturesUsd::default(),
212 sub.instrument,
213 PublicTrades,
214 )
215 })
216 .collect(),
217 )
218 .await
219 .map(|stream| {
220 tokio::spawn(stream.forward_to(
221 txs.trades.get(&exchange).unwrap().clone(),
222 ))
223 })
224 }
225 (ExchangeId::BinanceFuturesUsd, SubKind::OrderBooksL1) => {
226 init_market_stream(
227 STREAM_RECONNECTION_POLICY,
228 WebSocketSubscriber,
229 subs.into_iter()
230 .map(|sub| {
231 Subscription::<_, Instrument, _>::new(
232 BinanceFuturesUsd::default(),
233 sub.instrument,
234 OrderBooksL1,
235 )
236 })
237 .collect(),
238 )
239 .await
240 .map(|stream| {
241 tokio::spawn(stream.forward_to(
242 txs.l1s.get(&exchange).unwrap().clone(),
243 ))
244 })
245 }
246 (ExchangeId::BinanceFuturesUsd, SubKind::OrderBooksL2) => {
247 init_market_stream(
248 STREAM_RECONNECTION_POLICY,
249 WebSocketSubscriber,
250 subs.into_iter()
251 .map(|sub| {
252 Subscription::<_, Instrument, _>::new(
253 BinanceFuturesUsd::default(),
254 sub.instrument,
255 OrderBooksL2,
256 )
257 })
258 .collect(),
259 )
260 .await
261 .map(|stream| {
262 tokio::spawn(stream.forward_to(
263 txs.l2s.get(&exchange).unwrap().clone(),
264 ))
265 })
266 }
267 (ExchangeId::BinanceFuturesUsd, SubKind::Liquidations) => {
268 // `@forceOrder` is a `/market`-tier stream — construct the
269 // market-tier server type so the `/market/ws` URL is used.
270 // Output still forwards to the BinanceFuturesUsd liquidation
271 // tx (both server types share that ExchangeId).
272 init_market_stream(
273 STREAM_RECONNECTION_POLICY,
274 WebSocketSubscriber,
275 subs.into_iter()
276 .map(|sub| {
277 Subscription::<_, Instrument, _>::new(
278 BinanceFuturesUsdMarket::default(),
279 sub.instrument,
280 Liquidations,
281 )
282 })
283 .collect(),
284 )
285 .await
286 .map(|stream| {
287 tokio::spawn(stream.forward_to(
288 txs.liquidations.get(&exchange).unwrap().clone(),
289 ))
290 })
291 }
292 (ExchangeId::BinanceSpot, SubKind::Candles { interval }) => {
293 // Every sub in this group shares one interval (the
294 // `chunk_by` key is `(exchange, sub.kind)`), so the
295 // group-key `interval` equals each `sub.kind.interval`.
296 init_market_stream(
297 STREAM_RECONNECTION_POLICY,
298 WebSocketSubscriber,
299 subs.into_iter()
300 .map(|sub| {
301 Subscription::new(
302 BinanceSpot::default(),
303 sub.instrument,
304 Candles { interval },
305 )
306 })
307 .collect(),
308 )
309 .await
310 .map(|stream| {
311 tokio::spawn(stream.forward_to(
312 txs.candles.get(&exchange).unwrap().clone(),
313 ))
314 })
315 }
316 (
317 ExchangeId::BinanceFuturesUsd,
318 SubKind::Candles { interval },
319 ) => {
320 // Futures klines are a `/market`-tier stream — construct the
321 // market-tier server type (`/market/ws`). Output forwards to
322 // the BinanceFuturesUsd candles tx (shared ExchangeId).
323 // Group is homogeneous by `(exchange, sub.kind)`, so the
324 // group-key `interval` equals every `sub.kind.interval`.
325 init_market_stream(
326 STREAM_RECONNECTION_POLICY,
327 WebSocketSubscriber,
328 subs.into_iter()
329 .map(|sub| {
330 Subscription::<_, Instrument, _>::new(
331 BinanceFuturesUsdMarket::default(),
332 sub.instrument,
333 Candles { interval },
334 )
335 })
336 .collect(),
337 )
338 .await
339 .map(|stream| {
340 tokio::spawn(stream.forward_to(
341 txs.candles.get(&exchange).unwrap().clone(),
342 ))
343 })
344 }
345 (ExchangeId::Bitfinex, SubKind::PublicTrades) => {
346 init_market_stream(
347 STREAM_RECONNECTION_POLICY,
348 WebSocketSubscriber,
349 subs.into_iter()
350 .map(|sub| {
351 Subscription::new(
352 Bitfinex,
353 sub.instrument,
354 PublicTrades,
355 )
356 })
357 .collect(),
358 )
359 .await
360 .map(|stream| {
361 tokio::spawn(stream.forward_to(
362 txs.trades.get(&exchange).unwrap().clone(),
363 ))
364 })
365 }
366 (ExchangeId::Bitmex, SubKind::PublicTrades) => {
367 init_market_stream(
368 STREAM_RECONNECTION_POLICY,
369 WebSocketSubscriber,
370 subs.into_iter()
371 .map(|sub| {
372 Subscription::new(
373 Bitmex,
374 sub.instrument,
375 PublicTrades,
376 )
377 })
378 .collect(),
379 )
380 .await
381 .map(|stream| {
382 tokio::spawn(stream.forward_to(
383 txs.trades.get(&exchange).unwrap().clone(),
384 ))
385 })
386 }
387 (ExchangeId::BybitSpot, SubKind::PublicTrades) => {
388 init_market_stream(
389 STREAM_RECONNECTION_POLICY,
390 WebSocketSubscriber,
391 subs.into_iter()
392 .map(|sub| {
393 Subscription::new(
394 BybitSpot::default(),
395 sub.instrument,
396 PublicTrades,
397 )
398 })
399 .collect(),
400 )
401 .await
402 .map(|stream| {
403 tokio::spawn(stream.forward_to(
404 txs.trades.get(&exchange).unwrap().clone(),
405 ))
406 })
407 }
408 (ExchangeId::BybitSpot, SubKind::OrderBooksL1) => {
409 init_market_stream(
410 STREAM_RECONNECTION_POLICY,
411 WebSocketSubscriber,
412 subs.into_iter()
413 .map(|sub| {
414 Subscription::new(
415 BybitSpot::default(),
416 sub.instrument,
417 OrderBooksL1,
418 )
419 })
420 .collect(),
421 )
422 .await
423 .map(|stream| {
424 tokio::spawn(stream.forward_to(
425 txs.l1s.get(&exchange).unwrap().clone(),
426 ))
427 })
428 }
429 (ExchangeId::BybitSpot, SubKind::OrderBooksL2) => {
430 init_market_stream(
431 STREAM_RECONNECTION_POLICY,
432 WebSocketSubscriber,
433 subs.into_iter()
434 .map(|sub| {
435 Subscription::new(
436 BybitSpot::default(),
437 sub.instrument,
438 OrderBooksL2,
439 )
440 })
441 .collect(),
442 )
443 .await
444 .map(|stream| {
445 tokio::spawn(stream.forward_to(
446 txs.l2s.get(&exchange).unwrap().clone(),
447 ))
448 })
449 }
450 (ExchangeId::BybitPerpetualsUsd, SubKind::PublicTrades) => {
451 init_market_stream(
452 STREAM_RECONNECTION_POLICY,
453 WebSocketSubscriber,
454 subs.into_iter()
455 .map(|sub| {
456 Subscription::new(
457 BybitPerpetualsUsd::default(),
458 sub.instrument,
459 PublicTrades,
460 )
461 })
462 .collect(),
463 )
464 .await
465 .map(|stream| {
466 tokio::spawn(stream.forward_to(
467 txs.trades.get(&exchange).unwrap().clone(),
468 ))
469 })
470 }
471 (ExchangeId::BybitPerpetualsUsd, SubKind::OrderBooksL1) => {
472 init_market_stream(
473 STREAM_RECONNECTION_POLICY,
474 WebSocketSubscriber,
475 subs.into_iter()
476 .map(|sub| {
477 Subscription::new(
478 BybitPerpetualsUsd::default(),
479 sub.instrument,
480 OrderBooksL1,
481 )
482 })
483 .collect(),
484 )
485 .await
486 .map(|stream| {
487 tokio::spawn(stream.forward_to(
488 txs.l1s.get(&exchange).unwrap().clone(),
489 ))
490 })
491 }
492 (ExchangeId::BybitPerpetualsUsd, SubKind::OrderBooksL2) => {
493 init_market_stream(
494 STREAM_RECONNECTION_POLICY,
495 WebSocketSubscriber,
496 subs.into_iter()
497 .map(|sub| {
498 Subscription::new(
499 BybitPerpetualsUsd::default(),
500 sub.instrument,
501 OrderBooksL2,
502 )
503 })
504 .collect(),
505 )
506 .await
507 .map(|stream| {
508 tokio::spawn(stream.forward_to(
509 txs.l2s.get(&exchange).unwrap().clone(),
510 ))
511 })
512 }
513 (ExchangeId::Coinbase, SubKind::PublicTrades) => {
514 init_market_stream(
515 STREAM_RECONNECTION_POLICY,
516 WebSocketSubscriber,
517 subs.into_iter()
518 .map(|sub| {
519 Subscription::new(
520 Coinbase,
521 sub.instrument,
522 PublicTrades,
523 )
524 })
525 .collect(),
526 )
527 .await
528 .map(|stream| {
529 tokio::spawn(stream.forward_to(
530 txs.trades.get(&exchange).unwrap().clone(),
531 ))
532 })
533 }
534 (ExchangeId::GateioSpot, SubKind::PublicTrades) => {
535 init_market_stream(
536 STREAM_RECONNECTION_POLICY,
537 WebSocketSubscriber,
538 subs.into_iter()
539 .map(|sub| {
540 Subscription::new(
541 GateioSpot::default(),
542 sub.instrument,
543 PublicTrades,
544 )
545 })
546 .collect(),
547 )
548 .await
549 .map(|stream| {
550 tokio::spawn(stream.forward_to(
551 txs.trades.get(&exchange).unwrap().clone(),
552 ))
553 })
554 }
555 (ExchangeId::GateioFuturesUsd, SubKind::PublicTrades) => {
556 init_market_stream(
557 STREAM_RECONNECTION_POLICY,
558 WebSocketSubscriber,
559 subs.into_iter()
560 .map(|sub| {
561 Subscription::new(
562 GateioFuturesUsd::default(),
563 sub.instrument,
564 PublicTrades,
565 )
566 })
567 .collect(),
568 )
569 .await
570 .map(|stream| {
571 tokio::spawn(stream.forward_to(
572 txs.trades.get(&exchange).unwrap().clone(),
573 ))
574 })
575 }
576 (ExchangeId::GateioFuturesBtc, SubKind::PublicTrades) => {
577 init_market_stream(
578 STREAM_RECONNECTION_POLICY,
579 WebSocketSubscriber,
580 subs.into_iter()
581 .map(|sub| {
582 Subscription::new(
583 GateioFuturesBtc::default(),
584 sub.instrument,
585 PublicTrades,
586 )
587 })
588 .collect(),
589 )
590 .await
591 .map(|stream| {
592 tokio::spawn(stream.forward_to(
593 txs.trades.get(&exchange).unwrap().clone(),
594 ))
595 })
596 }
597 (ExchangeId::GateioPerpetualsUsd, SubKind::PublicTrades) => {
598 init_market_stream(
599 STREAM_RECONNECTION_POLICY,
600 WebSocketSubscriber,
601 subs.into_iter()
602 .map(|sub| {
603 Subscription::new(
604 GateioPerpetualsUsd::default(),
605 sub.instrument,
606 PublicTrades,
607 )
608 })
609 .collect(),
610 )
611 .await
612 .map(|stream| {
613 tokio::spawn(stream.forward_to(
614 txs.trades.get(&exchange).unwrap().clone(),
615 ))
616 })
617 }
618 (ExchangeId::GateioPerpetualsBtc, SubKind::PublicTrades) => {
619 init_market_stream(
620 STREAM_RECONNECTION_POLICY,
621 WebSocketSubscriber,
622 subs.into_iter()
623 .map(|sub| {
624 Subscription::new(
625 GateioPerpetualsBtc::default(),
626 sub.instrument,
627 PublicTrades,
628 )
629 })
630 .collect(),
631 )
632 .await
633 .map(|stream| {
634 tokio::spawn(stream.forward_to(
635 txs.trades.get(&exchange).unwrap().clone(),
636 ))
637 })
638 }
639 (ExchangeId::GateioOptions, SubKind::PublicTrades) => {
640 init_market_stream(
641 STREAM_RECONNECTION_POLICY,
642 WebSocketSubscriber,
643 subs.into_iter()
644 .map(|sub| {
645 Subscription::new(
646 GateioOptions::default(),
647 sub.instrument,
648 PublicTrades,
649 )
650 })
651 .collect(),
652 )
653 .await
654 .map(|stream| {
655 tokio::spawn(stream.forward_to(
656 txs.trades.get(&exchange).unwrap().clone(),
657 ))
658 })
659 }
660 (ExchangeId::Kraken, SubKind::PublicTrades) => {
661 init_market_stream(
662 STREAM_RECONNECTION_POLICY,
663 WebSocketSubscriber,
664 subs.into_iter()
665 .map(|sub| {
666 Subscription::new(
667 Kraken,
668 sub.instrument,
669 PublicTrades,
670 )
671 })
672 .collect(),
673 )
674 .await
675 .map(|stream| {
676 tokio::spawn(stream.forward_to(
677 txs.trades.get(&exchange).unwrap().clone(),
678 ))
679 })
680 }
681 (ExchangeId::Kraken, SubKind::OrderBooksL1) => {
682 init_market_stream(
683 STREAM_RECONNECTION_POLICY,
684 WebSocketSubscriber,
685 subs.into_iter()
686 .map(|sub| {
687 Subscription::new(
688 Kraken,
689 sub.instrument,
690 OrderBooksL1,
691 )
692 })
693 .collect(),
694 )
695 .await
696 .map(|stream| {
697 tokio::spawn(stream.forward_to(
698 txs.l1s.get(&exchange).unwrap().clone(),
699 ))
700 })
701 }
702 (ExchangeId::Okx, SubKind::PublicTrades) => init_market_stream(
703 STREAM_RECONNECTION_POLICY,
704 WebSocketSubscriber,
705 subs.into_iter()
706 .map(|sub| {
707 Subscription::new(Okx, sub.instrument, PublicTrades)
708 })
709 .collect(),
710 )
711 .await
712 .map(|stream| {
713 tokio::spawn(
714 stream.forward_to(
715 txs.trades.get(&exchange).unwrap().clone(),
716 ),
717 )
718 }),
719 (exchange, sub_kind) => {
720 Err(DataError::Unsupported { exchange, sub_kind })
721 }
722 }
723 }
724 });
725
726 try_join_all(batch_futures)
727 });
728
729 try_join_all(futures).await?;
730
731 Ok(Self {
732 trades: channels
733 .rxs
734 .trades
735 .into_iter()
736 .map(|(exchange, rx)| (exchange, rx.into_stream()))
737 .collect(),
738 l1s: channels
739 .rxs
740 .l1s
741 .into_iter()
742 .map(|(exchange, rx)| (exchange, rx.into_stream()))
743 .collect(),
744 l2s: channels
745 .rxs
746 .l2s
747 .into_iter()
748 .map(|(exchange, rx)| (exchange, rx.into_stream()))
749 .collect(),
750 liquidations: channels
751 .rxs
752 .liquidations
753 .into_iter()
754 .map(|(exchange, rx)| (exchange, rx.into_stream()))
755 .collect(),
756 candles: channels
757 .rxs
758 .candles
759 .into_iter()
760 .map(|(exchange, rx)| (exchange, rx.into_stream()))
761 .collect(),
762 })
763 }
764
765 /// Remove an exchange [`PublicTrade`] `Stream` from the [`DynamicStreams`] collection.
766 ///
767 /// Note that calling this method will permanently remove this `Stream` from [`Self`].
768 pub fn select_trades(
769 &mut self,
770 exchange: ExchangeId,
771 ) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, PublicTrade>>> {
772 self.trades.remove(&exchange)
773 }
774
775 /// Select and merge every exchange [`PublicTrade`] `Stream` using
776 /// [`SelectAll`](futures_util::stream::select_all::select_all).
777 pub fn select_all_trades(
778 &mut self,
779 ) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, PublicTrade>>> {
780 futures_util::stream::select_all::select_all(std::mem::take(&mut self.trades).into_values())
781 }
782
783 /// Remove an exchange [`OrderBookL1`] `Stream` from the [`DynamicStreams`] collection.
784 ///
785 /// Note that calling this method will permanently remove this `Stream` from [`Self`].
786 pub fn select_l1s(
787 &mut self,
788 exchange: ExchangeId,
789 ) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookL1>>> {
790 self.l1s.remove(&exchange)
791 }
792
793 /// Select and merge every exchange [`OrderBookL1`] `Stream` using
794 /// [`SelectAll`](futures_util::stream::select_all::select_all).
795 pub fn select_all_l1s(
796 &mut self,
797 ) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookL1>>> {
798 futures_util::stream::select_all::select_all(std::mem::take(&mut self.l1s).into_values())
799 }
800
801 /// Remove an exchange [`OrderBookEvent`] `Stream` from the [`DynamicStreams`] collection.
802 ///
803 /// Note that calling this method will permanently remove this `Stream` from [`Self`].
804 pub fn select_l2s(
805 &mut self,
806 exchange: ExchangeId,
807 ) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookEvent>>> {
808 self.l2s.remove(&exchange)
809 }
810
811 /// Select and merge every exchange [`OrderBookEvent`] `Stream` using
812 /// [`SelectAll`](futures_util::stream::select_all::select_all).
813 pub fn select_all_l2s(
814 &mut self,
815 ) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookEvent>>> {
816 futures_util::stream::select_all::select_all(std::mem::take(&mut self.l2s).into_values())
817 }
818
819 /// Remove an exchange [`Liquidation`] `Stream` from the [`DynamicStreams`] collection.
820 ///
821 /// Note that calling this method will permanently remove this `Stream` from [`Self`].
822 pub fn select_liquidations(
823 &mut self,
824 exchange: ExchangeId,
825 ) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Liquidation>>> {
826 self.liquidations.remove(&exchange)
827 }
828
829 /// Select and merge every exchange [`Liquidation`] `Stream` using
830 /// [`SelectAll`](futures_util::stream::select_all::select_all).
831 pub fn select_all_liquidations(
832 &mut self,
833 ) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Liquidation>>> {
834 futures_util::stream::select_all::select_all(
835 std::mem::take(&mut self.liquidations).into_values(),
836 )
837 }
838
839 /// Remove an exchange [`Candle`] `Stream` from the [`DynamicStreams`] collection.
840 ///
841 /// Note that calling this method will permanently remove this `Stream` from [`Self`].
842 ///
843 /// # Mixed intervals
844 ///
845 /// `DynamicStreams` keys candle streams by [`ExchangeId`] only, so subscribing to
846 /// multiple intervals on one exchange (e.g. `Candles { Min1 }` + `Candles { Hour1 }`
847 /// on `BinanceSpot`) merges them into this single per-exchange stream. Each distinct
848 /// interval is a separate upstream WebSocket subscription (mind venue per-connection
849 /// stream limits at high symbol × interval fan-out). Because [`Candle`] carries no
850 /// `interval` field, a consumer mixing intervals must recover it from `close_time`
851 /// spacing or the originating subscription; if per-interval routing matters, use the
852 /// typed [`Streams`](crate::streams::Streams) builder with one
853 /// [`StreamSelector`](crate::exchange::StreamSelector) per interval.
854 pub fn select_candles(
855 &mut self,
856 exchange: ExchangeId,
857 ) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Candle>>> {
858 self.candles.remove(&exchange)
859 }
860
861 /// Select and merge every exchange [`Candle`] `Stream` using
862 /// [`SelectAll`](futures_util::stream::select_all::select_all).
863 ///
864 /// See [`select_candles`](Self::select_candles) for how multiple intervals on one
865 /// exchange are merged — the merged `Candle`s carry no `interval` to disambiguate them.
866 pub fn select_all_candles(
867 &mut self,
868 ) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Candle>>> {
869 futures_util::stream::select_all::select_all(
870 std::mem::take(&mut self.candles).into_values(),
871 )
872 }
873
874 /// Select and merge every exchange `Stream` for every data type using [`select_all`](futures_util::stream::select_all::select_all)
875 ///
876 /// Note that using [`MarketStreamResult<Instrument, DataKind>`] as the `Output` is suitable for most
877 /// use cases.
878 pub fn select_all<Output>(self) -> impl Stream<Item = Output>
879 where
880 InstrumentKey: Send + 'static,
881 Output: 'static,
882 MarketStreamResult<InstrumentKey, PublicTrade>: Into<Output>,
883 MarketStreamResult<InstrumentKey, OrderBookL1>: Into<Output>,
884 MarketStreamResult<InstrumentKey, OrderBookEvent>: Into<Output>,
885 MarketStreamResult<InstrumentKey, Liquidation>: Into<Output>,
886 MarketStreamResult<InstrumentKey, Candle>: Into<Output>,
887 {
888 let Self {
889 trades,
890 l1s,
891 l2s,
892 liquidations,
893 candles,
894 } = self;
895
896 let trades = trades
897 .into_values()
898 .map(|stream| stream.map(MarketStreamResult::into).boxed());
899
900 let l1s = l1s
901 .into_values()
902 .map(|stream| stream.map(MarketStreamResult::into).boxed());
903
904 let l2s = l2s
905 .into_values()
906 .map(|stream| stream.map(MarketStreamResult::into).boxed());
907
908 let liquidations = liquidations
909 .into_values()
910 .map(|stream| stream.map(MarketStreamResult::into).boxed());
911
912 let candles = candles
913 .into_values()
914 .map(|stream| stream.map(MarketStreamResult::into).boxed());
915
916 let all = trades
917 .chain(l1s)
918 .chain(l2s)
919 .chain(liquidations)
920 .chain(candles);
921
922 futures_util::stream::select_all::select_all(all)
923 }
924}
925
926pub fn validate_batches<SubBatchIter, SubIter, Sub, Instrument>(
927 batches: SubBatchIter,
928) -> Result<Vec<Vec<Subscription<ExchangeId, Instrument, SubKind>>>, DataError>
929where
930 SubBatchIter: IntoIterator<Item = SubIter>,
931 SubIter: IntoIterator<Item = Sub>,
932 Sub: Into<Subscription<ExchangeId, Instrument, SubKind>>,
933 Instrument: InstrumentData + Ord,
934{
935 batches
936 .into_iter()
937 .map(validate_subscriptions::<SubIter, Sub, Instrument>)
938 .collect()
939}
940
941pub fn validate_subscriptions<SubIter, Sub, Instrument>(
942 batch: SubIter,
943) -> Result<Vec<Subscription<ExchangeId, Instrument, SubKind>>, DataError>
944where
945 SubIter: IntoIterator<Item = Sub>,
946 Sub: Into<Subscription<ExchangeId, Instrument, SubKind>>,
947 Instrument: InstrumentData + Ord,
948{
949 // Validate Subscriptions
950 let mut batch = batch
951 .into_iter()
952 .map(Sub::into)
953 .map(Validator::validate)
954 .collect::<Result<Vec<_>, SocketError>>()?;
955
956 // Remove duplicate Subscriptions
957 batch.sort();
958 batch.dedup();
959
960 Ok(batch)
961}
962
963struct Channels<InstrumentKey> {
964 txs: Arc<Txs<InstrumentKey>>,
965 rxs: Rxs<InstrumentKey>,
966}
967
968impl<'a, Instrument> TryFrom<&'a Vec<Vec<Subscription<ExchangeId, Instrument, SubKind>>>>
969 for Channels<Instrument::Key>
970where
971 Instrument: InstrumentData,
972{
973 type Error = DataError;
974
975 fn try_from(
976 value: &'a Vec<Vec<Subscription<ExchangeId, Instrument, SubKind>>>,
977 ) -> Result<Self, Self::Error> {
978 let mut txs = Txs::default();
979 let mut rxs = Rxs::default();
980
981 for sub in value.iter().flatten() {
982 match sub.kind {
983 SubKind::PublicTrades => {
984 if let (None, None) =
985 (txs.trades.get(&sub.exchange), rxs.trades.get(&sub.exchange))
986 {
987 let (tx, rx) = mpsc_unbounded();
988 txs.trades.insert(sub.exchange, tx);
989 rxs.trades.insert(sub.exchange, rx);
990 }
991 }
992 SubKind::OrderBooksL1 => {
993 if let (None, None) = (txs.l1s.get(&sub.exchange), rxs.l1s.get(&sub.exchange)) {
994 let (tx, rx) = mpsc_unbounded();
995 txs.l1s.insert(sub.exchange, tx);
996 rxs.l1s.insert(sub.exchange, rx);
997 }
998 }
999 SubKind::OrderBooksL2 => {
1000 if let (None, None) = (txs.l2s.get(&sub.exchange), rxs.l2s.get(&sub.exchange)) {
1001 let (tx, rx) = mpsc_unbounded();
1002 txs.l2s.insert(sub.exchange, tx);
1003 rxs.l2s.insert(sub.exchange, rx);
1004 }
1005 }
1006 SubKind::Liquidations => {
1007 if let (None, None) = (
1008 txs.liquidations.get(&sub.exchange),
1009 rxs.liquidations.get(&sub.exchange),
1010 ) {
1011 let (tx, rx) = mpsc_unbounded();
1012 txs.liquidations.insert(sub.exchange, tx);
1013 rxs.liquidations.insert(sub.exchange, rx);
1014 }
1015 }
1016 SubKind::Candles { .. } => {
1017 if let (None, None) = (
1018 txs.candles.get(&sub.exchange),
1019 rxs.candles.get(&sub.exchange),
1020 ) {
1021 let (tx, rx) = mpsc_unbounded();
1022 txs.candles.insert(sub.exchange, tx);
1023 rxs.candles.insert(sub.exchange, rx);
1024 }
1025 }
1026 sub_kind @ (SubKind::OrderBooksL3 | SubKind::Quotes) => {
1027 return Err(DataError::Unsupported {
1028 exchange: sub.exchange,
1029 sub_kind,
1030 });
1031 }
1032 }
1033 }
1034
1035 Ok(Channels {
1036 txs: Arc::new(txs),
1037 rxs,
1038 })
1039 }
1040}
1041
1042struct Txs<InstrumentKey> {
1043 trades: FnvHashMap<ExchangeId, UnboundedTx<MarketStreamResult<InstrumentKey, PublicTrade>>>,
1044 l1s: FnvHashMap<ExchangeId, UnboundedTx<MarketStreamResult<InstrumentKey, OrderBookL1>>>,
1045 l2s: FnvHashMap<ExchangeId, UnboundedTx<MarketStreamResult<InstrumentKey, OrderBookEvent>>>,
1046 liquidations:
1047 FnvHashMap<ExchangeId, UnboundedTx<MarketStreamResult<InstrumentKey, Liquidation>>>,
1048 candles: FnvHashMap<ExchangeId, UnboundedTx<MarketStreamResult<InstrumentKey, Candle>>>,
1049}
1050
1051impl<InstrumentKey> Default for Txs<InstrumentKey> {
1052 fn default() -> Self {
1053 Self {
1054 trades: Default::default(),
1055 l1s: Default::default(),
1056 l2s: Default::default(),
1057 liquidations: Default::default(),
1058 candles: Default::default(),
1059 }
1060 }
1061}
1062
1063struct Rxs<InstrumentKey> {
1064 trades: FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, PublicTrade>>>,
1065 l1s: FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, OrderBookL1>>>,
1066 l2s: FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, OrderBookEvent>>>,
1067 liquidations:
1068 FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, Liquidation>>>,
1069 candles: FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, Candle>>>,
1070}
1071
1072impl<InstrumentKey> Default for Rxs<InstrumentKey> {
1073 fn default() -> Self {
1074 Self {
1075 trades: Default::default(),
1076 l1s: Default::default(),
1077 l2s: Default::default(),
1078 liquidations: Default::default(),
1079 candles: Default::default(),
1080 }
1081 }
1082}