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