1#[cfg(feature = "alpaca")]
2use crate::exchange::alpaca::{AlpacaSubscriber, market::AlpacaInstrument};
3#[cfg(feature = "hyperliquid")]
4use crate::exchange::hyperliquid::market::HyperliquidInstrument;
5#[cfg(feature = "lse")]
6use crate::exchange::lse::{live::LseSubscriber, market::LseInstrument};
7#[cfg(feature = "massive")]
8use crate::exchange::massive::{MassiveSubscriber, market::MassiveInstrument};
9use crate::{
10 error::DataError,
11 instrument::InstrumentData,
12 streams::consumer::MarketStreamResult,
13 subscription::{
14 SubKind, Subscription,
15 book::{OrderBookEvent, OrderBookL1},
16 candle::Candle,
17 liquidation::Liquidation,
18 trade::PublicTrade,
19 },
20};
21use fnv::FnvHashMap;
22use futures::{Stream, stream::SelectAll};
23use futures_util::{StreamExt, future::join_all};
24use itertools::Itertools;
25use plan::{Route, Txs};
26use rustrade_instrument::exchange::ExchangeId;
27use rustrade_integration::{
28 Validator,
29 channel::{UnboundedRx, mpsc_unbounded},
30 error::SocketError,
31};
32use std::fmt::Debug;
33use tokio::task::JoinHandle;
34use tokio_stream::wrappers::UnboundedReceiverStream;
35use vecmap::VecMap;
36
37pub mod indexed;
38
39mod plan;
40
41#[derive(Debug)]
42pub struct DynamicStreams<InstrumentKey> {
43 pub trades:
44 VecMap<ExchangeId, UnboundedReceiverStream<MarketStreamResult<InstrumentKey, PublicTrade>>>,
45 pub l1s:
46 VecMap<ExchangeId, UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookL1>>>,
47 pub l2s: VecMap<
48 ExchangeId,
49 UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookEvent>>,
50 >,
51 pub liquidations:
52 VecMap<ExchangeId, UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Liquidation>>>,
53 pub candles:
54 VecMap<ExchangeId, UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Candle>>>,
55}
56
57#[derive(Clone, Debug, Default)]
92pub struct DynamicSubscribers {
93 #[cfg(feature = "alpaca")]
94 alpaca: Option<AlpacaSubscriber>,
95 #[cfg(feature = "lse")]
96 lse: Option<LseSubscriber>,
97 #[cfg(feature = "massive")]
98 massive: Option<MassiveSubscriber>,
99}
100
101impl DynamicSubscribers {
102 #[cfg(feature = "alpaca")]
120 #[must_use]
121 pub fn with_alpaca(mut self, subscriber: AlpacaSubscriber) -> Self {
122 self.alpaca = Some(subscriber);
123 self
124 }
125
126 #[cfg(feature = "lse")]
147 #[must_use]
148 pub fn with_lse(mut self, subscriber: LseSubscriber) -> Self {
149 self.lse = Some(subscriber);
150 self
151 }
152
153 #[cfg(feature = "massive")]
184 #[must_use]
185 pub fn with_massive(mut self, subscriber: MassiveSubscriber) -> Self {
186 self.massive = Some(subscriber);
187 self
188 }
189}
190
191pub trait DynamicInstrument: Route {}
216
217impl<Instrument> DynamicInstrument for Instrument where Instrument: Route {}
218
219#[cfg(feature = "alpaca")]
222pub trait DynamicAlpacaInstrument: AlpacaInstrument {}
223
224#[cfg(feature = "alpaca")]
225impl<Instrument> DynamicAlpacaInstrument for Instrument where Instrument: AlpacaInstrument {}
226
227#[cfg(not(feature = "alpaca"))]
229pub trait DynamicAlpacaInstrument {}
230
231#[cfg(not(feature = "alpaca"))]
232impl<Instrument> DynamicAlpacaInstrument for Instrument {}
233
234#[cfg(feature = "lse")]
240pub trait DynamicLseInstrument: LseInstrument {}
241
242#[cfg(feature = "lse")]
243impl<Instrument> DynamicLseInstrument for Instrument where Instrument: LseInstrument {}
244
245#[cfg(not(feature = "lse"))]
247pub trait DynamicLseInstrument {}
248
249#[cfg(not(feature = "lse"))]
250impl<Instrument> DynamicLseInstrument for Instrument {}
251
252#[cfg(feature = "massive")]
255pub trait DynamicMassiveInstrument: MassiveInstrument {}
256
257#[cfg(feature = "massive")]
258impl<Instrument> DynamicMassiveInstrument for Instrument where Instrument: MassiveInstrument {}
259
260#[cfg(not(feature = "massive"))]
262pub trait DynamicMassiveInstrument {}
263
264#[cfg(not(feature = "massive"))]
265impl<Instrument> DynamicMassiveInstrument for Instrument {}
266
267#[cfg(feature = "hyperliquid")]
270pub trait DynamicHyperliquidInstrument: HyperliquidInstrument {}
271
272#[cfg(feature = "hyperliquid")]
273impl<Instrument> DynamicHyperliquidInstrument for Instrument where Instrument: HyperliquidInstrument {}
274
275#[cfg(not(feature = "hyperliquid"))]
278pub trait DynamicHyperliquidInstrument {}
279
280#[cfg(not(feature = "hyperliquid"))]
281impl<Instrument> DynamicHyperliquidInstrument for Instrument {}
282
283impl<InstrumentKey> DynamicStreams<InstrumentKey> {
284 pub async fn init<SubBatchIter, SubIter, Sub, Instrument>(
294 subscription_batches: SubBatchIter,
295 ) -> Result<Self, DataError>
296 where
297 SubBatchIter: IntoIterator<Item = SubIter>,
298 SubIter: IntoIterator<Item = Sub>,
299 Sub: Into<Subscription<ExchangeId, Instrument, SubKind>>,
300 Instrument: DynamicInstrument + InstrumentData<Key = InstrumentKey>,
301 InstrumentKey: Debug + Clone + PartialEq + Send + Sync + 'static,
302 {
303 Self::init_with(&DynamicSubscribers::default(), subscription_batches).await
304 }
305
306 pub async fn init_with<SubBatchIter, SubIter, Sub, Instrument>(
344 subscribers: &DynamicSubscribers,
345 subscription_batches: SubBatchIter,
346 ) -> Result<Self, DataError>
347 where
348 SubBatchIter: IntoIterator<Item = SubIter>,
349 SubIter: IntoIterator<Item = Sub>,
350 Sub: Into<Subscription<ExchangeId, Instrument, SubKind>>,
351 Instrument: DynamicInstrument + InstrumentData<Key = InstrumentKey>,
352 InstrumentKey: Debug + Clone + PartialEq + Send + Sync + 'static,
353 {
354 let batches = validate_batches(subscription_batches)?;
356
357 let Channels { txs, rxs } = Channels::try_from(&batches)?;
359
360 let groups = route_batches(batches, &txs, subscribers)?;
361
362 settle(join_all(groups).await).await?;
363
364 Ok(Self {
365 trades: rxs
366 .trades
367 .into_iter()
368 .map(|(exchange, rx)| (exchange, rx.into_stream()))
369 .collect(),
370 l1s: rxs
371 .l1s
372 .into_iter()
373 .map(|(exchange, rx)| (exchange, rx.into_stream()))
374 .collect(),
375 l2s: rxs
376 .l2s
377 .into_iter()
378 .map(|(exchange, rx)| (exchange, rx.into_stream()))
379 .collect(),
380 liquidations: rxs
381 .liquidations
382 .into_iter()
383 .map(|(exchange, rx)| (exchange, rx.into_stream()))
384 .collect(),
385 candles: rxs
386 .candles
387 .into_iter()
388 .map(|(exchange, rx)| (exchange, rx.into_stream()))
389 .collect(),
390 })
391 }
392
393 pub fn select_trades(
397 &mut self,
398 exchange: ExchangeId,
399 ) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, PublicTrade>>> {
400 self.trades.remove(&exchange)
401 }
402
403 pub fn select_all_trades(
406 &mut self,
407 ) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, PublicTrade>>> {
408 futures_util::stream::select_all::select_all(std::mem::take(&mut self.trades).into_values())
409 }
410
411 pub fn select_l1s(
415 &mut self,
416 exchange: ExchangeId,
417 ) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookL1>>> {
418 self.l1s.remove(&exchange)
419 }
420
421 pub fn select_all_l1s(
424 &mut self,
425 ) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookL1>>> {
426 futures_util::stream::select_all::select_all(std::mem::take(&mut self.l1s).into_values())
427 }
428
429 pub fn select_l2s(
433 &mut self,
434 exchange: ExchangeId,
435 ) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookEvent>>> {
436 self.l2s.remove(&exchange)
437 }
438
439 pub fn select_all_l2s(
442 &mut self,
443 ) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookEvent>>> {
444 futures_util::stream::select_all::select_all(std::mem::take(&mut self.l2s).into_values())
445 }
446
447 pub fn select_liquidations(
451 &mut self,
452 exchange: ExchangeId,
453 ) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Liquidation>>> {
454 self.liquidations.remove(&exchange)
455 }
456
457 pub fn select_all_liquidations(
460 &mut self,
461 ) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Liquidation>>> {
462 futures_util::stream::select_all::select_all(
463 std::mem::take(&mut self.liquidations).into_values(),
464 )
465 }
466
467 pub fn select_candles(
483 &mut self,
484 exchange: ExchangeId,
485 ) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Candle>>> {
486 self.candles.remove(&exchange)
487 }
488
489 pub fn select_all_candles(
495 &mut self,
496 ) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Candle>>> {
497 futures_util::stream::select_all::select_all(
498 std::mem::take(&mut self.candles).into_values(),
499 )
500 }
501
502 pub fn select_all<Output>(self) -> impl Stream<Item = Output>
507 where
508 InstrumentKey: Send + 'static,
509 Output: 'static,
510 MarketStreamResult<InstrumentKey, PublicTrade>: Into<Output>,
511 MarketStreamResult<InstrumentKey, OrderBookL1>: Into<Output>,
512 MarketStreamResult<InstrumentKey, OrderBookEvent>: Into<Output>,
513 MarketStreamResult<InstrumentKey, Liquidation>: Into<Output>,
514 MarketStreamResult<InstrumentKey, Candle>: Into<Output>,
515 {
516 let Self {
517 trades,
518 l1s,
519 l2s,
520 liquidations,
521 candles,
522 } = self;
523
524 let trades = trades
525 .into_values()
526 .map(|stream| stream.map(MarketStreamResult::into).boxed());
527
528 let l1s = l1s
529 .into_values()
530 .map(|stream| stream.map(MarketStreamResult::into).boxed());
531
532 let l2s = l2s
533 .into_values()
534 .map(|stream| stream.map(MarketStreamResult::into).boxed());
535
536 let liquidations = liquidations
537 .into_values()
538 .map(|stream| stream.map(MarketStreamResult::into).boxed());
539
540 let candles = candles
541 .into_values()
542 .map(|stream| stream.map(MarketStreamResult::into).boxed());
543
544 let all = trades
545 .chain(l1s)
546 .chain(l2s)
547 .chain(liquidations)
548 .chain(candles);
549
550 futures_util::stream::select_all::select_all(all)
551 }
552}
553
554pub fn validate_batches<SubBatchIter, SubIter, Sub, Instrument>(
555 batches: SubBatchIter,
556) -> Result<Vec<Vec<Subscription<ExchangeId, Instrument, SubKind>>>, DataError>
557where
558 SubBatchIter: IntoIterator<Item = SubIter>,
559 SubIter: IntoIterator<Item = Sub>,
560 Sub: Into<Subscription<ExchangeId, Instrument, SubKind>>,
561 Instrument: InstrumentData + Ord,
562{
563 batches
564 .into_iter()
565 .map(validate_subscriptions::<SubIter, Sub, Instrument>)
566 .collect()
567}
568
569pub fn validate_subscriptions<SubIter, Sub, Instrument>(
570 batch: SubIter,
571) -> Result<Vec<Subscription<ExchangeId, Instrument, SubKind>>, DataError>
572where
573 SubIter: IntoIterator<Item = Sub>,
574 Sub: Into<Subscription<ExchangeId, Instrument, SubKind>>,
575 Instrument: InstrumentData + Ord,
576{
577 let mut batch = batch
579 .into_iter()
580 .map(Sub::into)
581 .map(Validator::validate)
582 .collect::<Result<Vec<_>, SocketError>>()?;
583
584 batch.sort();
586 batch.dedup();
587
588 Ok(batch)
589}
590
591fn route_batches<Instrument>(
596 batches: Vec<Vec<Subscription<ExchangeId, Instrument, SubKind>>>,
597 txs: &Txs<Instrument::Key>,
598 subscribers: &DynamicSubscribers,
599) -> Result<Vec<plan::GroupFuture>, DataError>
600where
601 Instrument: DynamicInstrument,
602{
603 batches
604 .into_iter()
605 .flat_map(|mut batch| {
606 batch.sort_unstable_by_key(|sub| (sub.exchange, sub.kind));
607 batch
608 .into_iter()
609 .chunk_by(|sub| (sub.exchange, sub.kind))
610 .into_iter()
611 .map(|(key, subs)| (key, subs.collect::<Vec<_>>()))
612 .collect::<Vec<_>>()
613 })
614 .map(|((exchange, sub_kind), subs)| {
615 Instrument::route(exchange, sub_kind, subs, txs, subscribers)
616 })
617 .collect()
618}
619
620async fn settle(results: Vec<Result<JoinHandle<()>, DataError>>) -> Result<(), DataError> {
628 let (forwarders, errors): (Vec<_>, Vec<_>) = results.into_iter().partition_result();
629
630 match errors.into_iter().next() {
631 None => Ok(()),
632 Some(error) => {
633 forwarders.iter().for_each(JoinHandle::abort);
634 join_all(forwarders).await;
635 Err(error)
636 }
637 }
638}
639
640struct Channels<InstrumentKey> {
641 txs: Txs<InstrumentKey>,
642 rxs: Rxs<InstrumentKey>,
643}
644
645impl<'a, Instrument> TryFrom<&'a Vec<Vec<Subscription<ExchangeId, Instrument, SubKind>>>>
646 for Channels<Instrument::Key>
647where
648 Instrument: InstrumentData,
649{
650 type Error = DataError;
651
652 fn try_from(
653 value: &'a Vec<Vec<Subscription<ExchangeId, Instrument, SubKind>>>,
654 ) -> Result<Self, Self::Error> {
655 let mut txs = Txs::default();
656 let mut rxs = Rxs::default();
657
658 for sub in value.iter().flatten() {
659 match sub.kind {
660 SubKind::PublicTrades => {
661 if let (None, None) =
662 (txs.trades.get(&sub.exchange), rxs.trades.get(&sub.exchange))
663 {
664 let (tx, rx) = mpsc_unbounded();
665 txs.trades.insert(sub.exchange, tx);
666 rxs.trades.insert(sub.exchange, rx);
667 }
668 }
669 SubKind::OrderBooksL1 => {
670 if let (None, None) = (txs.l1s.get(&sub.exchange), rxs.l1s.get(&sub.exchange)) {
671 let (tx, rx) = mpsc_unbounded();
672 txs.l1s.insert(sub.exchange, tx);
673 rxs.l1s.insert(sub.exchange, rx);
674 }
675 }
676 SubKind::OrderBooksL2 => {
677 if let (None, None) = (txs.l2s.get(&sub.exchange), rxs.l2s.get(&sub.exchange)) {
678 let (tx, rx) = mpsc_unbounded();
679 txs.l2s.insert(sub.exchange, tx);
680 rxs.l2s.insert(sub.exchange, rx);
681 }
682 }
683 SubKind::Liquidations => {
684 if let (None, None) = (
685 txs.liquidations.get(&sub.exchange),
686 rxs.liquidations.get(&sub.exchange),
687 ) {
688 let (tx, rx) = mpsc_unbounded();
689 txs.liquidations.insert(sub.exchange, tx);
690 rxs.liquidations.insert(sub.exchange, rx);
691 }
692 }
693 SubKind::Candles { .. } => {
694 if let (None, None) = (
695 txs.candles.get(&sub.exchange),
696 rxs.candles.get(&sub.exchange),
697 ) {
698 let (tx, rx) = mpsc_unbounded();
699 txs.candles.insert(sub.exchange, tx);
700 rxs.candles.insert(sub.exchange, rx);
701 }
702 }
703 sub_kind @ (SubKind::OrderBooksL3 | SubKind::Quotes) => {
704 return Err(DataError::Unsupported {
705 exchange: sub.exchange,
706 sub_kind,
707 });
708 }
709 }
710 }
711
712 Ok(Channels { txs, rxs })
713 }
714}
715
716struct Rxs<InstrumentKey> {
717 trades: FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, PublicTrade>>>,
718 l1s: FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, OrderBookL1>>>,
719 l2s: FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, OrderBookEvent>>>,
720 liquidations:
721 FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, Liquidation>>>,
722 candles: FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, Candle>>>,
723}
724
725impl<InstrumentKey> Default for Rxs<InstrumentKey> {
726 fn default() -> Self {
727 Self {
728 trades: Default::default(),
729 l1s: Default::default(),
730 l2s: Default::default(),
731 liquidations: Default::default(),
732 candles: Default::default(),
733 }
734 }
735}
736
737#[cfg(test)]
738#[allow(clippy::unwrap_used)] mod tests {
740 use super::*;
741 use crate::subscription::{candle::CandleInterval, exchange_supports_instrument_kind_sub_kind};
742 use chrono::{TimeZone, Utc};
743 use rust_decimal::Decimal;
744 use rustrade_instrument::instrument::{
745 kind::option::{OptionExercise, OptionKind},
746 market_data::{
747 MarketDataInstrument,
748 kind::{MarketDataFutureContract, MarketDataInstrumentKind, MarketDataOptionContract},
749 },
750 };
751
752 fn every_exchange() -> Vec<ExchangeId> {
754 use ExchangeId::*;
755
756 let every = vec![
757 Other,
758 Simulated,
759 Mock,
760 BinanceFuturesCoin,
761 BinanceFuturesUsd,
762 BinanceMargin,
763 BinanceOptions,
764 BinancePortfolioMargin,
765 BinanceSpot,
766 BinanceUs,
767 Bitazza,
768 Bitfinex,
769 Bitflyer,
770 Bitget,
771 Bitmart,
772 BitmartFuturesUsd,
773 Bitmex,
774 Bitso,
775 Bitstamp,
776 Bitvavo,
777 Bithumb,
778 BybitPerpetualsUsd,
779 BybitSpot,
780 Cexio,
781 Coinbase,
782 CoinbaseInternational,
783 Cryptocom,
784 DatabentoDbeq,
785 DatabentoGlbx,
786 DatabentoOpra,
787 DatabentoXnas,
788 DatabentoXnys,
789 Deribit,
790 GateioFuturesBtc,
791 GateioFuturesUsd,
792 GateioOptions,
793 GateioPerpetualsBtc,
794 GateioPerpetualsUsd,
795 GateioSpot,
796 Gemini,
797 Hitbtc,
798 Htx,
799 HyperliquidPerp,
800 HyperliquidSpot,
801 AlpacaBroker,
802 AlpacaCrypto,
803 AlpacaIex,
804 AlpacaSip,
805 Ibkr,
806 Kraken,
807 Kucoin,
808 Liquid,
809 Massive,
810 Mexc,
811 Okx,
812 Poloniex,
813 LseFx,
814 LseCrypto,
815 LseEquities,
816 LseFutures,
817 LseCfd,
818 LseOptions,
819 MassiveStocks,
820 MassiveCrypto,
821 MassiveForex,
822 MassiveOptions,
823 ];
824
825 for exchange in &every {
828 match exchange {
829 Other
830 | Simulated
831 | Mock
832 | BinanceFuturesCoin
833 | BinanceFuturesUsd
834 | BinanceMargin
835 | BinanceOptions
836 | BinancePortfolioMargin
837 | BinanceSpot
838 | BinanceUs
839 | Bitazza
840 | Bitfinex
841 | Bitflyer
842 | Bitget
843 | Bitmart
844 | BitmartFuturesUsd
845 | Bitmex
846 | Bitso
847 | Bitstamp
848 | Bitvavo
849 | Bithumb
850 | BybitPerpetualsUsd
851 | BybitSpot
852 | Cexio
853 | Coinbase
854 | CoinbaseInternational
855 | Cryptocom
856 | DatabentoDbeq
857 | DatabentoGlbx
858 | DatabentoOpra
859 | DatabentoXnas
860 | DatabentoXnys
861 | Deribit
862 | GateioFuturesBtc
863 | GateioFuturesUsd
864 | GateioOptions
865 | GateioPerpetualsBtc
866 | GateioPerpetualsUsd
867 | GateioSpot
868 | Gemini
869 | Hitbtc
870 | Htx
871 | HyperliquidPerp
872 | HyperliquidSpot
873 | AlpacaBroker
874 | AlpacaCrypto
875 | AlpacaIex
876 | AlpacaSip
877 | Ibkr
878 | Kraken
879 | Kucoin
880 | Liquid
881 | Massive
882 | Mexc
883 | Okx
884 | Poloniex
885 | LseFx
886 | LseCrypto
887 | LseEquities
888 | LseFutures
889 | LseCfd
890 | LseOptions
891 | MassiveStocks
892 | MassiveCrypto
893 | MassiveForex
894 | MassiveOptions => {}
895 }
896 }
897
898 every
899 }
900
901 fn instrument_kinds() -> [MarketDataInstrumentKind; 5] {
903 let expiry = Utc.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).unwrap();
904 let every = [
905 MarketDataInstrumentKind::Spot,
906 MarketDataInstrumentKind::Perpetual,
907 MarketDataInstrumentKind::Cfd,
908 MarketDataInstrumentKind::Future(MarketDataFutureContract { expiry }),
909 option(),
910 ];
911
912 for kind in &every {
915 match kind {
916 MarketDataInstrumentKind::Spot
917 | MarketDataInstrumentKind::Perpetual
918 | MarketDataInstrumentKind::Cfd
919 | MarketDataInstrumentKind::Future(_)
920 | MarketDataInstrumentKind::Option(_) => {}
921 }
922 }
923
924 every
925 }
926
927 fn sub_kinds() -> Vec<SubKind> {
929 let every: Vec<SubKind> = [
930 SubKind::PublicTrades,
931 SubKind::OrderBooksL1,
932 SubKind::OrderBooksL2,
933 SubKind::OrderBooksL3,
934 SubKind::Liquidations,
935 SubKind::Quotes,
936 ]
937 .into_iter()
938 .chain(
939 CandleInterval::ALL
940 .into_iter()
941 .map(|interval| SubKind::Candles { interval }),
942 )
943 .collect();
944
945 for sub_kind in &every {
948 match sub_kind {
949 SubKind::PublicTrades
950 | SubKind::OrderBooksL1
951 | SubKind::OrderBooksL2
952 | SubKind::OrderBooksL3
953 | SubKind::Liquidations
954 | SubKind::Candles { .. }
955 | SubKind::Quotes => {}
956 }
957 }
958
959 every
960 }
961
962 fn subscription(
963 exchange: ExchangeId,
964 kind: MarketDataInstrumentKind,
965 sub_kind: SubKind,
966 ) -> Subscription<ExchangeId, MarketDataInstrument, SubKind> {
967 Subscription::new(
968 exchange,
969 MarketDataInstrument::new("btc", "usd", kind),
970 sub_kind,
971 )
972 }
973
974 fn route(
976 batches: Vec<Vec<Subscription<ExchangeId, MarketDataInstrument, SubKind>>>,
977 subscribers: &DynamicSubscribers,
978 ) -> Result<Vec<plan::GroupFuture>, DataError> {
979 let Channels { txs, rxs: _ } = Channels::try_from(&batches)?;
980 route_batches(batches, &txs, subscribers)
981 }
982
983 fn every_subscriber() -> DynamicSubscribers {
985 let subscribers = DynamicSubscribers::default();
986
987 #[cfg(feature = "alpaca")]
988 let subscribers = subscribers.with_alpaca(alpaca_subscriber());
989
990 #[cfg(feature = "lse")]
991 let subscribers = subscribers.with_lse(LseSubscriber::new(
992 crate::exchange::lse::live::LseCredentials::new("test-key"),
993 ));
994
995 #[cfg(feature = "massive")]
996 let subscribers = subscribers.with_massive(massive_subscriber());
997
998 subscribers
999 }
1000
1001 #[cfg(feature = "massive")]
1002 fn massive_subscriber() -> MassiveSubscriber {
1003 MassiveSubscriber::new(crate::exchange::massive::MassiveCredentials::new(
1004 "test-key",
1005 ))
1006 }
1007
1008 fn option() -> MarketDataInstrumentKind {
1009 MarketDataInstrumentKind::Option(MarketDataOptionContract {
1010 kind: OptionKind::Call,
1011 exercise: OptionExercise::American,
1012 expiry: Utc.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).unwrap(),
1013 strike: Decimal::ONE_HUNDRED,
1014 })
1015 }
1016
1017 #[cfg(feature = "alpaca")]
1018 fn alpaca_subscriber() -> AlpacaSubscriber {
1019 AlpacaSubscriber::new(crate::exchange::alpaca::AlpacaCredentials::new(
1020 "test-key",
1021 "test-secret",
1022 ))
1023 }
1024
1025 fn feature_enabled(feature: &str) -> bool {
1026 match feature {
1027 "alpaca" => cfg!(feature = "alpaca"),
1028 "lse" => cfg!(feature = "lse"),
1029 "massive" => cfg!(feature = "massive"),
1030 "hyperliquid" => cfg!(feature = "hyperliquid"),
1031 other => panic!("no dynamic route is gated on the `{other}` feature"),
1032 }
1033 }
1034
1035 #[test]
1036 fn every_pair_the_support_matrix_accepts_is_routed() {
1037 let subscribers = every_subscriber();
1038 let mut routed = 0;
1039
1040 for exchange in every_exchange() {
1041 for kind in instrument_kinds() {
1042 for sub_kind in sub_kinds() {
1043 if !exchange_supports_instrument_kind_sub_kind(&exchange, &kind, sub_kind) {
1044 continue;
1045 }
1046
1047 match route(
1049 vec![vec![subscription(exchange, kind.clone(), sub_kind)]],
1050 &subscribers,
1051 ) {
1052 Ok(groups) => {
1053 assert_eq!(groups.len(), 1);
1054 routed += 1;
1055 }
1056 Err(DataError::FeatureDisabled { feature, .. }) => assert!(
1057 !feature_enabled(&feature),
1058 "{exchange} ({kind}, {sub_kind}) reported `{feature}` disabled in a \
1059 build that enables it"
1060 ),
1061 Err(error) => panic!(
1062 "the matrix accepts {exchange} ({kind}, {sub_kind}), but routing it \
1063 failed: {error}"
1064 ),
1065 }
1066 }
1067 }
1068 }
1069
1070 assert!(routed > 30, "only {routed} pairs were routed");
1072 }
1073
1074 #[test]
1075 fn every_pair_the_router_serves_is_accepted_by_the_support_matrix() {
1076 let subscribers = every_subscriber();
1077 let mut served = 0;
1078
1079 for exchange in every_exchange() {
1080 for sub_kind in sub_kinds() {
1081 match route(
1084 vec![vec![subscription(
1085 exchange,
1086 MarketDataInstrumentKind::Spot,
1087 sub_kind,
1088 )]],
1089 &subscribers,
1090 ) {
1091 Err(DataError::Unsupported { .. }) => continue,
1092 Ok(_) | Err(DataError::FeatureDisabled { .. }) => {}
1093 Err(error) => panic!(
1094 "routing {exchange} ({sub_kind}) failed with neither a route nor \
1095 Unsupported: {error}"
1096 ),
1097 }
1098 served += 1;
1099
1100 let candidates = match sub_kind {
1103 SubKind::Candles { .. } => CandleInterval::ALL
1104 .into_iter()
1105 .map(|interval| SubKind::Candles { interval })
1106 .collect(),
1107 other => vec![other],
1108 };
1109 let accepted = instrument_kinds().iter().any(|kind| {
1110 candidates.iter().any(|candidate| {
1111 exchange_supports_instrument_kind_sub_kind(&exchange, kind, *candidate)
1112 })
1113 });
1114 assert!(
1115 accepted,
1116 "the router serves {exchange} ({sub_kind}), but the support matrix accepts \
1117 it for no instrument kind"
1118 );
1119 }
1120 }
1121
1122 assert!(served > 30, "only {served} pairs were served");
1124 }
1125
1126 #[test]
1127 fn a_pair_the_support_matrix_refuses_is_refused_before_routing() {
1128 let error = validate_batches([[subscription(
1130 ExchangeId::LseOptions,
1131 option(),
1132 SubKind::OrderBooksL1,
1133 )]])
1134 .unwrap_err();
1135
1136 assert!(
1137 matches!(&error, DataError::Socket(message) if message.contains("lse_options")),
1138 "{error:?}"
1139 );
1140 }
1141
1142 #[cfg(feature = "alpaca")]
1143 #[test]
1144 fn one_alpaca_batch_across_feeds_and_kinds_shares_one_subscriber() {
1145 let subscriber = alpaca_subscriber();
1146 let subscribers = DynamicSubscribers::default().with_alpaca(subscriber.clone());
1147 let before = subscriber.connection_handles();
1148
1149 let groups = route(
1150 vec![vec![
1151 subscription(
1152 ExchangeId::AlpacaCrypto,
1153 MarketDataInstrumentKind::Spot,
1154 SubKind::PublicTrades,
1155 ),
1156 subscription(
1157 ExchangeId::AlpacaCrypto,
1158 MarketDataInstrumentKind::Spot,
1159 SubKind::OrderBooksL1,
1160 ),
1161 subscription(
1162 ExchangeId::AlpacaIex,
1163 MarketDataInstrumentKind::Spot,
1164 SubKind::PublicTrades,
1165 ),
1166 subscription(
1167 ExchangeId::AlpacaIex,
1168 MarketDataInstrumentKind::Spot,
1169 SubKind::OrderBooksL1,
1170 ),
1171 ]],
1172 &subscribers,
1173 )
1174 .unwrap();
1175
1176 assert_eq!(groups.len(), 4);
1179 assert_eq!(subscriber.connection_handles(), before + groups.len());
1180
1181 drop(groups);
1182 assert_eq!(subscriber.connection_handles(), before);
1183 }
1184
1185 #[cfg(feature = "alpaca")]
1186 #[test]
1187 fn an_alpaca_subscription_without_a_subscriber_fails_the_call_before_any_group_connects() {
1188 let error = route(
1189 vec![
1190 vec![subscription(
1191 ExchangeId::BinanceSpot,
1192 MarketDataInstrumentKind::Spot,
1193 SubKind::PublicTrades,
1194 )],
1195 vec![subscription(
1196 ExchangeId::AlpacaIex,
1197 MarketDataInstrumentKind::Spot,
1198 SubKind::OrderBooksL1,
1199 )],
1200 ],
1201 &DynamicSubscribers::default(),
1202 )
1203 .err()
1204 .unwrap();
1205
1206 assert_eq!(
1207 error,
1208 DataError::SubscriberRequired {
1209 exchange: ExchangeId::AlpacaIex
1210 }
1211 );
1212 }
1213
1214 #[cfg(not(feature = "alpaca"))]
1215 #[test]
1216 fn an_alpaca_subscription_without_the_feature_names_the_feature() {
1217 let error = route(
1218 vec![vec![subscription(
1219 ExchangeId::AlpacaCrypto,
1220 MarketDataInstrumentKind::Spot,
1221 SubKind::PublicTrades,
1222 )]],
1223 &DynamicSubscribers::default(),
1224 )
1225 .err()
1226 .unwrap();
1227
1228 assert_eq!(
1229 error,
1230 DataError::FeatureDisabled {
1231 exchange: ExchangeId::AlpacaCrypto,
1232 feature: "alpaca".to_owned(),
1233 }
1234 );
1235 }
1236
1237 #[test]
1238 fn alpaca_quotes_are_refused_before_routing_because_dynamic_streams_carry_no_quote() {
1239 let error = validate_batches([[subscription(
1240 ExchangeId::AlpacaCrypto,
1241 MarketDataInstrumentKind::Spot,
1242 SubKind::Quotes,
1243 )]])
1244 .unwrap_err();
1245
1246 assert!(
1247 matches!(&error, DataError::Socket(message) if message.contains("alpaca_crypto")),
1248 "{error:?}"
1249 );
1250 }
1251
1252 #[cfg(feature = "massive")]
1253 #[test]
1254 fn one_massive_batch_across_clusters_and_kinds_shares_one_subscriber() {
1255 let subscriber = massive_subscriber();
1256 let subscribers = DynamicSubscribers::default().with_massive(subscriber.clone());
1257 let before = subscriber.connection_handles();
1258
1259 let groups = route(
1260 vec![vec![
1261 subscription(
1262 ExchangeId::MassiveStocks,
1263 MarketDataInstrumentKind::Spot,
1264 SubKind::PublicTrades,
1265 ),
1266 subscription(
1267 ExchangeId::MassiveCrypto,
1268 MarketDataInstrumentKind::Spot,
1269 SubKind::PublicTrades,
1270 ),
1271 subscription(
1272 ExchangeId::MassiveCrypto,
1273 MarketDataInstrumentKind::Spot,
1274 SubKind::OrderBooksL1,
1275 ),
1276 subscription(
1277 ExchangeId::MassiveCrypto,
1278 MarketDataInstrumentKind::Spot,
1279 SubKind::Candles {
1280 interval: CandleInterval::Min1,
1281 },
1282 ),
1283 subscription(
1284 ExchangeId::MassiveForex,
1285 MarketDataInstrumentKind::Spot,
1286 SubKind::OrderBooksL1,
1287 ),
1288 subscription(ExchangeId::MassiveOptions, option(), SubKind::PublicTrades),
1289 ]],
1290 &subscribers,
1291 )
1292 .unwrap();
1293
1294 assert_eq!(groups.len(), 6);
1298 assert_eq!(subscriber.connection_handles(), before + groups.len());
1299
1300 drop(groups);
1301 assert_eq!(subscriber.connection_handles(), before);
1302 }
1303
1304 #[cfg(feature = "massive")]
1305 #[test]
1306 fn a_massive_subscription_without_a_subscriber_fails_the_call_before_any_group_connects() {
1307 let error = route(
1308 vec![
1309 vec![subscription(
1310 ExchangeId::BinanceSpot,
1311 MarketDataInstrumentKind::Spot,
1312 SubKind::PublicTrades,
1313 )],
1314 vec![subscription(
1315 ExchangeId::MassiveForex,
1316 MarketDataInstrumentKind::Spot,
1317 SubKind::OrderBooksL1,
1318 )],
1319 ],
1320 &DynamicSubscribers::default(),
1321 )
1322 .err()
1323 .unwrap();
1324
1325 assert_eq!(
1326 error,
1327 DataError::SubscriberRequired {
1328 exchange: ExchangeId::MassiveForex
1329 }
1330 );
1331 }
1332
1333 #[cfg(not(feature = "massive"))]
1334 #[test]
1335 fn a_massive_subscription_without_the_feature_names_the_feature() {
1336 let error = route(
1337 vec![vec![subscription(
1338 ExchangeId::MassiveCrypto,
1339 MarketDataInstrumentKind::Spot,
1340 SubKind::PublicTrades,
1341 )]],
1342 &DynamicSubscribers::default(),
1343 )
1344 .err()
1345 .unwrap();
1346
1347 assert_eq!(
1348 error,
1349 DataError::FeatureDisabled {
1350 exchange: ExchangeId::MassiveCrypto,
1351 feature: "massive".to_owned(),
1352 }
1353 );
1354 }
1355
1356 #[test]
1357 fn massive_pairs_outside_the_support_matrix_are_refused_before_routing() {
1358 let spot = MarketDataInstrumentKind::Spot;
1359 for (exchange, kind, sub_kind) in [
1360 (
1361 ExchangeId::MassiveForex,
1362 spot.clone(),
1363 SubKind::Candles {
1364 interval: CandleInterval::Min5,
1365 },
1366 ),
1367 (
1369 ExchangeId::MassiveForex,
1370 spot.clone(),
1371 SubKind::PublicTrades,
1372 ),
1373 (ExchangeId::MassiveForex, spot.clone(), SubKind::Quotes),
1374 (ExchangeId::MassiveOptions, spot, SubKind::PublicTrades),
1376 (ExchangeId::MassiveStocks, option(), SubKind::PublicTrades),
1377 ] {
1378 let error =
1379 validate_batches([[subscription(exchange, kind.clone(), sub_kind)]]).unwrap_err();
1380
1381 assert!(
1382 matches!(&error, DataError::Socket(message) if message.contains(exchange.as_str())),
1383 "{exchange} ({kind}, {sub_kind}): {error:?}"
1384 );
1385 }
1386 }
1387
1388 #[cfg(feature = "lse")]
1389 #[test]
1390 fn one_lse_batch_across_datasets_and_kinds_shares_one_subscriber() {
1391 let subscriber =
1392 LseSubscriber::new(crate::exchange::lse::live::LseCredentials::new("test-key"));
1393 let subscribers = DynamicSubscribers::default().with_lse(subscriber.clone());
1394 let before = subscriber.connection_handles();
1395
1396 let groups = route(
1397 vec![vec![
1398 subscription(
1399 ExchangeId::LseCrypto,
1400 MarketDataInstrumentKind::Spot,
1401 SubKind::PublicTrades,
1402 ),
1403 subscription(
1404 ExchangeId::LseCrypto,
1405 MarketDataInstrumentKind::Spot,
1406 SubKind::OrderBooksL1,
1407 ),
1408 subscription(
1409 ExchangeId::LseEquities,
1410 MarketDataInstrumentKind::Spot,
1411 SubKind::PublicTrades,
1412 ),
1413 subscription(
1414 ExchangeId::LseEquities,
1415 MarketDataInstrumentKind::Spot,
1416 SubKind::OrderBooksL1,
1417 ),
1418 ]],
1419 &subscribers,
1420 )
1421 .unwrap();
1422
1423 assert_eq!(groups.len(), 4);
1426 assert_eq!(subscriber.connection_handles(), before + groups.len());
1427
1428 drop(groups);
1429 assert_eq!(subscriber.connection_handles(), before);
1430 }
1431
1432 #[cfg(feature = "lse")]
1433 #[test]
1434 fn an_lse_subscription_without_a_subscriber_fails_the_call_before_any_group_connects() {
1435 let error = route(
1436 vec![
1437 vec![subscription(
1438 ExchangeId::BinanceSpot,
1439 MarketDataInstrumentKind::Spot,
1440 SubKind::PublicTrades,
1441 )],
1442 vec![subscription(
1443 ExchangeId::LseFx,
1444 MarketDataInstrumentKind::Spot,
1445 SubKind::PublicTrades,
1446 )],
1447 ],
1448 &DynamicSubscribers::default(),
1449 )
1450 .err()
1451 .unwrap();
1452
1453 assert_eq!(
1454 error,
1455 DataError::SubscriberRequired {
1456 exchange: ExchangeId::LseFx
1457 }
1458 );
1459 }
1460
1461 #[cfg(not(feature = "lse"))]
1462 #[test]
1463 fn an_lse_subscription_without_the_feature_names_the_feature() {
1464 let error = route(
1465 vec![vec![subscription(
1466 ExchangeId::LseFx,
1467 MarketDataInstrumentKind::Spot,
1468 SubKind::PublicTrades,
1469 )]],
1470 &DynamicSubscribers::default(),
1471 )
1472 .err()
1473 .unwrap();
1474
1475 assert_eq!(
1476 error,
1477 DataError::FeatureDisabled {
1478 exchange: ExchangeId::LseFx,
1479 feature: "lse".to_owned(),
1480 }
1481 );
1482 }
1483
1484 #[tokio::test]
1485 async fn a_failed_group_stops_every_forwarder_that_started() {
1486 struct OnDrop(Option<tokio::sync::oneshot::Sender<()>>);
1487 impl Drop for OnDrop {
1488 fn drop(&mut self) {
1489 let _ = self.0.take().map(|tx| tx.send(()));
1490 }
1491 }
1492
1493 let (dropped_tx, mut dropped) = tokio::sync::oneshot::channel();
1495 let guard = OnDrop(Some(dropped_tx));
1496 let forwarder = tokio::spawn(async move {
1497 let _guard = guard;
1498 std::future::pending::<()>().await;
1499 });
1500
1501 let error = settle(vec![
1502 Ok(forwarder),
1503 Err(DataError::SubscriberRequired {
1504 exchange: ExchangeId::LseFx,
1505 }),
1506 ])
1507 .await
1508 .unwrap_err();
1509
1510 assert_eq!(
1511 error,
1512 DataError::SubscriberRequired {
1513 exchange: ExchangeId::LseFx
1514 }
1515 );
1516 assert_eq!(dropped.try_recv(), Ok(()));
1519 }
1520
1521 #[tokio::test]
1522 async fn forwarders_keep_running_when_every_group_initialised() {
1523 let forwarder = tokio::spawn(std::future::pending::<()>());
1524 let abort = forwarder.abort_handle();
1525
1526 settle(vec![Ok(forwarder)]).await.unwrap();
1527 tokio::task::yield_now().await;
1528
1529 assert!(!abort.is_finished());
1530 abort.abort();
1531 }
1532}