Skip to main content

nautilus_common/
providers.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Instrument provider trait and shared instrument storage.
17//!
18//! Defines the [`InstrumentProvider`] trait for loading instrument definitions
19//! from venue APIs, and the [`InstrumentStore`] struct for caching them locally.
20
21use std::collections::HashMap;
22
23use ahash::AHashMap;
24use async_trait::async_trait;
25use nautilus_model::{
26    identifiers::InstrumentId,
27    instruments::{Instrument, InstrumentAny},
28};
29
30/// Local instrument storage with initialization tracking.
31///
32/// Provides `add`/`find`/`get_all` operations for instrument caching.
33/// Not thread-safe by itself; wrap in `Arc<RwLock<InstrumentStore>>` when
34/// sharing across async tasks or WebSocket handlers.
35#[derive(Debug, Default)]
36pub struct InstrumentStore {
37    instruments: AHashMap<InstrumentId, InstrumentAny>,
38    initialized: bool,
39}
40
41impl InstrumentStore {
42    /// Creates a new empty instrument store.
43    #[must_use]
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Adds an instrument to the store, replacing any existing entry with the same ID.
49    pub fn add(&mut self, instrument: InstrumentAny) {
50        self.instruments.insert(instrument.id(), instrument);
51    }
52
53    /// Adds multiple instruments to the store.
54    pub fn add_bulk(&mut self, instruments: Vec<InstrumentAny>) {
55        for instrument in instruments {
56            self.add(instrument);
57        }
58    }
59
60    /// Returns the instrument for the given ID, if found.
61    #[must_use]
62    pub fn find(&self, instrument_id: &InstrumentId) -> Option<&InstrumentAny> {
63        self.instruments.get(instrument_id)
64    }
65
66    /// Returns whether the store contains the given instrument ID.
67    #[must_use]
68    pub fn contains(&self, instrument_id: &InstrumentId) -> bool {
69        self.instruments.contains_key(instrument_id)
70    }
71
72    /// Returns all instruments as a map keyed by instrument ID.
73    #[must_use]
74    pub fn get_all(&self) -> &AHashMap<InstrumentId, InstrumentAny> {
75        &self.instruments
76    }
77
78    /// Returns all instruments as a vector.
79    #[must_use]
80    pub fn list_all(&self) -> Vec<&InstrumentAny> {
81        self.instruments.values().collect()
82    }
83
84    /// Returns the number of instruments in the store.
85    #[must_use]
86    pub fn count(&self) -> usize {
87        self.instruments.len()
88    }
89
90    /// Returns whether the store is empty.
91    #[must_use]
92    pub fn is_empty(&self) -> bool {
93        self.instruments.is_empty()
94    }
95
96    /// Returns whether the store has been marked as initialized.
97    #[must_use]
98    pub fn is_initialized(&self) -> bool {
99        self.initialized
100    }
101
102    /// Marks the store as initialized.
103    pub fn set_initialized(&mut self) {
104        self.initialized = true;
105    }
106
107    /// Clears all instruments and resets initialization state.
108    pub fn clear(&mut self) {
109        self.instruments.clear();
110        self.initialized = false;
111    }
112}
113
114/// Provides instrument definitions from a venue.
115///
116/// Implementations define how instruments are fetched from a venue API.
117/// The `store()` / `store_mut()` accessors expose the underlying
118/// [`InstrumentStore`] so that callers can query cached instruments.
119///
120/// # Thread safety
121///
122/// Provider instances are not intended to be sent across threads. The `?Send`
123/// bound allows implementations to hold non-Send state for Python interop.
124#[async_trait(?Send)]
125pub trait InstrumentProvider {
126    /// Returns a reference to the provider's instrument store.
127    fn store(&self) -> &InstrumentStore;
128
129    /// Returns a mutable reference to the provider's instrument store.
130    fn store_mut(&mut self) -> &mut InstrumentStore;
131
132    /// Loads all available instruments from the venue.
133    ///
134    /// Implementations should populate the store via `store_mut().add()`.
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if the loading operation fails.
139    async fn load_all(&mut self, filters: Option<&HashMap<String, String>>) -> anyhow::Result<()>;
140
141    /// Loads specific instruments by their IDs.
142    ///
143    /// The default implementation calls [`load`](Self::load) for each ID
144    /// sequentially. Adapters with batch APIs should override this.
145    ///
146    /// # Errors
147    ///
148    /// Returns an error if any instrument fails to load.
149    async fn load_ids(
150        &mut self,
151        instrument_ids: &[InstrumentId],
152        filters: Option<&HashMap<String, String>>,
153    ) -> anyhow::Result<()> {
154        for instrument_id in instrument_ids {
155            self.load(instrument_id, filters).await?;
156        }
157        Ok(())
158    }
159
160    /// Loads a single instrument by its ID.
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if the loading operation fails.
165    async fn load(
166        &mut self,
167        instrument_id: &InstrumentId,
168        filters: Option<&HashMap<String, String>>,
169    ) -> anyhow::Result<()>;
170}
171
172#[cfg(test)]
173mod tests {
174    use nautilus_model::instruments::{InstrumentAny, stubs::crypto_perpetual_ethusdt};
175    use rstest::rstest;
176
177    use super::*;
178
179    #[rstest]
180    fn test_instrument_store_default_is_empty() {
181        let store = InstrumentStore::new();
182        assert!(store.is_empty());
183        assert_eq!(store.count(), 0);
184        assert!(!store.is_initialized());
185    }
186
187    #[rstest]
188    fn test_instrument_store_add_and_find() {
189        let mut store = InstrumentStore::new();
190        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
191        let id = instrument.id();
192
193        store.add(instrument);
194
195        assert_eq!(store.count(), 1);
196        assert!(!store.is_empty());
197        assert!(store.contains(&id));
198        assert!(store.find(&id).is_some());
199    }
200
201    #[rstest]
202    fn test_instrument_store_add_bulk() {
203        let mut store = InstrumentStore::new();
204        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
205        let id = instrument.id();
206
207        store.add_bulk(vec![instrument]);
208
209        assert_eq!(store.count(), 1);
210        assert!(store.contains(&id));
211    }
212
213    #[rstest]
214    fn test_instrument_store_get_all() {
215        let mut store = InstrumentStore::new();
216        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
217
218        store.add(instrument);
219
220        let all = store.get_all();
221        assert_eq!(all.len(), 1);
222    }
223
224    #[rstest]
225    fn test_instrument_store_list_all() {
226        let mut store = InstrumentStore::new();
227        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
228
229        store.add(instrument);
230
231        let list = store.list_all();
232        assert_eq!(list.len(), 1);
233    }
234
235    #[rstest]
236    fn test_instrument_store_clear() {
237        let mut store = InstrumentStore::new();
238        let instrument = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
239
240        store.add(instrument);
241        store.set_initialized();
242        assert!(store.is_initialized());
243        assert_eq!(store.count(), 1);
244
245        store.clear();
246        assert!(!store.is_initialized());
247        assert!(store.is_empty());
248    }
249
250    #[rstest]
251    fn test_instrument_store_find_missing_returns_none() {
252        let store = InstrumentStore::new();
253        let id = InstrumentId::from("UNKNOWN-UNKNOWN.VENUE");
254        assert!(store.find(&id).is_none());
255        assert!(!store.contains(&id));
256    }
257
258    #[rstest]
259    fn test_instrument_store_add_replaces_existing() {
260        let mut store = InstrumentStore::new();
261        let instrument1 = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
262        let instrument2 = InstrumentAny::CryptoPerpetual(crypto_perpetual_ethusdt());
263        let id = instrument1.id();
264
265        store.add(instrument1);
266        store.add(instrument2);
267
268        assert_eq!(store.count(), 1);
269        assert!(store.contains(&id));
270    }
271}