1use std::sync::atomic::{AtomicBool, Ordering};
38
39use dashmap::DashMap;
40use nautilus_model::{
41 identifiers::InstrumentId,
42 instruments::{Instrument, InstrumentAny},
43};
44use ustr::Ustr;
45
46use crate::{grpc::OrderMarketParams, http::models::PerpetualMarket};
47
48#[derive(Debug, Default)]
54pub struct InstrumentCache {
55 instruments: DashMap<InstrumentId, InstrumentAny>,
57 clob_pair_id_index: DashMap<u32, InstrumentId>,
59 market_index: DashMap<Ustr, InstrumentId>,
61 market_params: DashMap<InstrumentId, PerpetualMarket>,
63 initialized: AtomicBool,
65}
66
67impl InstrumentCache {
68 #[must_use]
70 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn insert(&self, instrument: InstrumentAny, market: PerpetualMarket) {
78 let instrument_id = instrument.id();
79 let ticker = market.ticker;
80 let clob_pair_id = market.clob_pair_id;
81
82 self.instruments.insert(instrument_id, instrument);
84
85 self.clob_pair_id_index.insert(clob_pair_id, instrument_id);
87 self.market_index.insert(ticker, instrument_id);
88
89 self.market_params.insert(instrument_id, market);
91 }
92
93 pub fn insert_many(&self, items: Vec<(InstrumentAny, PerpetualMarket)>) {
97 for (instrument, market) in items {
98 self.insert(instrument, market);
99 }
100 self.initialized.store(true, Ordering::Release);
101 }
102
103 pub fn clear(&self) {
107 self.instruments.clear();
108 self.clob_pair_id_index.clear();
109 self.market_index.clear();
110 self.market_params.clear();
111 self.initialized.store(false, Ordering::Release);
112 }
113
114 pub fn insert_instrument_only(&self, instrument: InstrumentAny) {
120 let instrument_id = instrument.id();
121 let symbol = instrument_id.symbol.as_str();
122 let ticker = symbol.strip_suffix("-PERP").unwrap_or(symbol);
123 self.market_index.insert(Ustr::from(ticker), instrument_id);
124 self.instruments.insert(instrument_id, instrument);
125 }
126
127 pub fn insert_instruments_only(&self, instruments: Vec<InstrumentAny>) {
131 for instrument in instruments {
132 self.insert_instrument_only(instrument);
133 }
134 self.initialized.store(true, Ordering::Release);
135 }
136
137 #[must_use]
139 pub fn get(&self, instrument_id: &InstrumentId) -> Option<InstrumentAny> {
140 self.instruments.get(instrument_id).map(|r| r.clone())
141 }
142
143 #[must_use]
147 pub fn get_by_market(&self, ticker: &str) -> Option<InstrumentAny> {
148 let ticker_ustr = Ustr::from(ticker);
149 self.market_index
150 .get(&ticker_ustr)
151 .and_then(|instrument_id| self.instruments.get(&*instrument_id).map(|r| r.clone()))
152 }
153
154 #[must_use]
158 pub fn get_by_clob_id(&self, clob_pair_id: u32) -> Option<InstrumentAny> {
159 self.clob_pair_id_index
160 .get(&clob_pair_id)
161 .and_then(|instrument_id| self.instruments.get(&*instrument_id).map(|r| r.clone()))
162 }
163
164 #[must_use]
168 pub fn get_id_by_clob_id(&self, clob_pair_id: u32) -> Option<InstrumentId> {
169 self.clob_pair_id_index.get(&clob_pair_id).map(|r| *r)
170 }
171
172 #[must_use]
176 pub fn get_id_by_market(&self, ticker: &str) -> Option<InstrumentId> {
177 let ticker_ustr = Ustr::from(ticker);
178 self.market_index.get(&ticker_ustr).map(|r| *r)
179 }
180
181 #[must_use]
186 pub fn get_market_params(&self, instrument_id: &InstrumentId) -> Option<PerpetualMarket> {
187 self.market_params.get(instrument_id).map(|r| r.clone())
188 }
189
190 #[must_use]
195 pub fn get_order_market_params(
196 &self,
197 instrument_id: &InstrumentId,
198 ) -> Option<OrderMarketParams> {
199 self.get_market_params(instrument_id)
200 .map(|market| OrderMarketParams {
201 atomic_resolution: market.atomic_resolution,
202 clob_pair_id: market.clob_pair_id,
203 oracle_price: market.oracle_price,
204 quantum_conversion_exponent: market.quantum_conversion_exponent,
205 step_base_quantums: market.step_base_quantums,
206 subticks_per_tick: market.subticks_per_tick,
207 })
208 }
209
210 pub fn update_oracle_price(&self, ticker: &str, oracle_price: rust_decimal::Decimal) {
214 let ticker_ustr = Ustr::from(ticker);
215 if let Some(instrument_id) = self.market_index.get(&ticker_ustr)
216 && let Some(mut market) = self.market_params.get_mut(&*instrument_id)
217 {
218 market.oracle_price = Some(oracle_price);
219 }
220 }
221
222 #[must_use]
224 pub fn is_initialized(&self) -> bool {
225 self.initialized.load(Ordering::Acquire)
226 }
227
228 #[must_use]
230 pub fn len(&self) -> usize {
231 self.instruments.len()
232 }
233
234 #[must_use]
236 pub fn is_empty(&self) -> bool {
237 self.instruments.is_empty()
238 }
239
240 #[must_use]
244 pub fn all_instruments(&self) -> Vec<InstrumentAny> {
245 self.instruments.iter().map(|r| r.clone()).collect()
246 }
247
248 #[must_use]
250 pub fn all_instrument_ids(&self) -> Vec<InstrumentId> {
251 self.instruments.iter().map(|r| r.value().id()).collect()
252 }
253
254 #[must_use]
256 pub fn contains(&self, instrument_id: &InstrumentId) -> bool {
257 self.instruments.contains_key(instrument_id)
258 }
259
260 #[must_use]
262 pub fn contains_clob_id(&self, clob_pair_id: u32) -> bool {
263 self.clob_pair_id_index.contains_key(&clob_pair_id)
264 }
265
266 #[must_use]
268 pub fn contains_market(&self, ticker: &str) -> bool {
269 let ticker_ustr = Ustr::from(ticker);
270 self.market_index.contains_key(&ticker_ustr)
271 }
272
273 #[must_use]
278 pub fn to_instrument_id_map(&self) -> std::collections::HashMap<InstrumentId, InstrumentAny> {
279 self.instruments
280 .iter()
281 .map(|entry| (entry.value().id(), entry.value().clone()))
282 .collect()
283 }
284
285 #[must_use]
290 pub fn to_oracle_prices_map(
291 &self,
292 ) -> std::collections::HashMap<InstrumentId, rust_decimal::Decimal> {
293 self.market_params
294 .iter()
295 .filter_map(|entry| entry.value().oracle_price.map(|p| (*entry.key(), p)))
296 .collect()
297 }
298
299 pub fn log_missing_clob_pair_id(&self, clob_pair_id: u32) {
301 let known: Vec<(u32, String)> = self
302 .clob_pair_id_index
303 .iter()
304 .map(|entry| (*entry.key(), entry.value().symbol.as_str().to_string()))
305 .collect();
306
307 log::warn!(
308 "Instrument for clob_pair_id {clob_pair_id} not found in cache. \
309 Known CLOB pair IDs and symbols: {known:?}"
310 );
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use nautilus_core::UnixNanos;
317 use nautilus_model::{
318 identifiers::{InstrumentId, Symbol},
319 instruments::{CryptoPerpetual, InstrumentAny},
320 types::{Currency, Price, Quantity},
321 };
322 use rstest::rstest;
323 use rust_decimal_macros::dec;
324 use ustr::Ustr;
325
326 use super::*;
327 use crate::common::{consts::DYDX_VENUE, enums::DydxMarketStatus};
328
329 fn create_test_instrument(symbol: &str) -> InstrumentAny {
330 let instrument_id = InstrumentId::new(Symbol::new(symbol), *DYDX_VENUE);
331 InstrumentAny::CryptoPerpetual(
332 CryptoPerpetual::builder()
333 .instrument_id(instrument_id)
334 .raw_symbol(instrument_id.symbol)
335 .base_currency(Currency::BTC())
336 .quote_currency(Currency::USD())
337 .settlement_currency(Currency::USD())
338 .is_inverse(false)
339 .price_precision(1)
340 .size_precision(3)
341 .price_increment(Price::new(0.1, 1))
342 .size_increment(Quantity::new(0.001, 3))
343 .ts_event(UnixNanos::default())
344 .ts_init(UnixNanos::default())
345 .build()
346 .unwrap(),
347 )
348 }
349
350 fn create_test_market(ticker: &str, clob_pair_id: u32) -> PerpetualMarket {
351 PerpetualMarket {
352 clob_pair_id,
353 ticker: Ustr::from(ticker),
354 status: DydxMarketStatus::Active,
355 base_asset: Some(Ustr::from("BTC")),
356 quote_asset: Some(Ustr::from("USD")),
357 step_size: dec!(0.001),
358 tick_size: dec!(0.1),
359 index_price: Some(dec!(50000)),
360 oracle_price: Some(dec!(50000)),
361 price_change_24h: dec!(0),
362 next_funding_rate: dec!(0),
363 next_funding_at: None,
364 min_order_size: Some(dec!(0.001)),
365 market_type: None,
366 initial_margin_fraction: dec!(0.05),
367 maintenance_margin_fraction: dec!(0.03),
368 base_position_notional: None,
369 incremental_position_size: None,
370 incremental_initial_margin_fraction: None,
371 max_position_size: None,
372 open_interest: dec!(1000),
373 atomic_resolution: -10,
374 quantum_conversion_exponent: -9,
375 subticks_per_tick: 1000000,
376 step_base_quantums: 1000000,
377 is_reduce_only: false,
378 }
379 }
380
381 #[rstest]
382 fn test_insert_and_get() {
383 let cache = InstrumentCache::new();
384 let instrument = create_test_instrument("BTC-USD-PERP");
385 let instrument_id = instrument.id();
386 let market = create_test_market("BTC-USD", 0);
387
388 cache.insert(instrument, market);
389
390 let retrieved = cache.get(&instrument_id);
392 assert!(retrieved.is_some());
393 assert_eq!(retrieved.unwrap().id().symbol.as_str(), "BTC-USD-PERP");
394 }
395
396 #[rstest]
397 fn test_get_by_market() {
398 let cache = InstrumentCache::new();
399 let instrument = create_test_instrument("BTC-USD-PERP");
400 let market = create_test_market("BTC-USD", 0);
401
402 cache.insert(instrument, market);
403
404 let retrieved = cache.get_by_market("BTC-USD");
406 assert!(retrieved.is_some());
407 assert_eq!(retrieved.unwrap().id().symbol.as_str(), "BTC-USD-PERP");
408 }
409
410 #[rstest]
411 fn test_get_by_clob_id() {
412 let cache = InstrumentCache::new();
413 let instrument = create_test_instrument("BTC-USD-PERP");
414 let market = create_test_market("BTC-USD", 0);
415
416 cache.insert(instrument, market);
417
418 let retrieved = cache.get_by_clob_id(0);
420 assert!(retrieved.is_some());
421 assert_eq!(retrieved.unwrap().id().symbol.as_str(), "BTC-USD-PERP");
422
423 assert!(cache.get_by_clob_id(999).is_none());
425 }
426
427 #[rstest]
428 fn test_insert_many() {
429 let cache = InstrumentCache::new();
430
431 let items = vec![
432 (
433 create_test_instrument("BTC-USD-PERP"),
434 create_test_market("BTC-USD", 0),
435 ),
436 (
437 create_test_instrument("ETH-USD-PERP"),
438 create_test_market("ETH-USD", 1),
439 ),
440 ];
441
442 assert!(!cache.is_initialized());
443 cache.insert_many(items);
444 assert!(cache.is_initialized());
445
446 assert_eq!(cache.len(), 2);
447 assert!(cache.get_by_market("BTC-USD").is_some());
448 assert!(cache.get_by_market("ETH-USD").is_some());
449 assert!(cache.get_by_clob_id(0).is_some());
450 assert!(cache.get_by_clob_id(1).is_some());
451 }
452
453 #[rstest]
454 fn test_clear() {
455 let cache = InstrumentCache::new();
456 let instrument = create_test_instrument("BTC-USD-PERP");
457 let market = create_test_market("BTC-USD", 0);
458
459 cache.insert(instrument, market);
460 assert_eq!(cache.len(), 1);
461
462 cache.clear();
463 assert_eq!(cache.len(), 0);
464 assert!(!cache.is_initialized());
465 }
466
467 #[rstest]
468 fn test_get_market_params() {
469 let cache = InstrumentCache::new();
470 let instrument = create_test_instrument("BTC-USD-PERP");
471 let market = create_test_market("BTC-USD", 0);
472
473 cache.insert(instrument.clone(), market);
474
475 let params = cache.get_market_params(&instrument.id());
476 assert!(params.is_some());
477 let params = params.unwrap();
478 assert_eq!(params.clob_pair_id, 0);
479 assert_eq!(params.ticker, "BTC-USD");
480 }
481
482 #[rstest]
483 fn test_update_oracle_price() {
484 let cache = InstrumentCache::new();
485 let instrument = create_test_instrument("BTC-USD-PERP");
486 let market = create_test_market("BTC-USD", 0);
487
488 cache.insert(instrument.clone(), market);
489
490 let params = cache.get_market_params(&instrument.id()).unwrap();
492 assert_eq!(params.oracle_price, Some(dec!(50000)));
493
494 cache.update_oracle_price("BTC-USD", dec!(55000));
496
497 let params = cache.get_market_params(&instrument.id()).unwrap();
498 assert_eq!(params.oracle_price, Some(dec!(55000)));
499 }
500
501 #[rstest]
502 fn test_to_oracle_prices_map() {
503 let cache = InstrumentCache::new();
504
505 let items = vec![
506 (
507 create_test_instrument("BTC-USD-PERP"),
508 create_test_market("BTC-USD", 0),
509 ),
510 (
511 create_test_instrument("ETH-USD-PERP"),
512 create_test_market("ETH-USD", 1),
513 ),
514 ];
515
516 cache.insert_many(items);
517
518 cache.update_oracle_price("ETH-USD", dec!(3000));
520
521 let oracle_map = cache.to_oracle_prices_map();
522 assert_eq!(oracle_map.len(), 2);
523
524 let btc_id = InstrumentId::new(Symbol::new("BTC-USD-PERP"), *DYDX_VENUE);
526 assert_eq!(oracle_map.get(&btc_id), Some(&dec!(50000)));
527
528 let eth_id = InstrumentId::new(Symbol::new("ETH-USD-PERP"), *DYDX_VENUE);
530 assert_eq!(oracle_map.get(ð_id), Some(&dec!(3000)));
531 }
532
533 #[rstest]
534 fn test_get_order_market_params_with_none_oracle_price() {
535 let cache = InstrumentCache::new();
536 let instrument = create_test_instrument("WTI-USD-PERP");
537 let instrument_id = instrument.id();
538 let mut market = create_test_market("WTI-USD", 99);
539 market.oracle_price = None;
540
541 cache.insert(instrument, market);
542
543 let params = cache.get_order_market_params(&instrument_id).unwrap();
544 assert_eq!(params.oracle_price, None);
545 assert_eq!(params.clob_pair_id, 99);
546 }
547
548 #[rstest]
549 fn test_to_oracle_prices_map_excludes_none() {
550 let cache = InstrumentCache::new();
551
552 let mut market_no_oracle = create_test_market("WTI-USD", 99);
553 market_no_oracle.oracle_price = None;
554
555 let items = vec![
556 (
557 create_test_instrument("BTC-USD-PERP"),
558 create_test_market("BTC-USD", 0),
559 ),
560 (create_test_instrument("WTI-USD-PERP"), market_no_oracle),
561 ];
562
563 cache.insert_many(items);
564
565 let oracle_map = cache.to_oracle_prices_map();
566 assert_eq!(oracle_map.len(), 1);
567
568 let btc_id = InstrumentId::new(Symbol::new("BTC-USD-PERP"), *DYDX_VENUE);
569 assert_eq!(oracle_map.get(&btc_id), Some(&dec!(50000)));
570
571 let wti_id = InstrumentId::new(Symbol::new("WTI-USD-PERP"), *DYDX_VENUE);
572 assert_eq!(oracle_map.get(&wti_id), None);
573 }
574}