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 find_instrument_index(
216 &self,
217 instrument: &InstrumentNameExchange,
218 ) -> Result<InstrumentIndex, IndexError> {
219 if let Some(index) = self.instrument_names.get(instrument) {
220 return Ok(*index);
221 }
222
223 let index =
224 find_folded(&self.instrument_names_folded, instrument.name()).ok_or_else(|| {
225 IndexError::InstrumentIndex(format!(
226 "ExecutionInstrumentMap does not contain: {instrument}"
227 ))
228 })?;
229
230 debug!(
231 exchange = %self.exchange.value,
232 reported = %instrument,
233 registered = ?self.find_instrument_name_exchange(index).ok(),
234 "ExecutionInstrumentMap matched an instrument name ignoring case"
235 );
236 Ok(index)
237 }
238}
239
240fn fold_names<'a, Index>(
246 names: impl Iterator<Item = (&'a str, Index)>,
247) -> FnvHashMap<SmolStr, Index> {
248 let mut folded = FnvHashMap::default();
249 let mut shared = Vec::new();
250 for (name, index) in names {
251 let key = name.to_ascii_lowercase_smolstr();
252 if folded.insert(key.clone(), index).is_some() {
253 shared.push(key);
254 }
255 }
256 shared.sort_unstable();
257 shared.dedup();
258 for key in shared {
259 warn!(
260 name = %key,
261 "ExecutionInstrumentMap holds names that differ only in case - none resolves \
262 ignoring case"
263 );
264 folded.remove(&key);
265 }
266 folded
267}
268
269fn find_folded<Index: Copy>(folded: &FnvHashMap<SmolStr, Index>, name: &str) -> Option<Index> {
271 folded.get(&name.to_ascii_lowercase_smolstr()).copied()
272}
273
274pub fn generate_execution_instrument_map(
275 instruments: &IndexedInstruments,
276 exchange: ExchangeId,
277) -> Result<ExecutionInstrumentMap, IndexError> {
278 let exchange_index = instruments
279 .exchanges()
280 .iter()
281 .find_map(|keyed_exchange| (keyed_exchange.value == exchange).then_some(keyed_exchange.key))
282 .ok_or_else(|| {
283 IndexError::ExchangeIndex(format!(
284 "IndexedInstrument does not contain index for: {exchange}"
285 ))
286 })?;
287
288 Ok(ExecutionInstrumentMap::new(
289 Keyed::new(exchange_index, exchange),
290 instruments,
291 ))
292}
293
294#[cfg(test)]
295#[allow(clippy::unwrap_used)] mod tests {
297 use super::*;
298 use rustrade_instrument::{exchange::ExchangeId, test_utils};
299
300 fn indexed_instruments() -> IndexedInstruments {
301 let instruments = vec![
302 test_utils::instrument(ExchangeId::BinanceSpot, "BTC", "ETH"),
303 test_utils::instrument(ExchangeId::Coinbase, "BTC", "ETH"),
304 test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT"),
305 ];
306
307 IndexedInstruments::new(instruments)
308 }
309
310 #[test]
311 fn test_find_exchange_id() {
312 let instruments = indexed_instruments();
313 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
314
315 let exchange_id = kraken.find_exchange_id(kraken.exchange.key).unwrap();
316 assert_eq!(exchange_id, ExchangeId::Kraken);
317 }
318
319 #[test]
320 fn test_find_exchange_index() {
321 let instruments = indexed_instruments();
322 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
323
324 let exchange_index = kraken.find_exchange_index(ExchangeId::Kraken).unwrap();
325 assert_eq!(exchange_index, kraken.exchange.key);
326 }
327
328 #[test]
329 fn test_find_exchange_id_error() {
330 let instruments = indexed_instruments();
331 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
332
333 let binance_index = instruments
335 .exchanges()
336 .iter()
337 .find(|ex| ex.value == ExchangeId::BinanceSpot)
338 .map(|ex| ex.key)
339 .unwrap();
340
341 let result = kraken.find_exchange_id(binance_index);
342 assert!(result.is_err());
343 assert!(matches!(result, Err(KeyError::ExchangeId(_))));
344 }
345
346 #[test]
347 fn test_find_exchange_index_error() {
348 let instruments = indexed_instruments();
349 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
350
351 let result = kraken.find_exchange_index(ExchangeId::BinanceSpot);
352 assert!(result.is_err());
353 assert!(matches!(result, Err(IndexError::ExchangeIndex(_))));
354 }
355
356 #[test]
357 fn test_find_asset_name_exchange() {
358 let instruments = indexed_instruments();
359 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
360
361 let usdt = test_utils::asset("USDT");
362 let usdt_index = instruments
363 .find_asset_index(ExchangeId::Kraken, &usdt.name_internal)
364 .unwrap();
365
366 let usdt_exchange_name = kraken.find_asset_name_exchange(usdt_index).unwrap();
367 assert_eq!(usdt_exchange_name, &usdt.name_exchange);
368 }
369
370 #[test]
371 fn test_find_asset_index() {
372 let instruments = indexed_instruments();
373 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
374
375 let usdc = test_utils::asset("USDC");
376 let asset_index = kraken.find_asset_index(&usdc.name_exchange).unwrap();
377
378 let expected_index = instruments
379 .find_asset_index(ExchangeId::Kraken, &usdc.name_internal)
380 .unwrap();
381 assert_eq!(asset_index, expected_index);
382 }
383
384 #[test]
385 fn test_find_asset_index_error() {
386 let instruments = indexed_instruments();
387 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
388
389 let btc = test_utils::asset("BTC");
390 let result = kraken.find_asset_index(&btc.name_exchange);
391 assert!(result.is_err());
392 assert!(matches!(result, Err(IndexError::AssetIndex(_))));
393 }
394
395 #[test]
396 fn test_find_asset_name_exchange_error() {
397 let instruments = indexed_instruments();
398 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
399
400 let invalid_index = AssetIndex::new(999);
402 let result = kraken.find_asset_name_exchange(invalid_index);
403 assert!(result.is_err());
404 assert!(matches!(result, Err(KeyError::AssetKey(_))));
405 }
406
407 #[test]
408 fn test_find_instrument_name_exchange() {
409 let instruments = indexed_instruments();
410 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
411 let usdc_usdt = test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT");
412
413 let usdc_usdt_index = instruments
414 .find_instrument_index(ExchangeId::Kraken, &usdc_usdt.name_internal)
415 .unwrap();
416
417 let usdc_usdt_exchange_name = kraken
418 .find_instrument_name_exchange(usdc_usdt_index)
419 .unwrap();
420
421 assert_eq!(usdc_usdt_exchange_name, &usdc_usdt.name_exchange);
422 }
423
424 #[test]
425 fn test_find_own_instrument_name_exchange() {
426 let instruments = indexed_instruments();
427 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
428 let usdc_usdt = test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT");
429 let own = instruments
430 .find_instrument_index(ExchangeId::Kraken, &usdc_usdt.name_internal)
431 .unwrap();
432 let binance = test_utils::instrument(ExchangeId::BinanceSpot, "BTC", "ETH");
433 let other = instruments
434 .find_instrument_index(ExchangeId::BinanceSpot, &binance.name_internal)
435 .unwrap();
436
437 assert_eq!(
438 kraken.find_own_instrument_name_exchange(own),
439 Some(&usdc_usdt.name_exchange)
440 );
441 assert!(kraken.find_instrument_name_exchange(other).is_ok());
443 assert_eq!(kraken.find_own_instrument_name_exchange(other), None);
444 assert_eq!(
445 kraken.find_own_instrument_name_exchange(InstrumentIndex::new(99)),
446 None
447 );
448 }
449
450 #[test]
451 fn test_find_instrument_index() {
452 let instruments = indexed_instruments();
453 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
454
455 let usdc_usdt = test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT");
456 let instrument_index = kraken
457 .find_instrument_index(&usdc_usdt.name_exchange)
458 .unwrap();
459
460 let expected_index = instruments
461 .find_instrument_index(ExchangeId::Kraken, &usdc_usdt.name_internal)
462 .unwrap();
463 assert_eq!(instrument_index, expected_index);
464 }
465
466 #[test]
467 fn test_find_instrument_index_error() {
468 let instruments = indexed_instruments();
469 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
470
471 let btc_eth = test_utils::instrument(ExchangeId::Kraken, "BTC", "ETH");
472 let result = kraken.find_instrument_index(&btc_eth.name_exchange);
473 assert!(result.is_err());
474 assert!(matches!(result, Err(IndexError::InstrumentIndex(_))));
475 }
476
477 #[test]
478 fn test_find_instrument_name_exchange_error() {
479 let instruments = indexed_instruments();
480 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
481
482 let invalid_index = InstrumentIndex::new(999);
484 let result = kraken.find_instrument_name_exchange(invalid_index);
485 assert!(result.is_err());
486 assert!(matches!(result, Err(KeyError::InstrumentKey(_))));
487 }
488
489 #[test]
490 fn test_exchange_assets_iterator() {
491 let instruments = indexed_instruments();
492 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
493
494 let exchange_assets: Vec<&AssetNameExchange> = kraken.exchange_assets().collect();
495
496 let expected_assets = vec!["USDC", "USDT"];
498 for expected in &expected_assets {
499 assert!(
500 exchange_assets
501 .iter()
502 .any(|asset| asset.as_ref() == *expected)
503 );
504 }
505 }
506
507 #[test]
508 fn test_exchange_instruments_iterator() {
509 let instruments = indexed_instruments();
510 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
511
512 let exchange_instruments: Vec<&InstrumentNameExchange> =
513 kraken.exchange_instruments().collect();
514
515 assert_eq!(exchange_instruments.len(), 1);
517
518 let usdc_usdt = test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT");
520 assert!(exchange_instruments.contains(&&usdc_usdt.name_exchange));
521 }
522
523 #[test]
524 fn test_generate_execution_instrument_map_error() {
525 let instruments = indexed_instruments();
526
527 let result = generate_execution_instrument_map(&instruments, ExchangeId::Bitstamp);
529 assert!(result.is_err());
530 assert!(matches!(result, Err(IndexError::ExchangeIndex(_))));
531 }
532
533 #[test]
534 fn an_asset_name_in_another_case_resolves_to_the_registered_asset() {
535 let instruments = indexed_instruments();
536 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
537
538 let registered = kraken
539 .find_asset_index(&AssetNameExchange::new("USDT"))
540 .unwrap();
541
542 for reported in ["usdt", "Usdt", "uSdT"] {
543 assert_eq!(
544 kraken.find_asset_index(&AssetNameExchange::new(reported)),
545 Ok(registered),
546 "{reported}"
547 );
548 }
549 assert_eq!(
551 kraken.find_asset_name_exchange(registered).unwrap().name(),
552 "USDT"
553 );
554 }
555
556 #[test]
557 fn an_instrument_name_in_another_case_resolves_to_the_registered_instrument() {
558 let instruments = indexed_instruments();
559 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
560
561 let registered = kraken
562 .find_instrument_index(&InstrumentNameExchange::new("USDC_USDT"))
563 .unwrap();
564
565 assert_eq!(
566 kraken.find_instrument_index(&InstrumentNameExchange::new("usdc_usdt")),
567 Ok(registered)
568 );
569 assert_eq!(
570 kraken
571 .find_instrument_name_exchange(registered)
572 .unwrap()
573 .name(),
574 "USDC_USDT"
575 );
576 }
577
578 #[test]
579 fn ignoring_case_does_not_reach_another_exchanges_names() {
580 let instruments = indexed_instruments();
581 let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
582
583 assert!(matches!(
585 kraken.find_asset_index(&AssetNameExchange::new("btc")),
586 Err(IndexError::AssetIndex(_))
587 ));
588 assert!(matches!(
589 kraken.find_instrument_index(&InstrumentNameExchange::new("btc_eth")),
590 Err(IndexError::InstrumentIndex(_))
591 ));
592 }
593
594 #[test]
595 fn a_folded_name_two_names_share_resolves_to_neither() {
596 let folded = fold_names([("USD", 0), ("usd", 1), ("BTC", 2)].into_iter());
597
598 assert_eq!(find_folded(&folded, "Usd"), None);
599 assert_eq!(find_folded(&folded, "btc"), Some(2));
600 }
601}