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#[derive(Debug, Clone, Eq, PartialEq)]
32pub struct ExecutionInstrumentMap {
33 pub exchange: Keyed<ExchangeIndex, ExchangeId>,
35 pub assets: FnvIndexSet<ExchangeAsset<Asset>>,
38 pub instruments: FnvIndexSet<Instrument<Keyed<ExchangeIndex, ExchangeId>, AssetIndex>>,
41 asset_names: FnvHashMap<AssetNameExchange, AssetIndex>,
46 instrument_names: FnvHashMap<InstrumentNameExchange, InstrumentIndex>,
50 asset_names_folded: FnvHashMap<SmolStr, AssetIndex>,
52 instrument_names_folded: FnvHashMap<SmolStr, InstrumentIndex>,
55}
56
57impl ExecutionInstrumentMap {
58 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 pub fn exchange_assets(&self) -> impl Iterator<Item = &AssetNameExchange> {
117 self.asset_names.keys()
118 }
119
120 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 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 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 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 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
261fn 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
290fn 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)] mod 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 #[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 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 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 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 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 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 assert_eq!(exchange_instruments.len(), 1);
563
564 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 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 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 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}