1use std::{
22 path::PathBuf,
23 str::FromStr,
24 sync::{
25 Arc,
26 atomic::{AtomicBool, Ordering},
27 },
28 time::Duration,
29};
30
31use ahash::AHashMap;
32use databento::{dbn, live::Subscription};
33use indexmap::IndexMap;
34use nautilus_common::{
35 clients::DataClient,
36 live::{runner::get_data_event_sender, sender::EventSender},
37 messages::{
38 DataEvent, DataResponse,
39 data::{
40 BarsResponse, BookDeltasResponse, BookDepthResponse, InstrumentResponse,
41 InstrumentsResponse, QuotesResponse, RequestBars, RequestBookDeltas, RequestBookDepth,
42 RequestInstrument, RequestInstruments, RequestQuotes, RequestTrades, SubscribeBars,
43 SubscribeBookDeltas, SubscribeBookDepth, SubscribeCustomData, SubscribeInstrument,
44 SubscribeInstrumentStatus, SubscribeQuotes, SubscribeTrades, TradesResponse,
45 UnsubscribeBars, UnsubscribeBookDeltas, UnsubscribeBookDepth, UnsubscribeCustomData,
46 UnsubscribeInstrumentStatus, UnsubscribeQuotes, UnsubscribeTrades,
47 },
48 },
49};
50use nautilus_core::{
51 AtomicMap, DurationNanos, Params, UnixNanos,
52 datetime::datetime_to_unix_nanos,
53 time::{AtomicTime, get_atomic_clock_realtime},
54};
55use nautilus_live::task::TaskGroup;
56use nautilus_model::{
57 data::{BarType, CustomData, Data, DataType, custom::CustomDataTrait},
58 enums::{BarAggregation, PriceType},
59 identifiers::{ClientId, InstrumentId, Symbol, Venue},
60 instruments::{Instrument, InstrumentAny},
61};
62use parking_lot::Mutex;
63use tokio_util::sync::CancellationToken;
64
65use crate::{
66 common::{Credential, DATABENTO_VENUE, ohlcv_schema_from_aggregation},
67 historical::{DatabentoHistoricalClient, RangeQueryParams},
68 live::{DatabentoFeedHandler, DatabentoMessage, HandlerCommand},
69 loader::DatabentoDataLoader,
70 symbology::{infer_symbology_type, instrument_id_to_symbol_string},
71 types::{DatabentoImbalance, DatabentoStatistics, Dataset, PublisherId},
72};
73
74const PRICE_PRECISION_PARAM: &str = "price_precision";
75const SCHEMA_PARAM: &str = "schema";
76const MBP10_DEPTH: usize = 10;
77const QUOTE_SCHEMAS: &[dbn::Schema] = &[
78 dbn::Schema::Mbp1,
79 dbn::Schema::Bbo1S,
80 dbn::Schema::Bbo1M,
81 dbn::Schema::Cmbp1,
82 dbn::Schema::Cbbo1S,
83 dbn::Schema::Cbbo1M,
84 dbn::Schema::Tbbo,
85 dbn::Schema::Tcbbo,
86];
87const TRADE_SCHEMAS: &[dbn::Schema] = &[
88 dbn::Schema::Trades,
89 dbn::Schema::Tbbo,
90 dbn::Schema::Tcbbo,
91 dbn::Schema::Mbp1,
92 dbn::Schema::Cmbp1,
93];
94
95#[derive(Debug, Clone)]
97#[cfg_attr(
98 feature = "python",
99 pyo3::pyclass(module = "nautilus_trader.adapters.databento", from_py_object)
100)]
101#[cfg_attr(
102 feature = "python",
103 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.databento")
104)]
105pub struct DatabentoDataClientConfig {
106 pub(crate) credential: Credential,
108 pub publishers_filepath: PathBuf,
110 pub historical_base_url: Option<String>,
112 pub live_gateway_addr: Option<String>,
114 pub venue_dataset_map: IndexMap<String, String>,
116 pub use_exchange_as_venue: bool,
118 pub bars_timestamp_on_close: bool,
120 pub reconnect_timeout_mins: Option<u64>,
122}
123
124#[cfg(feature = "python")]
125nautilus_core::impl_pyo3_config_getters!(DatabentoDataClientConfig {
126 publishers_filepath: PathBuf,
127 historical_base_url: Option<String>,
128 live_gateway_addr: Option<String>,
129 use_exchange_as_venue: bool,
130 bars_timestamp_on_close: bool,
131 venue_dataset_map: IndexMap<String, String>,
132});
133
134impl DatabentoDataClientConfig {
135 #[must_use]
137 pub fn new(
138 api_key: impl Into<String>,
139 publishers_filepath: PathBuf,
140 use_exchange_as_venue: bool,
141 bars_timestamp_on_close: bool,
142 ) -> Self {
143 Self {
144 credential: Credential::new(api_key),
145 publishers_filepath,
146 venue_dataset_map: IndexMap::new(),
147 use_exchange_as_venue,
148 bars_timestamp_on_close,
149 reconnect_timeout_mins: Some(10), historical_base_url: None,
151 live_gateway_addr: None,
152 }
153 }
154
155 #[must_use]
157 pub fn api_key(&self) -> &str {
158 self.credential.api_key()
159 }
160
161 #[must_use]
163 pub fn api_key_masked(&self) -> String {
164 self.credential.api_key_masked()
165 }
166}
167
168#[cfg_attr(feature = "python", pyo3::pyclass)]
174#[cfg_attr(
175 feature = "python",
176 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.databento")
177)]
178#[derive(Debug)]
179pub struct DatabentoDataClient {
180 client_id: ClientId,
181 config: DatabentoDataClientConfig,
182 is_connected: AtomicBool,
183 historical: DatabentoHistoricalClient,
184 loader: DatabentoDataLoader,
185 cmd_channels: Arc<Mutex<AHashMap<String, tokio::sync::mpsc::UnboundedSender<HandlerCommand>>>>,
186 task_handles: TaskGroup,
187 cancellation_token: CancellationToken,
188 publisher_venue_map: Arc<IndexMap<PublisherId, Venue>>,
189 symbol_venue_map: Arc<AtomicMap<Symbol, Venue>>,
190 data_sender: EventSender<DataEvent>,
191}
192
193impl DatabentoDataClient {
194 pub fn new(
200 client_id: ClientId,
201 config: DatabentoDataClientConfig,
202 clock: &'static AtomicTime,
203 ) -> anyhow::Result<Self> {
204 let historical = match &config.historical_base_url {
205 Some(base_url) => DatabentoHistoricalClient::new_with_base_url(
206 config.credential.clone(),
207 config.publishers_filepath.clone(),
208 clock,
209 config.use_exchange_as_venue,
210 base_url,
211 )?,
212 None => DatabentoHistoricalClient::new(
213 config.credential.clone(),
214 config.publishers_filepath.clone(),
215 clock,
216 config.use_exchange_as_venue,
217 )?,
218 };
219
220 let mut loader = DatabentoDataLoader::new(Some(config.publishers_filepath.clone()))?;
222 for (venue, dataset) in &config.venue_dataset_map {
223 loader.set_dataset_for_venue(
224 Dataset::from(dataset.as_str()),
225 Venue::from(venue.as_str()),
226 );
227 }
228
229 let file_content = std::fs::read_to_string(&config.publishers_filepath)?;
231 let publishers_vec: Vec<crate::types::DatabentoPublisher> =
232 serde_json::from_str(&file_content)?;
233
234 let publisher_venue_map = publishers_vec
235 .into_iter()
236 .map(|p| (p.publisher_id, Venue::from(p.venue.as_str())))
237 .collect::<IndexMap<u16, Venue>>();
238
239 let data_sender = get_data_event_sender();
240
241 let task_handles = TaskGroup::new();
242
243 Ok(Self {
244 client_id,
245 config,
246 is_connected: AtomicBool::new(false),
247 historical,
248 loader,
249 cmd_channels: Arc::new(Mutex::new(AHashMap::new())),
250 cancellation_token: task_handles.cancellation_token(),
251 task_handles,
252 publisher_venue_map: Arc::new(publisher_venue_map),
253 symbol_venue_map: Arc::new(AtomicMap::new()),
254 data_sender,
255 })
256 }
257
258 #[must_use]
260 pub fn api_key(&self) -> &str {
261 self.config.api_key()
262 }
263
264 #[must_use]
266 pub fn api_key_masked(&self) -> String {
267 self.config.api_key_masked()
268 }
269
270 fn get_dataset_for_venue(&self, venue: Venue) -> anyhow::Result<String> {
276 self.loader
277 .get_dataset_for_venue(&venue)
278 .map(ToString::to_string)
279 .ok_or_else(|| anyhow::anyhow!("No dataset found for venue: {venue}"))
280 }
281
282 fn get_or_create_feed_handler(&self, dataset: &str) -> bool {
284 let mut channels = self.cmd_channels.lock();
285
286 if !channels.contains_key(dataset) {
287 log::debug!("Creating new feed handler for dataset: {dataset}");
288 let cmd_tx = self.initialize_live_feed(dataset.to_string());
289 channels.insert(dataset.to_string(), cmd_tx);
290
291 log::debug!("Feed handler created for dataset: {dataset}, channel stored");
292 return true;
293 }
294
295 false
296 }
297
298 fn send_subscription_to_dataset(
299 &self,
300 dataset: &str,
301 price_precision: Option<(Symbol, u8)>,
302 subscription: Subscription,
303 start_after_subscribe: bool,
304 ) -> anyhow::Result<()> {
305 let tx = {
306 let channels = self.cmd_channels.lock();
307 channels
308 .get(dataset)
309 .cloned()
310 .ok_or_else(|| anyhow::anyhow!("No feed handler found for dataset: {dataset}"))?
311 };
312
313 send_subscription_commands(
314 &tx,
315 dataset,
316 price_precision,
317 subscription,
318 start_after_subscribe,
319 )
320 }
321
322 fn subscribe_schema(
323 &self,
324 instrument_id: InstrumentId,
325 schema: dbn::Schema,
326 ) -> anyhow::Result<()> {
327 let symbol = instrument_id.symbol.to_string();
328 anyhow::ensure!(
331 infer_symbology_type(&symbol) == dbn::SType::RawSymbol,
332 "Unsupported symbol {symbol} for a Databento {schema} subscription, only raw symbols \
333 are available (not continuous or parent symbols)",
334 );
335
336 let dataset = self.get_dataset_for_venue(instrument_id.venue)?;
337 let start_after_subscribe = self.get_or_create_feed_handler(&dataset);
338
339 self.symbol_venue_map
340 .insert(instrument_id.symbol, instrument_id.venue);
341
342 let subscription = Subscription::builder()
343 .schema(schema)
344 .symbols(symbol)
345 .build();
346
347 self.send_subscription_to_dataset(&dataset, None, subscription, start_after_subscribe)
348 }
349
350 fn send_close_to_active_feeds(&self) {
351 let channels = self.cmd_channels.lock();
352 for (dataset, tx) in channels.iter() {
353 if let Err(e) = tx.send(HandlerCommand::Close) {
354 log::warn!("Failed to send close command to dataset {dataset}: {e}");
355 }
356 }
357 }
358
359 fn clear_feed_channels(&self) {
360 let mut channels = self.cmd_channels.lock();
361 channels.clear();
362 }
363
364 fn abort_active_tasks(&self) {
365 self.task_handles.begin_shutdown();
366 }
367
368 fn spawn_task<F>(&self, future: F)
369 where
370 F: std::future::Future<Output = ()> + Send + 'static,
371 {
372 if let Err(e) = self.task_handles.spawn(future) {
373 log::debug!("Skipping Databento task after shutdown began: {e}");
374 }
375 }
376
377 fn initialize_live_feed(
379 &self,
380 dataset: String,
381 ) -> tokio::sync::mpsc::UnboundedSender<HandlerCommand> {
382 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
383 let (msg_tx, msg_rx) = tokio::sync::mpsc::unbounded_channel();
384 let feed_dataset = dataset.clone();
385 let feed_channels = self.cmd_channels.clone();
386
387 let mut feed_handler = DatabentoFeedHandler::new(
388 self.config.credential.clone(),
389 dataset,
390 cmd_rx,
391 msg_tx,
392 (*self.publisher_venue_map).clone(),
393 self.symbol_venue_map.clone(),
394 self.config.use_exchange_as_venue,
395 self.config.bars_timestamp_on_close,
396 self.config.reconnect_timeout_mins,
397 );
398
399 if let Some(addr) = &self.config.live_gateway_addr {
400 feed_handler = feed_handler.with_gateway_addr(addr.clone());
401 }
402
403 let feed_future = async move {
404 let _ = feed_handler.run().await;
406 feed_channels.lock().remove(&feed_dataset);
407 };
408
409 let cancellation_token = self.cancellation_token.clone();
410 let data_sender = self.data_sender.clone();
411
412 let msg_future = async move {
414 let mut msg_rx = msg_rx;
415
416 loop {
417 tokio::select! {
418 msg = msg_rx.recv() => {
419 match msg {
420 Some(DatabentoMessage::Data(data)) => {
421 log::debug!("Received data: {data:?}");
422 if let Err(e) = data_sender.send(DataEvent::Data(data)) {
423 log::error!("Failed to send data event: {e}");
424 }
425 }
426 Some(DatabentoMessage::Instrument(instrument)) => {
427 log::debug!("Received instrument definition: {}", instrument.id());
428 if let Err(e) = data_sender.send(DataEvent::Instrument(*instrument)) {
429 log::error!("Failed to send instrument: {e}");
430 }
431 }
432 Some(DatabentoMessage::Status(status)) => {
433 log::debug!("Received status: {status:?}");
434 if let Err(e) =
435 data_sender.send(DataEvent::Data(Data::InstrumentStatus(status)))
436 {
437 log::error!("Failed to send status data event: {e}");
438 }
439 }
440 Some(DatabentoMessage::Imbalance(imbalance)) => {
441 log::debug!("Received imbalance: {imbalance:?}");
442 let instrument_id = imbalance.instrument_id;
443 let data = Data::Custom(custom_data_for_instrument(Arc::new(imbalance), instrument_id));
444 if let Err(e) = data_sender.send(DataEvent::Data(data)) {
445 log::error!("Failed to send imbalance data event: {e}");
446 }
447 }
448 Some(DatabentoMessage::Statistics(statistics)) => {
449 log::debug!("Received statistics: {statistics:?}");
450 let instrument_id = statistics.instrument_id;
451 let data = Data::Custom(custom_data_for_instrument(Arc::new(statistics), instrument_id));
452 if let Err(e) = data_sender.send(DataEvent::Data(data)) {
453 log::error!("Failed to send statistics data event: {e}");
454 }
455 }
456 Some(DatabentoMessage::SubscriptionAck(ack)) => {
457 log::debug!("Received subscription ack: {}", ack.message);
458 }
459 Some(DatabentoMessage::Error(error)) => {
460 log::error!("Feed handler error: {error}");
461 }
462 Some(DatabentoMessage::Close) => {
463 log::debug!("Feed handler closed");
464 break;
465 }
466 None => {
467 log::debug!("Message channel closed");
468 break;
469 }
470 }
471 }
472 () = cancellation_token.cancelled() => {
473 log::debug!("Message processing cancelled");
474 break;
475 }
476 }
477 }
478 };
479
480 if let Err(e) = self.task_handles.spawn(feed_future) {
481 log::warn!("Skipping Databento feed task after shutdown began: {e}");
482 }
483
484 if let Err(e) = self.task_handles.spawn(msg_future) {
485 log::warn!("Skipping Databento message task after shutdown began: {e}");
486 }
487
488 cmd_tx
489 }
490}
491
492#[async_trait::async_trait(?Send)]
493impl DataClient for DatabentoDataClient {
494 fn client_id(&self) -> ClientId {
496 self.client_id
497 }
498
499 fn venue(&self) -> Option<Venue> {
501 None
502 }
503
504 fn start(&mut self) -> anyhow::Result<()> {
510 log::debug!("Starting");
511 Ok(())
512 }
513
514 fn stop(&mut self) -> anyhow::Result<()> {
520 log::debug!("Stopping");
521
522 self.send_close_to_active_feeds();
523 self.clear_feed_channels();
524 self.cancellation_token.cancel();
525 self.abort_active_tasks();
526 self.is_connected.store(false, Ordering::Relaxed);
527
528 Ok(())
529 }
530
531 fn reset(&mut self) -> anyhow::Result<()> {
532 log::debug!("Resetting");
533 self.send_close_to_active_feeds();
534 self.clear_feed_channels();
535 self.abort_active_tasks();
536 self.is_connected.store(false, Ordering::Relaxed);
537 Ok(())
538 }
539
540 fn dispose(&mut self) -> anyhow::Result<()> {
541 log::debug!("Disposing");
542 self.stop()
543 }
544
545 async fn connect(&mut self) -> anyhow::Result<()> {
546 log::debug!("Connecting...");
547
548 if !self.task_handles.is_open() {
549 self.task_handles
550 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(2))
551 .await
552 .map_err(|e| anyhow::anyhow!("Failed to terminate Databento tasks: {e}"))?;
553 self.task_handles
554 .start_generation()
555 .map_err(|e| anyhow::anyhow!("Failed to start Databento task generation: {e}"))?;
556 self.cancellation_token = self.task_handles.cancellation_token();
557 }
558
559 self.is_connected.store(true, Ordering::Relaxed);
560
561 log::info!("Connected");
562 Ok(())
563 }
564
565 async fn disconnect(&mut self) -> anyhow::Result<()> {
566 log::debug!("Disconnecting...");
567
568 self.send_close_to_active_feeds();
569 self.clear_feed_channels();
570 self.task_handles.begin_shutdown();
571
572 let tasks_result = self
573 .task_handles
574 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(2))
575 .await
576 .map_err(|e| anyhow::anyhow!("Failed to terminate Databento tasks: {e}"));
577
578 self.is_connected.store(false, Ordering::Relaxed);
579
580 log::info!("Disconnected");
581 tasks_result
582 }
583
584 fn is_connected(&self) -> bool {
586 self.is_connected.load(Ordering::Relaxed)
587 }
588
589 fn is_disconnected(&self) -> bool {
590 !self.is_connected()
591 }
592
593 fn subscribe_instrument(&mut self, cmd: SubscribeInstrument) -> anyhow::Result<()> {
599 let dataset = self.get_dataset_for_venue(cmd.instrument_id.venue)?;
600 let start_after_subscribe = self.get_or_create_feed_handler(&dataset);
601
602 self.symbol_venue_map
603 .insert(cmd.instrument_id.symbol, cmd.instrument_id.venue);
604 let symbol = cmd.instrument_id.symbol.to_string();
605
606 let subscription = Subscription::builder()
607 .schema(databento::dbn::Schema::Definition)
608 .symbols(symbol)
609 .build();
610
611 self.send_subscription_to_dataset(&dataset, None, subscription, start_after_subscribe)?;
612
613 Ok(())
614 }
615
616 fn subscribe_quotes(&mut self, cmd: SubscribeQuotes) -> anyhow::Result<()> {
622 let dataset = self.get_dataset_for_venue(cmd.instrument_id.venue)?;
623 let symbol = cmd.instrument_id.symbol.to_string();
624 let price_precision = price_precision_from_params(cmd.params.as_ref())?
625 .map(|precision| (cmd.instrument_id.symbol, precision));
626 let schema = schema_from_params(cmd.params.as_ref(), dbn::Schema::Mbp1, QUOTE_SCHEMAS)?;
627
628 let subscription = Subscription::builder()
629 .schema(schema)
630 .symbols(symbol)
631 .build();
632
633 let start_after_subscribe = self.get_or_create_feed_handler(&dataset);
634 self.symbol_venue_map
635 .insert(cmd.instrument_id.symbol, cmd.instrument_id.venue);
636
637 self.send_subscription_to_dataset(
638 &dataset,
639 price_precision,
640 subscription,
641 start_after_subscribe,
642 )?;
643
644 Ok(())
645 }
646
647 fn subscribe_trades(&mut self, cmd: SubscribeTrades) -> anyhow::Result<()> {
653 let dataset = self.get_dataset_for_venue(cmd.instrument_id.venue)?;
654 let symbol = cmd.instrument_id.symbol.to_string();
655 let price_precision = price_precision_from_params(cmd.params.as_ref())?
656 .map(|precision| (cmd.instrument_id.symbol, precision));
657 let schema = schema_from_params(cmd.params.as_ref(), dbn::Schema::Trades, TRADE_SCHEMAS)?;
658
659 let subscription = Subscription::builder()
660 .schema(schema)
661 .symbols(symbol)
662 .build();
663
664 let start_after_subscribe = self.get_or_create_feed_handler(&dataset);
665 self.symbol_venue_map
666 .insert(cmd.instrument_id.symbol, cmd.instrument_id.venue);
667
668 self.send_subscription_to_dataset(
669 &dataset,
670 price_precision,
671 subscription,
672 start_after_subscribe,
673 )?;
674
675 Ok(())
676 }
677
678 fn subscribe_book_deltas(&mut self, cmd: SubscribeBookDeltas) -> anyhow::Result<()> {
684 let dataset = self.get_dataset_for_venue(cmd.instrument_id.venue)?;
685 let start_after_subscribe = self.get_or_create_feed_handler(&dataset);
686
687 self.symbol_venue_map
688 .insert(cmd.instrument_id.symbol, cmd.instrument_id.venue);
689 let symbol = cmd.instrument_id.symbol.to_string();
690
691 let subscription = Subscription::builder()
692 .schema(databento::dbn::Schema::Mbo) .symbols(symbol)
694 .build();
695
696 self.send_subscription_to_dataset(&dataset, None, subscription, start_after_subscribe)?;
697
698 Ok(())
699 }
700
701 fn subscribe(&mut self, cmd: SubscribeCustomData) -> anyhow::Result<()> {
709 let schema = custom_data_schema(&cmd.data_type)?;
710 let instrument_id = custom_data_instrument_id(&cmd.data_type)?;
711 self.subscribe_schema(instrument_id, schema)
712 }
713
714 fn subscribe_book_depth(&mut self, cmd: SubscribeBookDepth) -> anyhow::Result<()> {
721 if let Some(depth) = cmd.depth {
722 anyhow::ensure!(
723 depth.get() == MBP10_DEPTH,
724 "Unsupported book depth {depth} for Databento `mbp-10`, only a depth of {MBP10_DEPTH} is available",
725 );
726 }
727
728 self.subscribe_schema(cmd.instrument_id, dbn::Schema::Mbp10)
729 }
730
731 fn subscribe_bars(&mut self, cmd: SubscribeBars) -> anyhow::Result<()> {
738 let schema = ohlcv_schema(cmd.bar_type)?;
739 self.subscribe_schema(cmd.bar_type.instrument_id(), schema)
740 }
741
742 fn subscribe_instrument_status(
748 &mut self,
749 cmd: SubscribeInstrumentStatus,
750 ) -> anyhow::Result<()> {
751 let dataset = self.get_dataset_for_venue(cmd.instrument_id.venue)?;
752 let start_after_subscribe = self.get_or_create_feed_handler(&dataset);
753
754 self.symbol_venue_map
755 .insert(cmd.instrument_id.symbol, cmd.instrument_id.venue);
756 let symbol = cmd.instrument_id.symbol.to_string();
757
758 let subscription = Subscription::builder()
759 .schema(databento::dbn::Schema::Status)
760 .symbols(symbol)
761 .build();
762
763 self.send_subscription_to_dataset(&dataset, None, subscription, start_after_subscribe)?;
764
765 Ok(())
766 }
767
768 fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
770 log::debug!(
774 "Databento does not support granular unsubscribing - ignoring unsubscribe request for {}",
775 cmd.instrument_id
776 );
777
778 Ok(())
779 }
780
781 fn unsubscribe_trades(&mut self, cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
782 log::debug!(
786 "Databento does not support granular unsubscribing - ignoring unsubscribe request for {}",
787 cmd.instrument_id
788 );
789
790 Ok(())
791 }
792
793 fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
794 log::debug!(
798 "Databento does not support granular unsubscribing - ignoring unsubscribe request for {}",
799 cmd.instrument_id
800 );
801
802 Ok(())
803 }
804
805 fn unsubscribe(&mut self, cmd: &UnsubscribeCustomData) -> anyhow::Result<()> {
806 log::debug!(
810 "Databento does not support granular unsubscribing - ignoring unsubscribe request for {}",
811 cmd.data_type
812 );
813
814 Ok(())
815 }
816
817 fn unsubscribe_book_depth(&mut self, cmd: &UnsubscribeBookDepth) -> anyhow::Result<()> {
818 log::debug!(
822 "Databento does not support granular unsubscribing - ignoring unsubscribe request for {}",
823 cmd.instrument_id
824 );
825
826 Ok(())
827 }
828
829 fn unsubscribe_bars(&mut self, cmd: &UnsubscribeBars) -> anyhow::Result<()> {
830 log::debug!(
834 "Databento does not support granular unsubscribing - ignoring unsubscribe request for {}",
835 cmd.bar_type
836 );
837
838 Ok(())
839 }
840
841 fn unsubscribe_instrument_status(
842 &mut self,
843 cmd: &UnsubscribeInstrumentStatus,
844 ) -> anyhow::Result<()> {
845 log::debug!(
849 "Databento does not support granular unsubscribing - ignoring unsubscribe request for {}",
850 cmd.instrument_id
851 );
852
853 Ok(())
854 }
855
856 fn request_instruments(&self, request: RequestInstruments) -> anyhow::Result<()> {
857 log::debug!("Request instruments: {request:?}");
858
859 let historical_client = self.historical.clone();
860 let data_sender = self.data_sender.clone();
861 let dataset = request
862 .venue
863 .map(|venue| self.get_dataset_for_venue(venue))
864 .transpose()?
865 .unwrap_or_else(|| "GLBX.MDP3".to_string());
866 let request_id = request.request_id;
867 let client_id = request.client_id.unwrap_or(self.client_id);
868 let venue = request.venue.unwrap_or(*DATABENTO_VENUE);
869 let start_nanos = datetime_to_unix_nanos(request.start);
870 let end_nanos = datetime_to_unix_nanos(request.end);
871 let request_params = request.params;
872 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
873
874 self.spawn_task(async move {
875 let query_params = instruments_query_params(dataset, query_start, query_end);
876
877 match historical_client.get_range_instruments(query_params).await {
878 Ok(instruments) => {
879 log::debug!("Retrieved {} instruments", instruments.len());
880
881 let response = DataResponse::Instruments(InstrumentsResponse::new(
882 request_id,
883 client_id,
884 venue,
885 instruments,
886 start_nanos,
887 end_nanos,
888 get_atomic_clock_realtime().get_time_ns(),
889 request_params,
890 ));
891
892 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
893 log::error!("Failed to send instruments response: {e}");
894 }
895 }
896 Err(e) => {
897 log::error!("Failed to request instruments: {e}");
898 let response = DataResponse::Instruments(InstrumentsResponse::new(
899 request_id,
900 client_id,
901 venue,
902 Vec::new(),
903 start_nanos,
904 end_nanos,
905 get_atomic_clock_realtime().get_time_ns(),
906 request_params,
907 ));
908
909 send_data_response(&data_sender, response, "empty instruments");
910 }
911 }
912 });
913
914 Ok(())
915 }
916
917 fn request_instrument(&self, request: RequestInstrument) -> anyhow::Result<()> {
918 log::debug!("Request instrument: {request:?}");
919
920 let dataset = self.get_dataset_for_venue(request.instrument_id.venue)?;
921 let historical_client = self.historical.clone();
922 let data_sender = self.data_sender.clone();
923 let instrument_id = request.instrument_id;
924 let request_id = request.request_id;
925 let client_id = request.client_id.unwrap_or(self.client_id);
926 let start_nanos = datetime_to_unix_nanos(request.start);
927 let end_nanos = datetime_to_unix_nanos(request.end);
928 let request_params = request.params;
929 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
930
931 self.spawn_task(async move {
932 let query_params =
933 instrument_query_params(dataset, instrument_id, query_start, query_end);
934
935 match historical_client.get_range_instruments(query_params).await {
936 Ok(instruments) => {
937 let instrument = requested_instrument(instruments, instrument_id);
938
939 let Some(instrument) = instrument else {
940 log::error!("Instrument not found: {instrument_id}");
941 return;
942 };
943
944 let response = DataResponse::Instrument(Box::new(InstrumentResponse::new(
945 request_id,
946 client_id,
947 instrument.id(),
948 instrument,
949 start_nanos,
950 end_nanos,
951 get_atomic_clock_realtime().get_time_ns(),
952 request_params,
953 )));
954
955 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
956 log::error!("Failed to send instrument response: {e}");
957 }
958 }
959 Err(e) => {
960 log::error!("Failed to request instrument {instrument_id}: {e}");
961 }
962 }
963 });
964
965 Ok(())
966 }
967
968 fn request_quotes(&self, request: RequestQuotes) -> anyhow::Result<()> {
969 log::debug!("Request quotes: {request:?}");
970
971 let historical_client = self.historical.clone();
972 let data_sender = self.data_sender.clone();
973 let dataset = self.get_dataset_for_venue(request.instrument_id.venue)?;
974 let instrument_id = request.instrument_id;
975 let symbols = historical_client.prepare_symbols_from_instrument_ids(&[instrument_id]);
976 let request_id = request.request_id;
977 let client_id = request.client_id.unwrap_or(self.client_id);
978 let start_nanos = datetime_to_unix_nanos(request.start);
979 let end_nanos = datetime_to_unix_nanos(request.end);
980 let limit = request.limit.map(|limit| limit.get() as u64);
981 let request_params = request.params;
982 let price_precision = price_precision_from_params(request_params.as_ref())?;
983 let schema = schema_from_params(request_params.as_ref(), dbn::Schema::Mbp1, QUOTE_SCHEMAS)?
984 .to_string();
985 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
986
987 self.spawn_task(async move {
988 seed_price_precision_if_needed(
989 &historical_client,
990 dataset.as_str(),
991 instrument_id,
992 query_start,
993 query_end,
994 price_precision,
995 )
996 .await;
997
998 let params = RangeQueryParams {
999 dataset,
1000 symbols,
1001 start: query_start,
1002 end: query_end,
1003 limit,
1004 price_precision,
1005 };
1006
1007 match historical_client
1008 .get_range_quotes(params, Some(schema))
1009 .await
1010 {
1011 Ok(quotes) => {
1012 log::debug!("Retrieved {} quotes", quotes.len());
1013 let response = DataResponse::Quotes(QuotesResponse::new(
1014 request_id,
1015 client_id,
1016 instrument_id,
1017 quotes,
1018 start_nanos,
1019 end_nanos,
1020 get_atomic_clock_realtime().get_time_ns(),
1021 request_params,
1022 ));
1023
1024 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
1025 log::error!("Failed to send quotes response: {e}");
1026 }
1027 }
1028 Err(e) => {
1029 log::error!("Failed to request quotes: {e}");
1030 let response = DataResponse::Quotes(QuotesResponse::new(
1031 request_id,
1032 client_id,
1033 instrument_id,
1034 Vec::new(),
1035 start_nanos,
1036 end_nanos,
1037 get_atomic_clock_realtime().get_time_ns(),
1038 request_params,
1039 ));
1040
1041 send_data_response(&data_sender, response, "empty quotes");
1042 }
1043 }
1044 });
1045
1046 Ok(())
1047 }
1048
1049 fn request_trades(&self, request: RequestTrades) -> anyhow::Result<()> {
1050 log::debug!("Request trades: {request:?}");
1051
1052 let historical_client = self.historical.clone();
1053 let data_sender = self.data_sender.clone();
1054 let dataset = self.get_dataset_for_venue(request.instrument_id.venue)?;
1055 let instrument_id = request.instrument_id;
1056 let symbols = historical_client.prepare_symbols_from_instrument_ids(&[instrument_id]);
1057 let request_id = request.request_id;
1058 let client_id = request.client_id.unwrap_or(self.client_id);
1059 let start_nanos = datetime_to_unix_nanos(request.start);
1060 let end_nanos = datetime_to_unix_nanos(request.end);
1061 let limit = request.limit.map(|limit| limit.get() as u64);
1062 let request_params = request.params;
1063 let price_precision = price_precision_from_params(request_params.as_ref())?;
1064 let schema =
1065 schema_from_params(request_params.as_ref(), dbn::Schema::Trades, TRADE_SCHEMAS)?
1066 .to_string();
1067 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
1068
1069 self.spawn_task(async move {
1070 seed_price_precision_if_needed(
1071 &historical_client,
1072 dataset.as_str(),
1073 instrument_id,
1074 query_start,
1075 query_end,
1076 price_precision,
1077 )
1078 .await;
1079
1080 let params = RangeQueryParams {
1081 dataset,
1082 symbols,
1083 start: query_start,
1084 end: query_end,
1085 limit,
1086 price_precision,
1087 };
1088
1089 match historical_client
1090 .get_range_trades(params, Some(schema))
1091 .await
1092 {
1093 Ok(trades) => {
1094 log::debug!("Retrieved {} trades", trades.len());
1095 let response = DataResponse::Trades(TradesResponse::new(
1096 request_id,
1097 client_id,
1098 instrument_id,
1099 trades,
1100 start_nanos,
1101 end_nanos,
1102 get_atomic_clock_realtime().get_time_ns(),
1103 request_params,
1104 ));
1105
1106 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
1107 log::error!("Failed to send trades response: {e}");
1108 }
1109 }
1110 Err(e) => {
1111 log::error!("Failed to request trades: {e}");
1112 let response = DataResponse::Trades(TradesResponse::new(
1113 request_id,
1114 client_id,
1115 instrument_id,
1116 Vec::new(),
1117 start_nanos,
1118 end_nanos,
1119 get_atomic_clock_realtime().get_time_ns(),
1120 request_params,
1121 ));
1122
1123 send_data_response(&data_sender, response, "empty trades");
1124 }
1125 }
1126 });
1127
1128 Ok(())
1129 }
1130
1131 fn request_bars(&self, request: RequestBars) -> anyhow::Result<()> {
1132 log::debug!("Request bars: {request:?}");
1133
1134 let historical_client = self.historical.clone();
1135 let data_sender = self.data_sender.clone();
1136 let instrument_id = request.bar_type.instrument_id();
1137 let dataset = self.get_dataset_for_venue(instrument_id.venue)?;
1138 let symbols = historical_client.prepare_symbols_from_instrument_ids(&[instrument_id]);
1139 let request_id = request.request_id;
1140 let client_id = request.client_id.unwrap_or(self.client_id);
1141 let bar_type = request.bar_type;
1142 let start_nanos = datetime_to_unix_nanos(request.start);
1143 let end_nanos = datetime_to_unix_nanos(request.end);
1144 let limit = request.limit.map(|limit| limit.get() as u64);
1145 let request_params = request.params;
1146 let price_precision = price_precision_from_params(request_params.as_ref())?;
1147 let timestamp_on_close = self.config.bars_timestamp_on_close;
1148 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
1149
1150 self.spawn_task(async move {
1151 seed_price_precision_if_needed(
1152 &historical_client,
1153 dataset.as_str(),
1154 instrument_id,
1155 query_start,
1156 query_end,
1157 price_precision,
1158 )
1159 .await;
1160
1161 let params = RangeQueryParams {
1162 dataset,
1163 symbols,
1164 start: query_start,
1165 end: query_end,
1166 limit,
1167 price_precision,
1168 };
1169
1170 let aggregation = match bar_type.spec().aggregation {
1171 BarAggregation::Second => BarAggregation::Second,
1172 BarAggregation::Minute => BarAggregation::Minute,
1173 BarAggregation::Hour => BarAggregation::Hour,
1174 BarAggregation::Day => BarAggregation::Day,
1175 _ => {
1176 log::error!(
1177 "Unsupported bar aggregation: {:?}",
1178 bar_type.spec().aggregation
1179 );
1180 let response = DataResponse::Bars(BarsResponse::new(
1181 request_id,
1182 client_id,
1183 bar_type,
1184 Vec::new(),
1185 start_nanos,
1186 end_nanos,
1187 get_atomic_clock_realtime().get_time_ns(),
1188 request_params,
1189 ));
1190
1191 send_data_response(&data_sender, response, "empty bars");
1192 return;
1193 }
1194 };
1195
1196 match historical_client
1197 .get_range_bars(params, aggregation, timestamp_on_close)
1198 .await
1199 {
1200 Ok(bars) => {
1201 log::debug!("Retrieved {} bars", bars.len());
1202 let response = DataResponse::Bars(BarsResponse::new(
1203 request_id,
1204 client_id,
1205 bar_type,
1206 bars,
1207 start_nanos,
1208 end_nanos,
1209 get_atomic_clock_realtime().get_time_ns(),
1210 request_params,
1211 ));
1212
1213 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
1214 log::error!("Failed to send bars response: {e}");
1215 }
1216 }
1217 Err(e) => {
1218 log::error!("Failed to request bars: {e}");
1219 let response = DataResponse::Bars(BarsResponse::new(
1220 request_id,
1221 client_id,
1222 bar_type,
1223 Vec::new(),
1224 start_nanos,
1225 end_nanos,
1226 get_atomic_clock_realtime().get_time_ns(),
1227 request_params,
1228 ));
1229
1230 send_data_response(&data_sender, response, "empty bars");
1231 }
1232 }
1233 });
1234
1235 Ok(())
1236 }
1237
1238 fn request_book_depth(&self, request: RequestBookDepth) -> anyhow::Result<()> {
1239 log::debug!("Request book depth: {request:?}");
1240
1241 let historical_client = self.historical.clone();
1242 let data_sender = self.data_sender.clone();
1243 let dataset = self.get_dataset_for_venue(request.instrument_id.venue)?;
1244 let instrument_id = request.instrument_id;
1245 let symbols = historical_client.prepare_symbols_from_instrument_ids(&[instrument_id]);
1246 let request_id = request.request_id;
1247 let client_id = request.client_id.unwrap_or(self.client_id);
1248 let start_nanos = datetime_to_unix_nanos(request.start);
1249 let end_nanos = datetime_to_unix_nanos(request.end);
1250 let limit = request.limit.map(|limit| limit.get() as u64);
1251 let depth = request.depth.map(|depth| depth.get());
1252 let request_params = request.params;
1253 let price_precision = price_precision_from_params(request_params.as_ref())?;
1254 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
1255
1256 self.spawn_task(async move {
1257 seed_price_precision_if_needed(
1258 &historical_client,
1259 dataset.as_str(),
1260 instrument_id,
1261 query_start,
1262 query_end,
1263 price_precision,
1264 )
1265 .await;
1266
1267 let params = RangeQueryParams {
1268 dataset,
1269 symbols,
1270 start: query_start,
1271 end: query_end,
1272 limit,
1273 price_precision,
1274 };
1275
1276 match historical_client
1277 .get_range_order_book_depth(params, depth)
1278 .await
1279 {
1280 Ok(depths) => {
1281 log::debug!("Retrieved {} order book depths", depths.len());
1282 let response = DataResponse::BookDepth(BookDepthResponse::new(
1283 request_id,
1284 client_id,
1285 instrument_id,
1286 depths,
1287 start_nanos,
1288 end_nanos,
1289 get_atomic_clock_realtime().get_time_ns(),
1290 request_params,
1291 ));
1292
1293 send_data_response(&data_sender, response, "book depth");
1294 }
1295 Err(e) => {
1296 log::error!("Failed to request order book depths: {e}");
1297 let response = DataResponse::BookDepth(BookDepthResponse::new(
1298 request_id,
1299 client_id,
1300 instrument_id,
1301 Vec::new(),
1302 start_nanos,
1303 end_nanos,
1304 get_atomic_clock_realtime().get_time_ns(),
1305 request_params,
1306 ));
1307
1308 send_data_response(&data_sender, response, "empty book depth");
1309 }
1310 }
1311 });
1312
1313 Ok(())
1314 }
1315
1316 fn request_book_deltas(&self, request: RequestBookDeltas) -> anyhow::Result<()> {
1317 log::debug!("Request book deltas: {request:?}");
1318
1319 let historical_client = self.historical.clone();
1320 let data_sender = self.data_sender.clone();
1321 let dataset = self.get_dataset_for_venue(request.instrument_id.venue)?;
1322 let instrument_id = request.instrument_id;
1323 let symbols = historical_client.prepare_symbols_from_instrument_ids(&[instrument_id]);
1324 let request_id = request.request_id;
1325 let client_id = request.client_id.unwrap_or(self.client_id);
1326 let start_nanos = datetime_to_unix_nanos(request.start);
1327 let end_nanos = datetime_to_unix_nanos(request.end);
1328 let limit = request.limit.map(|limit| limit.get() as u64);
1329 let request_params = request.params;
1330 let price_precision = price_precision_from_params(request_params.as_ref())?;
1331 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
1332
1333 self.spawn_task(async move {
1334 seed_price_precision_if_needed(
1335 &historical_client,
1336 dataset.as_str(),
1337 instrument_id,
1338 query_start,
1339 query_end,
1340 price_precision,
1341 )
1342 .await;
1343
1344 let params = RangeQueryParams {
1345 dataset,
1346 symbols,
1347 start: query_start,
1348 end: query_end,
1349 limit,
1350 price_precision,
1351 };
1352
1353 match historical_client.get_range_order_book_deltas(params).await {
1354 Ok(deltas) => {
1355 log::debug!("Retrieved {} order book deltas", deltas.len());
1356 let response = BookDeltasResponse::new(
1357 request_id,
1358 client_id,
1359 instrument_id,
1360 deltas,
1361 start_nanos,
1362 end_nanos,
1363 get_atomic_clock_realtime().get_time_ns(),
1364 request_params,
1365 );
1366
1367 for response in partition_book_deltas_response(response) {
1368 send_data_response(
1369 &data_sender,
1370 DataResponse::BookDeltas(response),
1371 "book deltas",
1372 );
1373 }
1374 }
1375 Err(e) => {
1376 log::error!("Failed to request order book deltas: {e}");
1377 let response = DataResponse::BookDeltas(BookDeltasResponse::new(
1378 request_id,
1379 client_id,
1380 instrument_id,
1381 Vec::new(),
1382 start_nanos,
1383 end_nanos,
1384 get_atomic_clock_realtime().get_time_ns(),
1385 request_params,
1386 ));
1387
1388 send_data_response(&data_sender, response, "empty book deltas");
1389 }
1390 }
1391 });
1392
1393 Ok(())
1394 }
1395}
1396
1397fn instruments_query_params(
1398 dataset: String,
1399 start_nanos: UnixNanos,
1400 end_nanos: Option<UnixNanos>,
1401) -> RangeQueryParams {
1402 RangeQueryParams {
1403 dataset,
1404 symbols: vec!["ALL_SYMBOLS".to_string()],
1405 start: start_nanos,
1406 end: end_nanos,
1407 limit: None,
1408 price_precision: None,
1409 }
1410}
1411
1412fn instrument_query_params(
1413 dataset: String,
1414 instrument_id: InstrumentId,
1415 start_nanos: UnixNanos,
1416 end_nanos: Option<UnixNanos>,
1417) -> RangeQueryParams {
1418 RangeQueryParams {
1419 dataset,
1420 symbols: vec![instrument_id_to_symbol_string(
1421 instrument_id,
1422 &mut AHashMap::new(),
1423 )],
1424 start: start_nanos,
1425 end: end_nanos,
1426 limit: None,
1427 price_precision: None,
1428 }
1429}
1430
1431fn resolve_request_time_range(
1432 start_nanos: Option<UnixNanos>,
1433 end_nanos: Option<UnixNanos>,
1434) -> (UnixNanos, Option<UnixNanos>) {
1435 let mut end = end_nanos.unwrap_or_else(|| get_atomic_clock_realtime().get_time_ns());
1436 let mut start = start_nanos.unwrap_or_else(|| start_of_utc_day(end));
1437
1438 if start > end {
1439 start = end;
1440 }
1441
1442 if start == end {
1443 if end.is_zero() {
1444 end += DurationNanos::new(1);
1445 } else {
1446 start -= DurationNanos::new(1);
1447 }
1448 }
1449
1450 (start, Some(end))
1451}
1452
1453fn start_of_utc_day(timestamp: UnixNanos) -> UnixNanos {
1454 timestamp.floor(DurationNanos::from_days(1))
1455}
1456
1457async fn seed_price_precision_if_needed(
1458 historical_client: &DatabentoHistoricalClient,
1459 dataset: &str,
1460 instrument_id: InstrumentId,
1461 start_nanos: UnixNanos,
1462 end_nanos: Option<UnixNanos>,
1463 price_precision: Option<u8>,
1464) {
1465 if price_precision.is_some()
1466 || historical_client
1467 .price_precision(instrument_id.symbol)
1468 .is_some()
1469 {
1470 return;
1471 }
1472
1473 let query_start = start_of_utc_day(start_nanos);
1474 let query_params =
1475 instrument_query_params(dataset.to_string(), instrument_id, query_start, end_nanos);
1476
1477 if let Err(e) = historical_client.get_range_instruments(query_params).await {
1478 log::warn!("Failed to seed price precision for {instrument_id}: {e}");
1479 }
1480}
1481
1482fn send_data_response(data_sender: &EventSender<DataEvent>, response: DataResponse, label: &str) {
1483 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
1484 log::error!("Failed to send {label} response: {e}");
1485 }
1486}
1487
1488fn partition_book_deltas_response(mut response: BookDeltasResponse) -> Vec<BookDeltasResponse> {
1489 if response.data.is_empty() {
1490 return vec![response];
1491 }
1492
1493 let mut partitions = IndexMap::new();
1494
1495 for delta in std::mem::take(&mut response.data) {
1496 partitions
1497 .entry(delta.instrument_id)
1498 .or_insert_with(Vec::new)
1499 .push(delta);
1500 }
1501
1502 partitions
1503 .into_iter()
1504 .map(|(instrument_id, data)| {
1505 let mut child = response.clone();
1506 child.instrument_id = instrument_id;
1507 child.data = data;
1508 child
1509 })
1510 .collect()
1511}
1512
1513fn requested_instrument(
1514 instruments: Vec<InstrumentAny>,
1515 instrument_id: InstrumentId,
1516) -> Option<InstrumentAny> {
1517 instruments
1518 .into_iter()
1519 .rev()
1520 .find(|instrument| instrument.id() == instrument_id)
1521}
1522
1523fn price_precision_from_params(params: Option<&Params>) -> anyhow::Result<Option<u8>> {
1524 let Some(price_precision) = params.and_then(|params| params.get_u64(PRICE_PRECISION_PARAM))
1525 else {
1526 return Ok(None);
1527 };
1528
1529 Ok(Some(u8::try_from(price_precision).map_err(|_| {
1530 anyhow::anyhow!(
1531 "`{PRICE_PRECISION_PARAM}` must be less than or equal to {}",
1532 u8::MAX
1533 )
1534 })?))
1535}
1536
1537fn custom_data_for_instrument(
1538 data: Arc<dyn CustomDataTrait>,
1539 instrument_id: InstrumentId,
1540) -> CustomData {
1541 let data_type = DataType::new(data.type_name(), None, Some(instrument_id.to_string()));
1542 CustomData::new(data, data_type)
1543}
1544
1545fn custom_data_schema(data_type: &DataType) -> anyhow::Result<dbn::Schema> {
1546 match data_type.type_name() {
1547 name if name == DatabentoStatistics::type_name_static() => Ok(dbn::Schema::Statistics),
1548 name if name == DatabentoImbalance::type_name_static() => Ok(dbn::Schema::Imbalance),
1549 name => anyhow::bail!(
1550 "Unsupported custom data type for Databento live subscription: {name}, expected {} or {}",
1551 DatabentoStatistics::type_name_static(),
1552 DatabentoImbalance::type_name_static(),
1553 ),
1554 }
1555}
1556
1557fn custom_data_instrument_id(data_type: &DataType) -> anyhow::Result<InstrumentId> {
1558 anyhow::ensure!(
1560 data_type
1561 .metadata()
1562 .is_none_or(|metadata| metadata.is_empty()),
1563 "{} subscriptions do not support metadata, received {}",
1564 data_type.type_name(),
1565 data_type.metadata_str(),
1566 );
1567
1568 let identifier = data_type.identifier().ok_or_else(|| {
1569 anyhow::anyhow!(
1570 "{} subscriptions require an instrument ID identifier",
1571 data_type.type_name()
1572 )
1573 })?;
1574
1575 InstrumentId::from_str(identifier)
1576 .map_err(|e| anyhow::anyhow!("Invalid instrument ID identifier `{identifier}`: {e}"))
1577}
1578
1579fn ohlcv_schema(bar_type: BarType) -> anyhow::Result<dbn::Schema> {
1580 let spec = bar_type.spec();
1581 anyhow::ensure!(
1582 spec.price_type == PriceType::Last,
1583 "Unsupported bar price type for Databento OHLCV: {spec}, only LAST is emitted",
1584 );
1585 anyhow::ensure!(
1586 spec.step.get() == 1,
1587 "Unsupported bar step for Databento OHLCV: {spec}, only a step of 1 is available",
1588 );
1589
1590 ohlcv_schema_from_aggregation(spec.aggregation)
1591}
1592
1593fn schema_from_params(
1594 params: Option<&Params>,
1595 default_schema: dbn::Schema,
1596 allowed_schemas: &[dbn::Schema],
1597) -> anyhow::Result<dbn::Schema> {
1598 let schema = if let Some(schema) = params.and_then(|params| params.get_str(SCHEMA_PARAM)) {
1599 dbn::Schema::from_str(schema)?
1600 } else {
1601 default_schema
1602 };
1603
1604 if allowed_schemas.contains(&schema) {
1605 return Ok(schema);
1606 }
1607
1608 let allowed = allowed_schemas
1609 .iter()
1610 .map(dbn::Schema::as_str)
1611 .collect::<Vec<_>>()
1612 .join(", ");
1613 anyhow::bail!(
1614 "Invalid `{SCHEMA_PARAM}` '{}'. Must be one of: {allowed}",
1615 schema.as_str()
1616 );
1617}
1618
1619fn send_subscription_commands(
1620 tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1621 dataset: &str,
1622 price_precision: Option<(Symbol, u8)>,
1623 subscription: Subscription,
1624 start_after_subscribe: bool,
1625) -> anyhow::Result<()> {
1626 if let Some((symbol, precision)) = price_precision {
1627 tx.send(HandlerCommand::SetPricePrecision(symbol, precision))
1628 .map_err(|e| anyhow::anyhow!("Failed to send command to dataset {dataset}: {e}"))?;
1629 }
1630
1631 tx.send(HandlerCommand::Subscribe(subscription))
1632 .map_err(|e| anyhow::anyhow!("Failed to send command to dataset {dataset}: {e}"))?;
1633
1634 if start_after_subscribe {
1635 tx.send(HandlerCommand::Start)
1636 .map_err(|e| anyhow::anyhow!("Failed to send command to dataset {dataset}: {e}"))?;
1637 }
1638
1639 Ok(())
1640}
1641
1642#[cfg(test)]
1643mod tests {
1644 use std::{num::NonZeroUsize, path::PathBuf};
1645
1646 use databento::dbn::{
1647 MappingInterval, SymbolMapping,
1648 decode::{DbnMetadata, DecodeRecord, dbn::Decoder},
1649 encode::EncodeRecord,
1650 };
1651 use nautilus_common::{
1652 live::runner::replace_data_event_sender,
1653 msgbus::{self, switchboard::get_custom_topic},
1654 };
1655 use nautilus_core::UUID4;
1656 use nautilus_model::{
1657 data::OrderBookDelta,
1658 enums::BookType,
1659 identifiers::{ClientId, InstrumentId},
1660 instruments::{CurrencyPair, InstrumentAny},
1661 types::{Currency, Price, Quantity},
1662 };
1663 use rstest::rstest;
1664 use serde_json::json;
1665 use time::macros::date;
1666 use tokio::{
1667 io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader},
1668 net::TcpListener,
1669 };
1670
1671 use super::*;
1672 use crate::enums::{DatabentoStatisticType, DatabentoStatisticUpdateAction};
1673
1674 #[derive(Clone, Copy)]
1675 enum SubscribeKind {
1676 Quotes,
1677 Trades,
1678 }
1679
1680 fn currency_pair(instrument_id: &str) -> InstrumentAny {
1681 currency_pair_with_ts_init(instrument_id, UnixNanos::default())
1682 }
1683
1684 fn test_data_client() -> DatabentoDataClient {
1685 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
1686 replace_data_event_sender(sender);
1687
1688 let config = DatabentoDataClientConfig::new(
1689 "32-character-with-lots-of-filler",
1690 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("publishers.json"),
1691 true,
1692 true,
1693 );
1694 DatabentoDataClient::new(
1695 ClientId::from("DATABENTO-TEST"),
1696 config,
1697 get_atomic_clock_realtime(),
1698 )
1699 .expect("test client should initialize")
1700 }
1701
1702 #[rstest]
1703 #[tokio::test]
1704 async fn test_stop_closes_task_admission_until_disconnect_drains() {
1705 let mut client = test_data_client();
1706
1707 client
1708 .task_handles
1709 .spawn(async { std::future::pending::<()>().await })
1710 .unwrap();
1711 client.is_connected.store(true, Ordering::Relaxed);
1712
1713 client.stop().unwrap();
1714
1715 assert!(!client.task_handles.is_open());
1716 assert!(client.is_disconnected());
1717
1718 client.disconnect().await.unwrap();
1719 assert!(client.task_handles.is_empty());
1720 }
1721
1722 #[rstest]
1723 #[case("EQUS", "EQUS.PLUS")] #[case("GLBX", "EQUS.MINI")] fn test_venue_dataset_map_overrides_default(#[case] venue: &str, #[case] dataset: &str) {
1726 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
1727 replace_data_event_sender(sender);
1728
1729 let mut config = DatabentoDataClientConfig::new(
1730 "32-character-with-lots-of-filler",
1731 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("publishers.json"),
1732 true,
1733 true,
1734 );
1735 config.venue_dataset_map = IndexMap::from([(venue.to_string(), dataset.to_string())]);
1736
1737 let client = DatabentoDataClient::new(
1738 ClientId::from("DATABENTO-TEST"),
1739 config,
1740 get_atomic_clock_realtime(),
1741 )
1742 .expect("test client should initialize");
1743
1744 assert_eq!(
1745 client.get_dataset_for_venue(Venue::from(venue)).unwrap(),
1746 dataset
1747 );
1748
1749 assert_eq!(
1751 client.get_dataset_for_venue(Venue::from("XCBO")).unwrap(),
1752 "OPRA.PILLAR"
1753 );
1754 }
1755
1756 #[rstest]
1757 fn test_config_base_url_overrides_default_to_none() {
1758 let config = DatabentoDataClientConfig::new(
1759 "32-character-with-lots-of-filler",
1760 PathBuf::from("test_publishers.json"),
1761 true,
1762 true,
1763 );
1764
1765 assert!(config.historical_base_url.is_none());
1766 assert!(config.live_gateway_addr.is_none());
1767 }
1768
1769 #[rstest]
1770 fn test_historical_base_url_override_invalid_errors() {
1771 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
1772 replace_data_event_sender(sender);
1773
1774 let mut config = DatabentoDataClientConfig::new(
1775 "32-character-with-lots-of-filler",
1776 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("publishers.json"),
1777 true,
1778 true,
1779 );
1780 config.historical_base_url = Some("not a url".to_string());
1781
1782 let result = DatabentoDataClient::new(
1783 ClientId::from("DATABENTO-TEST"),
1784 config,
1785 get_atomic_clock_realtime(),
1786 );
1787
1788 assert!(result.is_err());
1789 }
1790
1791 fn subscribe_quotes_cmd(params: Option<Params>) -> SubscribeQuotes {
1792 SubscribeQuotes::new(
1793 InstrumentId::from("ESM4.GLBX"),
1794 Some(ClientId::from("DATABENTO-TEST")),
1795 None,
1796 UUID4::new(),
1797 UnixNanos::default(),
1798 None,
1799 params,
1800 )
1801 }
1802
1803 fn subscribe_trades_cmd(params: Option<Params>) -> SubscribeTrades {
1804 SubscribeTrades::new(
1805 InstrumentId::from("ESM4.GLBX"),
1806 Some(ClientId::from("DATABENTO-TEST")),
1807 None,
1808 UUID4::new(),
1809 UnixNanos::default(),
1810 None,
1811 params,
1812 )
1813 }
1814
1815 fn subscribe_book_depth_cmd(depth: Option<NonZeroUsize>) -> SubscribeBookDepth {
1816 SubscribeBookDepth::new(
1817 InstrumentId::from("ESM4.GLBX"),
1818 BookType::L2_MBP,
1819 Some(ClientId::from("DATABENTO-TEST")),
1820 None,
1821 UUID4::new(),
1822 UnixNanos::default(),
1823 depth,
1824 true,
1825 None,
1826 None,
1827 )
1828 }
1829
1830 fn currency_pair_with_ts_init(instrument_id: &str, ts_init: UnixNanos) -> InstrumentAny {
1831 let instrument_id = InstrumentId::from(instrument_id);
1832 InstrumentAny::CurrencyPair(
1833 CurrencyPair::builder()
1834 .instrument_id(instrument_id)
1835 .raw_symbol(instrument_id.symbol)
1836 .base_currency(Currency::from("BTC"))
1837 .quote_currency(Currency::from("USDT"))
1838 .price_precision(2)
1839 .size_precision(6)
1840 .price_increment(Price::from("0.01"))
1841 .size_increment(Quantity::from("0.000001"))
1842 .ts_event(UnixNanos::default())
1843 .ts_init(ts_init)
1844 .build()
1845 .unwrap(),
1846 )
1847 }
1848
1849 #[rstest]
1850 fn test_instruments_query_params_requests_all_symbols() {
1851 let start = UnixNanos::from(1_000_000_000);
1852 let end = UnixNanos::from(2_000_000_000);
1853
1854 let params = instruments_query_params("GLBX.MDP3".to_string(), start, Some(end));
1855
1856 assert_eq!(params.dataset, "GLBX.MDP3");
1857 assert_eq!(params.symbols, vec!["ALL_SYMBOLS"]);
1858 assert_eq!(params.start, start);
1859 assert_eq!(params.end, Some(end));
1860 assert_eq!(params.limit, None);
1861 assert_eq!(params.price_precision, None);
1862 }
1863
1864 #[rstest]
1865 fn test_instrument_query_params_requests_single_symbol() {
1866 let instrument_id = InstrumentId::from("ESM4.GLBX");
1867
1868 let start = UnixNanos::from(1_000_000_000);
1869 let end = UnixNanos::from(2_000_000_000);
1870
1871 let params =
1872 instrument_query_params("GLBX.MDP3".to_string(), instrument_id, start, Some(end));
1873
1874 assert_eq!(params.dataset, "GLBX.MDP3");
1875 assert_eq!(params.symbols, vec!["ESM4"]);
1876 assert_eq!(params.start, start);
1877 assert_eq!(params.end, Some(end));
1878 assert_eq!(params.limit, None);
1879 assert_eq!(params.price_precision, None);
1880 }
1881
1882 #[rstest]
1883 #[case::intraday(1_782_302_400_000_000_001)]
1884 #[case::midnight(1_782_259_200_000_000_000)]
1885 #[case::day_end(1_782_345_599_999_999_999)]
1886 #[tokio::test]
1887 async fn test_seed_price_precision_includes_midnight(#[case] start: u64) {
1888 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1889 .join("test_data/test_data.definition.futures_contract.dbn.zst");
1890 let mut decoder = Decoder::from_zstd_file(path).unwrap();
1891 let mut metadata = decoder.metadata().clone();
1892 let record = decoder
1893 .decode_record::<dbn::InstrumentDefMsg>()
1894 .unwrap()
1895 .unwrap();
1896 metadata.symbols = vec!["ESU6".to_string()];
1897
1898 metadata.mappings = vec![SymbolMapping {
1899 raw_symbol: "ESU6".to_string(),
1900 intervals: vec![MappingInterval {
1901 start_date: date!(2026 - 06 - 24),
1902 end_date: date!(2026 - 06 - 25),
1903 symbol: record.hd.instrument_id.to_string(),
1904 }],
1905 }];
1906
1907 let mut body = Vec::new();
1908 {
1909 let mut encoder = dbn::encode::dbn::Encoder::with_zstd(&mut body, &metadata).unwrap();
1910 encoder.encode_record(record).unwrap();
1911 encoder.flush().unwrap();
1912 }
1913
1914 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1915 let base_url = format!("http://{}/", listener.local_addr().unwrap());
1916
1917 let server = tokio::spawn(serve_historical_response(listener, body));
1918
1919 let client = DatabentoHistoricalClient::new_with_base_url(
1920 Credential::new("32-character-with-lots-of-filler"),
1921 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("publishers.json"),
1922 get_atomic_clock_realtime(),
1923 false,
1924 &base_url,
1925 )
1926 .unwrap();
1927 let instrument_id = InstrumentId::from("ESU6.GLBX");
1928 let end = UnixNanos::from(1_782_345_600_000_000_123);
1929
1930 tokio::time::timeout(
1931 std::time::Duration::from_secs(5),
1932 seed_price_precision_if_needed(
1933 &client,
1934 "GLBX.MDP3",
1935 instrument_id,
1936 start.into(),
1937 Some(end),
1938 None,
1939 ),
1940 )
1941 .await
1942 .unwrap();
1943 let request = server.await.unwrap();
1944 let fields: AHashMap<_, _> = request
1945 .split('&')
1946 .map(|field| field.split_once('=').unwrap())
1947 .collect();
1948
1949 assert_eq!(fields["dataset"], "GLBX.MDP3");
1950 assert_eq!(fields["schema"], "definition");
1951 assert_eq!(fields["symbols"], "ESU6");
1952 assert_eq!(fields["start"], "1782259200000000000");
1953 assert_eq!(fields["end"], end.to_string());
1954 assert_eq!(client.price_precision(instrument_id.symbol), Some(2));
1955 }
1956
1957 async fn serve_historical_response(listener: TcpListener, body: Vec<u8>) -> String {
1958 let (stream, _) = listener.accept().await.unwrap();
1959 let mut stream = BufReader::new(stream);
1960 let mut line = String::new();
1961 stream.read_line(&mut line).await.unwrap();
1962 assert_eq!(line, "POST /v0/timeseries.get_range HTTP/1.1\r\n");
1963 let mut content_length = None;
1964
1965 loop {
1966 line.clear();
1967 stream.read_line(&mut line).await.unwrap();
1968
1969 if line == "\r\n" {
1970 break;
1971 }
1972
1973 if let Some((name, value)) = line.split_once(':')
1974 && name.eq_ignore_ascii_case("content-length")
1975 {
1976 content_length = Some(value.trim().parse::<usize>().unwrap());
1977 }
1978 }
1979
1980 let mut request = vec![0; content_length.unwrap()];
1981 stream.read_exact(&mut request).await.unwrap();
1982 let headers = format!(
1983 "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1984 body.len(),
1985 );
1986 stream.write_all(headers.as_bytes()).await.unwrap();
1987 stream.write_all(&body).await.unwrap();
1988 stream.shutdown().await.unwrap();
1989 String::from_utf8(request).unwrap()
1990 }
1991
1992 #[rstest]
1993 #[case::explicit(Some(5), None)]
1994 #[case::cached(None, Some(3))]
1995 #[tokio::test]
1996 async fn test_seed_price_precision_skips_known_precision(
1997 #[case] price_precision: Option<u8>,
1998 #[case] cached_precision: Option<u8>,
1999 ) {
2000 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
2001 let base_url = format!("http://{}/", listener.local_addr().unwrap());
2002
2003 let client = DatabentoHistoricalClient::new_with_base_url(
2004 Credential::new("32-character-with-lots-of-filler"),
2005 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("publishers.json"),
2006 get_atomic_clock_realtime(),
2007 false,
2008 &base_url,
2009 )
2010 .unwrap();
2011 let instrument_id = InstrumentId::from("ESU6.GLBX");
2012 if let Some(precision) = cached_precision {
2013 client.set_price_precision(instrument_id.symbol, precision);
2014 }
2015
2016 tokio::select! {
2017 () = seed_price_precision_if_needed(
2018 &client,
2019 "GLBX.MDP3",
2020 instrument_id,
2021 1_706_443_200_000_000_001.into(),
2022 Some(1_706_486_400_000_000_123.into()),
2023 price_precision,
2024 ) => {}
2025 result = listener.accept() => panic!("Unexpected definition request: {result:?}"),
2026 () = tokio::time::sleep(std::time::Duration::from_secs(5)) => {
2027 panic!("Precision seeding did not finish");
2028 }
2029 }
2030
2031 assert_eq!(
2032 client.price_precision(instrument_id.symbol),
2033 cached_precision
2034 );
2035 }
2036
2037 #[rstest]
2038 fn test_resolve_request_time_range_defaults_to_end_day() {
2039 let end = UnixNanos::from(1_706_443_200_000_000_001);
2040
2041 let (start, resolved_end) = resolve_request_time_range(None, Some(end));
2042
2043 assert_eq!(start, UnixNanos::from(1_706_400_000_000_000_000));
2044 assert_eq!(resolved_end, Some(end));
2045 }
2046
2047 #[rstest]
2048 fn test_resolve_request_time_range_makes_empty_interval_non_empty() {
2049 let end = UnixNanos::from(1_706_443_200_000_000_001);
2050
2051 let (start, resolved_end) = resolve_request_time_range(Some(end), Some(end));
2052
2053 assert_eq!(start, end - DurationNanos::new(1));
2054 assert_eq!(resolved_end, Some(end));
2055 }
2056
2057 #[rstest]
2058 fn test_requested_instrument_filters_exact_id() {
2059 let requested_id = InstrumentId::from("BTCUSDT.BINANCE");
2060 let instruments = vec![
2061 currency_pair("ETHUSDT.BINANCE"),
2062 currency_pair("BTCUSDT.BINANCE"),
2063 ];
2064
2065 let instrument = requested_instrument(instruments, requested_id).expect("instrument");
2066
2067 assert_eq!(instrument.id(), requested_id);
2068 }
2069
2070 #[rstest]
2071 fn test_requested_instrument_returns_latest_matching_id() {
2072 let requested_id = InstrumentId::from("BTCUSDT.BINANCE");
2073 let instruments = vec![
2074 currency_pair_with_ts_init("BTCUSDT.BINANCE", UnixNanos::from(1)),
2075 currency_pair_with_ts_init("BTCUSDT.BINANCE", UnixNanos::from(2)),
2076 ];
2077
2078 let instrument = requested_instrument(instruments, requested_id).expect("instrument");
2079
2080 assert_eq!(instrument.ts_init(), UnixNanos::from(2));
2081 }
2082
2083 #[rstest]
2084 fn test_requested_instrument_returns_none_on_miss() {
2085 let instruments = vec![currency_pair("ETHUSDT.BINANCE")];
2086
2087 let instrument = requested_instrument(instruments, InstrumentId::from("BTCUSDT.BINANCE"));
2088
2089 assert!(instrument.is_none());
2090 }
2091
2092 #[rstest]
2093 fn test_price_precision_from_params() {
2094 let mut params = Params::new();
2095 params.insert(PRICE_PRECISION_PARAM.to_string(), json!(5));
2096
2097 let price_precision = price_precision_from_params(Some(¶ms)).unwrap();
2098
2099 assert_eq!(price_precision, Some(5));
2100 }
2101
2102 #[rstest]
2103 fn test_price_precision_from_params_rejects_out_of_range_value() {
2104 let mut params = Params::new();
2105 params.insert(
2106 PRICE_PRECISION_PARAM.to_string(),
2107 json!(u64::from(u8::MAX) + 1),
2108 );
2109
2110 let result = price_precision_from_params(Some(¶ms));
2111
2112 assert!(result.is_err());
2113 }
2114
2115 #[rstest]
2116 fn test_partition_book_deltas_response_by_child_instrument() {
2117 let correlation_id = UUID4::new();
2118 let client_id = ClientId::from("DATABENTO-TEST");
2119 let parent = InstrumentId::from("ES.FUT.GLBX");
2120 let child_a = InstrumentId::from("ESM6.GLBX");
2121 let child_b = InstrumentId::from("ESU6.GLBX");
2122 let deltas = vec![
2123 OrderBookDelta::clear(child_a, 1, UnixNanos::from(1_000), UnixNanos::from(1_000)),
2124 OrderBookDelta::clear(child_b, 2, UnixNanos::from(2_000), UnixNanos::from(2_000)),
2125 OrderBookDelta::clear(child_a, 3, UnixNanos::from(3_000), UnixNanos::from(3_000)),
2126 ];
2127 let mut params = Params::new();
2128 params.insert(PRICE_PRECISION_PARAM.to_string(), json!(5));
2129 let response = BookDeltasResponse::new(
2130 correlation_id,
2131 client_id,
2132 parent,
2133 deltas.clone(),
2134 Some(UnixNanos::from(500)),
2135 Some(UnixNanos::from(4_000)),
2136 UnixNanos::from(5_000),
2137 Some(params.clone()),
2138 );
2139
2140 let responses = partition_book_deltas_response(response);
2141
2142 assert_eq!(responses.len(), 2);
2143 assert_eq!(responses[0].correlation_id, correlation_id);
2144 assert_eq!(responses[1].correlation_id, correlation_id);
2145 assert_eq!(responses[0].client_id, client_id);
2146 assert_eq!(responses[1].client_id, client_id);
2147 assert_eq!(responses[0].instrument_id, child_a);
2148 assert_eq!(responses[1].instrument_id, child_b);
2149 assert_eq!(responses[0].data, vec![deltas[0], deltas[2]]);
2150 assert_eq!(responses[1].data, vec![deltas[1]]);
2151 assert_eq!(responses[0].start, Some(UnixNanos::from(500)));
2152 assert_eq!(responses[1].start, Some(UnixNanos::from(500)));
2153 assert_eq!(responses[0].end, Some(UnixNanos::from(4_000)));
2154 assert_eq!(responses[1].end, Some(UnixNanos::from(4_000)));
2155 assert_eq!(responses[0].ts_init, UnixNanos::from(5_000));
2156 assert_eq!(responses[1].ts_init, UnixNanos::from(5_000));
2157 assert_eq!(responses[0].params, Some(params.clone()));
2158 assert_eq!(responses[1].params, Some(params));
2159 }
2160
2161 #[rstest]
2162 fn test_partition_book_deltas_response_preserves_homogeneous_response() {
2163 let correlation_id = UUID4::new();
2164 let client_id = ClientId::from("DATABENTO-TEST");
2165 let instrument_id = InstrumentId::from("ESM6.GLBX");
2166 let delta = OrderBookDelta::clear(
2167 instrument_id,
2168 1,
2169 UnixNanos::from(1_000),
2170 UnixNanos::from(1_000),
2171 );
2172 let response = BookDeltasResponse::new(
2173 correlation_id,
2174 client_id,
2175 instrument_id,
2176 vec![delta],
2177 Some(UnixNanos::from(500)),
2178 Some(UnixNanos::from(1_500)),
2179 UnixNanos::from(2_000),
2180 None,
2181 );
2182
2183 let responses = partition_book_deltas_response(response);
2184
2185 assert_eq!(responses.len(), 1);
2186 assert_eq!(responses[0].correlation_id, correlation_id);
2187 assert_eq!(responses[0].client_id, client_id);
2188 assert_eq!(responses[0].instrument_id, instrument_id);
2189 assert_eq!(responses[0].data, vec![delta]);
2190 assert_eq!(responses[0].start, Some(UnixNanos::from(500)));
2191 assert_eq!(responses[0].end, Some(UnixNanos::from(1_500)));
2192 assert_eq!(responses[0].ts_init, UnixNanos::from(2_000));
2193 assert_eq!(responses[0].params, None);
2194 }
2195
2196 #[rstest]
2197 fn test_partition_book_deltas_response_preserves_empty_parent_response() {
2198 let correlation_id = UUID4::new();
2199 let parent = InstrumentId::from("ES.FUT.GLBX");
2200 let response = BookDeltasResponse::new(
2201 correlation_id,
2202 ClientId::from("DATABENTO-TEST"),
2203 parent,
2204 Vec::new(),
2205 None,
2206 None,
2207 UnixNanos::from(1_000),
2208 None,
2209 );
2210
2211 let responses = partition_book_deltas_response(response);
2212
2213 assert_eq!(responses.len(), 1);
2214 let response = &responses[0];
2215 assert_eq!(response.correlation_id, correlation_id);
2216 assert_eq!(response.instrument_id, parent);
2217 assert!(response.data.is_empty());
2218 }
2219
2220 #[rstest]
2221 fn test_schema_from_params_returns_default() {
2222 let schema = schema_from_params(None, dbn::Schema::Mbp1, QUOTE_SCHEMAS).unwrap();
2223
2224 assert_eq!(schema, dbn::Schema::Mbp1);
2225 }
2226
2227 #[rstest]
2228 fn test_schema_from_params_accepts_allowed_value() {
2229 let mut params = Params::new();
2230 params.insert(SCHEMA_PARAM.to_string(), json!("tbbo"));
2231
2232 let schema = schema_from_params(Some(¶ms), dbn::Schema::Mbp1, QUOTE_SCHEMAS).unwrap();
2233
2234 assert_eq!(schema, dbn::Schema::Tbbo);
2235 }
2236
2237 #[rstest]
2238 fn test_schema_from_params_rejects_disallowed_value() {
2239 let mut params = Params::new();
2240 params.insert(SCHEMA_PARAM.to_string(), json!("mbo"));
2241
2242 let result = schema_from_params(Some(¶ms), dbn::Schema::Mbp1, QUOTE_SCHEMAS);
2243
2244 assert!(result.is_err());
2245 }
2246
2247 #[rstest]
2248 #[case::quotes(SubscribeKind::Quotes)]
2249 #[case::trades(SubscribeKind::Trades)]
2250 fn test_invalid_subscribe_params_do_not_create_feed_handler(#[case] kind: SubscribeKind) {
2251 let mut client = test_data_client();
2252 let mut params = Params::new();
2253 params.insert(SCHEMA_PARAM.to_string(), json!("definition"));
2254
2255 let result = match kind {
2256 SubscribeKind::Quotes => client.subscribe_quotes(subscribe_quotes_cmd(Some(params))),
2257 SubscribeKind::Trades => client.subscribe_trades(subscribe_trades_cmd(Some(params))),
2258 };
2259
2260 assert!(result.is_err());
2261 assert!(client.cmd_channels.lock().is_empty());
2262 }
2263
2264 #[rstest]
2265 fn test_send_subscription_commands_starts_after_subscribe() {
2266 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
2267 let subscription = Subscription::builder()
2268 .schema(dbn::Schema::Mbp1)
2269 .symbols(vec!["ESM4"])
2270 .build();
2271
2272 send_subscription_commands(
2273 &tx,
2274 "GLBX.MDP3",
2275 Some((Symbol::from("ESM4"), 2)),
2276 subscription,
2277 true,
2278 )
2279 .unwrap();
2280
2281 assert!(matches!(
2282 rx.try_recv().unwrap(),
2283 HandlerCommand::SetPricePrecision(symbol, 2) if symbol == Symbol::from("ESM4")
2284 ));
2285 assert!(matches!(
2286 rx.try_recv().unwrap(),
2287 HandlerCommand::Subscribe(sub) if sub.schema == dbn::Schema::Mbp1
2288 ));
2289 assert!(matches!(rx.try_recv().unwrap(), HandlerCommand::Start));
2290 }
2291
2292 #[rstest]
2293 #[case::second("ESM4.GLBX-1-SECOND-LAST-EXTERNAL", dbn::Schema::Ohlcv1S)]
2294 #[case::minute("ESM4.GLBX-1-MINUTE-LAST-EXTERNAL", dbn::Schema::Ohlcv1M)]
2295 #[case::hour("ESM4.GLBX-1-HOUR-LAST-EXTERNAL", dbn::Schema::Ohlcv1H)]
2296 #[case::day("ESM4.GLBX-1-DAY-LAST-EXTERNAL", dbn::Schema::Ohlcv1D)]
2297 fn test_ohlcv_schema_maps_aggregation(#[case] bar_type: &str, #[case] expected: dbn::Schema) {
2298 let schema = ohlcv_schema(BarType::from(bar_type)).unwrap();
2299
2300 assert_eq!(schema, expected);
2301 }
2302
2303 #[rstest]
2304 #[case::step("ESM4.GLBX-5-MINUTE-LAST-EXTERNAL")]
2305 #[case::aggregation("ESM4.GLBX-100-TICK-LAST-EXTERNAL")]
2306 #[case::bid("ESM4.GLBX-1-MINUTE-BID-EXTERNAL")]
2307 #[case::mid("ESM4.GLBX-1-MINUTE-MID-EXTERNAL")]
2308 fn test_ohlcv_schema_rejects_unsupported_bar_type(#[case] bar_type: &str) {
2309 assert!(ohlcv_schema(BarType::from(bar_type)).is_err());
2310 }
2311
2312 #[rstest]
2313 #[case::statistics("DatabentoStatistics", dbn::Schema::Statistics)]
2314 #[case::imbalance("DatabentoImbalance", dbn::Schema::Imbalance)]
2315 fn test_subscribe_custom_data_sends_schema_subscription(
2316 #[case] type_name: &str,
2317 #[case] expected: dbn::Schema,
2318 ) {
2319 let mut client = test_data_client();
2320 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
2321 client
2322 .cmd_channels
2323 .lock()
2324 .insert("GLBX.MDP3".to_string(), tx);
2325 let command = SubscribeCustomData::new(
2326 Some(ClientId::from("DATABENTO-TEST")),
2327 None,
2328 DataType::new(type_name, None, Some("ESM4.GLBX".to_string())),
2329 UUID4::new(),
2330 UnixNanos::default(),
2331 None,
2332 None,
2333 );
2334
2335 client.subscribe(command).unwrap();
2336
2337 let HandlerCommand::Subscribe(subscription) = rx.try_recv().unwrap() else {
2338 panic!("expected subscription");
2339 };
2340 assert_eq!(subscription.schema, expected);
2341 assert_eq!(
2342 subscription.symbols,
2343 databento::Symbols::Symbols(vec!["ESM4".to_string()])
2344 );
2345 assert!(rx.try_recv().is_err());
2346 }
2347
2348 #[rstest]
2349 #[case::unsupported_type("DatabentoOther", Some("ESM4.GLBX"))]
2350 #[case::missing_identifier("DatabentoStatistics", None)]
2351 #[case::invalid_identifier("DatabentoStatistics", Some("ESM4"))]
2352 fn test_subscribe_custom_data_rejects_invalid_data_type(
2353 #[case] type_name: &str,
2354 #[case] identifier: Option<&str>,
2355 ) {
2356 let mut client = test_data_client();
2357 let command = SubscribeCustomData::new(
2358 Some(ClientId::from("DATABENTO-TEST")),
2359 None,
2360 DataType::new(type_name, None, identifier.map(str::to_string)),
2361 UUID4::new(),
2362 UnixNanos::default(),
2363 None,
2364 None,
2365 );
2366
2367 assert!(client.subscribe(command).is_err());
2368 assert!(client.cmd_channels.lock().is_empty());
2369 }
2370
2371 #[rstest]
2372 fn test_custom_data_for_instrument_matches_subscribed_topic() {
2373 let instrument_id = InstrumentId::from("ESM4.GLBX");
2374 let statistics = DatabentoStatistics::new(
2375 instrument_id,
2376 DatabentoStatisticType::SettlementPrice,
2377 DatabentoStatisticUpdateAction::Added,
2378 Some(Price::from("5000.25")),
2379 None,
2380 1,
2381 2,
2382 3,
2383 UnixNanos::from(4),
2384 5,
2385 UnixNanos::from(6),
2386 UnixNanos::from(7),
2387 UnixNanos::from(8),
2388 );
2389 let subscribed = DataType::new("DatabentoStatistics", None, Some("ESM4.GLBX".to_string()));
2390
2391 let custom = custom_data_for_instrument(Arc::new(statistics), instrument_id);
2392
2393 assert_eq!(custom.data_type, subscribed);
2394 assert_eq!(
2395 get_custom_topic(&custom.data_type),
2396 get_custom_topic(&subscribed)
2397 );
2398 assert_eq!(custom.data_type.identifier(), Some("ESM4.GLBX"));
2399 }
2400
2401 #[rstest]
2402 fn test_subscribe_schema_sends_raw_symbol() {
2403 let client = test_data_client();
2404 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
2405 client
2406 .cmd_channels
2407 .lock()
2408 .insert("GLBX.MDP3".to_string(), tx);
2409
2410 client
2411 .subscribe_schema(InstrumentId::from("ESM4.GLBX"), dbn::Schema::Ohlcv1M)
2412 .unwrap();
2413
2414 let HandlerCommand::Subscribe(subscription) = rx.try_recv().unwrap() else {
2415 panic!("expected subscription");
2416 };
2417 assert_eq!(subscription.stype_in, dbn::SType::RawSymbol);
2418 assert_eq!(
2419 subscription.symbols,
2420 databento::Symbols::Symbols(vec!["ESM4".to_string()])
2421 );
2422 }
2423
2424 #[rstest]
2425 #[case::continuous("ES.c.0.GLBX")]
2426 #[case::parent("ES.FUT.GLBX")]
2427 #[case::instrument_id("12345.GLBX")]
2428 fn test_subscribe_schema_rejects_non_raw_symbols(#[case] instrument_id: &str) {
2429 let client = test_data_client();
2430
2431 let result = client.subscribe_schema(InstrumentId::from(instrument_id), dbn::Schema::Mbp10);
2432
2433 assert!(result.is_err());
2434 assert!(client.cmd_channels.lock().is_empty());
2435 }
2436
2437 #[rstest]
2438 fn test_subscribe_custom_data_rejects_metadata() {
2439 let mut client = test_data_client();
2440 let mut metadata = Params::new();
2441 metadata.insert("venue".to_string(), json!("GLBX"));
2442 let command = SubscribeCustomData::new(
2443 Some(ClientId::from("DATABENTO-TEST")),
2444 None,
2445 DataType::new(
2446 "DatabentoStatistics",
2447 Some(metadata),
2448 Some("ESM4.GLBX".to_string()),
2449 ),
2450 UUID4::new(),
2451 UnixNanos::default(),
2452 None,
2453 None,
2454 );
2455
2456 assert!(client.subscribe(command).is_err());
2457 assert!(client.cmd_channels.lock().is_empty());
2458 }
2459
2460 #[rstest]
2461 fn test_subscribed_custom_data_reaches_subscriber() {
2462 let instrument_id = InstrumentId::from("ESM4.GLBX");
2463 let subscribed = DataType::new("DatabentoStatistics", None, Some("ESM4.GLBX".to_string()));
2464 let other = DataType::new("DatabentoStatistics", None, Some("ESU4.GLBX".to_string()));
2465 let handler = msgbus::stubs::get_message_saving_handler::<CustomData>(None);
2466 msgbus::subscribe_any(get_custom_topic(&subscribed).into(), handler.clone(), None);
2467 let statistics = DatabentoStatistics::new(
2468 instrument_id,
2469 DatabentoStatisticType::SettlementPrice,
2470 DatabentoStatisticUpdateAction::Added,
2471 Some(Price::from("5000.25")),
2472 None,
2473 1,
2474 2,
2475 3,
2476 UnixNanos::from(4),
2477 5,
2478 UnixNanos::from(6),
2479 UnixNanos::from(7),
2480 UnixNanos::from(8),
2481 );
2482 let custom = custom_data_for_instrument(Arc::new(statistics), instrument_id);
2483
2484 msgbus::publish_any(get_custom_topic(&custom.data_type), &custom);
2485 msgbus::publish_any(get_custom_topic(&other), &custom);
2486
2487 let received = msgbus::stubs::get_saved_messages::<CustomData>(&handler);
2488 assert_eq!(received, vec![custom]);
2489 }
2490
2491 #[rstest]
2492 fn test_subscribe_book_depth_accepts_depth_ten() {
2493 let mut client = test_data_client();
2494 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
2495 client
2496 .cmd_channels
2497 .lock()
2498 .insert("GLBX.MDP3".to_string(), tx);
2499
2500 client
2501 .subscribe_book_depth(subscribe_book_depth_cmd(NonZeroUsize::new(10)))
2502 .unwrap();
2503
2504 let HandlerCommand::Subscribe(subscription) = rx.try_recv().unwrap() else {
2505 panic!("expected subscription");
2506 };
2507 assert_eq!(subscription.schema, dbn::Schema::Mbp10);
2508 }
2509
2510 #[rstest]
2511 #[case::five(5)]
2512 #[case::twenty(20)]
2513 fn test_subscribe_book_depth_rejects_other_depths(#[case] depth: usize) {
2514 let mut client = test_data_client();
2515
2516 let result =
2517 client.subscribe_book_depth(subscribe_book_depth_cmd(NonZeroUsize::new(depth)));
2518
2519 assert!(result.is_err());
2520 assert!(client.cmd_channels.lock().is_empty());
2521 }
2522
2523 #[rstest]
2524 fn test_subscribe_bars_sends_ohlcv_subscription() {
2525 let mut client = test_data_client();
2526 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
2527 client
2528 .cmd_channels
2529 .lock()
2530 .insert("GLBX.MDP3".to_string(), tx);
2531 let command = SubscribeBars::new(
2532 BarType::from("ESM4.GLBX-1-MINUTE-LAST-EXTERNAL"),
2533 Some(ClientId::from("DATABENTO-TEST")),
2534 None,
2535 UUID4::new(),
2536 UnixNanos::default(),
2537 None,
2538 None,
2539 );
2540
2541 client.subscribe_bars(command).unwrap();
2542
2543 let HandlerCommand::Subscribe(subscription) = rx.try_recv().unwrap() else {
2544 panic!("expected subscription");
2545 };
2546 assert_eq!(subscription.schema, dbn::Schema::Ohlcv1M);
2547 assert_eq!(
2548 subscription.symbols,
2549 databento::Symbols::Symbols(vec!["ESM4".to_string()])
2550 );
2551 assert!(rx.try_recv().is_err());
2552 }
2553
2554 #[rstest]
2555 fn test_subscribe_book_depth_sends_mbp10_subscription() {
2556 let mut client = test_data_client();
2557 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
2558 client
2559 .cmd_channels
2560 .lock()
2561 .insert("GLBX.MDP3".to_string(), tx);
2562 client
2563 .subscribe_book_depth(subscribe_book_depth_cmd(None))
2564 .unwrap();
2565
2566 let HandlerCommand::Subscribe(subscription) = rx.try_recv().unwrap() else {
2567 panic!("expected subscription");
2568 };
2569 assert_eq!(subscription.schema, dbn::Schema::Mbp10);
2570 assert_eq!(
2571 subscription.symbols,
2572 databento::Symbols::Symbols(vec!["ESM4".to_string()])
2573 );
2574 assert!(rx.try_recv().is_err());
2575 }
2576
2577 #[rstest]
2578 fn test_send_subscription_commands_without_precision_or_start() {
2579 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
2580 let subscription = Subscription::builder()
2581 .schema(dbn::Schema::Mbp1)
2582 .symbols(vec!["ESM4"])
2583 .build();
2584
2585 send_subscription_commands(&tx, "GLBX.MDP3", None, subscription, false).unwrap();
2586
2587 assert!(matches!(
2588 rx.try_recv().unwrap(),
2589 HandlerCommand::Subscribe(sub) if sub.schema == dbn::Schema::Mbp1
2590 ));
2591 assert!(matches!(
2592 rx.try_recv(),
2593 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
2594 ));
2595 }
2596}