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