use crate::{
error::DataError,
event::DataKind,
instrument::MarketInstrumentData,
streams::{
builder::dynamic::{DynamicStreams, DynamicSubscribers},
consumer::{MarketStreamEvent, MarketStreamResult},
reconnect::stream::ReconnectingStream,
},
subscription::{SubKind, Subscription},
};
use futures::Stream;
use itertools::Itertools;
use rustrade_instrument::{
Keyed,
exchange::ExchangeId,
index::{IndexedInstruments, error::IndexError},
instrument::{InstrumentIndex, market_data::MarketDataInstrument},
};
use tracing::warn;
pub async fn init_indexed_multi_exchange_market_stream(
instruments: &IndexedInstruments,
sub_kinds: &[SubKind],
) -> Result<impl Stream<Item = MarketStreamEvent<InstrumentIndex, DataKind>> + use<>, DataError> {
init_indexed_multi_exchange_market_stream_with(
&DynamicSubscribers::default(),
instruments,
sub_kinds,
)
.await
}
pub async fn init_indexed_multi_exchange_market_stream_with(
subscribers: &DynamicSubscribers,
instruments: &IndexedInstruments,
sub_kinds: &[SubKind],
) -> Result<impl Stream<Item = MarketStreamEvent<InstrumentIndex, DataKind>> + use<>, DataError> {
let subscriptions = generate_indexed_market_data_subscription_batches(instruments, sub_kinds);
let stream = DynamicStreams::init_with(subscribers, subscriptions)
.await?
.select_all::<MarketStreamResult<InstrumentIndex, DataKind>>()
.with_error_handler(|error| warn!(?error, "MarketStream generated error"));
Ok(stream)
}
pub fn generate_indexed_market_data_subscription_batches(
instruments: &IndexedInstruments,
sub_kinds: &[SubKind],
) -> Vec<Vec<Subscription<ExchangeId, MarketInstrumentData<InstrumentIndex>, SubKind>>> {
let instruments = instruments.instruments().iter().map(|keyed| {
let exchange = keyed.value.data_exchange().value;
let instrument = MarketInstrumentData::from(keyed);
Keyed::new(exchange, instrument)
});
let instruments = instruments.sorted_unstable_by_key(|exchange| exchange.key);
instruments
.chunk_by(|exchange| exchange.key)
.into_iter()
.map(|(_exchange, instruments)| {
instruments
.into_iter()
.flat_map(
|Keyed {
key: exchange,
value: instrument,
}| {
sub_kinds
.iter()
.map(move |kind| Subscription::new(exchange, instrument.clone(), *kind))
},
)
.collect::<Vec<_>>()
})
.collect()
}
pub fn index_market_data_subscription_batches<SubBatchIter, SubIter, Sub>(
instruments: &IndexedInstruments,
batches: SubBatchIter,
) -> Result<
Vec<Vec<Subscription<ExchangeId, Keyed<InstrumentIndex, MarketDataInstrument>>>>,
DataError,
>
where
SubBatchIter: IntoIterator<Item = SubIter>,
SubIter: IntoIterator<Item = Sub>,
Sub: Into<Subscription<ExchangeId, MarketDataInstrument, SubKind>>,
{
batches
.into_iter()
.map(|batch| batch
.into_iter()
.map(|sub| {
let sub = sub.into();
let base_index = instruments.find_asset_index(sub.exchange, &sub.instrument.base)?;
let quote_index = instruments.find_asset_index(sub.exchange, &sub.instrument.quote)?;
let find_instrument = |exchange, kind, base, quote| {
instruments
.instruments()
.iter()
.find_map(|indexed| {
(
indexed.value.exchange.value == exchange
&& indexed.value.kind.eq_market_data_instrument_kind(kind)
&& indexed.value.underlying.base == base
&& indexed.value.underlying.quote == quote
).then_some(indexed.key)
})
.ok_or(IndexError::InstrumentIndex(format!(
"Instrument: ({}, {}, {}, {}) must be present in indexed instruments: {:?}",
exchange, kind, base, quote, instruments.instruments()
)))
};
let instrument_index = find_instrument(sub.exchange, &sub.instrument.kind, base_index, quote_index)?;
Ok(Subscription {
exchange: sub.exchange,
instrument: Keyed::new(instrument_index, sub.instrument),
kind: sub.kind,
})
})
.collect()
)
.collect()
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
use super::*;
use rustrade_instrument::{
Underlying,
asset::Asset,
index::error::IndexError,
instrument::{
Instrument, data_venue::DataVenue, market_data::kind::MarketDataInstrumentKind,
name::InstrumentNameExchange,
},
};
fn bp_priced_by_lse() -> Instrument<ExchangeId, Asset> {
Instrument::spot(
ExchangeId::Ibkr,
"ibkr-bp",
"BP",
Underlying::new(
Asset::new_from_exchange("bp"),
Asset::new_from_exchange("usd"),
),
None,
)
.with_data_venue(DataVenue::new(
ExchangeId::LseEquities,
Some(InstrumentNameExchange::new("BP.L")),
))
}
#[test]
fn a_subscription_is_built_against_the_data_venue_and_its_symbol() {
let instruments = IndexedInstruments::new([bp_priced_by_lse()]);
let batches = generate_indexed_market_data_subscription_batches(
&instruments,
&[SubKind::PublicTrades],
);
let subscriptions = batches.into_iter().flatten().collect::<Vec<_>>();
assert_eq!(subscriptions.len(), 1);
assert_eq!(subscriptions[0].exchange, ExchangeId::LseEquities);
assert_eq!(
subscriptions[0].instrument.name_exchange,
InstrumentNameExchange::new("BP.L")
);
}
#[test]
fn a_single_venue_subscription_is_unchanged_by_the_data_venue_fallback() {
let instrument = Instrument::spot(
ExchangeId::BinanceSpot,
"binance_spot-btc_usdt",
"BTCUSDT",
Underlying::new(
Asset::new_from_exchange("btc"),
Asset::new_from_exchange("usdt"),
),
None,
);
let instruments = IndexedInstruments::new([instrument]);
let batches = generate_indexed_market_data_subscription_batches(
&instruments,
&[SubKind::PublicTrades],
);
let subscriptions = batches.into_iter().flatten().collect::<Vec<_>>();
assert_eq!(subscriptions.len(), 1);
assert_eq!(subscriptions[0].exchange, ExchangeId::BinanceSpot);
assert_eq!(
subscriptions[0].instrument.name_exchange,
InstrumentNameExchange::new("BTCUSDT")
);
}
#[test]
fn instruments_are_batched_by_data_venue_not_execution_venue() {
let aapl = Instrument::spot(
ExchangeId::AlpacaBroker,
"alpaca_broker-aapl",
"AAPL",
Underlying::new(
Asset::new_from_exchange("aapl"),
Asset::new_from_exchange("usd"),
),
None,
)
.with_data_venue(DataVenue::new_same_name(ExchangeId::LseEquities));
let instruments = IndexedInstruments::new([aapl, bp_priced_by_lse()]);
let batches = generate_indexed_market_data_subscription_batches(
&instruments,
&[SubKind::PublicTrades],
);
assert_eq!(batches.len(), 1, "{batches:?}");
assert_eq!(batches[0].len(), 2);
assert!(
batches[0]
.iter()
.all(|sub| sub.exchange == ExchangeId::LseEquities),
"{:?}",
batches[0]
);
}
#[test]
fn hand_written_subscription_naming_the_data_venue_is_rejected() {
let instruments = IndexedInstruments::new([bp_priced_by_lse()]);
let subscription = Subscription::new(
ExchangeId::LseEquities,
MarketDataInstrument::new("bp", "usd", MarketDataInstrumentKind::Spot),
SubKind::PublicTrades,
);
let result = index_market_data_subscription_batches(&instruments, [[subscription]]);
assert!(
matches!(result, Err(DataError::Index(IndexError::AssetIndex(_)))),
"the data venue registers no assets, so neither leg can resolve: {result:?}"
);
}
#[test]
fn hand_written_subscription_naming_the_execution_venue_succeeds_on_the_wrong_feed() {
let instruments = IndexedInstruments::new([bp_priced_by_lse()]);
let subscription = Subscription::new(
ExchangeId::Ibkr,
MarketDataInstrument::new("bp", "usd", MarketDataInstrumentKind::Spot),
SubKind::PublicTrades,
);
let batches = index_market_data_subscription_batches(&instruments, [[subscription]])
.expect(
"naming the execution venue resolves: its assets and instrument are registered",
);
let indexed = &batches[0][0];
assert_eq!(indexed.instrument.key, InstrumentIndex(0));
assert_eq!(indexed.exchange, ExchangeId::Ibkr);
assert_ne!(
indexed.exchange,
instruments.instruments()[0].value.data_exchange().value,
"the subscription names a venue the instrument is explicitly not priced on"
);
}
}