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 {
827 match exchange {
828 Other
829 | Simulated
830 | Mock
831 | BinanceFuturesCoin
832 | BinanceFuturesUsd
833 | BinanceMargin
834 | BinanceOptions
835 | BinancePortfolioMargin
836 | BinanceSpot
837 | BinanceUs
838 | Bitazza
839 | Bitfinex
840 | Bitflyer
841 | Bitget
842 | Bitmart
843 | BitmartFuturesUsd
844 | Bitmex
845 | Bitso
846 | Bitstamp
847 | Bitvavo
848 | Bithumb
849 | BybitPerpetualsUsd
850 | BybitSpot
851 | Cexio
852 | Coinbase
853 | CoinbaseInternational
854 | Cryptocom
855 | DatabentoDbeq
856 | DatabentoGlbx
857 | DatabentoOpra
858 | DatabentoXnas
859 | DatabentoXnys
860 | Deribit
861 | GateioFuturesBtc
862 | GateioFuturesUsd
863 | GateioOptions
864 | GateioPerpetualsBtc
865 | GateioPerpetualsUsd
866 | GateioSpot
867 | Gemini
868 | Hitbtc
869 | Htx
870 | HyperliquidPerp
871 | HyperliquidSpot
872 | AlpacaBroker
873 | AlpacaCrypto
874 | AlpacaIex
875 | AlpacaSip
876 | Ibkr
877 | Kraken
878 | Kucoin
879 | Liquid
880 | Massive
881 | Mexc
882 | Okx
883 | Poloniex
884 | LseFx
885 | LseCrypto
886 | LseEquities
887 | LseFutures
888 | LseCfd
889 | LseOptions
890 | MassiveStocks
891 | MassiveCrypto
892 | MassiveForex
893 | MassiveOptions => {}
894 }
895 }
896
897 every
898 }
899
900 fn instrument_kinds() -> [MarketDataInstrumentKind; 5] {
901 let expiry = Utc.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).unwrap();
902 [
903 MarketDataInstrumentKind::Spot,
904 MarketDataInstrumentKind::Perpetual,
905 MarketDataInstrumentKind::Cfd,
906 MarketDataInstrumentKind::Future(MarketDataFutureContract { expiry }),
907 option(),
908 ]
909 }
910
911 fn sub_kinds() -> Vec<SubKind> {
912 [
913 SubKind::PublicTrades,
914 SubKind::OrderBooksL1,
915 SubKind::OrderBooksL2,
916 SubKind::OrderBooksL3,
917 SubKind::Liquidations,
918 SubKind::Quotes,
919 ]
920 .into_iter()
921 .chain(
922 CandleInterval::ALL
923 .into_iter()
924 .map(|interval| SubKind::Candles { interval }),
925 )
926 .collect()
927 }
928
929 fn subscription(
930 exchange: ExchangeId,
931 kind: MarketDataInstrumentKind,
932 sub_kind: SubKind,
933 ) -> Subscription<ExchangeId, MarketDataInstrument, SubKind> {
934 Subscription::new(
935 exchange,
936 MarketDataInstrument::new("btc", "usd", kind),
937 sub_kind,
938 )
939 }
940
941 fn route(
943 batches: Vec<Vec<Subscription<ExchangeId, MarketDataInstrument, SubKind>>>,
944 subscribers: &DynamicSubscribers,
945 ) -> Result<Vec<plan::GroupFuture>, DataError> {
946 let Channels { txs, rxs: _ } = Channels::try_from(&batches)?;
947 route_batches(batches, &txs, subscribers)
948 }
949
950 fn every_subscriber() -> DynamicSubscribers {
952 let subscribers = DynamicSubscribers::default();
953
954 #[cfg(feature = "alpaca")]
955 let subscribers = subscribers.with_alpaca(alpaca_subscriber());
956
957 #[cfg(feature = "lse")]
958 let subscribers = subscribers.with_lse(LseSubscriber::new(
959 crate::exchange::lse::live::LseCredentials::new("test-key"),
960 ));
961
962 #[cfg(feature = "massive")]
963 let subscribers = subscribers.with_massive(massive_subscriber());
964
965 subscribers
966 }
967
968 #[cfg(feature = "massive")]
969 fn massive_subscriber() -> MassiveSubscriber {
970 MassiveSubscriber::new(crate::exchange::massive::MassiveCredentials::new(
971 "test-key",
972 ))
973 }
974
975 fn option() -> MarketDataInstrumentKind {
976 MarketDataInstrumentKind::Option(MarketDataOptionContract {
977 kind: OptionKind::Call,
978 exercise: OptionExercise::American,
979 expiry: Utc.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).unwrap(),
980 strike: Decimal::ONE_HUNDRED,
981 })
982 }
983
984 #[cfg(feature = "alpaca")]
985 fn alpaca_subscriber() -> AlpacaSubscriber {
986 AlpacaSubscriber::new(crate::exchange::alpaca::AlpacaCredentials::new(
987 "test-key",
988 "test-secret",
989 ))
990 }
991
992 fn feature_enabled(feature: &str) -> bool {
993 match feature {
994 "alpaca" => cfg!(feature = "alpaca"),
995 "lse" => cfg!(feature = "lse"),
996 "massive" => cfg!(feature = "massive"),
997 "hyperliquid" => cfg!(feature = "hyperliquid"),
998 other => panic!("no dynamic route is gated on the `{other}` feature"),
999 }
1000 }
1001
1002 #[test]
1003 fn every_pair_the_support_matrix_accepts_is_routed() {
1004 let subscribers = every_subscriber();
1005 let mut routed = 0;
1006
1007 for exchange in every_exchange() {
1008 for kind in instrument_kinds() {
1009 for sub_kind in sub_kinds() {
1010 if !exchange_supports_instrument_kind_sub_kind(&exchange, &kind, sub_kind) {
1011 continue;
1012 }
1013
1014 match route(
1016 vec![vec![subscription(exchange, kind.clone(), sub_kind)]],
1017 &subscribers,
1018 ) {
1019 Ok(groups) => {
1020 assert_eq!(groups.len(), 1);
1021 routed += 1;
1022 }
1023 Err(DataError::FeatureDisabled { feature, .. }) => assert!(
1024 !feature_enabled(&feature),
1025 "{exchange} ({kind}, {sub_kind}) reported `{feature}` disabled in a \
1026 build that enables it"
1027 ),
1028 Err(error) => panic!(
1029 "the matrix accepts {exchange} ({kind}, {sub_kind}), but routing it \
1030 failed: {error}"
1031 ),
1032 }
1033 }
1034 }
1035 }
1036
1037 assert!(routed > 30, "only {routed} pairs were routed");
1039 }
1040
1041 #[test]
1042 fn a_pair_the_support_matrix_refuses_is_refused_before_routing() {
1043 let error = validate_batches([[subscription(
1045 ExchangeId::LseOptions,
1046 option(),
1047 SubKind::OrderBooksL1,
1048 )]])
1049 .unwrap_err();
1050
1051 assert!(
1052 matches!(&error, DataError::Socket(message) if message.contains("lse_options")),
1053 "{error:?}"
1054 );
1055 }
1056
1057 #[cfg(feature = "alpaca")]
1058 #[test]
1059 fn one_alpaca_batch_across_feeds_and_kinds_shares_one_subscriber() {
1060 let subscriber = alpaca_subscriber();
1061 let subscribers = DynamicSubscribers::default().with_alpaca(subscriber.clone());
1062 let before = subscriber.connection_handles();
1063
1064 let groups = route(
1065 vec![vec![
1066 subscription(
1067 ExchangeId::AlpacaCrypto,
1068 MarketDataInstrumentKind::Spot,
1069 SubKind::PublicTrades,
1070 ),
1071 subscription(
1072 ExchangeId::AlpacaCrypto,
1073 MarketDataInstrumentKind::Spot,
1074 SubKind::OrderBooksL1,
1075 ),
1076 subscription(
1077 ExchangeId::AlpacaIex,
1078 MarketDataInstrumentKind::Spot,
1079 SubKind::PublicTrades,
1080 ),
1081 subscription(
1082 ExchangeId::AlpacaIex,
1083 MarketDataInstrumentKind::Spot,
1084 SubKind::OrderBooksL1,
1085 ),
1086 ]],
1087 &subscribers,
1088 )
1089 .unwrap();
1090
1091 assert_eq!(groups.len(), 4);
1094 assert_eq!(subscriber.connection_handles(), before + groups.len());
1095
1096 drop(groups);
1097 assert_eq!(subscriber.connection_handles(), before);
1098 }
1099
1100 #[cfg(feature = "alpaca")]
1101 #[test]
1102 fn an_alpaca_subscription_without_a_subscriber_fails_the_call_before_any_group_connects() {
1103 let error = route(
1104 vec![
1105 vec![subscription(
1106 ExchangeId::BinanceSpot,
1107 MarketDataInstrumentKind::Spot,
1108 SubKind::PublicTrades,
1109 )],
1110 vec![subscription(
1111 ExchangeId::AlpacaIex,
1112 MarketDataInstrumentKind::Spot,
1113 SubKind::OrderBooksL1,
1114 )],
1115 ],
1116 &DynamicSubscribers::default(),
1117 )
1118 .err()
1119 .unwrap();
1120
1121 assert_eq!(
1122 error,
1123 DataError::SubscriberRequired {
1124 exchange: ExchangeId::AlpacaIex
1125 }
1126 );
1127 }
1128
1129 #[cfg(not(feature = "alpaca"))]
1130 #[test]
1131 fn an_alpaca_subscription_without_the_feature_names_the_feature() {
1132 let error = route(
1133 vec![vec![subscription(
1134 ExchangeId::AlpacaCrypto,
1135 MarketDataInstrumentKind::Spot,
1136 SubKind::PublicTrades,
1137 )]],
1138 &DynamicSubscribers::default(),
1139 )
1140 .err()
1141 .unwrap();
1142
1143 assert_eq!(
1144 error,
1145 DataError::FeatureDisabled {
1146 exchange: ExchangeId::AlpacaCrypto,
1147 feature: "alpaca".to_owned(),
1148 }
1149 );
1150 }
1151
1152 #[test]
1153 fn alpaca_quotes_are_refused_before_routing_because_dynamic_streams_carry_no_quote() {
1154 let error = validate_batches([[subscription(
1155 ExchangeId::AlpacaCrypto,
1156 MarketDataInstrumentKind::Spot,
1157 SubKind::Quotes,
1158 )]])
1159 .unwrap_err();
1160
1161 assert!(
1162 matches!(&error, DataError::Socket(message) if message.contains("alpaca_crypto")),
1163 "{error:?}"
1164 );
1165 }
1166
1167 #[cfg(feature = "massive")]
1168 #[test]
1169 fn one_massive_batch_across_clusters_and_kinds_shares_one_subscriber() {
1170 let subscriber = massive_subscriber();
1171 let subscribers = DynamicSubscribers::default().with_massive(subscriber.clone());
1172 let before = subscriber.connection_handles();
1173
1174 let groups = route(
1175 vec![vec![
1176 subscription(
1177 ExchangeId::MassiveStocks,
1178 MarketDataInstrumentKind::Spot,
1179 SubKind::PublicTrades,
1180 ),
1181 subscription(
1182 ExchangeId::MassiveCrypto,
1183 MarketDataInstrumentKind::Spot,
1184 SubKind::PublicTrades,
1185 ),
1186 subscription(
1187 ExchangeId::MassiveCrypto,
1188 MarketDataInstrumentKind::Spot,
1189 SubKind::OrderBooksL1,
1190 ),
1191 subscription(
1192 ExchangeId::MassiveCrypto,
1193 MarketDataInstrumentKind::Spot,
1194 SubKind::Candles {
1195 interval: CandleInterval::Min1,
1196 },
1197 ),
1198 subscription(
1199 ExchangeId::MassiveForex,
1200 MarketDataInstrumentKind::Spot,
1201 SubKind::OrderBooksL1,
1202 ),
1203 subscription(ExchangeId::MassiveOptions, option(), SubKind::PublicTrades),
1204 ]],
1205 &subscribers,
1206 )
1207 .unwrap();
1208
1209 assert_eq!(groups.len(), 6);
1213 assert_eq!(subscriber.connection_handles(), before + groups.len());
1214
1215 drop(groups);
1216 assert_eq!(subscriber.connection_handles(), before);
1217 }
1218
1219 #[cfg(feature = "massive")]
1220 #[test]
1221 fn a_massive_subscription_without_a_subscriber_fails_the_call_before_any_group_connects() {
1222 let error = route(
1223 vec![
1224 vec![subscription(
1225 ExchangeId::BinanceSpot,
1226 MarketDataInstrumentKind::Spot,
1227 SubKind::PublicTrades,
1228 )],
1229 vec![subscription(
1230 ExchangeId::MassiveForex,
1231 MarketDataInstrumentKind::Spot,
1232 SubKind::OrderBooksL1,
1233 )],
1234 ],
1235 &DynamicSubscribers::default(),
1236 )
1237 .err()
1238 .unwrap();
1239
1240 assert_eq!(
1241 error,
1242 DataError::SubscriberRequired {
1243 exchange: ExchangeId::MassiveForex
1244 }
1245 );
1246 }
1247
1248 #[cfg(not(feature = "massive"))]
1249 #[test]
1250 fn a_massive_subscription_without_the_feature_names_the_feature() {
1251 let error = route(
1252 vec![vec![subscription(
1253 ExchangeId::MassiveCrypto,
1254 MarketDataInstrumentKind::Spot,
1255 SubKind::PublicTrades,
1256 )]],
1257 &DynamicSubscribers::default(),
1258 )
1259 .err()
1260 .unwrap();
1261
1262 assert_eq!(
1263 error,
1264 DataError::FeatureDisabled {
1265 exchange: ExchangeId::MassiveCrypto,
1266 feature: "massive".to_owned(),
1267 }
1268 );
1269 }
1270
1271 #[test]
1272 fn massive_pairs_outside_the_support_matrix_are_refused_before_routing() {
1273 let spot = MarketDataInstrumentKind::Spot;
1274 for (exchange, kind, sub_kind) in [
1275 (
1276 ExchangeId::MassiveForex,
1277 spot.clone(),
1278 SubKind::Candles {
1279 interval: CandleInterval::Min5,
1280 },
1281 ),
1282 (
1284 ExchangeId::MassiveForex,
1285 spot.clone(),
1286 SubKind::PublicTrades,
1287 ),
1288 (ExchangeId::MassiveForex, spot.clone(), SubKind::Quotes),
1289 (ExchangeId::MassiveOptions, spot, SubKind::PublicTrades),
1291 (ExchangeId::MassiveStocks, option(), SubKind::PublicTrades),
1292 ] {
1293 let error =
1294 validate_batches([[subscription(exchange, kind.clone(), sub_kind)]]).unwrap_err();
1295
1296 assert!(
1297 matches!(&error, DataError::Socket(message) if message.contains(exchange.as_str())),
1298 "{exchange} ({kind}, {sub_kind}): {error:?}"
1299 );
1300 }
1301 }
1302
1303 #[cfg(feature = "lse")]
1304 #[test]
1305 fn one_lse_batch_across_datasets_and_kinds_shares_one_subscriber() {
1306 let subscriber =
1307 LseSubscriber::new(crate::exchange::lse::live::LseCredentials::new("test-key"));
1308 let subscribers = DynamicSubscribers::default().with_lse(subscriber.clone());
1309 let before = subscriber.connection_handles();
1310
1311 let groups = route(
1312 vec![vec![
1313 subscription(
1314 ExchangeId::LseCrypto,
1315 MarketDataInstrumentKind::Spot,
1316 SubKind::PublicTrades,
1317 ),
1318 subscription(
1319 ExchangeId::LseCrypto,
1320 MarketDataInstrumentKind::Spot,
1321 SubKind::OrderBooksL1,
1322 ),
1323 subscription(
1324 ExchangeId::LseEquities,
1325 MarketDataInstrumentKind::Spot,
1326 SubKind::PublicTrades,
1327 ),
1328 subscription(
1329 ExchangeId::LseEquities,
1330 MarketDataInstrumentKind::Spot,
1331 SubKind::OrderBooksL1,
1332 ),
1333 ]],
1334 &subscribers,
1335 )
1336 .unwrap();
1337
1338 assert_eq!(groups.len(), 4);
1341 assert_eq!(subscriber.connection_handles(), before + groups.len());
1342
1343 drop(groups);
1344 assert_eq!(subscriber.connection_handles(), before);
1345 }
1346
1347 #[cfg(feature = "lse")]
1348 #[test]
1349 fn an_lse_subscription_without_a_subscriber_fails_the_call_before_any_group_connects() {
1350 let error = route(
1351 vec![
1352 vec![subscription(
1353 ExchangeId::BinanceSpot,
1354 MarketDataInstrumentKind::Spot,
1355 SubKind::PublicTrades,
1356 )],
1357 vec![subscription(
1358 ExchangeId::LseFx,
1359 MarketDataInstrumentKind::Spot,
1360 SubKind::PublicTrades,
1361 )],
1362 ],
1363 &DynamicSubscribers::default(),
1364 )
1365 .err()
1366 .unwrap();
1367
1368 assert_eq!(
1369 error,
1370 DataError::SubscriberRequired {
1371 exchange: ExchangeId::LseFx
1372 }
1373 );
1374 }
1375
1376 #[cfg(not(feature = "lse"))]
1377 #[test]
1378 fn an_lse_subscription_without_the_feature_names_the_feature() {
1379 let error = route(
1380 vec![vec![subscription(
1381 ExchangeId::LseFx,
1382 MarketDataInstrumentKind::Spot,
1383 SubKind::PublicTrades,
1384 )]],
1385 &DynamicSubscribers::default(),
1386 )
1387 .err()
1388 .unwrap();
1389
1390 assert_eq!(
1391 error,
1392 DataError::FeatureDisabled {
1393 exchange: ExchangeId::LseFx,
1394 feature: "lse".to_owned(),
1395 }
1396 );
1397 }
1398
1399 #[tokio::test]
1400 async fn a_failed_group_stops_every_forwarder_that_started() {
1401 struct OnDrop(Option<tokio::sync::oneshot::Sender<()>>);
1402 impl Drop for OnDrop {
1403 fn drop(&mut self) {
1404 let _ = self.0.take().map(|tx| tx.send(()));
1405 }
1406 }
1407
1408 let (dropped_tx, mut dropped) = tokio::sync::oneshot::channel();
1410 let guard = OnDrop(Some(dropped_tx));
1411 let forwarder = tokio::spawn(async move {
1412 let _guard = guard;
1413 std::future::pending::<()>().await;
1414 });
1415
1416 let error = settle(vec![
1417 Ok(forwarder),
1418 Err(DataError::SubscriberRequired {
1419 exchange: ExchangeId::LseFx,
1420 }),
1421 ])
1422 .await
1423 .unwrap_err();
1424
1425 assert_eq!(
1426 error,
1427 DataError::SubscriberRequired {
1428 exchange: ExchangeId::LseFx
1429 }
1430 );
1431 assert_eq!(dropped.try_recv(), Ok(()));
1434 }
1435
1436 #[tokio::test]
1437 async fn forwarders_keep_running_when_every_group_initialised() {
1438 let forwarder = tokio::spawn(std::future::pending::<()>());
1439 let abort = forwarder.abort_handle();
1440
1441 settle(vec![Ok(forwarder)]).await.unwrap();
1442 tokio::task::yield_now().await;
1443
1444 assert!(!abort.is_finished());
1445 abort.abort();
1446 }
1447}