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