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;
11
12/// Indexed instrument map used to associate the internal Barter representation of instruments and
13/// assets with the [`ExecutionClient`](super::client::ExecutionClient) representation.
14///
15/// Similarly, when the execution manager received an [`AccountEvent`](super::AccountEvent)
16/// from the execution API, it needs to determine the internal representation of the associated
17/// assets and instruments.
18///
19/// eg/ `InstrumentNameExchange("XBT-USDT")` <--> `InstrumentIndex(1)` <br>
20/// eg/ `AssetNameExchange("XBT")` <--> `AssetIndex(1)`
21#[derive(Debug, Clone, Eq, PartialEq)]
22pub struct ExecutionInstrumentMap {
23    /// The exchange associated with this execution map.
24    pub exchange: Keyed<ExchangeIndex, ExchangeId>,
25    /// Collection of assets available by the engine with their
26    /// exchange-specific representations. This holds all indexed assets.
27    pub assets: FnvIndexSet<ExchangeAsset<Asset>>,
28    /// Collection of instruments available by the engine. This holds all
29    /// indexed instruments.
30    pub instruments: FnvIndexSet<Instrument<Keyed<ExchangeIndex, ExchangeId>, AssetIndex>>,
31    /// Map from exchange-specific asset names to internal asset indices for
32    /// fast lookups.
33    pub asset_names: FnvHashMap<AssetNameExchange, AssetIndex>,
34    /// Map from exchange-specific instrument names to internal instrument
35    /// indices for fast lookups.
36    pub instrument_names: FnvHashMap<InstrumentNameExchange, InstrumentIndex>,
37}
38
39impl ExecutionInstrumentMap {
40    /// Construct a new [`Self`] using the provided indexed assets and instruments.
41    pub fn new(
42        exchange: Keyed<ExchangeIndex, ExchangeId>,
43        instruments: &IndexedInstruments,
44    ) -> Self {
45        let asset_names = instruments
46            .assets()
47            .iter()
48            .filter_map(|Keyed { key, value }| {
49                (value.exchange == exchange.value)
50                    .then_some((value.asset.name_exchange.clone(), *key))
51            })
52            .collect();
53
54        let assets = instruments
55            .assets()
56            .iter()
57            .map(|Keyed { value, .. }| value.clone())
58            .collect();
59
60        let instrument_names = instruments
61            .instruments()
62            .iter()
63            .filter_map(|Keyed { key, value }| {
64                (value.exchange.value == exchange.value)
65                    .then_some((value.name_exchange.clone(), *key))
66            })
67            .collect();
68
69        let instruments = instruments
70            .instruments()
71            .iter()
72            .map(|Keyed { value, .. }| value.clone())
73            .collect();
74
75        Self {
76            exchange,
77            asset_names,
78            instrument_names,
79            assets,
80            instruments,
81        }
82    }
83
84    pub fn exchange_assets(&self) -> impl Iterator<Item = &AssetNameExchange> {
85        self.asset_names.keys()
86    }
87
88    pub fn exchange_instruments(&self) -> impl Iterator<Item = &InstrumentNameExchange> {
89        self.instrument_names.keys()
90    }
91
92    pub fn find_exchange_id(&self, exchange: ExchangeIndex) -> Result<ExchangeId, KeyError> {
93        if self.exchange.key == exchange {
94            Ok(self.exchange.value)
95        } else {
96            Err(KeyError::ExchangeId(format!(
97                "ExecutionInstrumentMap does not contain {exchange}"
98            )))
99        }
100    }
101
102    pub fn find_exchange_index(&self, exchange: ExchangeId) -> Result<ExchangeIndex, IndexError> {
103        if self.exchange.value == exchange {
104            Ok(self.exchange.key)
105        } else {
106            Err(IndexError::ExchangeIndex(format!(
107                "ExecutionInstrumentMap does not contain {exchange}"
108            )))
109        }
110    }
111
112    pub fn find_asset_name_exchange(
113        &self,
114        asset: AssetIndex,
115    ) -> Result<&AssetNameExchange, KeyError> {
116        self.assets
117            .get_index(asset.index())
118            .ok_or_else(|| {
119                KeyError::AssetKey(format!("ExecutionInstrumentMap does not contain: {asset}"))
120            })
121            .map(|asset| &asset.asset.name_exchange)
122    }
123
124    pub fn find_asset_index(&self, asset: &AssetNameExchange) -> Result<AssetIndex, IndexError> {
125        self.asset_names.get(asset).copied().ok_or_else(|| {
126            IndexError::AssetIndex(format!("ExecutionInstrumentMap does not contain: {asset}"))
127        })
128    }
129
130    pub fn find_instrument_name_exchange(
131        &self,
132        instrument: InstrumentIndex,
133    ) -> Result<&InstrumentNameExchange, KeyError> {
134        self.instruments
135            .get_index(instrument.index())
136            .ok_or_else(|| {
137                KeyError::InstrumentKey(format!(
138                    "ExecutionInstrumentMap does not contain: {instrument}"
139                ))
140            })
141            .map(|instrument| &instrument.name_exchange)
142    }
143
144    pub fn find_instrument_index(
145        &self,
146        instrument: &InstrumentNameExchange,
147    ) -> Result<InstrumentIndex, IndexError> {
148        self.instrument_names
149            .get(instrument)
150            .copied()
151            .ok_or_else(|| {
152                IndexError::InstrumentIndex(format!(
153                    "ExecutionInstrumentMap does not contain: {instrument}"
154                ))
155            })
156    }
157}
158
159pub fn generate_execution_instrument_map(
160    instruments: &IndexedInstruments,
161    exchange: ExchangeId,
162) -> Result<ExecutionInstrumentMap, IndexError> {
163    let exchange_index = instruments
164        .exchanges()
165        .iter()
166        .find_map(|keyed_exchange| (keyed_exchange.value == exchange).then_some(keyed_exchange.key))
167        .ok_or_else(|| {
168            IndexError::ExchangeIndex(format!(
169                "IndexedInstrument does not contain index for: {exchange}"
170            ))
171        })?;
172
173    Ok(ExecutionInstrumentMap::new(
174        Keyed::new(exchange_index, exchange),
175        instruments,
176    ))
177}
178
179#[cfg(test)]
180#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
181mod tests {
182    use super::*;
183    use rustrade_instrument::{exchange::ExchangeId, test_utils};
184
185    fn indexed_instruments() -> IndexedInstruments {
186        let instruments = vec![
187            test_utils::instrument(ExchangeId::BinanceSpot, "BTC", "ETH"),
188            test_utils::instrument(ExchangeId::Coinbase, "BTC", "ETH"),
189            test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT"),
190        ];
191
192        IndexedInstruments::new(instruments)
193    }
194
195    #[test]
196    fn test_find_exchange_id() {
197        let instruments = indexed_instruments();
198        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
199
200        let exchange_id = kraken.find_exchange_id(kraken.exchange.key).unwrap();
201        assert_eq!(exchange_id, ExchangeId::Kraken);
202    }
203
204    #[test]
205    fn test_find_exchange_index() {
206        let instruments = indexed_instruments();
207        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
208
209        let exchange_index = kraken.find_exchange_index(ExchangeId::Kraken).unwrap();
210        assert_eq!(exchange_index, kraken.exchange.key);
211    }
212
213    #[test]
214    fn test_find_exchange_id_error() {
215        let instruments = indexed_instruments();
216        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
217
218        // Create a different exchange index that doesn't match
219        let binance_index = instruments
220            .exchanges()
221            .iter()
222            .find(|ex| ex.value == ExchangeId::BinanceSpot)
223            .map(|ex| ex.key)
224            .unwrap();
225
226        let result = kraken.find_exchange_id(binance_index);
227        assert!(result.is_err());
228        assert!(matches!(result, Err(KeyError::ExchangeId(_))));
229    }
230
231    #[test]
232    fn test_find_exchange_index_error() {
233        let instruments = indexed_instruments();
234        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
235
236        let result = kraken.find_exchange_index(ExchangeId::BinanceSpot);
237        assert!(result.is_err());
238        assert!(matches!(result, Err(IndexError::ExchangeIndex(_))));
239    }
240
241    #[test]
242    fn test_find_asset_name_exchange() {
243        let instruments = indexed_instruments();
244        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
245
246        let usdt = test_utils::asset("USDT");
247        let usdt_index = instruments
248            .find_asset_index(ExchangeId::Kraken, &usdt.name_internal)
249            .unwrap();
250
251        let usdt_exchange_name = kraken.find_asset_name_exchange(usdt_index).unwrap();
252        assert_eq!(usdt_exchange_name, &usdt.name_exchange);
253    }
254
255    #[test]
256    fn test_find_asset_index() {
257        let instruments = indexed_instruments();
258        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
259
260        let usdc = test_utils::asset("USDC");
261        let asset_index = kraken.find_asset_index(&usdc.name_exchange).unwrap();
262
263        let expected_index = instruments
264            .find_asset_index(ExchangeId::Kraken, &usdc.name_internal)
265            .unwrap();
266        assert_eq!(asset_index, expected_index);
267    }
268
269    #[test]
270    fn test_find_asset_index_error() {
271        let instruments = indexed_instruments();
272        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
273
274        let btc = test_utils::asset("BTC");
275        let result = kraken.find_asset_index(&btc.name_exchange);
276        assert!(result.is_err());
277        assert!(matches!(result, Err(IndexError::AssetIndex(_))));
278    }
279
280    #[test]
281    fn test_find_asset_name_exchange_error() {
282        let instruments = indexed_instruments();
283        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
284
285        // Try to find asset with invalid index
286        let invalid_index = AssetIndex::new(999);
287        let result = kraken.find_asset_name_exchange(invalid_index);
288        assert!(result.is_err());
289        assert!(matches!(result, Err(KeyError::AssetKey(_))));
290    }
291
292    #[test]
293    fn test_find_instrument_name_exchange() {
294        let instruments = indexed_instruments();
295        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
296        let usdc_usdt = test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT");
297
298        let usdc_usdt_index = instruments
299            .find_instrument_index(ExchangeId::Kraken, &usdc_usdt.name_internal)
300            .unwrap();
301
302        let usdc_usdt_exchange_name = kraken
303            .find_instrument_name_exchange(usdc_usdt_index)
304            .unwrap();
305
306        assert_eq!(usdc_usdt_exchange_name, &usdc_usdt.name_exchange);
307    }
308
309    #[test]
310    fn test_find_instrument_index() {
311        let instruments = indexed_instruments();
312        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
313
314        let usdc_usdt = test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT");
315        let instrument_index = kraken
316            .find_instrument_index(&usdc_usdt.name_exchange)
317            .unwrap();
318
319        let expected_index = instruments
320            .find_instrument_index(ExchangeId::Kraken, &usdc_usdt.name_internal)
321            .unwrap();
322        assert_eq!(instrument_index, expected_index);
323    }
324
325    #[test]
326    fn test_find_instrument_index_error() {
327        let instruments = indexed_instruments();
328        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
329
330        let btc_eth = test_utils::instrument(ExchangeId::Kraken, "BTC", "ETH");
331        let result = kraken.find_instrument_index(&btc_eth.name_exchange);
332        assert!(result.is_err());
333        assert!(matches!(result, Err(IndexError::InstrumentIndex(_))));
334    }
335
336    #[test]
337    fn test_find_instrument_name_exchange_error() {
338        let instruments = indexed_instruments();
339        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
340
341        // Try to find instrument with invalid index
342        let invalid_index = InstrumentIndex::new(999);
343        let result = kraken.find_instrument_name_exchange(invalid_index);
344        assert!(result.is_err());
345        assert!(matches!(result, Err(KeyError::InstrumentKey(_))));
346    }
347
348    #[test]
349    fn test_exchange_assets_iterator() {
350        let instruments = indexed_instruments();
351        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
352
353        let exchange_assets: Vec<&AssetNameExchange> = kraken.exchange_assets().collect();
354
355        // Verify that the iterator returns the expected assets for Kraken
356        let expected_assets = vec!["USDC", "USDT"];
357        for expected in &expected_assets {
358            assert!(
359                exchange_assets
360                    .iter()
361                    .any(|asset| asset.as_ref() == *expected)
362            );
363        }
364    }
365
366    #[test]
367    fn test_exchange_instruments_iterator() {
368        let instruments = indexed_instruments();
369        let kraken = generate_execution_instrument_map(&instruments, ExchangeId::Kraken).unwrap();
370
371        let exchange_instruments: Vec<&InstrumentNameExchange> =
372            kraken.exchange_instruments().collect();
373
374        // Should have exactly one instrument for Kraken
375        assert_eq!(exchange_instruments.len(), 1);
376
377        // Verify it contains the USDC-USDT instrument
378        let usdc_usdt = test_utils::instrument(ExchangeId::Kraken, "USDC", "USDT");
379        assert!(exchange_instruments.contains(&&usdc_usdt.name_exchange));
380    }
381
382    #[test]
383    fn test_generate_execution_instrument_map_error() {
384        let instruments = indexed_instruments();
385
386        // Try to generate map for exchange not in indexed instruments
387        let result = generate_execution_instrument_map(&instruments, ExchangeId::Bitstamp);
388        assert!(result.is_err());
389        assert!(matches!(result, Err(IndexError::ExchangeIndex(_))));
390    }
391}