#[cfg(feature = "alpaca")]
use crate::exchange::alpaca::{AlpacaSubscriber, market::AlpacaInstrument};
#[cfg(feature = "hyperliquid")]
use crate::exchange::hyperliquid::market::HyperliquidInstrument;
#[cfg(feature = "lse")]
use crate::exchange::lse::{live::LseSubscriber, market::LseInstrument};
#[cfg(feature = "massive")]
use crate::exchange::massive::{MassiveSubscriber, market::MassiveInstrument};
use crate::{
error::DataError,
instrument::InstrumentData,
streams::consumer::MarketStreamResult,
subscription::{
SubKind, Subscription,
book::{OrderBookEvent, OrderBookL1},
candle::Candle,
liquidation::Liquidation,
trade::PublicTrade,
},
};
use fnv::FnvHashMap;
use futures::{Stream, stream::SelectAll};
use futures_util::{StreamExt, future::join_all};
use itertools::Itertools;
use plan::{Route, Txs};
use rustrade_instrument::exchange::ExchangeId;
use rustrade_integration::{
Validator,
channel::{UnboundedRx, mpsc_unbounded},
error::SocketError,
};
use std::fmt::Debug;
use tokio::task::JoinHandle;
use tokio_stream::wrappers::UnboundedReceiverStream;
use vecmap::VecMap;
pub mod indexed;
mod plan;
#[derive(Debug)]
pub struct DynamicStreams<InstrumentKey> {
pub trades:
VecMap<ExchangeId, UnboundedReceiverStream<MarketStreamResult<InstrumentKey, PublicTrade>>>,
pub l1s:
VecMap<ExchangeId, UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookL1>>>,
pub l2s: VecMap<
ExchangeId,
UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookEvent>>,
>,
pub liquidations:
VecMap<ExchangeId, UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Liquidation>>>,
pub candles:
VecMap<ExchangeId, UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Candle>>>,
}
#[derive(Clone, Debug, Default)]
pub struct DynamicSubscribers {
#[cfg(feature = "alpaca")]
alpaca: Option<AlpacaSubscriber>,
#[cfg(feature = "lse")]
lse: Option<LseSubscriber>,
#[cfg(feature = "massive")]
massive: Option<MassiveSubscriber>,
}
impl DynamicSubscribers {
#[cfg(feature = "alpaca")]
#[must_use]
pub fn with_alpaca(mut self, subscriber: AlpacaSubscriber) -> Self {
self.alpaca = Some(subscriber);
self
}
#[cfg(feature = "lse")]
#[must_use]
pub fn with_lse(mut self, subscriber: LseSubscriber) -> Self {
self.lse = Some(subscriber);
self
}
#[cfg(feature = "massive")]
#[must_use]
pub fn with_massive(mut self, subscriber: MassiveSubscriber) -> Self {
self.massive = Some(subscriber);
self
}
}
pub trait DynamicInstrument: Route {}
impl<Instrument> DynamicInstrument for Instrument where Instrument: Route {}
#[cfg(feature = "alpaca")]
pub trait DynamicAlpacaInstrument: AlpacaInstrument {}
#[cfg(feature = "alpaca")]
impl<Instrument> DynamicAlpacaInstrument for Instrument where Instrument: AlpacaInstrument {}
#[cfg(not(feature = "alpaca"))]
pub trait DynamicAlpacaInstrument {}
#[cfg(not(feature = "alpaca"))]
impl<Instrument> DynamicAlpacaInstrument for Instrument {}
#[cfg(feature = "lse")]
pub trait DynamicLseInstrument: LseInstrument {}
#[cfg(feature = "lse")]
impl<Instrument> DynamicLseInstrument for Instrument where Instrument: LseInstrument {}
#[cfg(not(feature = "lse"))]
pub trait DynamicLseInstrument {}
#[cfg(not(feature = "lse"))]
impl<Instrument> DynamicLseInstrument for Instrument {}
#[cfg(feature = "massive")]
pub trait DynamicMassiveInstrument: MassiveInstrument {}
#[cfg(feature = "massive")]
impl<Instrument> DynamicMassiveInstrument for Instrument where Instrument: MassiveInstrument {}
#[cfg(not(feature = "massive"))]
pub trait DynamicMassiveInstrument {}
#[cfg(not(feature = "massive"))]
impl<Instrument> DynamicMassiveInstrument for Instrument {}
#[cfg(feature = "hyperliquid")]
pub trait DynamicHyperliquidInstrument: HyperliquidInstrument {}
#[cfg(feature = "hyperliquid")]
impl<Instrument> DynamicHyperliquidInstrument for Instrument where Instrument: HyperliquidInstrument {}
#[cfg(not(feature = "hyperliquid"))]
pub trait DynamicHyperliquidInstrument {}
#[cfg(not(feature = "hyperliquid"))]
impl<Instrument> DynamicHyperliquidInstrument for Instrument {}
impl<InstrumentKey> DynamicStreams<InstrumentKey> {
pub async fn init<SubBatchIter, SubIter, Sub, Instrument>(
subscription_batches: SubBatchIter,
) -> Result<Self, DataError>
where
SubBatchIter: IntoIterator<Item = SubIter>,
SubIter: IntoIterator<Item = Sub>,
Sub: Into<Subscription<ExchangeId, Instrument, SubKind>>,
Instrument: DynamicInstrument + InstrumentData<Key = InstrumentKey>,
InstrumentKey: Debug + Clone + PartialEq + Send + Sync + 'static,
{
Self::init_with(&DynamicSubscribers::default(), subscription_batches).await
}
pub async fn init_with<SubBatchIter, SubIter, Sub, Instrument>(
subscribers: &DynamicSubscribers,
subscription_batches: SubBatchIter,
) -> Result<Self, DataError>
where
SubBatchIter: IntoIterator<Item = SubIter>,
SubIter: IntoIterator<Item = Sub>,
Sub: Into<Subscription<ExchangeId, Instrument, SubKind>>,
Instrument: DynamicInstrument + InstrumentData<Key = InstrumentKey>,
InstrumentKey: Debug + Clone + PartialEq + Send + Sync + 'static,
{
let batches = validate_batches(subscription_batches)?;
let Channels { txs, rxs } = Channels::try_from(&batches)?;
let groups = route_batches(batches, &txs, subscribers)?;
settle(join_all(groups).await).await?;
Ok(Self {
trades: rxs
.trades
.into_iter()
.map(|(exchange, rx)| (exchange, rx.into_stream()))
.collect(),
l1s: rxs
.l1s
.into_iter()
.map(|(exchange, rx)| (exchange, rx.into_stream()))
.collect(),
l2s: rxs
.l2s
.into_iter()
.map(|(exchange, rx)| (exchange, rx.into_stream()))
.collect(),
liquidations: rxs
.liquidations
.into_iter()
.map(|(exchange, rx)| (exchange, rx.into_stream()))
.collect(),
candles: rxs
.candles
.into_iter()
.map(|(exchange, rx)| (exchange, rx.into_stream()))
.collect(),
})
}
pub fn select_trades(
&mut self,
exchange: ExchangeId,
) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, PublicTrade>>> {
self.trades.remove(&exchange)
}
pub fn select_all_trades(
&mut self,
) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, PublicTrade>>> {
futures_util::stream::select_all::select_all(std::mem::take(&mut self.trades).into_values())
}
pub fn select_l1s(
&mut self,
exchange: ExchangeId,
) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookL1>>> {
self.l1s.remove(&exchange)
}
pub fn select_all_l1s(
&mut self,
) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookL1>>> {
futures_util::stream::select_all::select_all(std::mem::take(&mut self.l1s).into_values())
}
pub fn select_l2s(
&mut self,
exchange: ExchangeId,
) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookEvent>>> {
self.l2s.remove(&exchange)
}
pub fn select_all_l2s(
&mut self,
) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, OrderBookEvent>>> {
futures_util::stream::select_all::select_all(std::mem::take(&mut self.l2s).into_values())
}
pub fn select_liquidations(
&mut self,
exchange: ExchangeId,
) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Liquidation>>> {
self.liquidations.remove(&exchange)
}
pub fn select_all_liquidations(
&mut self,
) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Liquidation>>> {
futures_util::stream::select_all::select_all(
std::mem::take(&mut self.liquidations).into_values(),
)
}
pub fn select_candles(
&mut self,
exchange: ExchangeId,
) -> Option<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Candle>>> {
self.candles.remove(&exchange)
}
pub fn select_all_candles(
&mut self,
) -> SelectAll<UnboundedReceiverStream<MarketStreamResult<InstrumentKey, Candle>>> {
futures_util::stream::select_all::select_all(
std::mem::take(&mut self.candles).into_values(),
)
}
pub fn select_all<Output>(self) -> impl Stream<Item = Output>
where
InstrumentKey: Send + 'static,
Output: 'static,
MarketStreamResult<InstrumentKey, PublicTrade>: Into<Output>,
MarketStreamResult<InstrumentKey, OrderBookL1>: Into<Output>,
MarketStreamResult<InstrumentKey, OrderBookEvent>: Into<Output>,
MarketStreamResult<InstrumentKey, Liquidation>: Into<Output>,
MarketStreamResult<InstrumentKey, Candle>: Into<Output>,
{
let Self {
trades,
l1s,
l2s,
liquidations,
candles,
} = self;
let trades = trades
.into_values()
.map(|stream| stream.map(MarketStreamResult::into).boxed());
let l1s = l1s
.into_values()
.map(|stream| stream.map(MarketStreamResult::into).boxed());
let l2s = l2s
.into_values()
.map(|stream| stream.map(MarketStreamResult::into).boxed());
let liquidations = liquidations
.into_values()
.map(|stream| stream.map(MarketStreamResult::into).boxed());
let candles = candles
.into_values()
.map(|stream| stream.map(MarketStreamResult::into).boxed());
let all = trades
.chain(l1s)
.chain(l2s)
.chain(liquidations)
.chain(candles);
futures_util::stream::select_all::select_all(all)
}
}
pub fn validate_batches<SubBatchIter, SubIter, Sub, Instrument>(
batches: SubBatchIter,
) -> Result<Vec<Vec<Subscription<ExchangeId, Instrument, SubKind>>>, DataError>
where
SubBatchIter: IntoIterator<Item = SubIter>,
SubIter: IntoIterator<Item = Sub>,
Sub: Into<Subscription<ExchangeId, Instrument, SubKind>>,
Instrument: InstrumentData + Ord,
{
batches
.into_iter()
.map(validate_subscriptions::<SubIter, Sub, Instrument>)
.collect()
}
pub fn validate_subscriptions<SubIter, Sub, Instrument>(
batch: SubIter,
) -> Result<Vec<Subscription<ExchangeId, Instrument, SubKind>>, DataError>
where
SubIter: IntoIterator<Item = Sub>,
Sub: Into<Subscription<ExchangeId, Instrument, SubKind>>,
Instrument: InstrumentData + Ord,
{
let mut batch = batch
.into_iter()
.map(Sub::into)
.map(Validator::validate)
.collect::<Result<Vec<_>, SocketError>>()?;
batch.sort();
batch.dedup();
Ok(batch)
}
fn route_batches<Instrument>(
batches: Vec<Vec<Subscription<ExchangeId, Instrument, SubKind>>>,
txs: &Txs<Instrument::Key>,
subscribers: &DynamicSubscribers,
) -> Result<Vec<plan::GroupFuture>, DataError>
where
Instrument: DynamicInstrument,
{
batches
.into_iter()
.flat_map(|mut batch| {
batch.sort_unstable_by_key(|sub| (sub.exchange, sub.kind));
batch
.into_iter()
.chunk_by(|sub| (sub.exchange, sub.kind))
.into_iter()
.map(|(key, subs)| (key, subs.collect::<Vec<_>>()))
.collect::<Vec<_>>()
})
.map(|((exchange, sub_kind), subs)| {
Instrument::route(exchange, sub_kind, subs, txs, subscribers)
})
.collect()
}
async fn settle(results: Vec<Result<JoinHandle<()>, DataError>>) -> Result<(), DataError> {
let (forwarders, errors): (Vec<_>, Vec<_>) = results.into_iter().partition_result();
match errors.into_iter().next() {
None => Ok(()),
Some(error) => {
forwarders.iter().for_each(JoinHandle::abort);
join_all(forwarders).await;
Err(error)
}
}
}
struct Channels<InstrumentKey> {
txs: Txs<InstrumentKey>,
rxs: Rxs<InstrumentKey>,
}
impl<'a, Instrument> TryFrom<&'a Vec<Vec<Subscription<ExchangeId, Instrument, SubKind>>>>
for Channels<Instrument::Key>
where
Instrument: InstrumentData,
{
type Error = DataError;
fn try_from(
value: &'a Vec<Vec<Subscription<ExchangeId, Instrument, SubKind>>>,
) -> Result<Self, Self::Error> {
let mut txs = Txs::default();
let mut rxs = Rxs::default();
for sub in value.iter().flatten() {
match sub.kind {
SubKind::PublicTrades => {
if let (None, None) =
(txs.trades.get(&sub.exchange), rxs.trades.get(&sub.exchange))
{
let (tx, rx) = mpsc_unbounded();
txs.trades.insert(sub.exchange, tx);
rxs.trades.insert(sub.exchange, rx);
}
}
SubKind::OrderBooksL1 => {
if let (None, None) = (txs.l1s.get(&sub.exchange), rxs.l1s.get(&sub.exchange)) {
let (tx, rx) = mpsc_unbounded();
txs.l1s.insert(sub.exchange, tx);
rxs.l1s.insert(sub.exchange, rx);
}
}
SubKind::OrderBooksL2 => {
if let (None, None) = (txs.l2s.get(&sub.exchange), rxs.l2s.get(&sub.exchange)) {
let (tx, rx) = mpsc_unbounded();
txs.l2s.insert(sub.exchange, tx);
rxs.l2s.insert(sub.exchange, rx);
}
}
SubKind::Liquidations => {
if let (None, None) = (
txs.liquidations.get(&sub.exchange),
rxs.liquidations.get(&sub.exchange),
) {
let (tx, rx) = mpsc_unbounded();
txs.liquidations.insert(sub.exchange, tx);
rxs.liquidations.insert(sub.exchange, rx);
}
}
SubKind::Candles { .. } => {
if let (None, None) = (
txs.candles.get(&sub.exchange),
rxs.candles.get(&sub.exchange),
) {
let (tx, rx) = mpsc_unbounded();
txs.candles.insert(sub.exchange, tx);
rxs.candles.insert(sub.exchange, rx);
}
}
sub_kind @ (SubKind::OrderBooksL3 | SubKind::Quotes) => {
return Err(DataError::Unsupported {
exchange: sub.exchange,
sub_kind,
});
}
}
}
Ok(Channels { txs, rxs })
}
}
struct Rxs<InstrumentKey> {
trades: FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, PublicTrade>>>,
l1s: FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, OrderBookL1>>>,
l2s: FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, OrderBookEvent>>>,
liquidations:
FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, Liquidation>>>,
candles: FnvHashMap<ExchangeId, UnboundedRx<MarketStreamResult<InstrumentKey, Candle>>>,
}
impl<InstrumentKey> Default for Rxs<InstrumentKey> {
fn default() -> Self {
Self {
trades: Default::default(),
l1s: Default::default(),
l2s: Default::default(),
liquidations: Default::default(),
candles: Default::default(),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use crate::subscription::{candle::CandleInterval, exchange_supports_instrument_kind_sub_kind};
use chrono::{TimeZone, Utc};
use rust_decimal::Decimal;
use rustrade_instrument::instrument::{
kind::option::{OptionExercise, OptionKind},
market_data::{
MarketDataInstrument,
kind::{MarketDataFutureContract, MarketDataInstrumentKind, MarketDataOptionContract},
},
};
fn every_exchange() -> Vec<ExchangeId> {
use ExchangeId::*;
let every = vec![
Other,
Simulated,
Mock,
BinanceFuturesCoin,
BinanceFuturesUsd,
BinanceMargin,
BinanceOptions,
BinancePortfolioMargin,
BinanceSpot,
BinanceUs,
Bitazza,
Bitfinex,
Bitflyer,
Bitget,
Bitmart,
BitmartFuturesUsd,
Bitmex,
Bitso,
Bitstamp,
Bitvavo,
Bithumb,
BybitPerpetualsUsd,
BybitSpot,
Cexio,
Coinbase,
CoinbaseInternational,
Cryptocom,
DatabentoDbeq,
DatabentoGlbx,
DatabentoOpra,
DatabentoXnas,
DatabentoXnys,
Deribit,
GateioFuturesBtc,
GateioFuturesUsd,
GateioOptions,
GateioPerpetualsBtc,
GateioPerpetualsUsd,
GateioSpot,
Gemini,
Hitbtc,
Htx,
HyperliquidPerp,
HyperliquidSpot,
AlpacaBroker,
AlpacaCrypto,
AlpacaIex,
AlpacaSip,
Ibkr,
Kraken,
Kucoin,
Liquid,
Massive,
Mexc,
Okx,
Poloniex,
LseFx,
LseCrypto,
LseEquities,
LseFutures,
LseCfd,
LseOptions,
MassiveStocks,
MassiveCrypto,
MassiveForex,
MassiveOptions,
];
for exchange in &every {
match exchange {
Other
| Simulated
| Mock
| BinanceFuturesCoin
| BinanceFuturesUsd
| BinanceMargin
| BinanceOptions
| BinancePortfolioMargin
| BinanceSpot
| BinanceUs
| Bitazza
| Bitfinex
| Bitflyer
| Bitget
| Bitmart
| BitmartFuturesUsd
| Bitmex
| Bitso
| Bitstamp
| Bitvavo
| Bithumb
| BybitPerpetualsUsd
| BybitSpot
| Cexio
| Coinbase
| CoinbaseInternational
| Cryptocom
| DatabentoDbeq
| DatabentoGlbx
| DatabentoOpra
| DatabentoXnas
| DatabentoXnys
| Deribit
| GateioFuturesBtc
| GateioFuturesUsd
| GateioOptions
| GateioPerpetualsBtc
| GateioPerpetualsUsd
| GateioSpot
| Gemini
| Hitbtc
| Htx
| HyperliquidPerp
| HyperliquidSpot
| AlpacaBroker
| AlpacaCrypto
| AlpacaIex
| AlpacaSip
| Ibkr
| Kraken
| Kucoin
| Liquid
| Massive
| Mexc
| Okx
| Poloniex
| LseFx
| LseCrypto
| LseEquities
| LseFutures
| LseCfd
| LseOptions
| MassiveStocks
| MassiveCrypto
| MassiveForex
| MassiveOptions => {}
}
}
every
}
fn instrument_kinds() -> [MarketDataInstrumentKind; 5] {
let expiry = Utc.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).unwrap();
let every = [
MarketDataInstrumentKind::Spot,
MarketDataInstrumentKind::Perpetual,
MarketDataInstrumentKind::Cfd,
MarketDataInstrumentKind::Future(MarketDataFutureContract { expiry }),
option(),
];
for kind in &every {
match kind {
MarketDataInstrumentKind::Spot
| MarketDataInstrumentKind::Perpetual
| MarketDataInstrumentKind::Cfd
| MarketDataInstrumentKind::Future(_)
| MarketDataInstrumentKind::Option(_) => {}
}
}
every
}
fn sub_kinds() -> Vec<SubKind> {
let every: Vec<SubKind> = [
SubKind::PublicTrades,
SubKind::OrderBooksL1,
SubKind::OrderBooksL2,
SubKind::OrderBooksL3,
SubKind::Liquidations,
SubKind::Quotes,
]
.into_iter()
.chain(
CandleInterval::ALL
.into_iter()
.map(|interval| SubKind::Candles { interval }),
)
.collect();
for sub_kind in &every {
match sub_kind {
SubKind::PublicTrades
| SubKind::OrderBooksL1
| SubKind::OrderBooksL2
| SubKind::OrderBooksL3
| SubKind::Liquidations
| SubKind::Candles { .. }
| SubKind::Quotes => {}
}
}
every
}
fn subscription(
exchange: ExchangeId,
kind: MarketDataInstrumentKind,
sub_kind: SubKind,
) -> Subscription<ExchangeId, MarketDataInstrument, SubKind> {
Subscription::new(
exchange,
MarketDataInstrument::new("btc", "usd", kind),
sub_kind,
)
}
fn route(
batches: Vec<Vec<Subscription<ExchangeId, MarketDataInstrument, SubKind>>>,
subscribers: &DynamicSubscribers,
) -> Result<Vec<plan::GroupFuture>, DataError> {
let Channels { txs, rxs: _ } = Channels::try_from(&batches)?;
route_batches(batches, &txs, subscribers)
}
fn every_subscriber() -> DynamicSubscribers {
let subscribers = DynamicSubscribers::default();
#[cfg(feature = "alpaca")]
let subscribers = subscribers.with_alpaca(alpaca_subscriber());
#[cfg(feature = "lse")]
let subscribers = subscribers.with_lse(LseSubscriber::new(
crate::exchange::lse::live::LseCredentials::new("test-key"),
));
#[cfg(feature = "massive")]
let subscribers = subscribers.with_massive(massive_subscriber());
subscribers
}
#[cfg(feature = "massive")]
fn massive_subscriber() -> MassiveSubscriber {
MassiveSubscriber::new(crate::exchange::massive::MassiveCredentials::new(
"test-key",
))
}
fn option() -> MarketDataInstrumentKind {
MarketDataInstrumentKind::Option(MarketDataOptionContract {
kind: OptionKind::Call,
exercise: OptionExercise::American,
expiry: Utc.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).unwrap(),
strike: Decimal::ONE_HUNDRED,
})
}
#[cfg(feature = "alpaca")]
fn alpaca_subscriber() -> AlpacaSubscriber {
AlpacaSubscriber::new(crate::exchange::alpaca::AlpacaCredentials::new(
"test-key",
"test-secret",
))
}
fn feature_enabled(feature: &str) -> bool {
match feature {
"alpaca" => cfg!(feature = "alpaca"),
"lse" => cfg!(feature = "lse"),
"massive" => cfg!(feature = "massive"),
"hyperliquid" => cfg!(feature = "hyperliquid"),
other => panic!("no dynamic route is gated on the `{other}` feature"),
}
}
#[test]
fn every_pair_the_support_matrix_accepts_is_routed() {
let subscribers = every_subscriber();
let mut routed = 0;
for exchange in every_exchange() {
for kind in instrument_kinds() {
for sub_kind in sub_kinds() {
if !exchange_supports_instrument_kind_sub_kind(&exchange, &kind, sub_kind) {
continue;
}
match route(
vec![vec![subscription(exchange, kind.clone(), sub_kind)]],
&subscribers,
) {
Ok(groups) => {
assert_eq!(groups.len(), 1);
routed += 1;
}
Err(DataError::FeatureDisabled { feature, .. }) => assert!(
!feature_enabled(&feature),
"{exchange} ({kind}, {sub_kind}) reported `{feature}` disabled in a \
build that enables it"
),
Err(error) => panic!(
"the matrix accepts {exchange} ({kind}, {sub_kind}), but routing it \
failed: {error}"
),
}
}
}
}
assert!(routed > 30, "only {routed} pairs were routed");
}
#[test]
fn every_pair_the_router_serves_is_accepted_by_the_support_matrix() {
let subscribers = every_subscriber();
let mut served = 0;
for exchange in every_exchange() {
for sub_kind in sub_kinds() {
match route(
vec![vec![subscription(
exchange,
MarketDataInstrumentKind::Spot,
sub_kind,
)]],
&subscribers,
) {
Err(DataError::Unsupported { .. }) => continue,
Ok(_) | Err(DataError::FeatureDisabled { .. }) => {}
Err(error) => panic!(
"routing {exchange} ({sub_kind}) failed with neither a route nor \
Unsupported: {error}"
),
}
served += 1;
let candidates = match sub_kind {
SubKind::Candles { .. } => CandleInterval::ALL
.into_iter()
.map(|interval| SubKind::Candles { interval })
.collect(),
other => vec![other],
};
let accepted = instrument_kinds().iter().any(|kind| {
candidates.iter().any(|candidate| {
exchange_supports_instrument_kind_sub_kind(&exchange, kind, *candidate)
})
});
assert!(
accepted,
"the router serves {exchange} ({sub_kind}), but the support matrix accepts \
it for no instrument kind"
);
}
}
assert!(served > 30, "only {served} pairs were served");
}
#[test]
fn a_pair_the_support_matrix_refuses_is_refused_before_routing() {
let error = validate_batches([[subscription(
ExchangeId::LseOptions,
option(),
SubKind::OrderBooksL1,
)]])
.unwrap_err();
assert!(
matches!(&error, DataError::Socket(message) if message.contains("lse_options")),
"{error:?}"
);
}
#[cfg(feature = "hyperliquid")]
#[test]
fn hyperliquid_spot_trades_and_depth_are_validated_and_routed() {
let batch: Vec<_> = [SubKind::PublicTrades, SubKind::OrderBooksL2]
.into_iter()
.map(|sub_kind| {
subscription(
ExchangeId::HyperliquidSpot,
MarketDataInstrumentKind::Spot,
sub_kind,
)
})
.collect();
let batches = validate_batches([batch]).unwrap();
let groups = route(batches, &DynamicSubscribers::default()).unwrap();
assert_eq!(groups.len(), 2);
}
#[cfg(not(feature = "hyperliquid"))]
#[test]
fn a_hyperliquid_spot_subscription_without_the_feature_names_the_feature() {
let error = route(
vec![vec![subscription(
ExchangeId::HyperliquidSpot,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
)]],
&DynamicSubscribers::default(),
)
.err()
.unwrap();
assert_eq!(
error,
DataError::FeatureDisabled {
exchange: ExchangeId::HyperliquidSpot,
feature: "hyperliquid".to_owned(),
}
);
}
#[cfg(feature = "alpaca")]
#[test]
fn one_alpaca_batch_across_feeds_and_kinds_shares_one_subscriber() {
let subscriber = alpaca_subscriber();
let subscribers = DynamicSubscribers::default().with_alpaca(subscriber.clone());
let before = subscriber.connection_handles();
let groups = route(
vec![vec![
subscription(
ExchangeId::AlpacaCrypto,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
),
subscription(
ExchangeId::AlpacaCrypto,
MarketDataInstrumentKind::Spot,
SubKind::OrderBooksL1,
),
subscription(
ExchangeId::AlpacaIex,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
),
subscription(
ExchangeId::AlpacaIex,
MarketDataInstrumentKind::Spot,
SubKind::OrderBooksL1,
),
]],
&subscribers,
)
.unwrap();
assert_eq!(groups.len(), 4);
assert_eq!(subscriber.connection_handles(), before + groups.len());
drop(groups);
assert_eq!(subscriber.connection_handles(), before);
}
#[cfg(feature = "alpaca")]
#[test]
fn an_alpaca_subscription_without_a_subscriber_fails_the_call_before_any_group_connects() {
let error = route(
vec![
vec![subscription(
ExchangeId::BinanceSpot,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
)],
vec![subscription(
ExchangeId::AlpacaIex,
MarketDataInstrumentKind::Spot,
SubKind::OrderBooksL1,
)],
],
&DynamicSubscribers::default(),
)
.err()
.unwrap();
assert_eq!(
error,
DataError::SubscriberRequired {
exchange: ExchangeId::AlpacaIex
}
);
}
#[cfg(not(feature = "alpaca"))]
#[test]
fn an_alpaca_subscription_without_the_feature_names_the_feature() {
let error = route(
vec![vec![subscription(
ExchangeId::AlpacaCrypto,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
)]],
&DynamicSubscribers::default(),
)
.err()
.unwrap();
assert_eq!(
error,
DataError::FeatureDisabled {
exchange: ExchangeId::AlpacaCrypto,
feature: "alpaca".to_owned(),
}
);
}
#[test]
fn alpaca_quotes_are_refused_before_routing_because_dynamic_streams_carry_no_quote() {
let error = validate_batches([[subscription(
ExchangeId::AlpacaCrypto,
MarketDataInstrumentKind::Spot,
SubKind::Quotes,
)]])
.unwrap_err();
assert!(
matches!(&error, DataError::Socket(message) if message.contains("alpaca_crypto")),
"{error:?}"
);
}
#[cfg(feature = "massive")]
#[test]
fn one_massive_batch_across_clusters_and_kinds_shares_one_subscriber() {
let subscriber = massive_subscriber();
let subscribers = DynamicSubscribers::default().with_massive(subscriber.clone());
let before = subscriber.connection_handles();
let groups = route(
vec![vec![
subscription(
ExchangeId::MassiveStocks,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
),
subscription(
ExchangeId::MassiveCrypto,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
),
subscription(
ExchangeId::MassiveCrypto,
MarketDataInstrumentKind::Spot,
SubKind::OrderBooksL1,
),
subscription(
ExchangeId::MassiveCrypto,
MarketDataInstrumentKind::Spot,
SubKind::Candles {
interval: CandleInterval::Min1,
},
),
subscription(
ExchangeId::MassiveForex,
MarketDataInstrumentKind::Spot,
SubKind::OrderBooksL1,
),
subscription(ExchangeId::MassiveOptions, option(), SubKind::PublicTrades),
]],
&subscribers,
)
.unwrap();
assert_eq!(groups.len(), 6);
assert_eq!(subscriber.connection_handles(), before + groups.len());
drop(groups);
assert_eq!(subscriber.connection_handles(), before);
}
#[cfg(feature = "massive")]
#[test]
fn a_massive_subscription_without_a_subscriber_fails_the_call_before_any_group_connects() {
let error = route(
vec![
vec![subscription(
ExchangeId::BinanceSpot,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
)],
vec![subscription(
ExchangeId::MassiveForex,
MarketDataInstrumentKind::Spot,
SubKind::OrderBooksL1,
)],
],
&DynamicSubscribers::default(),
)
.err()
.unwrap();
assert_eq!(
error,
DataError::SubscriberRequired {
exchange: ExchangeId::MassiveForex
}
);
}
#[cfg(not(feature = "massive"))]
#[test]
fn a_massive_subscription_without_the_feature_names_the_feature() {
let error = route(
vec![vec![subscription(
ExchangeId::MassiveCrypto,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
)]],
&DynamicSubscribers::default(),
)
.err()
.unwrap();
assert_eq!(
error,
DataError::FeatureDisabled {
exchange: ExchangeId::MassiveCrypto,
feature: "massive".to_owned(),
}
);
}
#[test]
fn massive_pairs_outside_the_support_matrix_are_refused_before_routing() {
let spot = MarketDataInstrumentKind::Spot;
for (exchange, kind, sub_kind) in [
(
ExchangeId::MassiveForex,
spot.clone(),
SubKind::Candles {
interval: CandleInterval::Min5,
},
),
(
ExchangeId::MassiveForex,
spot.clone(),
SubKind::PublicTrades,
),
(ExchangeId::MassiveForex, spot.clone(), SubKind::Quotes),
(ExchangeId::MassiveOptions, spot, SubKind::PublicTrades),
(ExchangeId::MassiveStocks, option(), SubKind::PublicTrades),
] {
let error =
validate_batches([[subscription(exchange, kind.clone(), sub_kind)]]).unwrap_err();
assert!(
matches!(&error, DataError::Socket(message) if message.contains(exchange.as_str())),
"{exchange} ({kind}, {sub_kind}): {error:?}"
);
}
}
#[cfg(feature = "lse")]
#[test]
fn one_lse_batch_across_datasets_and_kinds_shares_one_subscriber() {
let subscriber =
LseSubscriber::new(crate::exchange::lse::live::LseCredentials::new("test-key"));
let subscribers = DynamicSubscribers::default().with_lse(subscriber.clone());
let before = subscriber.connection_handles();
let groups = route(
vec![vec![
subscription(
ExchangeId::LseCrypto,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
),
subscription(
ExchangeId::LseCrypto,
MarketDataInstrumentKind::Spot,
SubKind::OrderBooksL1,
),
subscription(
ExchangeId::LseEquities,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
),
subscription(
ExchangeId::LseEquities,
MarketDataInstrumentKind::Spot,
SubKind::OrderBooksL1,
),
]],
&subscribers,
)
.unwrap();
assert_eq!(groups.len(), 4);
assert_eq!(subscriber.connection_handles(), before + groups.len());
drop(groups);
assert_eq!(subscriber.connection_handles(), before);
}
#[cfg(feature = "lse")]
#[test]
fn an_lse_subscription_without_a_subscriber_fails_the_call_before_any_group_connects() {
let error = route(
vec![
vec![subscription(
ExchangeId::BinanceSpot,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
)],
vec![subscription(
ExchangeId::LseFx,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
)],
],
&DynamicSubscribers::default(),
)
.err()
.unwrap();
assert_eq!(
error,
DataError::SubscriberRequired {
exchange: ExchangeId::LseFx
}
);
}
#[cfg(not(feature = "lse"))]
#[test]
fn an_lse_subscription_without_the_feature_names_the_feature() {
let error = route(
vec![vec![subscription(
ExchangeId::LseFx,
MarketDataInstrumentKind::Spot,
SubKind::PublicTrades,
)]],
&DynamicSubscribers::default(),
)
.err()
.unwrap();
assert_eq!(
error,
DataError::FeatureDisabled {
exchange: ExchangeId::LseFx,
feature: "lse".to_owned(),
}
);
}
#[tokio::test]
async fn a_failed_group_stops_every_forwarder_that_started() {
struct OnDrop(Option<tokio::sync::oneshot::Sender<()>>);
impl Drop for OnDrop {
fn drop(&mut self) {
let _ = self.0.take().map(|tx| tx.send(()));
}
}
let (dropped_tx, mut dropped) = tokio::sync::oneshot::channel();
let guard = OnDrop(Some(dropped_tx));
let forwarder = tokio::spawn(async move {
let _guard = guard;
std::future::pending::<()>().await;
});
let error = settle(vec![
Ok(forwarder),
Err(DataError::SubscriberRequired {
exchange: ExchangeId::LseFx,
}),
])
.await
.unwrap_err();
assert_eq!(
error,
DataError::SubscriberRequired {
exchange: ExchangeId::LseFx
}
);
assert_eq!(dropped.try_recv(), Ok(()));
}
#[tokio::test]
async fn forwarders_keep_running_when_every_group_initialised() {
let forwarder = tokio::spawn(std::future::pending::<()>());
let abort = forwarder.abort_handle();
settle(vec![Ok(forwarder)]).await.unwrap();
tokio::task::yield_now().await;
assert!(!abort.is_finished());
abort.abort();
}
}