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