Skip to main content

rustrade_execution/
map.rs

1use crate::error::KeyError;
2use fnv::FnvHashMap;
3use rustrade_instrument::{
4    Keyed,
5    asset::{Asset, AssetIndex, ExchangeAsset, name::AssetNameExchange},
6    exchange::{ExchangeId, ExchangeIndex},
7    index::{IndexedInstruments, error::IndexError},
8    instrument::{Instrument, InstrumentIndex, name::InstrumentNameExchange},
9};
10use rustrade_integration::collection::FnvIndexSet;
11use smol_str::{SmolStr, StrExt};
12use tracing::{debug, warn};
13
14/// Indexed instrument map used to associate the internal Barter representation of instruments and
15/// assets with the [`ExecutionClient`](super::client::ExecutionClient) representation.
16///
17/// Similarly, when the execution manager received an [`AccountEvent`](super::AccountEvent)
18/// from the execution API, it needs to determine the internal representation of the associated
19/// assets and instruments.
20///
21/// eg/ `InstrumentNameExchange("XBT-USDT")` <--> `InstrumentIndex(1)` <br>
22/// eg/ `AssetNameExchange("XBT")` <--> `AssetIndex(1)`
23///
24/// # Names are matched ignoring ASCII case
25/// [`Self::find_asset_index`] and [`Self::find_instrument_index`] resolve a name the way it was
26/// registered first, and failing that ignoring ASCII case. Venues and clients do not always spell a
27/// name the way it was registered (`usd` against `USD`), and a name that fails to resolve loses the
28/// event carrying it. Ignoring case cannot merge two registered names, because
29/// [`IndexedInstruments`] rejects two on one exchange that differ only in case. The lookups in the
30/// other direction, from an index to a name, return the registered spelling.
31#[derive(Debug, Clone, Eq, PartialEq)]
32pub struct ExecutionInstrumentMap {
33    /// The exchange associated with this execution map.
34    pub exchange: Keyed<ExchangeIndex, ExchangeId>,
35    /// Collection of assets available by the engine with their
36    /// exchange-specific representations. This holds all indexed assets.
37    pub assets: FnvIndexSet<ExchangeAsset<Asset>>,
38    /// Collection of instruments available by the engine. This holds all
39    /// indexed instruments.
40    pub instruments: FnvIndexSet<Instrument<Keyed<ExchangeIndex, ExchangeId>, AssetIndex>>,
41    /// Map from exchange-specific asset names, as registered, to internal asset indices.
42    ///
43    /// Private, with [`Self::asset_names_folded`], because that index is derived from this map: a
44    /// change to one alone would make the exact and case-insensitive lookups disagree.
45    asset_names: FnvHashMap<AssetNameExchange, AssetIndex>,
46    /// Map from exchange-specific instrument names, as registered, to internal instrument indices.
47    ///
48    /// Private for the reason [`Self::asset_names`] is.
49    instrument_names: FnvHashMap<InstrumentNameExchange, InstrumentIndex>,
50    /// [`Self::asset_names`] keyed by the ASCII-lowercased name, for the case-insensitive lookup.
51    asset_names_folded: FnvHashMap<SmolStr, AssetIndex>,
52    /// [`Self::instrument_names`] keyed by the ASCII-lowercased name, for the case-insensitive
53    /// lookup.
54    instrument_names_folded: FnvHashMap<SmolStr, InstrumentIndex>,
55}
56
57impl ExecutionInstrumentMap {
58    /// Construct a new [`Self`] using the provided indexed assets and instruments.
59    pub fn new(
60        exchange: Keyed<ExchangeIndex, ExchangeId>,
61        instruments: &IndexedInstruments,
62    ) -> Self {
63        let asset_names: FnvHashMap<AssetNameExchange, AssetIndex> = instruments
64            .assets()
65            .iter()
66            .filter_map(|Keyed { key, value }| {
67                (value.exchange == exchange.value)
68                    .then_some((value.asset.name_exchange.clone(), *key))
69            })
70            .collect();
71
72        let assets = instruments
73            .assets()
74            .iter()
75            .map(|Keyed { value, .. }| value.clone())
76            .collect();
77
78        let instrument_names: FnvHashMap<InstrumentNameExchange, InstrumentIndex> = instruments
79            .instruments()
80            .iter()
81            .filter_map(|Keyed { key, value }| {
82                (value.exchange.value == exchange.value)
83                    .then_some((value.name_exchange.clone(), *key))
84            })
85            .collect();
86
87        let asset_names_folded = fold_names(
88            asset_names
89                .iter()
90                .map(|(name, index)| (name.name().as_str(), *index)),
91        );
92        let instrument_names_folded = fold_names(
93            instrument_names
94                .iter()
95                .map(|(name, index)| (name.name().as_str(), *index)),
96        );
97
98        let instruments = instruments
99            .instruments()
100            .iter()
101            .map(|Keyed { value, .. }| value.clone())
102            .collect();
103
104        Self {
105            exchange,
106            asset_names,
107            instrument_names,
108            assets,
109            instruments,
110            asset_names_folded,
111            instrument_names_folded,
112        }
113    }
114
115    /// The names of this exchange's assets, as registered.
116    pub fn exchange_assets(&self) -> impl Iterator<Item = &AssetNameExchange> {
117        self.asset_names.keys()
118    }
119
120    /// The names of this exchange's instruments, as registered.
121    pub fn exchange_instruments(&self) -> impl Iterator<Item = &InstrumentNameExchange> {
122        self.instrument_names.keys()
123    }
124
125    pub fn find_exchange_id(&self, exchange: ExchangeIndex) -> Result<ExchangeId, KeyError> {
126        if self.exchange.key == exchange {
127            Ok(self.exchange.value)
128        } else {
129            Err(KeyError::ExchangeId(format!(
130                "ExecutionInstrumentMap does not contain {exchange}"
131            )))
132        }
133    }
134
135    pub fn find_exchange_index(&self, exchange: ExchangeId) -> Result<ExchangeIndex, IndexError> {
136        if self.exchange.value == exchange {
137            Ok(self.exchange.key)
138        } else {
139            Err(IndexError::ExchangeIndex(format!(
140                "ExecutionInstrumentMap does not contain {exchange}"
141            )))
142        }
143    }
144
145    pub fn find_asset_name_exchange(
146        &self,
147        asset: AssetIndex,
148    ) -> Result<&AssetNameExchange, KeyError> {
149        self.assets
150            .get_index(asset.index())
151            .ok_or_else(|| {
152                KeyError::AssetKey(format!("ExecutionInstrumentMap does not contain: {asset}"))
153            })
154            .map(|asset| &asset.asset.name_exchange)
155    }
156
157    /// Find the [`AssetIndex`] of the asset this exchange reports as `asset`.
158    ///
159    /// Matches the registered spelling first, and failing that ignores ASCII case, logging the
160    /// case-insensitive match at `debug!` with both spellings. See the
161    /// [type-level note](Self#names-are-matched-ignoring-ascii-case).
162    pub fn find_asset_index(&self, asset: &AssetNameExchange) -> Result<AssetIndex, IndexError> {
163        if let Some(index) = self.asset_names.get(asset) {
164            return Ok(*index);
165        }
166
167        let index = find_folded(&self.asset_names_folded, asset.name()).ok_or_else(|| {
168            IndexError::AssetIndex(format!("ExecutionInstrumentMap does not contain: {asset}"))
169        })?;
170
171        debug!(
172            exchange = %self.exchange.value,
173            reported = %asset,
174            registered = ?self.find_asset_name_exchange(index).ok(),
175            "ExecutionInstrumentMap matched an asset name ignoring case"
176        );
177        Ok(index)
178    }
179
180    pub fn find_instrument_name_exchange(
181        &self,
182        instrument: InstrumentIndex,
183    ) -> Result<&InstrumentNameExchange, KeyError> {
184        self.instruments
185            .get_index(instrument.index())
186            .ok_or_else(|| {
187                KeyError::InstrumentKey(format!(
188                    "ExecutionInstrumentMap does not contain: {instrument}"
189                ))
190            })
191            .map(|instrument| &instrument.name_exchange)
192    }
193
194    /// The name this exchange knows `instrument` by, or `None` when the instrument is listed on a
195    /// different exchange or `instrument` is not indexed. The two are not told apart.
196    ///
197    /// Unlike [`Self::find_instrument_name_exchange`], which resolves any indexed instrument
198    /// whatever its exchange, this answers "does this exchange trade it?". Use it to decide which
199    /// venue an instrument-keyed event belongs to.
200    pub fn find_own_instrument_name_exchange(
201        &self,
202        instrument: InstrumentIndex,
203    ) -> Option<&InstrumentNameExchange> {
204        self.instruments
205            .get_index(instrument.index())
206            .filter(|listed| listed.exchange.key == self.exchange.key)
207            .map(|listed| &listed.name_exchange)
208    }
209
210    /// The names this exchange knows its instruments by, for those whose market data comes from
211    /// `data_exchange`: see [`Instrument::data_exchange`].
212    ///
213    /// An instrument with no [`DataVenue`](rustrade_instrument::instrument::data_venue::DataVenue)
214    /// is priced from the exchange it is listed on, so for `data_exchange` equal to this exchange
215    /// this yields every such instrument, and for another exchange only those a `DataVenue` sources
216    /// from it. Use it to decide which of a venue's instruments a market stream event that names
217    /// only its exchange, such as a reconnection, is about.
218    pub fn own_instruments_priced_from(
219        &self,
220        data_exchange: ExchangeId,
221    ) -> impl Iterator<Item = &InstrumentNameExchange> {
222        self.instruments
223            .iter()
224            .filter(move |listed| {
225                listed.exchange.key == self.exchange.key
226                    && listed.data_exchange().value == data_exchange
227            })
228            .map(|listed| &listed.name_exchange)
229    }
230
231    /// Find the [`InstrumentIndex`] of the instrument this exchange reports as `instrument`.
232    ///
233    /// Matches the registered spelling first, and failing that ignores ASCII case, logging the
234    /// case-insensitive match at `debug!` with both spellings. See the
235    /// [type-level note](Self#names-are-matched-ignoring-ascii-case).
236    pub fn find_instrument_index(
237        &self,
238        instrument: &InstrumentNameExchange,
239    ) -> Result<InstrumentIndex, IndexError> {
240        if let Some(index) = self.instrument_names.get(instrument) {
241            return Ok(*index);
242        }
243
244        let index =
245            find_folded(&self.instrument_names_folded, instrument.name()).ok_or_else(|| {
246                IndexError::InstrumentIndex(format!(
247                    "ExecutionInstrumentMap does not contain: {instrument}"
248                ))
249            })?;
250
251        debug!(
252            exchange = %self.exchange.value,
253            reported = %instrument,
254            registered = ?self.find_instrument_name_exchange(index).ok(),
255            "ExecutionInstrumentMap matched an instrument name ignoring case"
256        );
257        Ok(index)
258    }
259}
260
261/// Key each name by its ASCII-lowercased form.
262///
263/// A key two names share is left out, with a `warn!`, so a lookup by it misses rather than picks
264/// one of them. [`IndexedInstruments`] rejects such a pair on one exchange, so this guards only an
265/// invariant.
266fn fold_names<'a, Index>(
267    names: impl Iterator<Item = (&'a str, Index)>,
268) -> FnvHashMap<SmolStr, Index> {
269    let mut folded = FnvHashMap::default();
270    let mut shared = Vec::new();
271    for (name, index) in names {
272        let key = name.to_ascii_lowercase_smolstr();
273        if folded.insert(key.clone(), index).is_some() {
274            shared.push(key);
275        }
276    }
277    shared.sort_unstable();
278    shared.dedup();
279    for key in shared {
280        warn!(
281            name = %key,
282            "ExecutionInstrumentMap holds names that differ only in case - none resolves \
283             ignoring case"
284        );
285        folded.remove(&key);
286    }
287    folded
288}
289
290/// Look `name` up in a map built by [`fold_names`].
291fn find_folded<Index: Copy>(folded: &FnvHashMap<SmolStr, Index>, name: &str) -> Option<Index> {
292    folded.get(&name.to_ascii_lowercase_smolstr()).copied()
293}
294
295pub fn generate_execution_instrument_map(
296    instruments: &IndexedInstruments,
297    exchange: ExchangeId,
298) -> Result<ExecutionInstrumentMap, IndexError> {
299    let exchange_index = instruments
300        .exchanges()
301        .iter()
302        .find_map(|keyed_exchange| (keyed_exchange.value == exchange).then_some(keyed_exchange.key))
303        .ok_or_else(|| {
304            IndexError::ExchangeIndex(format!(
305                "IndexedInstrument does not contain index for: {exchange}"
306            ))
307        })?;
308
309    Ok(ExecutionInstrumentMap::new(
310        Keyed::new(exchange_index, exchange),
311        instruments,
312    ))
313}
314
315#[cfg(test)]
316#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
317mod tests {
318    use super::*;
319    use rustrade_instrument::{exchange::ExchangeId, test_utils};
320
321    fn indexed_instruments() -> IndexedInstruments {
322        let instruments = vec![
323            test_utils::instrument(ExchangeId::BinanceSpot, "BTC", "ETH"),
324            test_utils::instrument(ExchangeId::Coinbase, "BTC", "ETH"),
325            test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT"),
326        ];
327
328        IndexedInstruments::new(instruments)
329    }
330
331    /// Only this exchange's instruments, and of those only the ones priced from the exchange asked
332    /// about: their own by default, another through a `DataVenue`.
333    #[test]
334    fn own_instruments_priced_from_follows_each_instruments_data_exchange() {
335        use rustrade_instrument::instrument::data_venue::DataVenue;
336
337        let instruments = IndexedInstruments::new(vec![
338            test_utils::instrument(ExchangeId::Kraken, "BTC", "USD"),
339            test_utils::instrument(ExchangeId::Kraken, "ETH", "USD")
340                .with_data_venue(DataVenue::new(ExchangeId::BinanceSpot, None)),
341            test_utils::instrument(ExchangeId::BinanceSpot, "SOL", "USDT"),
342        ]);
343        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
344        let priced_from = |exchange| {
345            kraken
346                .own_instruments_priced_from(exchange)
347                .map(|name| name.name().to_string())
348                .collect::<Vec<_>>()
349        };
350
351        assert_eq!(priced_from(ExchangeId::Kraken), ["BTC_USD"]);
352        assert_eq!(priced_from(ExchangeId::BinanceSpot), ["ETH_USD"]);
353        assert!(priced_from(ExchangeId::Coinbase).is_empty());
354    }
355
356    #[test]
357    fn test_find_exchange_id() {
358        let instruments = indexed_instruments();
359        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
360
361        let exchange_id = kraken.find_exchange_id(kraken.exchange.key).unwrap();
362        assert_eq!(exchange_id, ExchangeId::Kraken);
363    }
364
365    #[test]
366    fn test_find_exchange_index() {
367        let instruments = indexed_instruments();
368        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
369
370        let exchange_index = kraken.find_exchange_index(ExchangeId::Kraken).unwrap();
371        assert_eq!(exchange_index, kraken.exchange.key);
372    }
373
374    #[test]
375    fn test_find_exchange_id_error() {
376        let instruments = indexed_instruments();
377        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
378
379        // Create a different exchange index that doesn't match
380        let binance_index = instruments
381            .exchanges()
382            .iter()
383            .find(|ex| ex.value == ExchangeId::BinanceSpot)
384            .map(|ex| ex.key)
385            .unwrap();
386
387        let result = kraken.find_exchange_id(binance_index);
388        assert!(result.is_err());
389        assert!(matches!(result, Err(KeyError::ExchangeId(_))));
390    }
391
392    #[test]
393    fn test_find_exchange_index_error() {
394        let instruments = indexed_instruments();
395        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
396
397        let result = kraken.find_exchange_index(ExchangeId::BinanceSpot);
398        assert!(result.is_err());
399        assert!(matches!(result, Err(IndexError::ExchangeIndex(_))));
400    }
401
402    #[test]
403    fn test_find_asset_name_exchange() {
404        let instruments = indexed_instruments();
405        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
406
407        let usdt = test_utils::asset("USDT");
408        let usdt_index = instruments
409            .find_asset_index(ExchangeId::Kraken, &usdt.name_internal)
410            .unwrap();
411
412        let usdt_exchange_name = kraken.find_asset_name_exchange(usdt_index).unwrap();
413        assert_eq!(usdt_exchange_name, &usdt.name_exchange);
414    }
415
416    #[test]
417    fn test_find_asset_index() {
418        let instruments = indexed_instruments();
419        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
420
421        let usdc = test_utils::asset("USDC");
422        let asset_index = kraken.find_asset_index(&usdc.name_exchange).unwrap();
423
424        let expected_index = instruments
425            .find_asset_index(ExchangeId::Kraken, &usdc.name_internal)
426            .unwrap();
427        assert_eq!(asset_index, expected_index);
428    }
429
430    #[test]
431    fn test_find_asset_index_error() {
432        let instruments = indexed_instruments();
433        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
434
435        let btc = test_utils::asset("BTC");
436        let result = kraken.find_asset_index(&btc.name_exchange);
437        assert!(result.is_err());
438        assert!(matches!(result, Err(IndexError::AssetIndex(_))));
439    }
440
441    #[test]
442    fn test_find_asset_name_exchange_error() {
443        let instruments = indexed_instruments();
444        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
445
446        // Try to find asset with invalid index
447        let invalid_index = AssetIndex::new(999);
448        let result = kraken.find_asset_name_exchange(invalid_index);
449        assert!(result.is_err());
450        assert!(matches!(result, Err(KeyError::AssetKey(_))));
451    }
452
453    #[test]
454    fn test_find_instrument_name_exchange() {
455        let instruments = indexed_instruments();
456        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
457        let usdc_usdt = test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT");
458
459        let usdc_usdt_index = instruments
460            .find_instrument_index(ExchangeId::Kraken, &usdc_usdt.name_internal)
461            .unwrap();
462
463        let usdc_usdt_exchange_name = kraken
464            .find_instrument_name_exchange(usdc_usdt_index)
465            .unwrap();
466
467        assert_eq!(usdc_usdt_exchange_name, &usdc_usdt.name_exchange);
468    }
469
470    #[test]
471    fn test_find_own_instrument_name_exchange() {
472        let instruments = indexed_instruments();
473        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
474        let usdc_usdt = test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT");
475        let own = instruments
476            .find_instrument_index(ExchangeId::Kraken, &usdc_usdt.name_internal)
477            .unwrap();
478        let binance = test_utils::instrument(ExchangeId::BinanceSpot, "BTC", "ETH");
479        let other = instruments
480            .find_instrument_index(ExchangeId::BinanceSpot, &binance.name_internal)
481            .unwrap();
482
483        assert_eq!(
484            kraken.find_own_instrument_name_exchange(own),
485            Some(&usdc_usdt.name_exchange)
486        );
487        // Indexed, so the plain lookup resolves it, but it is not this exchange's to trade.
488        assert!(kraken.find_instrument_name_exchange(other).is_ok());
489        assert_eq!(kraken.find_own_instrument_name_exchange(other), None);
490        assert_eq!(
491            kraken.find_own_instrument_name_exchange(InstrumentIndex::new(99)),
492            None
493        );
494    }
495
496    #[test]
497    fn test_find_instrument_index() {
498        let instruments = indexed_instruments();
499        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
500
501        let usdc_usdt = test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT");
502        let instrument_index = kraken
503            .find_instrument_index(&usdc_usdt.name_exchange)
504            .unwrap();
505
506        let expected_index = instruments
507            .find_instrument_index(ExchangeId::Kraken, &usdc_usdt.name_internal)
508            .unwrap();
509        assert_eq!(instrument_index, expected_index);
510    }
511
512    #[test]
513    fn test_find_instrument_index_error() {
514        let instruments = indexed_instruments();
515        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
516
517        let btc_eth = test_utils::instrument(ExchangeId::Kraken, "BTC", "ETH");
518        let result = kraken.find_instrument_index(&btc_eth.name_exchange);
519        assert!(result.is_err());
520        assert!(matches!(result, Err(IndexError::InstrumentIndex(_))));
521    }
522
523    #[test]
524    fn test_find_instrument_name_exchange_error() {
525        let instruments = indexed_instruments();
526        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
527
528        // Try to find instrument with invalid index
529        let invalid_index = InstrumentIndex::new(999);
530        let result = kraken.find_instrument_name_exchange(invalid_index);
531        assert!(result.is_err());
532        assert!(matches!(result, Err(KeyError::InstrumentKey(_))));
533    }
534
535    #[test]
536    fn test_exchange_assets_iterator() {
537        let instruments = indexed_instruments();
538        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
539
540        let exchange_assets: Vec<&AssetNameExchange> = kraken.exchange_assets().collect();
541
542        // Verify that the iterator returns the expected assets for Kraken
543        let expected_assets = vec!["USDC", "USDT"];
544        for expected in &expected_assets {
545            assert!(
546                exchange_assets
547                    .iter()
548                    .any(|asset| asset.as_ref() == *expected)
549            );
550        }
551    }
552
553    #[test]
554    fn test_exchange_instruments_iterator() {
555        let instruments = indexed_instruments();
556        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
557
558        let exchange_instruments: Vec<&InstrumentNameExchange> =
559            kraken.exchange_instruments().collect();
560
561        // Should have exactly one instrument for Kraken
562        assert_eq!(exchange_instruments.len(), 1);
563
564        // Verify it contains the USDC-USDT instrument
565        let usdc_usdt = test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT");
566        assert!(exchange_instruments.contains(&&usdc_usdt.name_exchange));
567    }
568
569    #[test]
570    fn test_generate_execution_instrument_map_error() {
571        let instruments = indexed_instruments();
572
573        // Try to generate map for exchange not in indexed instruments
574        let result = generate_execution_instrument_map(&instruments, ExchangeId::Bitstamp);
575        assert!(result.is_err());
576        assert!(matches!(result, Err(IndexError::ExchangeIndex(_))));
577    }
578
579    #[test]
580    fn an_asset_name_in_another_case_resolves_to_the_registered_asset() {
581        let instruments = indexed_instruments();
582        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
583
584        let registered = kraken
585            .find_asset_index(&AssetNameExchange::new("USDT"))
586            .unwrap();
587
588        for reported in ["usdt", "Usdt", "uSdT"] {
589            assert_eq!(
590                kraken.find_asset_index(&AssetNameExchange::new(reported)),
591                Ok(registered),
592                "{reported}"
593            );
594        }
595        // The other direction keeps the registered spelling.
596        assert_eq!(
597            kraken.find_asset_name_exchange(registered).unwrap().name(),
598            "USDT"
599        );
600    }
601
602    #[test]
603    fn an_instrument_name_in_another_case_resolves_to_the_registered_instrument() {
604        let instruments = indexed_instruments();
605        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
606
607        let registered = kraken
608            .find_instrument_index(&InstrumentNameExchange::new("USDC_USDT"))
609            .unwrap();
610
611        assert_eq!(
612            kraken.find_instrument_index(&InstrumentNameExchange::new("usdc_usdt")),
613            Ok(registered)
614        );
615        assert_eq!(
616            kraken
617                .find_instrument_name_exchange(registered)
618                .unwrap()
619                .name(),
620            "USDC_USDT"
621        );
622    }
623
624    #[test]
625    fn ignoring_case_does_not_reach_another_exchanges_names() {
626        let instruments = indexed_instruments();
627        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
628
629        // `BTC` and `BTC_ETH` are registered, but on Binance and Coinbase only.
630        assert!(matches!(
631            kraken.find_asset_index(&AssetNameExchange::new("btc")),
632            Err(IndexError::AssetIndex(_))
633        ));
634        assert!(matches!(
635            kraken.find_instrument_index(&InstrumentNameExchange::new("btc_eth")),
636            Err(IndexError::InstrumentIndex(_))
637        ));
638    }
639
640    #[test]
641    fn a_folded_name_two_names_share_resolves_to_neither() {
642        let folded = fold_names([("USD", 0), ("usd", 1), ("BTC", 2)].into_iter());
643
644        assert_eq!(find_folded(&folded, "Usd"), None);
645        assert_eq!(find_folded(&folded, "btc"), Some(2));
646    }
647}