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