Skip to main content

nautilus_databento/
factories.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//! Factory functions for creating Databento clients and components.
17
18use std::{any::Any, cell::RefCell, fmt::Debug, path::PathBuf, rc::Rc};
19
20use indexmap::IndexMap;
21use nautilus_common::{
22    cache::CacheView,
23    clients::DataClient,
24    clock::Clock,
25    factories::{ClientConfig, DataClientFactory},
26};
27use nautilus_core::{
28    string::secret::REDACTED,
29    time::{AtomicTime, get_atomic_clock_realtime},
30};
31use nautilus_model::identifiers::ClientId;
32
33use crate::{
34    common::{Credential, DATABENTO},
35    data::{DatabentoDataClient, DatabentoDataClientConfig},
36    historical::DatabentoHistoricalClient,
37};
38
39/// Configuration for Databento data clients used with `LiveNode`.
40#[derive(Clone)]
41#[cfg_attr(
42    feature = "python",
43    pyo3::pyclass(
44        module = "nautilus_trader.core.nautilus_pyo3.databento",
45        from_py_object
46    )
47)]
48#[cfg_attr(
49    feature = "python",
50    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.databento")
51)]
52pub struct DatabentoLiveClientConfig {
53    /// Databento API credential.
54    credential: Credential,
55    /// Path to publishers.json file.
56    pub publishers_filepath: PathBuf,
57    /// Venue-to-dataset overrides applied on top of the mappings populated from Databento's
58    /// canonical publishers.json (keys are venue codes, values are dataset codes).
59    pub venue_dataset_map: IndexMap<String, String>,
60    /// Whether to use exchange as venue for GLBX instruments.
61    pub use_exchange_as_venue: bool,
62    /// Whether to timestamp bars on close.
63    pub bars_timestamp_on_close: bool,
64}
65
66impl Debug for DatabentoLiveClientConfig {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct(stringify!(DatabentoLiveClientConfig))
69            .field("credential", &REDACTED)
70            .field("publishers_filepath", &self.publishers_filepath)
71            .field("venue_dataset_map", &self.venue_dataset_map)
72            .field("use_exchange_as_venue", &self.use_exchange_as_venue)
73            .field("bars_timestamp_on_close", &self.bars_timestamp_on_close)
74            .finish()
75    }
76}
77
78impl DatabentoLiveClientConfig {
79    /// Creates a new [`DatabentoLiveClientConfig`] instance.
80    #[must_use]
81    pub fn new(
82        api_key: impl Into<String>,
83        publishers_filepath: PathBuf,
84        use_exchange_as_venue: bool,
85        bars_timestamp_on_close: bool,
86    ) -> Self {
87        Self {
88            credential: Credential::new(api_key),
89            publishers_filepath,
90            venue_dataset_map: IndexMap::new(),
91            use_exchange_as_venue,
92            bars_timestamp_on_close,
93        }
94    }
95
96    /// Returns the API key associated with this config.
97    #[must_use]
98    pub fn api_key(&self) -> &str {
99        self.credential.api_key()
100    }
101
102    /// Returns a masked version of the API key for logging purposes.
103    #[must_use]
104    pub fn api_key_masked(&self) -> String {
105        self.credential.api_key_masked()
106    }
107}
108
109impl ClientConfig for DatabentoLiveClientConfig {
110    fn as_any(&self) -> &dyn Any {
111        self
112    }
113}
114
115/// Factory for creating Databento data clients.
116#[derive(Debug, Clone)]
117#[cfg_attr(
118    feature = "python",
119    pyo3::pyclass(
120        module = "nautilus_trader.core.nautilus_pyo3.databento",
121        from_py_object
122    )
123)]
124#[cfg_attr(
125    feature = "python",
126    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.databento")
127)]
128pub struct DatabentoDataClientFactory;
129
130impl DatabentoDataClientFactory {
131    /// Creates a new [`DatabentoDataClientFactory`] instance.
132    #[must_use]
133    pub const fn new() -> Self {
134        Self
135    }
136
137    /// Creates a new [`DatabentoDataClient`] instance.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if the client cannot be created or publisher configuration cannot be loaded.
142    pub fn create_live_data_client(
143        client_id: ClientId,
144        api_key: impl Into<String>,
145        publishers_filepath: PathBuf,
146        use_exchange_as_venue: bool,
147        bars_timestamp_on_close: bool,
148        clock: &'static AtomicTime,
149    ) -> anyhow::Result<DatabentoDataClient> {
150        let config = DatabentoDataClientConfig::new(
151            api_key,
152            publishers_filepath,
153            use_exchange_as_venue,
154            bars_timestamp_on_close,
155        );
156
157        DatabentoDataClient::new(client_id, config, clock)
158    }
159
160    /// Creates a new [`DatabentoDataClient`] instance with a custom configuration.
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if the client cannot be created.
165    pub fn create_live_data_client_with_config(
166        client_id: ClientId,
167        config: DatabentoDataClientConfig,
168        clock: &'static AtomicTime,
169    ) -> anyhow::Result<DatabentoDataClient> {
170        DatabentoDataClient::new(client_id, config, clock)
171    }
172}
173
174impl Default for DatabentoDataClientFactory {
175    fn default() -> Self {
176        Self::new()
177    }
178}
179
180impl DataClientFactory for DatabentoDataClientFactory {
181    fn create(
182        &self,
183        name: &str,
184        config: &dyn ClientConfig,
185        _cache: CacheView,
186        _clock: Rc<RefCell<dyn Clock>>,
187    ) -> anyhow::Result<Box<dyn DataClient>> {
188        let databento_config = config
189            .as_any()
190            .downcast_ref::<DatabentoLiveClientConfig>()
191            .ok_or_else(|| {
192                anyhow::anyhow!(
193                    "Invalid config type for DatabentoDataClientFactory. Expected DatabentoLiveClientConfig, was {config:?}"
194                )
195            })?;
196
197        let client_id = ClientId::from(name);
198        let mut config = DatabentoDataClientConfig::new(
199            databento_config.api_key(),
200            databento_config.publishers_filepath.clone(),
201            databento_config.use_exchange_as_venue,
202            databento_config.bars_timestamp_on_close,
203        );
204        config.venue_dataset_map = databento_config.venue_dataset_map.clone();
205
206        let client = DatabentoDataClient::new(client_id, config, get_atomic_clock_realtime())?;
207        Ok(Box::new(client))
208    }
209
210    fn name(&self) -> &'static str {
211        DATABENTO
212    }
213
214    fn config_type(&self) -> &'static str {
215        "DatabentoLiveClientConfig"
216    }
217}
218
219/// Factory for creating Databento historical clients.
220#[derive(Debug)]
221pub struct DatabentoHistoricalClientFactory;
222
223impl DatabentoHistoricalClientFactory {
224    /// Creates a new [`DatabentoHistoricalClient`] instance.
225    ///
226    /// # Errors
227    ///
228    /// Returns an error if the client cannot be created or publisher configuration cannot be loaded.
229    pub fn create(
230        api_key: String,
231        publishers_filepath: PathBuf,
232        use_exchange_as_venue: bool,
233        clock: &'static AtomicTime,
234    ) -> anyhow::Result<DatabentoHistoricalClient> {
235        DatabentoHistoricalClient::new(
236            Credential::new(api_key),
237            publishers_filepath,
238            clock,
239            use_exchange_as_venue,
240        )
241    }
242}
243
244/// Builder for [`DatabentoDataClientConfig`].
245#[derive(Debug, Default)]
246pub struct DatabentoDataClientConfigBuilder {
247    api_key: Option<String>,
248    dataset: Option<String>,
249    publishers_filepath: Option<PathBuf>,
250    use_exchange_as_venue: bool,
251    bars_timestamp_on_close: bool,
252}
253
254impl DatabentoDataClientConfigBuilder {
255    /// Creates a new [`DatabentoDataClientConfigBuilder`].
256    #[must_use]
257    pub fn new() -> Self {
258        Self::default()
259    }
260
261    /// Sets the API key.
262    #[must_use]
263    pub fn api_key(mut self, api_key: String) -> Self {
264        self.api_key = Some(api_key);
265        self
266    }
267
268    /// Sets the dataset.
269    #[must_use]
270    pub fn dataset(mut self, dataset: String) -> Self {
271        self.dataset = Some(dataset);
272        self
273    }
274
275    /// Sets the publishers filepath.
276    #[must_use]
277    pub fn publishers_filepath(mut self, filepath: PathBuf) -> Self {
278        self.publishers_filepath = Some(filepath);
279        self
280    }
281
282    /// Sets whether to use exchange as venue.
283    #[must_use]
284    pub const fn use_exchange_as_venue(mut self, use_exchange: bool) -> Self {
285        self.use_exchange_as_venue = use_exchange;
286        self
287    }
288
289    /// Sets whether to timestamp bars on close.
290    #[must_use]
291    pub const fn bars_timestamp_on_close(mut self, timestamp_on_close: bool) -> Self {
292        self.bars_timestamp_on_close = timestamp_on_close;
293        self
294    }
295
296    /// Builds the [`DatabentoDataClientConfig`].
297    ///
298    /// # Errors
299    ///
300    /// Returns an error if required fields are missing.
301    pub fn build(self) -> anyhow::Result<DatabentoDataClientConfig> {
302        let api_key = self
303            .api_key
304            .ok_or_else(|| anyhow::anyhow!("API key is required"))?;
305        let publishers_filepath = self
306            .publishers_filepath
307            .ok_or_else(|| anyhow::anyhow!("Publishers filepath is required"))?;
308
309        Ok(DatabentoDataClientConfig::new(
310            api_key,
311            publishers_filepath,
312            self.use_exchange_as_venue,
313            self.bars_timestamp_on_close,
314        ))
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use nautilus_core::time::get_atomic_clock_realtime;
321    use rstest::rstest;
322
323    use super::*;
324
325    #[rstest]
326    fn test_config_builder() {
327        let config = DatabentoDataClientConfigBuilder::new()
328            .api_key("test_key".to_string())
329            .dataset("GLBX.MDP3".to_string())
330            .publishers_filepath(PathBuf::from("test_publishers.json"))
331            .use_exchange_as_venue(true)
332            .bars_timestamp_on_close(false)
333            .build();
334
335        assert!(config.is_ok());
336        let config = config.unwrap();
337        assert_eq!(config.api_key(), "test_key");
338        assert!(config.use_exchange_as_venue);
339        assert!(!config.bars_timestamp_on_close);
340    }
341
342    #[rstest]
343    fn test_config_builder_missing_required_fields() {
344        let config = DatabentoDataClientConfigBuilder::new()
345            .api_key("test_key".to_string())
346            // Missing dataset and publishers_filepath
347            .build();
348
349        assert!(config.is_err());
350    }
351
352    #[rstest]
353    fn test_historical_client_factory() {
354        let api_key = "test-000000000000000000000000000".to_string();
355        let publishers_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("publishers.json");
356        let clock = get_atomic_clock_realtime();
357
358        let result =
359            DatabentoHistoricalClientFactory::create(api_key, publishers_path, false, clock);
360
361        assert!(result.is_ok());
362    }
363
364    #[rstest]
365    fn test_live_data_client_factory_missing_publishers() {
366        let client_id = ClientId::from("DATABENTO-001");
367        let api_key = "test_key".to_string();
368        let publishers_path = PathBuf::from("nonexistent_publishers.json");
369        let clock = get_atomic_clock_realtime();
370
371        let result = DatabentoDataClientFactory::create_live_data_client(
372            client_id,
373            api_key,
374            publishers_path,
375            false,
376            true,
377            clock,
378        );
379
380        assert!(result.is_err());
381    }
382}