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 = "hyperliquid")]
1143 #[test]
1144 fn hyperliquid_spot_trades_and_depth_are_validated_and_routed() {
1145 let batch: Vec<_> = [SubKind::PublicTrades, SubKind::OrderBooksL2]
1146 .into_iter()
1147 .map(|sub_kind| {
1148 subscription(
1149 ExchangeId::HyperliquidSpot,
1150 MarketDataInstrumentKind::Spot,
1151 sub_kind,
1152 )
1153 })
1154 .collect();
1155
1156 let batches = validate_batches([batch]).unwrap();
1157 let groups = route(batches, &DynamicSubscribers::default()).unwrap();
1158
1159 assert_eq!(groups.len(), 2);
1161 }
1162
1163 #[cfg(not(feature = "hyperliquid"))]
1164 #[test]
1165 fn a_hyperliquid_spot_subscription_without_the_feature_names_the_feature() {
1166 let error = route(
1167 vec![vec![subscription(
1168 ExchangeId::HyperliquidSpot,
1169 MarketDataInstrumentKind::Spot,
1170 SubKind::PublicTrades,
1171 )]],
1172 &DynamicSubscribers::default(),
1173 )
1174 .err()
1175 .unwrap();
1176
1177 assert_eq!(
1178 error,
1179 DataError::FeatureDisabled {
1180 exchange: ExchangeId::HyperliquidSpot,
1181 feature: "hyperliquid".to_owned(),
1182 }
1183 );
1184 }
1185
1186 #[cfg(feature = "alpaca")]
1187 #[test]
1188 fn one_alpaca_batch_across_feeds_and_kinds_shares_one_subscriber() {
1189 let subscriber = alpaca_subscriber();
1190 let subscribers = DynamicSubscribers::default().with_alpaca(subscriber.clone());
1191 let before = subscriber.connection_handles();
1192
1193 let groups = route(
1194 vec![vec![
1195 subscription(
1196 ExchangeId::AlpacaCrypto,
1197 MarketDataInstrumentKind::Spot,
1198 SubKind::PublicTrades,
1199 ),
1200 subscription(
1201 ExchangeId::AlpacaCrypto,
1202 MarketDataInstrumentKind::Spot,
1203 SubKind::OrderBooksL1,
1204 ),
1205 subscription(
1206 ExchangeId::AlpacaIex,
1207 MarketDataInstrumentKind::Spot,
1208 SubKind::PublicTrades,
1209 ),
1210 subscription(
1211 ExchangeId::AlpacaIex,
1212 MarketDataInstrumentKind::Spot,
1213 SubKind::OrderBooksL1,
1214 ),
1215 ]],
1216 &subscribers,
1217 )
1218 .unwrap();
1219
1220 assert_eq!(groups.len(), 4);
1223 assert_eq!(subscriber.connection_handles(), before + groups.len());
1224
1225 drop(groups);
1226 assert_eq!(subscriber.connection_handles(), before);
1227 }
1228
1229 #[cfg(feature = "alpaca")]
1230 #[test]
1231 fn an_alpaca_subscription_without_a_subscriber_fails_the_call_before_any_group_connects() {
1232 let error = route(
1233 vec![
1234 vec![subscription(
1235 ExchangeId::BinanceSpot,
1236 MarketDataInstrumentKind::Spot,
1237 SubKind::PublicTrades,
1238 )],
1239 vec![subscription(
1240 ExchangeId::AlpacaIex,
1241 MarketDataInstrumentKind::Spot,
1242 SubKind::OrderBooksL1,
1243 )],
1244 ],
1245 &DynamicSubscribers::default(),
1246 )
1247 .err()
1248 .unwrap();
1249
1250 assert_eq!(
1251 error,
1252 DataError::SubscriberRequired {
1253 exchange: ExchangeId::AlpacaIex
1254 }
1255 );
1256 }
1257
1258 #[cfg(not(feature = "alpaca"))]
1259 #[test]
1260 fn an_alpaca_subscription_without_the_feature_names_the_feature() {
1261 let error = route(
1262 vec![vec![subscription(
1263 ExchangeId::AlpacaCrypto,
1264 MarketDataInstrumentKind::Spot,
1265 SubKind::PublicTrades,
1266 )]],
1267 &DynamicSubscribers::default(),
1268 )
1269 .err()
1270 .unwrap();
1271
1272 assert_eq!(
1273 error,
1274 DataError::FeatureDisabled {
1275 exchange: ExchangeId::AlpacaCrypto,
1276 feature: "alpaca".to_owned(),
1277 }
1278 );
1279 }
1280
1281 #[test]
1282 fn alpaca_quotes_are_refused_before_routing_because_dynamic_streams_carry_no_quote() {
1283 let error = validate_batches([[subscription(
1284 ExchangeId::AlpacaCrypto,
1285 MarketDataInstrumentKind::Spot,
1286 SubKind::Quotes,
1287 )]])
1288 .unwrap_err();
1289
1290 assert!(
1291 matches!(&error, DataError::Socket(message) if message.contains("alpaca_crypto")),
1292 "{error:?}"
1293 );
1294 }
1295
1296 #[cfg(feature = "massive")]
1297 #[test]
1298 fn one_massive_batch_across_clusters_and_kinds_shares_one_subscriber() {
1299 let subscriber = massive_subscriber();
1300 let subscribers = DynamicSubscribers::default().with_massive(subscriber.clone());
1301 let before = subscriber.connection_handles();
1302
1303 let groups = route(
1304 vec![vec![
1305 subscription(
1306 ExchangeId::MassiveStocks,
1307 MarketDataInstrumentKind::Spot,
1308 SubKind::PublicTrades,
1309 ),
1310 subscription(
1311 ExchangeId::MassiveCrypto,
1312 MarketDataInstrumentKind::Spot,
1313 SubKind::PublicTrades,
1314 ),
1315 subscription(
1316 ExchangeId::MassiveCrypto,
1317 MarketDataInstrumentKind::Spot,
1318 SubKind::OrderBooksL1,
1319 ),
1320 subscription(
1321 ExchangeId::MassiveCrypto,
1322 MarketDataInstrumentKind::Spot,
1323 SubKind::Candles {
1324 interval: CandleInterval::Min1,
1325 },
1326 ),
1327 subscription(
1328 ExchangeId::MassiveForex,
1329 MarketDataInstrumentKind::Spot,
1330 SubKind::OrderBooksL1,
1331 ),
1332 subscription(ExchangeId::MassiveOptions, option(), SubKind::PublicTrades),
1333 ]],
1334 &subscribers,
1335 )
1336 .unwrap();
1337
1338 assert_eq!(groups.len(), 6);
1342 assert_eq!(subscriber.connection_handles(), before + groups.len());
1343
1344 drop(groups);
1345 assert_eq!(subscriber.connection_handles(), before);
1346 }
1347
1348 #[cfg(feature = "massive")]
1349 #[test]
1350 fn a_massive_subscription_without_a_subscriber_fails_the_call_before_any_group_connects() {
1351 let error = route(
1352 vec![
1353 vec![subscription(
1354 ExchangeId::BinanceSpot,
1355 MarketDataInstrumentKind::Spot,
1356 SubKind::PublicTrades,
1357 )],
1358 vec![subscription(
1359 ExchangeId::MassiveForex,
1360 MarketDataInstrumentKind::Spot,
1361 SubKind::OrderBooksL1,
1362 )],
1363 ],
1364 &DynamicSubscribers::default(),
1365 )
1366 .err()
1367 .unwrap();
1368
1369 assert_eq!(
1370 error,
1371 DataError::SubscriberRequired {
1372 exchange: ExchangeId::MassiveForex
1373 }
1374 );
1375 }
1376
1377 #[cfg(not(feature = "massive"))]
1378 #[test]
1379 fn a_massive_subscription_without_the_feature_names_the_feature() {
1380 let error = route(
1381 vec![vec![subscription(
1382 ExchangeId::MassiveCrypto,
1383 MarketDataInstrumentKind::Spot,
1384 SubKind::PublicTrades,
1385 )]],
1386 &DynamicSubscribers::default(),
1387 )
1388 .err()
1389 .unwrap();
1390
1391 assert_eq!(
1392 error,
1393 DataError::FeatureDisabled {
1394 exchange: ExchangeId::MassiveCrypto,
1395 feature: "massive".to_owned(),
1396 }
1397 );
1398 }
1399
1400 #[test]
1401 fn massive_pairs_outside_the_support_matrix_are_refused_before_routing() {
1402 let spot = MarketDataInstrumentKind::Spot;
1403 for (exchange, kind, sub_kind) in [
1404 (
1405 ExchangeId::MassiveForex,
1406 spot.clone(),
1407 SubKind::Candles {
1408 interval: CandleInterval::Min5,
1409 },
1410 ),
1411 (
1413 ExchangeId::MassiveForex,
1414 spot.clone(),
1415 SubKind::PublicTrades,
1416 ),
1417 (ExchangeId::MassiveForex, spot.clone(), SubKind::Quotes),
1418 (ExchangeId::MassiveOptions, spot, SubKind::PublicTrades),
1420 (ExchangeId::MassiveStocks, option(), SubKind::PublicTrades),
1421 ] {
1422 let error =
1423 validate_batches([[subscription(exchange, kind.clone(), sub_kind)]]).unwrap_err();
1424
1425 assert!(
1426 matches!(&error, DataError::Socket(message) if message.contains(exchange.as_str())),
1427 "{exchange} ({kind}, {sub_kind}): {error:?}"
1428 );
1429 }
1430 }
1431
1432 #[cfg(feature = "lse")]
1433 #[test]
1434 fn one_lse_batch_across_datasets_and_kinds_shares_one_subscriber() {
1435 let subscriber =
1436 LseSubscriber::new(crate::exchange::lse::live::LseCredentials::new("test-key"));
1437 let subscribers = DynamicSubscribers::default().with_lse(subscriber.clone());
1438 let before = subscriber.connection_handles();
1439
1440 let groups = route(
1441 vec![vec![
1442 subscription(
1443 ExchangeId::LseCrypto,
1444 MarketDataInstrumentKind::Spot,
1445 SubKind::PublicTrades,
1446 ),
1447 subscription(
1448 ExchangeId::LseCrypto,
1449 MarketDataInstrumentKind::Spot,
1450 SubKind::OrderBooksL1,
1451 ),
1452 subscription(
1453 ExchangeId::LseEquities,
1454 MarketDataInstrumentKind::Spot,
1455 SubKind::PublicTrades,
1456 ),
1457 subscription(
1458 ExchangeId::LseEquities,
1459 MarketDataInstrumentKind::Spot,
1460 SubKind::OrderBooksL1,
1461 ),
1462 ]],
1463 &subscribers,
1464 )
1465 .unwrap();
1466
1467 assert_eq!(groups.len(), 4);
1470 assert_eq!(subscriber.connection_handles(), before + groups.len());
1471
1472 drop(groups);
1473 assert_eq!(subscriber.connection_handles(), before);
1474 }
1475
1476 #[cfg(feature = "lse")]
1477 #[test]
1478 fn an_lse_subscription_without_a_subscriber_fails_the_call_before_any_group_connects() {
1479 let error = route(
1480 vec![
1481 vec![subscription(
1482 ExchangeId::BinanceSpot,
1483 MarketDataInstrumentKind::Spot,
1484 SubKind::PublicTrades,
1485 )],
1486 vec![subscription(
1487 ExchangeId::LseFx,
1488 MarketDataInstrumentKind::Spot,
1489 SubKind::PublicTrades,
1490 )],
1491 ],
1492 &DynamicSubscribers::default(),
1493 )
1494 .err()
1495 .unwrap();
1496
1497 assert_eq!(
1498 error,
1499 DataError::SubscriberRequired {
1500 exchange: ExchangeId::LseFx
1501 }
1502 );
1503 }
1504
1505 #[cfg(not(feature = "lse"))]
1506 #[test]
1507 fn an_lse_subscription_without_the_feature_names_the_feature() {
1508 let error = route(
1509 vec![vec![subscription(
1510 ExchangeId::LseFx,
1511 MarketDataInstrumentKind::Spot,
1512 SubKind::PublicTrades,
1513 )]],
1514 &DynamicSubscribers::default(),
1515 )
1516 .err()
1517 .unwrap();
1518
1519 assert_eq!(
1520 error,
1521 DataError::FeatureDisabled {
1522 exchange: ExchangeId::LseFx,
1523 feature: "lse".to_owned(),
1524 }
1525 );
1526 }
1527
1528 #[tokio::test]
1529 async fn a_failed_group_stops_every_forwarder_that_started() {
1530 struct OnDrop(Option<tokio::sync::oneshot::Sender<()>>);
1531 impl Drop for OnDrop {
1532 fn drop(&mut self) {
1533 let _ = self.0.take().map(|tx| tx.send(()));
1534 }
1535 }
1536
1537 let (dropped_tx, mut dropped) = tokio::sync::oneshot::channel();
1539 let guard = OnDrop(Some(dropped_tx));
1540 let forwarder = tokio::spawn(async move {
1541 let _guard = guard;
1542 std::future::pending::<()>().await;
1543 });
1544
1545 let error = settle(vec![
1546 Ok(forwarder),
1547 Err(DataError::SubscriberRequired {
1548 exchange: ExchangeId::LseFx,
1549 }),
1550 ])
1551 .await
1552 .unwrap_err();
1553
1554 assert_eq!(
1555 error,
1556 DataError::SubscriberRequired {
1557 exchange: ExchangeId::LseFx
1558 }
1559 );
1560 assert_eq!(dropped.try_recv(), Ok(()));
1563 }
1564
1565 #[tokio::test]
1566 async fn forwarders_keep_running_when_every_group_initialised() {
1567 let forwarder = tokio::spawn(std::future::pending::<()>());
1568 let abort = forwarder.abort_handle();
1569
1570 settle(vec![Ok(forwarder)]).await.unwrap();
1571 tokio::task::yield_now().await;
1572
1573 assert!(!abort.is_finished());
1574 abort.abort();
1575 }
1576}