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