Skip to main content

rustrade_instrument/index/
mod.rs

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
12/// Contains error variants that can occur when working with an [`IndexedInstruments`] collection.
13pub mod error;
14
15/// Indexed collection of exchanges, assets, and instruments.
16///
17/// Initialise incrementally via the [`IndexedInstrumentsBuilder`], or all at once via the
18/// constructor.
19///
20/// The indexed collection is useful for creating efficient O(1) constant lookup state management
21/// systems where the state is keyed on an instrument, asset, or exchange.
22///
23/// For example uses cases, see the central `rustrade` crate `EngineState` design.
24///
25/// # Index Relationships
26/// - `ExchangeIndex`: Unique index for each [`ExchangeId`] added during initialisation.
27/// - `InstrumentIndex`: Unique identifier for each [`Instrument`] added during initialisation.
28/// - `AssetIndex`: Unique identifier for each [`ExchangeAsset`] added during initialisation.
29#[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    /// Initialises a new `IndexedInstruments` from an iterator of [`Instrument`]s.
39    ///
40    /// This method indexes all unique exchanges, assets, and instruments, creating efficient
41    /// lookup tables for each entity type.
42    ///
43    /// Note that once an `IndexedInstruments` has been constructed, it cannot be mutated (this
44    /// could invalidate existing index lookup tables).
45    ///
46    /// For incremental initialisation, see the [`IndexedInstrumentsBuilder`].
47    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    /// Returns a new [`IndexedInstrumentsBuilder`] useful for incremental initialisation of
61    /// `IndexedInstruments`.
62    pub fn builder() -> IndexedInstrumentsBuilder {
63        IndexedInstrumentsBuilder::default()
64    }
65
66    /// Returns a reference to the [`ExchangeIndex`] <--> [`ExchangeId`] associations.
67    pub fn exchanges(&self) -> &[Keyed<ExchangeIndex, ExchangeId>] {
68        &self.exchanges
69    }
70
71    /// Returns a reference to the [`AssetIndex`] <--> [`ExchangeAsset`] associations.
72    pub fn assets(&self) -> &[Keyed<AssetIndex, ExchangeAsset<Asset>>] {
73        &self.assets
74    }
75
76    /// Returns a reference to the [`InstrumentIndex`] <--> [`Instrument`] associations.
77    pub fn instruments(
78        &self,
79    ) -> &[Keyed<InstrumentIndex, Instrument<Keyed<ExchangeIndex, ExchangeId>, AssetIndex>>] {
80        &self.instruments
81    }
82
83    /// Finds the [`ExchangeIndex`] associated with the provided [`ExchangeId`].
84    ///
85    /// # Arguments
86    /// * `exchange` - The exchange ID to look up
87    ///
88    /// # Returns
89    /// * `Ok(ExchangeIndex)` - exchange found.
90    /// * `Err(IndexError)` - exchange not found.
91    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    /// Finds the [`AssetIndex`] associated with the provided `ExchangeId` and `AssetNameInterval`.
106    ///
107    /// # Arguments
108    /// * `exchange` - The `ExchangeId` associated with the asset.
109    /// * `name` - The `AssetNameInternal` associated with the asset (eg/ "btc", "usdt", etc).
110    ///
111    /// # Returns
112    /// * `Ok(AssetIndex)` - exchange asset found.
113    /// * `Err(IndexError)` - exchange asset not found.
114    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    /// Finds the [`InstrumentIndex`] associated with the provided `ExchangeId` and
133    /// `InstrumentNameInternal`.
134    ///
135    /// # Arguments
136    /// * `exchange` - The `ExchangeId` associated with the instrument.
137    /// * `name` - The `InstrumentNameInternal` associated with the instrument (eg/ binance_spot_btc_usdt).
138    ///
139    /// # Returns
140    /// * `Ok(AssetIndex)` - instrument found.
141    /// * `Err(IndexError)` - instrument not found.
142    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)] // Test code: panics on bad input are acceptable
216mod 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        // Test creating empty IndexedInstruments
232        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        // Test creating with single instrument
238        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); // BTC and USDT
243        assert_eq!(actual.instruments().len(), 1);
244
245        // Verify exchanges indexes
246        assert_eq!(actual.exchanges()[0].value, ExchangeId::BinanceSpot);
247
248        // Verify asset indexes
249        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        // Very instrument indexes
259        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        // Should have 2 exchanges, 4 assets (BTC, ETH, USDT, USD), and 3 instruments
287        assert_eq!(indexed.exchanges().len(), 2);
288        assert_eq!(indexed.assets().len(), 5);
289        assert_eq!(indexed.instruments().len(), 3);
290
291        // Verify exchanges
292        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        // Test finding existing exchanges
306        assert!(indexed.find_exchange_index(ExchangeId::BinanceSpot).is_ok());
307        assert!(indexed.find_exchange_index(ExchangeId::Coinbase).is_ok());
308
309        // Test finding non-existent exchange
310        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        // Test finding existing assets
323        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        // Test finding asset with wrong exchange
340        let err = indexed
341            .find_asset_index(ExchangeId::Kraken, &AssetNameInternal::from("btc"))
342            .unwrap_err();
343        assert!(matches!(err, IndexError::AssetIndex(_)));
344
345        // Test finding non-existent asset
346        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        // Test finding existing instruments
366        assert!(
367            indexed
368                .find_instrument_index(ExchangeId::BinanceSpot, &btc_usdt)
369                .is_ok()
370        );
371
372        // Test finding instrument with wrong exchange
373        let err = indexed
374            .find_instrument_index(ExchangeId::Kraken, &btc_usdt)
375            .unwrap_err();
376        assert!(matches!(err, IndexError::AssetIndex(_)));
377
378        // Test finding non-existent instrument
379        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        // Test finding existing exchange
400        let result = find_exchange_by_exchange_id(&exchanges, &ExchangeId::BinanceSpot);
401        assert_eq!(result.unwrap(), ExchangeIndex(0));
402
403        // Test finding non-existent exchange
404        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        // Test finding existing asset
428        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        // Test finding asset with wrong exchange
436        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        // Test finding non-existent asset
445        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        // Test with duplicate instruments
457        let instruments = vec![
458            instrument(ExchangeId::BinanceSpot, "btc", "usdt"),
459            instrument(ExchangeId::BinanceSpot, "btc", "usdt"),
460        ];
461        let indexed = IndexedInstruments::new(instruments);
462
463        // Should deduplicate exchanges and assets
464        assert_eq!(indexed.exchanges().len(), 1);
465        assert_eq!(indexed.assets().len(), 2);
466        assert_eq!(indexed.instruments().len(), 1); // Instruments aren't deduplicated
467
468        // Test with same asset on different exchanges
469        let instruments = vec![
470            instrument(ExchangeId::BinanceSpot, "btc", "usdt"),
471            instrument(ExchangeId::Coinbase, "btc", "usdt"),
472        ];
473        let indexed = IndexedInstruments::new(instruments);
474
475        // Should have separate entries for same asset on different exchanges
476        assert_eq!(indexed.exchanges().len(), 2);
477        assert_eq!(indexed.assets().len(), 4); // BTC and USDT on both exchanges
478        assert_eq!(indexed.instruments().len(), 2);
479    }
480}