Skip to main content

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}