1use crate::{
2 Keyed,
3 asset::{Asset, AssetIndex, ExchangeAsset, name::AssetNameInternal},
4 exchange::{ExchangeId, ExchangeIndex},
5 index::{builder::IndexedInstrumentsBuilder, error::IndexError},
6 instrument::{Instrument, InstrumentIndex, name::InstrumentNameInternal},
7};
8use serde::{Deserialize, Serialize};
9
10pub mod builder;
11
12pub mod error;
14
15#[derive(Debug, Clone, PartialEq, PartialOrd, Deserialize, Serialize)]
30pub struct IndexedInstruments {
31 exchanges: Vec<Keyed<ExchangeIndex, ExchangeId>>,
32 assets: Vec<Keyed<AssetIndex, ExchangeAsset<Asset>>>,
33 instruments:
34 Vec<Keyed<InstrumentIndex, Instrument<Keyed<ExchangeIndex, ExchangeId>, AssetIndex>>>,
35}
36
37impl IndexedInstruments {
38 pub fn new<Iter, I>(instruments: Iter) -> Self
48 where
49 Iter: IntoIterator<Item = I>,
50 I: Into<Instrument<ExchangeId, Asset>>,
51 {
52 instruments
53 .into_iter()
54 .fold(Self::builder(), |builder, instrument| {
55 builder.add_instrument(instrument.into())
56 })
57 .build()
58 }
59
60 pub fn builder() -> IndexedInstrumentsBuilder {
63 IndexedInstrumentsBuilder::default()
64 }
65
66 pub fn exchanges(&self) -> &[Keyed<ExchangeIndex, ExchangeId>] {
68 &self.exchanges
69 }
70
71 pub fn assets(&self) -> &[Keyed<AssetIndex, ExchangeAsset<Asset>>] {
73 &self.assets
74 }
75
76 pub fn instruments(
78 &self,
79 ) -> &[Keyed<InstrumentIndex, Instrument<Keyed<ExchangeIndex, ExchangeId>, AssetIndex>>] {
80 &self.instruments
81 }
82
83 pub fn find_exchange_index(&self, exchange: ExchangeId) -> Result<ExchangeIndex, IndexError> {
92 find_exchange_by_exchange_id(&self.exchanges, &exchange)
93 }
94
95 pub fn find_exchange(&self, index: ExchangeIndex) -> Result<ExchangeId, IndexError> {
96 self.exchanges
97 .iter()
98 .find(|keyed| keyed.key == index)
99 .map(|keyed| keyed.value)
100 .ok_or(IndexError::ExchangeIndex(format!(
101 "ExchangeIndex: {index} is not present in indexed instrument exchanges"
102 )))
103 }
104
105 pub fn find_asset_index(
115 &self,
116 exchange: ExchangeId,
117 name: &AssetNameInternal,
118 ) -> Result<AssetIndex, IndexError> {
119 find_asset_by_exchange_and_name_internal(&self.assets, exchange, name)
120 }
121
122 pub fn find_asset(&self, index: AssetIndex) -> Result<&ExchangeAsset<Asset>, IndexError> {
123 self.assets
124 .iter()
125 .find(|keyed| keyed.key == index)
126 .map(|keyed| &keyed.value)
127 .ok_or(IndexError::AssetIndex(format!(
128 "AssetIndex: {index} is not present in indexed instrument assets"
129 )))
130 }
131
132 pub fn find_instrument_index(
143 &self,
144 exchange: ExchangeId,
145 name: &InstrumentNameInternal,
146 ) -> Result<InstrumentIndex, IndexError> {
147 self.instruments
148 .iter()
149 .find_map(|indexed| {
150 (indexed.value.exchange.value == exchange && indexed.value.name_internal == *name)
151 .then_some(indexed.key)
152 })
153 .ok_or(IndexError::AssetIndex(format!(
154 "Asset: ({}, {}) is not present in indexed instrument assets: {:?}",
155 exchange, name, self.assets
156 )))
157 }
158
159 pub fn find_instrument(
160 &self,
161 index: InstrumentIndex,
162 ) -> Result<&Instrument<Keyed<ExchangeIndex, ExchangeId>, AssetIndex>, IndexError> {
163 self.instruments
164 .iter()
165 .find(|keyed| keyed.key == index)
166 .map(|keyed| &keyed.value)
167 .ok_or(IndexError::InstrumentIndex(format!(
168 "InstrumentIndex: {index} is not present in indexed instrument instruments"
169 )))
170 }
171}
172
173impl<I> FromIterator<I> for IndexedInstruments
174where
175 I: Into<Instrument<ExchangeId, Asset>>,
176{
177 fn from_iter<Iter>(iter: Iter) -> Self
178 where
179 Iter: IntoIterator<Item = I>,
180 {
181 Self::new(iter)
182 }
183}
184
185fn find_exchange_by_exchange_id(
186 haystack: &[Keyed<ExchangeIndex, ExchangeId>],
187 needle: &ExchangeId,
188) -> Result<ExchangeIndex, IndexError> {
189 haystack
190 .iter()
191 .find_map(|indexed| (indexed.value == *needle).then_some(indexed.key))
192 .ok_or(IndexError::ExchangeIndex(format!(
193 "Exchange: {needle} is not present in indexed instrument exchanges: {haystack:?}"
194 )))
195}
196
197fn find_asset_by_exchange_and_name_internal(
198 haystack: &[Keyed<AssetIndex, ExchangeAsset<Asset>>],
199 needle_exchange: ExchangeId,
200 needle_name: &AssetNameInternal,
201) -> Result<AssetIndex, IndexError> {
202 haystack
203 .iter()
204 .find_map(|indexed| {
205 (indexed.value.exchange == needle_exchange
206 && indexed.value.asset.name_internal == *needle_name)
207 .then_some(indexed.key)
208 })
209 .ok_or(IndexError::AssetIndex(format!(
210 "Asset: ({needle_exchange}, {needle_name}) is not present in indexed instrument assets: {haystack:?}"
211 )))
212}
213
214#[cfg(test)]
215#[allow(clippy::unwrap_used)] mod tests {
217 use super::*;
218
219 use crate::{
220 Underlying,
221 asset::Asset,
222 exchange::ExchangeId,
223 instrument::{
224 kind::InstrumentKind, name::InstrumentNameExchange, quote::InstrumentQuoteAsset,
225 },
226 test_utils::{exchange_asset, instrument},
227 };
228
229 #[test]
230 fn test_indexed_instruments_new() {
231 let empty = IndexedInstruments::new(std::iter::empty::<Instrument<ExchangeId, Asset>>());
233 assert!(empty.exchanges().is_empty());
234 assert!(empty.assets().is_empty());
235 assert!(empty.instruments().is_empty());
236
237 let instrument = instrument(ExchangeId::BinanceSpot, "btc", "usdt");
239 let actual = IndexedInstruments::new(std::iter::once(instrument));
240
241 assert_eq!(actual.exchanges().len(), 1);
242 assert_eq!(actual.assets().len(), 2); assert_eq!(actual.instruments().len(), 1);
244
245 assert_eq!(actual.exchanges()[0].value, ExchangeId::BinanceSpot);
247
248 assert_eq!(
250 actual.assets()[0].value,
251 exchange_asset(ExchangeId::BinanceSpot, "btc"),
252 );
253 assert_eq!(
254 actual.assets()[1].value,
255 exchange_asset(ExchangeId::BinanceSpot, "usdt"),
256 );
257
258 assert_eq!(
260 actual.instruments()[0].value,
261 Instrument {
262 exchange: Keyed::new(ExchangeIndex(0), ExchangeId::BinanceSpot),
263 name_exchange: InstrumentNameExchange::new("btc_usdt"),
264 name_internal: InstrumentNameInternal::new("binance_spot-btc_usdt"),
265 underlying: Underlying {
266 base: AssetIndex(0),
267 quote: AssetIndex(1),
268 },
269 quote: InstrumentQuoteAsset::UnderlyingQuote,
270 kind: InstrumentKind::Spot,
271 spec: None
272 }
273 );
274 }
275
276 #[test]
277 fn test_indexed_instruments_multiple() {
278 let instruments = vec![
279 instrument(ExchangeId::BinanceSpot, "BTC", "USDT"),
280 instrument(ExchangeId::BinanceSpot, "ETH", "USDT"),
281 instrument(ExchangeId::Coinbase, "BTC", "USD"),
282 ];
283
284 let indexed = IndexedInstruments::new(instruments);
285
286 assert_eq!(indexed.exchanges().len(), 2);
288 assert_eq!(indexed.assets().len(), 5);
289 assert_eq!(indexed.instruments().len(), 3);
290
291 let exchanges: Vec<_> = indexed.exchanges().iter().map(|e| e.value).collect();
293 assert!(exchanges.contains(&ExchangeId::BinanceSpot));
294 assert!(exchanges.contains(&ExchangeId::Coinbase));
295 }
296
297 #[test]
298 fn test_find_exchange_index() {
299 let instruments = vec![
300 instrument(ExchangeId::BinanceSpot, "BTC", "USDT"),
301 instrument(ExchangeId::Coinbase, "ETH", "USD"),
302 ];
303 let indexed = IndexedInstruments::new(instruments);
304
305 assert!(indexed.find_exchange_index(ExchangeId::BinanceSpot).is_ok());
307 assert!(indexed.find_exchange_index(ExchangeId::Coinbase).is_ok());
308
309 let err = indexed.find_exchange_index(ExchangeId::Kraken).unwrap_err();
311 assert!(matches!(err, IndexError::ExchangeIndex(_)));
312 }
313
314 #[test]
315 fn test_find_asset_index() {
316 let instruments = vec![
317 instrument(ExchangeId::BinanceSpot, "BTC", "USDT"),
318 instrument(ExchangeId::Coinbase, "ETH", "USD"),
319 ];
320 let indexed = IndexedInstruments::new(instruments);
321
322 assert!(
324 indexed
325 .find_asset_index(ExchangeId::BinanceSpot, &AssetNameInternal::from("btc"))
326 .is_ok()
327 );
328 assert!(
329 indexed
330 .find_asset_index(ExchangeId::BinanceSpot, &AssetNameInternal::from("usdt"))
331 .is_ok()
332 );
333 assert!(
334 indexed
335 .find_asset_index(ExchangeId::Coinbase, &AssetNameInternal::from("eth"))
336 .is_ok()
337 );
338
339 let err = indexed
341 .find_asset_index(ExchangeId::Kraken, &AssetNameInternal::from("btc"))
342 .unwrap_err();
343 assert!(matches!(err, IndexError::AssetIndex(_)));
344
345 let err = indexed
347 .find_asset_index(
348 ExchangeId::BinanceSpot,
349 &AssetNameInternal::from("nonexistent"),
350 )
351 .unwrap_err();
352 assert!(matches!(err, IndexError::AssetIndex(_)));
353 }
354
355 #[test]
356 fn test_find_instrument_index() {
357 let instruments = vec![
358 instrument(ExchangeId::BinanceSpot, "btc", "usdt"),
359 instrument(ExchangeId::Coinbase, "eth", "usd"),
360 ];
361
362 let indexed = IndexedInstruments::new(instruments);
363 let btc_usdt = InstrumentNameInternal::from("binance_spot-btc_usdt");
364
365 assert!(
367 indexed
368 .find_instrument_index(ExchangeId::BinanceSpot, &btc_usdt)
369 .is_ok()
370 );
371
372 let err = indexed
374 .find_instrument_index(ExchangeId::Kraken, &btc_usdt)
375 .unwrap_err();
376 assert!(matches!(err, IndexError::AssetIndex(_)));
377
378 let nonexistent = InstrumentNameInternal::from("nonexistent");
380 let err = indexed
381 .find_instrument_index(ExchangeId::BinanceSpot, &nonexistent)
382 .unwrap_err();
383 assert!(matches!(err, IndexError::AssetIndex(_)));
384 }
385
386 #[test]
387 fn test_private_find_exchange_by_exchange_id() {
388 let exchanges = vec![
389 Keyed {
390 key: ExchangeIndex(0),
391 value: ExchangeId::BinanceSpot,
392 },
393 Keyed {
394 key: ExchangeIndex(1),
395 value: ExchangeId::Coinbase,
396 },
397 ];
398
399 let result = find_exchange_by_exchange_id(&exchanges, &ExchangeId::BinanceSpot);
401 assert_eq!(result.unwrap(), ExchangeIndex(0));
402
403 let err = find_exchange_by_exchange_id(&exchanges, &ExchangeId::Kraken).unwrap_err();
405 assert!(matches!(err, IndexError::ExchangeIndex(_)));
406 }
407
408 #[test]
409 fn test_private_find_asset_by_exchange_and_name_internal() {
410 let assets = vec![
411 Keyed {
412 key: AssetIndex(0),
413 value: ExchangeAsset {
414 exchange: ExchangeId::BinanceSpot,
415 asset: Asset::new_from_exchange("BTC"),
416 },
417 },
418 Keyed {
419 key: AssetIndex(1),
420 value: ExchangeAsset {
421 exchange: ExchangeId::BinanceSpot,
422 asset: Asset::new_from_exchange("USDT"),
423 },
424 },
425 ];
426
427 let result = find_asset_by_exchange_and_name_internal(
429 &assets,
430 ExchangeId::BinanceSpot,
431 &AssetNameInternal::from("btc"),
432 );
433 assert_eq!(result.unwrap(), AssetIndex(0));
434
435 let err = find_asset_by_exchange_and_name_internal(
437 &assets,
438 ExchangeId::Kraken,
439 &AssetNameInternal::from("btc"),
440 )
441 .unwrap_err();
442 assert!(matches!(err, IndexError::AssetIndex(_)));
443
444 let err = find_asset_by_exchange_and_name_internal(
446 &assets,
447 ExchangeId::BinanceSpot,
448 &AssetNameInternal::from("nonexistent"),
449 )
450 .unwrap_err();
451 assert!(matches!(err, IndexError::AssetIndex(_)));
452 }
453
454 #[test]
455 fn test_duplicates_are_filtered_correctly() {
456 let instruments = vec![
458 instrument(ExchangeId::BinanceSpot, "btc", "usdt"),
459 instrument(ExchangeId::BinanceSpot, "btc", "usdt"),
460 ];
461 let indexed = IndexedInstruments::new(instruments);
462
463 assert_eq!(indexed.exchanges().len(), 1);
465 assert_eq!(indexed.assets().len(), 2);
466 assert_eq!(indexed.instruments().len(), 1); let instruments = vec![
470 instrument(ExchangeId::BinanceSpot, "btc", "usdt"),
471 instrument(ExchangeId::Coinbase, "btc", "usdt"),
472 ];
473 let indexed = IndexedInstruments::new(instruments);
474
475 assert_eq!(indexed.exchanges().len(), 2);
477 assert_eq!(indexed.assets().len(), 4); assert_eq!(indexed.instruments().len(), 2);
479 }
480}