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