use crate::{
Keyed,
asset::{Asset, AssetIndex, ExchangeAsset, name::AssetNameInternal},
exchange::{ExchangeId, ExchangeIndex},
index::{builder::IndexedInstrumentsBuilder, error::IndexError},
instrument::{Instrument, InstrumentIndex, name::InstrumentNameInternal},
};
use serde::{Deserialize, Serialize, Serializer, ser::SerializeSeq};
pub mod builder;
pub mod error;
#[derive(Debug, Clone, PartialEq, PartialOrd, Deserialize)]
#[serde(try_from = "Vec<Instrument<ExchangeId, Asset>>")]
pub struct IndexedInstruments {
exchanges: Vec<Keyed<ExchangeIndex, ExchangeId>>,
assets: Vec<Keyed<AssetIndex, ExchangeAsset<Asset>>>,
instruments:
Vec<Keyed<InstrumentIndex, Instrument<Keyed<ExchangeIndex, ExchangeId>, AssetIndex>>>,
}
impl IndexedInstruments {
pub fn new<Iter, I>(instruments: Iter) -> Self
where
Iter: IntoIterator<Item = I>,
I: Into<Instrument<ExchangeId, Asset>>,
{
#[allow(clippy::panic)] Self::try_new(instruments)
.unwrap_or_else(|error| panic!("failed to build IndexedInstruments: {error}"))
}
pub fn try_new<Iter, I>(instruments: Iter) -> Result<Self, IndexError>
where
Iter: IntoIterator<Item = I>,
I: Into<Instrument<ExchangeId, Asset>>,
{
instruments
.into_iter()
.fold(Self::builder(), |builder, instrument| {
builder.add_instrument(instrument.into())
})
.try_build()
}
pub fn builder() -> IndexedInstrumentsBuilder {
IndexedInstrumentsBuilder::default()
}
pub fn exchanges(&self) -> &[Keyed<ExchangeIndex, ExchangeId>] {
&self.exchanges
}
pub fn assets(&self) -> &[Keyed<AssetIndex, ExchangeAsset<Asset>>] {
&self.assets
}
pub fn instruments(
&self,
) -> &[Keyed<InstrumentIndex, Instrument<Keyed<ExchangeIndex, ExchangeId>, AssetIndex>>] {
&self.instruments
}
pub fn find_exchange_index(&self, exchange: ExchangeId) -> Result<ExchangeIndex, IndexError> {
find_exchange_by_exchange_id(&self.exchanges, &exchange)
}
pub fn find_exchange(&self, index: ExchangeIndex) -> Result<ExchangeId, IndexError> {
self.exchanges
.iter()
.find(|keyed| keyed.key == index)
.map(|keyed| keyed.value)
.ok_or(IndexError::ExchangeIndex(format!(
"ExchangeIndex: {index} is not present in indexed instrument exchanges"
)))
}
pub fn find_asset_index(
&self,
exchange: ExchangeId,
name: &AssetNameInternal,
) -> Result<AssetIndex, IndexError> {
find_asset_by_exchange_and_name_internal(&self.assets, exchange, name)
}
pub fn find_asset(&self, index: AssetIndex) -> Result<&ExchangeAsset<Asset>, IndexError> {
self.assets
.iter()
.find(|keyed| keyed.key == index)
.map(|keyed| &keyed.value)
.ok_or(IndexError::AssetIndex(format!(
"AssetIndex: {index} is not present in indexed instrument assets"
)))
}
pub fn find_instrument_index(
&self,
exchange: ExchangeId,
name: &InstrumentNameInternal,
) -> Result<InstrumentIndex, IndexError> {
self.instruments
.iter()
.find_map(|indexed| {
(indexed.value.exchange.value == exchange && indexed.value.name_internal == *name)
.then_some(indexed.key)
})
.ok_or_else(|| {
IndexError::InstrumentIndex(format!(
"Instrument: ({exchange}, {name}) is not present in indexed instruments: {:?}",
self.instruments
.iter()
.map(|keyed| format!(
"({}, {})",
keyed.value.exchange.value, keyed.value.name_internal
))
.collect::<Vec<_>>()
))
})
}
pub fn find_instrument(
&self,
index: InstrumentIndex,
) -> Result<&Instrument<Keyed<ExchangeIndex, ExchangeId>, AssetIndex>, IndexError> {
self.instruments
.iter()
.find(|keyed| keyed.key == index)
.map(|keyed| &keyed.value)
.ok_or(IndexError::InstrumentIndex(format!(
"InstrumentIndex: {index} is not present in indexed instrument instruments"
)))
}
}
impl IndexedInstruments {
fn unindexed_instrument(
&self,
instrument: &Instrument<Keyed<ExchangeIndex, ExchangeId>, AssetIndex>,
) -> Result<Instrument<ExchangeId, Asset>, IndexError> {
instrument
.clone()
.map_exchange_key(|exchange| exchange.value)
.map_asset_key_with_lookup(|index| {
self.assets
.get(index.index())
.filter(|keyed| keyed.key == *index)
.map(|keyed| keyed.value.asset.clone())
.ok_or_else(|| {
IndexError::AssetIndex(format!(
"AssetIndex: {index} of instrument {} is not present in indexed \
instrument assets",
instrument.name_internal
))
})
})
}
}
impl Serialize for IndexedInstruments {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut seq = serializer.serialize_seq(Some(self.instruments.len()))?;
for keyed in &self.instruments {
let instrument = self
.unindexed_instrument(&keyed.value)
.map_err(serde::ser::Error::custom)?;
seq.serialize_element(&instrument)?;
}
seq.end()
}
}
impl TryFrom<Vec<Instrument<ExchangeId, Asset>>> for IndexedInstruments {
type Error = IndexError;
fn try_from(instruments: Vec<Instrument<ExchangeId, Asset>>) -> Result<Self, Self::Error> {
Self::try_new(instruments)
}
}
impl<I> FromIterator<I> for IndexedInstruments
where
I: Into<Instrument<ExchangeId, Asset>>,
{
fn from_iter<Iter>(iter: Iter) -> Self
where
Iter: IntoIterator<Item = I>,
{
Self::new(iter)
}
}
fn find_exchange_by_exchange_id(
haystack: &[Keyed<ExchangeIndex, ExchangeId>],
needle: &ExchangeId,
) -> Result<ExchangeIndex, IndexError> {
haystack
.iter()
.find_map(|indexed| (indexed.value == *needle).then_some(indexed.key))
.ok_or(IndexError::ExchangeIndex(format!(
"Exchange: {needle} is not present in indexed instrument exchanges: {haystack:?}"
)))
}
fn find_asset_by_exchange_and_name_internal(
haystack: &[Keyed<AssetIndex, ExchangeAsset<Asset>>],
needle_exchange: ExchangeId,
needle_name: &AssetNameInternal,
) -> Result<AssetIndex, IndexError> {
haystack
.iter()
.find_map(|indexed| {
(indexed.value.exchange == needle_exchange
&& indexed.value.asset.name_internal == *needle_name)
.then_some(indexed.key)
})
.ok_or(IndexError::AssetIndex(format!(
"Asset: ({needle_exchange}, {needle_name}) is not present in indexed instrument assets: {haystack:?}"
)))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
use crate::{
Underlying,
asset::Asset,
exchange::ExchangeId,
instrument::{
kind::InstrumentKind, name::InstrumentNameExchange, quote::InstrumentQuoteAsset,
},
test_utils::{exchange_asset, instrument},
};
use rust_decimal::Decimal;
#[test]
fn test_indexed_instruments_new() {
let empty = IndexedInstruments::new(std::iter::empty::<Instrument<ExchangeId, Asset>>());
assert!(empty.exchanges().is_empty());
assert!(empty.assets().is_empty());
assert!(empty.instruments().is_empty());
let instrument = instrument(ExchangeId::BinanceSpot, "btc", "usdt");
let actual = IndexedInstruments::new(std::iter::once(instrument));
assert_eq!(actual.exchanges().len(), 1);
assert_eq!(actual.assets().len(), 2); assert_eq!(actual.instruments().len(), 1);
assert_eq!(actual.exchanges()[0].value, ExchangeId::BinanceSpot);
assert_eq!(
actual.assets()[0].value,
exchange_asset(ExchangeId::BinanceSpot, "btc"),
);
assert_eq!(
actual.assets()[1].value,
exchange_asset(ExchangeId::BinanceSpot, "usdt"),
);
assert_eq!(
actual.instruments()[0].value,
Instrument {
exchange: Keyed::new(ExchangeIndex(0), ExchangeId::BinanceSpot),
data_venue: None,
name_exchange: InstrumentNameExchange::new("btc_usdt"),
name_internal: InstrumentNameInternal::new("binance_spot-btc_usdt"),
underlying: Underlying {
base: AssetIndex(0),
quote: AssetIndex(1),
},
quote: InstrumentQuoteAsset::UnderlyingQuote,
kind: InstrumentKind::Spot,
spec: None
}
);
}
#[test]
fn test_indexed_instruments_multiple() {
let instruments = vec![
instrument(ExchangeId::BinanceSpot, "BTC", "USDT"),
instrument(ExchangeId::BinanceSpot, "ETH", "USDT"),
instrument(ExchangeId::Coinbase, "BTC", "USD"),
];
let indexed = IndexedInstruments::new(instruments);
assert_eq!(indexed.exchanges().len(), 2);
assert_eq!(indexed.assets().len(), 5);
assert_eq!(indexed.instruments().len(), 3);
let exchanges: Vec<_> = indexed.exchanges().iter().map(|e| e.value).collect();
assert!(exchanges.contains(&ExchangeId::BinanceSpot));
assert!(exchanges.contains(&ExchangeId::Coinbase));
}
#[test]
fn test_find_exchange_index() {
let instruments = vec![
instrument(ExchangeId::BinanceSpot, "BTC", "USDT"),
instrument(ExchangeId::Coinbase, "ETH", "USD"),
];
let indexed = IndexedInstruments::new(instruments);
assert!(indexed.find_exchange_index(ExchangeId::BinanceSpot).is_ok());
assert!(indexed.find_exchange_index(ExchangeId::Coinbase).is_ok());
let err = indexed.find_exchange_index(ExchangeId::Kraken).unwrap_err();
assert!(matches!(err, IndexError::ExchangeIndex(_)));
}
#[test]
fn test_find_asset_index() {
let instruments = vec![
instrument(ExchangeId::BinanceSpot, "BTC", "USDT"),
instrument(ExchangeId::Coinbase, "ETH", "USD"),
];
let indexed = IndexedInstruments::new(instruments);
assert!(
indexed
.find_asset_index(ExchangeId::BinanceSpot, &AssetNameInternal::from("btc"))
.is_ok()
);
assert!(
indexed
.find_asset_index(ExchangeId::BinanceSpot, &AssetNameInternal::from("usdt"))
.is_ok()
);
assert!(
indexed
.find_asset_index(ExchangeId::Coinbase, &AssetNameInternal::from("eth"))
.is_ok()
);
let err = indexed
.find_asset_index(ExchangeId::Kraken, &AssetNameInternal::from("btc"))
.unwrap_err();
assert!(matches!(err, IndexError::AssetIndex(_)));
let err = indexed
.find_asset_index(
ExchangeId::BinanceSpot,
&AssetNameInternal::from("nonexistent"),
)
.unwrap_err();
assert!(matches!(err, IndexError::AssetIndex(_)));
}
#[test]
fn test_find_instrument_index() {
let instruments = vec![
instrument(ExchangeId::BinanceSpot, "btc", "usdt"),
instrument(ExchangeId::Coinbase, "eth", "usd"),
];
let indexed = IndexedInstruments::new(instruments);
let btc_usdt = InstrumentNameInternal::from("binance_spot-btc_usdt");
assert!(
indexed
.find_instrument_index(ExchangeId::BinanceSpot, &btc_usdt)
.is_ok()
);
let err = indexed
.find_instrument_index(ExchangeId::Kraken, &btc_usdt)
.unwrap_err();
assert!(matches!(err, IndexError::InstrumentIndex(_)));
let nonexistent = InstrumentNameInternal::from("nonexistent");
let err = indexed
.find_instrument_index(ExchangeId::BinanceSpot, &nonexistent)
.unwrap_err();
let IndexError::InstrumentIndex(message) = err else {
panic!("expected IndexError::InstrumentIndex, got {err:?}");
};
assert!(message.contains("(binance_spot, nonexistent)"), "{message}");
assert!(message.contains("binance_spot-btc_usdt"), "{message}");
assert!(!message.contains("ExchangeAsset"), "{message}");
}
#[test]
fn test_private_find_exchange_by_exchange_id() {
let exchanges = vec![
Keyed {
key: ExchangeIndex(0),
value: ExchangeId::BinanceSpot,
},
Keyed {
key: ExchangeIndex(1),
value: ExchangeId::Coinbase,
},
];
let result = find_exchange_by_exchange_id(&exchanges, &ExchangeId::BinanceSpot);
assert_eq!(result.unwrap(), ExchangeIndex(0));
let err = find_exchange_by_exchange_id(&exchanges, &ExchangeId::Kraken).unwrap_err();
assert!(matches!(err, IndexError::ExchangeIndex(_)));
}
#[test]
fn test_private_find_asset_by_exchange_and_name_internal() {
let assets = vec![
Keyed {
key: AssetIndex(0),
value: ExchangeAsset {
exchange: ExchangeId::BinanceSpot,
asset: Asset::new_from_exchange("BTC"),
},
},
Keyed {
key: AssetIndex(1),
value: ExchangeAsset {
exchange: ExchangeId::BinanceSpot,
asset: Asset::new_from_exchange("USDT"),
},
},
];
let result = find_asset_by_exchange_and_name_internal(
&assets,
ExchangeId::BinanceSpot,
&AssetNameInternal::from("btc"),
);
assert_eq!(result.unwrap(), AssetIndex(0));
let err = find_asset_by_exchange_and_name_internal(
&assets,
ExchangeId::Kraken,
&AssetNameInternal::from("btc"),
)
.unwrap_err();
assert!(matches!(err, IndexError::AssetIndex(_)));
let err = find_asset_by_exchange_and_name_internal(
&assets,
ExchangeId::BinanceSpot,
&AssetNameInternal::from("nonexistent"),
)
.unwrap_err();
assert!(matches!(err, IndexError::AssetIndex(_)));
}
#[test]
fn test_duplicates_are_filtered_correctly() {
let instruments = vec![
instrument(ExchangeId::BinanceSpot, "btc", "usdt"),
instrument(ExchangeId::BinanceSpot, "btc", "usdt"),
];
let indexed = IndexedInstruments::new(instruments);
assert_eq!(indexed.exchanges().len(), 1);
assert_eq!(indexed.assets().len(), 2);
assert_eq!(indexed.instruments().len(), 1);
let instruments = vec![
instrument(ExchangeId::BinanceSpot, "btc", "usdt"),
instrument(ExchangeId::Coinbase, "btc", "usdt"),
];
let indexed = IndexedInstruments::new(instruments);
assert_eq!(indexed.exchanges().len(), 2);
assert_eq!(indexed.assets().len(), 4); assert_eq!(indexed.instruments().len(), 2);
}
fn serde_fixture() -> Vec<Instrument<ExchangeId, Asset>> {
use crate::instrument::{
data_venue::DataVenue,
kind::perpetual::PerpetualContract,
spec::{
InstrumentSpec, InstrumentSpecNotional, InstrumentSpecPrice,
InstrumentSpecQuantity, OrderQuantityUnits,
},
};
let mut kraken_eth_usd = instrument(ExchangeId::Kraken, "eth", "usd");
kraken_eth_usd.spec = Some(InstrumentSpec::new(
InstrumentSpecPrice::new(Decimal::ONE, Decimal::ONE),
InstrumentSpecQuantity::new(
OrderQuantityUnits::Asset(Asset::new_from_exchange("eth_lot")),
Decimal::ONE,
Decimal::ONE,
),
InstrumentSpecNotional::new(Decimal::ONE),
));
vec![
kraken_eth_usd,
instrument(ExchangeId::BinanceSpot, "btc", "usdt"),
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)),
Instrument::new(
ExchangeId::BinanceFuturesUsd,
"binance_futures_usd-btc_usdt-perp",
"BTCUSDT",
Underlying::new(
Asset::new_from_exchange("btc"),
Asset::new_from_exchange("usdt"),
),
InstrumentQuoteAsset::UnderlyingQuote,
InstrumentKind::Perpetual(PerpetualContract {
contract_size: Decimal::ONE,
settlement_asset: Asset::new_from_exchange("usdc"),
}),
None,
),
]
}
#[test]
fn test_serde_round_trip_reproduces_the_same_indices() {
let indexed = IndexedInstruments::new(serde_fixture());
let json = serde_json::to_string(&indexed).unwrap();
let restored: IndexedInstruments = serde_json::from_str(&json).unwrap();
assert_eq!(restored, indexed);
assert!(
restored
.find_asset_index(ExchangeId::Kraken, &AssetNameInternal::from("eth_lot"))
.is_ok()
);
assert!(
restored
.exchanges()
.iter()
.any(|keyed| keyed.value == ExchangeId::LseEquities)
);
}
#[test]
fn test_serialises_as_the_plain_instrument_list() {
let instruments = serde_fixture();
let indexed = IndexedInstruments::new(instruments.clone());
let mut expected = instruments;
expected.sort();
assert_eq!(
serde_json::to_value(&indexed).unwrap(),
serde_json::to_value(&expected).unwrap()
);
}
#[test]
fn test_deserialise_rejects_a_duplicate_name_internal() {
let mut instruments = vec![
instrument(ExchangeId::BinanceSpot, "btc", "usdt"),
instrument(ExchangeId::BinanceSpot, "eth", "usdt"),
];
instruments[1].name_internal = instruments[0].name_internal.clone();
let json = serde_json::to_string(&instruments).unwrap();
let err = serde_json::from_str::<IndexedInstruments>(&json)
.unwrap_err()
.to_string();
assert!(err.contains("duplicate InstrumentNameInternal"), "{err}");
}
#[test]
fn test_deserialise_rejects_a_non_positive_contract_size() {
let mut instruments = serde_fixture();
for instrument in &mut instruments {
if let InstrumentKind::Perpetual(contract) = &mut instrument.kind {
contract.contract_size = Decimal::ZERO;
}
}
let json = serde_json::to_string(&instruments).unwrap();
let err = serde_json::from_str::<IndexedInstruments>(&json)
.unwrap_err()
.to_string();
assert!(err.contains("contract_size"), "{err}");
}
#[test]
fn test_deserialise_rejects_the_former_index_bearing_format() {
let json =
r#"{"exchanges":[{"key":0,"value":"binance_spot"}],"assets":[],"instruments":[]}"#;
assert!(serde_json::from_str::<IndexedInstruments>(json).is_err());
}
}