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};
pub mod builder;
pub mod error;
#[derive(Debug, Clone, PartialEq, PartialOrd, Deserialize, Serialize)]
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>>,
{
instruments
.into_iter()
.fold(Self::builder(), |builder, instrument| {
builder.add_instrument(instrument.into())
})
.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(IndexError::AssetIndex(format!(
"Asset: ({}, {}) is not present in indexed instrument assets: {:?}",
exchange, name, self.assets
)))
}
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<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},
};
#[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),
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::AssetIndex(_)));
let nonexistent = InstrumentNameInternal::from("nonexistent");
let err = indexed
.find_instrument_index(ExchangeId::BinanceSpot, &nonexistent)
.unwrap_err();
assert!(matches!(err, IndexError::AssetIndex(_)));
}
#[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);
}
}