nautilus_databento/
factories.rs

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