nautilus_common/
providers.rs1use std::collections::HashMap;
22
23use ahash::RandomState;
24use async_trait::async_trait;
25use indexmap::IndexMap;
26use nautilus_model::{
27 identifiers::InstrumentId,
28 instruments::{Instrument, InstrumentAny},
29};
30
31#[derive(Debug, Default)]
41pub struct InstrumentStore {
42 instruments: IndexMap<InstrumentId, InstrumentAny, RandomState>,
43 initialized: bool,
44}
45
46impl InstrumentStore {
47 #[must_use]
49 pub fn new() -> Self {
50 Self::default()
51 }
52
53 pub fn add(&mut self, instrument: InstrumentAny) {
55 self.instruments.insert(instrument.id(), instrument);
56 }
57
58 pub fn add_bulk(&mut self, instruments: Vec<InstrumentAny>) {
60 for instrument in instruments {
61 self.add(instrument);
62 }
63 }
64
65 #[must_use]
67 pub fn find(&self, instrument_id: &InstrumentId) -> Option<&InstrumentAny> {
68 self.instruments.get(instrument_id)
69 }
70
71 #[must_use]
73 pub fn contains(&self, instrument_id: &InstrumentId) -> bool {
74 self.instruments.contains_key(instrument_id)
75 }
76
77 #[must_use]
79 pub fn get_all(&self) -> &IndexMap<InstrumentId, InstrumentAny, RandomState> {
80 &self.instruments
81 }
82
83 #[must_use]
85 pub fn list_all(&self) -> Vec<&InstrumentAny> {
86 self.instruments.values().collect()
87 }
88
89 #[must_use]
91 pub fn count(&self) -> usize {
92 self.instruments.len()
93 }
94
95 #[must_use]
97 pub fn is_empty(&self) -> bool {
98 self.instruments.is_empty()
99 }
100
101 #[must_use]
103 pub fn is_initialized(&self) -> bool {
104 self.initialized
105 }
106
107 pub fn set_initialized(&mut self) {
109 self.initialized = true;
110 }
111
112 pub fn clear(&mut self) {
114 self.instruments.clear();
115 self.initialized = false;
116 }
117}
118
119#[async_trait(?Send)]
130pub trait InstrumentProvider {
131 fn store(&self) -> &InstrumentStore;
133
134 fn store_mut(&mut self) -> &mut InstrumentStore;
136
137 async fn load_all(&mut self, filters: Option<&HashMap<String, String>>) -> anyhow::Result<()>;
145
146 async fn load_ids(
155 &mut self,
156 instrument_ids: &[InstrumentId],
157 filters: Option<&HashMap<String, String>>,
158 ) -> anyhow::Result<()> {
159 for instrument_id in instrument_ids {
160 self.load(instrument_id, filters).await?;
161 }
162 Ok(())
163 }
164
165 async fn load(
171 &mut self,
172 instrument_id: &InstrumentId,
173 filters: Option<&HashMap<String, String>>,
174 ) -> anyhow::Result<()>;
175}
176
177#[cfg(test)]
178mod tests {
179 use nautilus_model::instruments::{InstrumentAny, stubs::crypto_perpetual_ethusdt};
180 use rstest::rstest;
181
182 use super::*;
183
184 #[rstest]
185 fn test_instrument_store_default_is_empty() {
186 let store = InstrumentStore::new();
187 assert!(store.is_empty());
188 assert_eq!(store.count(), 0);
189 assert!(!store.is_initialized());
190 }
191
192 #[rstest]
193 fn test_instrument_store_add_and_find() {
194 let mut store = InstrumentStore::new();
195 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
196 let id = instrument.id();
197
198 store.add(instrument);
199
200 assert_eq!(store.count(), 1);
201 assert!(!store.is_empty());
202 assert!(store.contains(&id));
203 assert!(store.find(&id).is_some());
204 }
205
206 #[rstest]
207 fn test_instrument_store_add_bulk() {
208 let mut store = InstrumentStore::new();
209 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
210 let id = instrument.id();
211
212 store.add_bulk(vec![instrument]);
213
214 assert_eq!(store.count(), 1);
215 assert!(store.contains(&id));
216 }
217
218 #[rstest]
219 fn test_instrument_store_get_all() {
220 let mut store = InstrumentStore::new();
221 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
222
223 store.add(instrument);
224
225 let all = store.get_all();
226 assert_eq!(all.len(), 1);
227 }
228
229 #[rstest]
230 fn test_instrument_store_list_all() {
231 let mut store = InstrumentStore::new();
232 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
233
234 store.add(instrument);
235
236 let list = store.list_all();
237 assert_eq!(list.len(), 1);
238 }
239
240 #[rstest]
241 fn test_instrument_store_iterates_in_insertion_order() {
242 let mut store = InstrumentStore::new();
243 let base = crypto_perpetual_ethusdt();
244
245 let ids = [
248 "SOLUSDT-PERP.BINANCE",
249 "ADAUSDT-PERP.BINANCE",
250 "XRPUSDT-PERP.BINANCE",
251 "BTCUSDT-PERP.BINANCE",
252 "DOTUSDT-PERP.BINANCE",
253 "AVAXUSDT-PERP.BINANCE",
254 ];
255 let expected: Vec<InstrumentId> = ids.iter().map(|id| InstrumentId::from(*id)).collect();
256
257 for id in ids {
258 let mut variant = base.clone();
259 variant.id = InstrumentId::from(id);
260 store.add(InstrumentAny::CryptoPerpetual(variant));
261 }
262
263 let keys: Vec<InstrumentId> = store.get_all().keys().copied().collect();
264 let listed: Vec<InstrumentId> = store.list_all().into_iter().map(Instrument::id).collect();
265
266 assert_eq!(keys, expected);
267 assert_eq!(listed, expected);
268 }
269
270 #[rstest]
271 fn test_instrument_store_clear() {
272 let mut store = InstrumentStore::new();
273 let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
274
275 store.add(instrument);
276 store.set_initialized();
277 assert!(store.is_initialized());
278 assert_eq!(store.count(), 1);
279
280 store.clear();
281 assert!(!store.is_initialized());
282 assert!(store.is_empty());
283 }
284
285 #[rstest]
286 fn test_instrument_store_find_missing_returns_none() {
287 let store = InstrumentStore::new();
288 let id = InstrumentId::from("UNKNOWN-UNKNOWN.VENUE");
289 assert!(store.find(&id).is_none());
290 assert!(!store.contains(&id));
291 }
292
293 #[rstest]
294 fn test_instrument_store_add_replaces_existing() {
295 let mut store = InstrumentStore::new();
296 let instrument1 = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
297 let instrument2 = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
298 let id = instrument1.id();
299
300 store.add(instrument1);
301 store.add(instrument2);
302
303 assert_eq!(store.count(), 1);
304 assert!(store.contains(&id));
305 }
306}