1use std::{
22 fmt::Debug,
23 path::PathBuf,
24 str::FromStr,
25 sync::{
26 Arc, Mutex,
27 atomic::{AtomicBool, Ordering},
28 },
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, runtime::get_runtime, task::TaskHandles},
37 messages::{
38 DataEvent, DataResponse,
39 data::{
40 BarsResponse, BookDeltasResponse, BookDepthResponse, InstrumentResponse,
41 InstrumentsResponse, QuotesResponse, RequestBars, RequestBookDeltas, RequestBookDepth,
42 RequestInstrument, RequestInstruments, RequestQuotes, RequestTrades,
43 SubscribeBookDeltas, SubscribeInstrument, SubscribeInstrumentStatus, SubscribeQuotes,
44 SubscribeTrades, TradesResponse, UnsubscribeBookDeltas, UnsubscribeInstrumentStatus,
45 UnsubscribeQuotes, UnsubscribeTrades,
46 },
47 },
48};
49use nautilus_core::{
50 AtomicMap, MUTEX_POISONED, Params, UnixNanos,
51 datetime::{NANOSECONDS_IN_DAY, datetime_to_unix_nanos},
52 string::secret::REDACTED,
53 time::{AtomicTime, get_atomic_clock_realtime},
54};
55use nautilus_model::{
56 data::{CustomData, Data},
57 enums::BarAggregation,
58 identifiers::{ClientId, InstrumentId, Symbol, Venue},
59 instruments::{Instrument, InstrumentAny},
60};
61use tokio_util::sync::CancellationToken;
62
63use crate::{
64 common::{Credential, DATABENTO_VENUE},
65 historical::{DatabentoHistoricalClient, RangeQueryParams},
66 live::{DatabentoFeedHandler, DatabentoMessage, HandlerCommand},
67 loader::DatabentoDataLoader,
68 symbology::instrument_id_to_symbol_string,
69 types::{Dataset, PublisherId},
70};
71
72const PRICE_PRECISION_PARAM: &str = "price_precision";
73const SCHEMA_PARAM: &str = "schema";
74const QUOTE_SCHEMAS: &[dbn::Schema] = &[
75 dbn::Schema::Mbp1,
76 dbn::Schema::Bbo1S,
77 dbn::Schema::Bbo1M,
78 dbn::Schema::Cmbp1,
79 dbn::Schema::Cbbo1S,
80 dbn::Schema::Cbbo1M,
81 dbn::Schema::Tbbo,
82 dbn::Schema::Tcbbo,
83];
84const TRADE_SCHEMAS: &[dbn::Schema] = &[
85 dbn::Schema::Trades,
86 dbn::Schema::Tbbo,
87 dbn::Schema::Tcbbo,
88 dbn::Schema::Mbp1,
89 dbn::Schema::Cmbp1,
90];
91
92#[derive(Clone)]
94pub struct DatabentoDataClientConfig {
95 pub(crate) credential: Credential,
97 pub publishers_filepath: PathBuf,
99 pub venue_dataset_map: IndexMap<String, String>,
101 pub use_exchange_as_venue: bool,
103 pub bars_timestamp_on_close: bool,
105 pub reconnect_timeout_mins: Option<u64>,
107}
108
109impl Debug for DatabentoDataClientConfig {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.debug_struct(stringify!(DatabentoDataClientConfig))
112 .field("credential", &REDACTED)
113 .field("publishers_filepath", &self.publishers_filepath)
114 .field("venue_dataset_map", &self.venue_dataset_map)
115 .field("use_exchange_as_venue", &self.use_exchange_as_venue)
116 .field("bars_timestamp_on_close", &self.bars_timestamp_on_close)
117 .field("reconnect_timeout_mins", &self.reconnect_timeout_mins)
118 .finish()
119 }
120}
121
122impl DatabentoDataClientConfig {
123 #[must_use]
125 pub fn new(
126 api_key: impl Into<String>,
127 publishers_filepath: PathBuf,
128 use_exchange_as_venue: bool,
129 bars_timestamp_on_close: bool,
130 ) -> Self {
131 Self {
132 credential: Credential::new(api_key),
133 publishers_filepath,
134 venue_dataset_map: IndexMap::new(),
135 use_exchange_as_venue,
136 bars_timestamp_on_close,
137 reconnect_timeout_mins: Some(10), }
139 }
140
141 #[must_use]
143 pub fn api_key(&self) -> &str {
144 self.credential.api_key()
145 }
146
147 #[must_use]
149 pub fn api_key_masked(&self) -> String {
150 self.credential.api_key_masked()
151 }
152}
153
154#[cfg_attr(feature = "python", pyo3::pyclass)]
160#[cfg_attr(
161 feature = "python",
162 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.databento")
163)]
164#[derive(Debug)]
165pub struct DatabentoDataClient {
166 client_id: ClientId,
168 config: DatabentoDataClientConfig,
170 is_connected: AtomicBool,
172 historical: DatabentoHistoricalClient,
174 loader: DatabentoDataLoader,
176 cmd_channels: Arc<Mutex<AHashMap<String, tokio::sync::mpsc::UnboundedSender<HandlerCommand>>>>,
178 task_handles: Arc<TaskHandles>,
180 cancellation_token: CancellationToken,
182 publisher_venue_map: Arc<IndexMap<PublisherId, Venue>>,
184 symbol_venue_map: Arc<AtomicMap<Symbol, Venue>>,
186 data_sender: tokio::sync::mpsc::UnboundedSender<DataEvent>,
188}
189
190impl DatabentoDataClient {
191 pub fn new(
197 client_id: ClientId,
198 config: DatabentoDataClientConfig,
199 clock: &'static AtomicTime,
200 ) -> anyhow::Result<Self> {
201 let historical = DatabentoHistoricalClient::new(
202 config.credential.clone(),
203 config.publishers_filepath.clone(),
204 clock,
205 config.use_exchange_as_venue,
206 )?;
207
208 let mut loader = DatabentoDataLoader::new(Some(config.publishers_filepath.clone()))?;
210 for (venue, dataset) in &config.venue_dataset_map {
211 loader.set_dataset_for_venue(
212 Dataset::from(dataset.as_str()),
213 Venue::from(venue.as_str()),
214 );
215 }
216
217 let file_content = std::fs::read_to_string(&config.publishers_filepath)?;
219 let publishers_vec: Vec<crate::types::DatabentoPublisher> =
220 serde_json::from_str(&file_content)?;
221
222 let publisher_venue_map = publishers_vec
223 .into_iter()
224 .map(|p| (p.publisher_id, Venue::from(p.venue.as_str())))
225 .collect::<IndexMap<u16, Venue>>();
226
227 let data_sender = get_data_event_sender();
228
229 Ok(Self {
230 client_id,
231 config,
232 is_connected: AtomicBool::new(false),
233 historical,
234 loader,
235 cmd_channels: Arc::new(Mutex::new(AHashMap::new())),
236 task_handles: Arc::new(TaskHandles::default()),
237 cancellation_token: CancellationToken::new(),
238 publisher_venue_map: Arc::new(publisher_venue_map),
239 symbol_venue_map: Arc::new(AtomicMap::new()),
240 data_sender,
241 })
242 }
243
244 #[must_use]
246 pub fn api_key(&self) -> &str {
247 self.config.api_key()
248 }
249
250 #[must_use]
252 pub fn api_key_masked(&self) -> String {
253 self.config.api_key_masked()
254 }
255
256 fn get_dataset_for_venue(&self, venue: Venue) -> anyhow::Result<String> {
262 self.loader
263 .get_dataset_for_venue(&venue)
264 .map(ToString::to_string)
265 .ok_or_else(|| anyhow::anyhow!("No dataset found for venue: {venue}"))
266 }
267
268 fn get_or_create_feed_handler(&self, dataset: &str) -> bool {
270 let mut channels = self.cmd_channels.lock().expect(MUTEX_POISONED);
271
272 if !channels.contains_key(dataset) {
273 log::debug!("Creating new feed handler for dataset: {dataset}");
274 let cmd_tx = self.initialize_live_feed(dataset.to_string());
275 channels.insert(dataset.to_string(), cmd_tx);
276
277 log::debug!("Feed handler created for dataset: {dataset}, channel stored");
278 return true;
279 }
280
281 false
282 }
283
284 fn send_subscription_to_dataset(
285 &self,
286 dataset: &str,
287 price_precision: Option<(Symbol, u8)>,
288 subscription: Subscription,
289 start_after_subscribe: bool,
290 ) -> anyhow::Result<()> {
291 let tx = {
292 let channels = self.cmd_channels.lock().expect(MUTEX_POISONED);
293 channels
294 .get(dataset)
295 .cloned()
296 .ok_or_else(|| anyhow::anyhow!("No feed handler found for dataset: {dataset}"))?
297 };
298
299 send_subscription_commands(
300 &tx,
301 dataset,
302 price_precision,
303 subscription,
304 start_after_subscribe,
305 )
306 }
307
308 fn send_close_to_active_feeds(&self) {
309 let channels = self.cmd_channels.lock().expect(MUTEX_POISONED);
310 for (dataset, tx) in channels.iter() {
311 if let Err(e) = tx.send(HandlerCommand::Close) {
312 log::warn!("Failed to send close command to dataset {dataset}: {e}");
313 }
314 }
315 }
316
317 fn clear_feed_channels(&self) {
318 let mut channels = self.cmd_channels.lock().expect(MUTEX_POISONED);
319 channels.clear();
320 }
321
322 fn abort_active_tasks(&self) {
323 self.task_handles.abort_all();
324 }
325
326 fn initialize_live_feed(
328 &self,
329 dataset: String,
330 ) -> tokio::sync::mpsc::UnboundedSender<HandlerCommand> {
331 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
332 let (msg_tx, msg_rx) = tokio::sync::mpsc::unbounded_channel();
333 let feed_dataset = dataset.clone();
334 let feed_channels = self.cmd_channels.clone();
335
336 let mut feed_handler = DatabentoFeedHandler::new(
337 self.config.credential.clone(),
338 dataset,
339 cmd_rx,
340 msg_tx,
341 (*self.publisher_venue_map).clone(),
342 self.symbol_venue_map.clone(),
343 self.config.use_exchange_as_venue,
344 self.config.bars_timestamp_on_close,
345 self.config.reconnect_timeout_mins,
346 );
347
348 let feed_handle = get_runtime().spawn(async move {
349 if let Err(e) = feed_handler.run().await {
350 log::error!("Feed handler error: {e}");
351 }
352 feed_channels
353 .lock()
354 .expect(MUTEX_POISONED)
355 .remove(&feed_dataset);
356 });
357
358 let cancellation_token = self.cancellation_token.clone();
359 let data_sender = self.data_sender.clone();
360
361 let msg_handle = get_runtime().spawn(async move {
363 let mut msg_rx = msg_rx;
364
365 loop {
366 tokio::select! {
367 msg = msg_rx.recv() => {
368 match msg {
369 Some(DatabentoMessage::Data(data)) => {
370 log::debug!("Received data: {data:?}");
371 if let Err(e) = data_sender.send(DataEvent::Data(data)) {
372 log::error!("Failed to send data event: {e}");
373 }
374 }
375 Some(DatabentoMessage::Instrument(instrument)) => {
376 log::debug!("Received instrument definition: {}", instrument.id());
377 if let Err(e) = data_sender.send(DataEvent::Instrument(*instrument)) {
378 log::error!("Failed to send instrument: {e}");
379 }
380 }
381 Some(DatabentoMessage::Status(status)) => {
382 log::debug!("Received status: {status:?}");
383 if let Err(e) =
384 data_sender.send(DataEvent::Data(Data::InstrumentStatus(status)))
385 {
386 log::error!("Failed to send status data event: {e}");
387 }
388 }
389 Some(DatabentoMessage::Imbalance(imbalance)) => {
390 log::debug!("Received imbalance: {imbalance:?}");
391 let data = Data::Custom(CustomData::from_arc(Arc::new(imbalance)));
392 if let Err(e) = data_sender.send(DataEvent::Data(data)) {
393 log::error!("Failed to send imbalance data event: {e}");
394 }
395 }
396 Some(DatabentoMessage::Statistics(statistics)) => {
397 log::debug!("Received statistics: {statistics:?}");
398 let data = Data::Custom(CustomData::from_arc(Arc::new(statistics)));
399 if let Err(e) = data_sender.send(DataEvent::Data(data)) {
400 log::error!("Failed to send statistics data event: {e}");
401 }
402 }
403 Some(DatabentoMessage::SubscriptionAck(ack)) => {
404 log::debug!("Received subscription ack: {}", ack.message);
405 }
406 Some(DatabentoMessage::Error(error)) => {
407 log::error!("Feed handler error: {error}");
408 }
409 Some(DatabentoMessage::Close) => {
410 log::debug!("Feed handler closed");
411 break;
412 }
413 None => {
414 log::debug!("Message channel closed");
415 break;
416 }
417 }
418 }
419 () = cancellation_token.cancelled() => {
420 log::debug!("Message processing cancelled");
421 break;
422 }
423 }
424 }
425 });
426
427 self.task_handles.push(feed_handle);
428 self.task_handles.push(msg_handle);
429
430 cmd_tx
431 }
432}
433
434#[async_trait::async_trait(?Send)]
435impl DataClient for DatabentoDataClient {
436 fn client_id(&self) -> ClientId {
438 self.client_id
439 }
440
441 fn venue(&self) -> Option<Venue> {
443 None
444 }
445
446 fn start(&mut self) -> anyhow::Result<()> {
452 log::debug!("Starting");
453 Ok(())
454 }
455
456 fn stop(&mut self) -> anyhow::Result<()> {
462 log::debug!("Stopping");
463
464 self.send_close_to_active_feeds();
465 self.clear_feed_channels();
466 self.cancellation_token.cancel();
467 self.abort_active_tasks();
468
469 self.cancellation_token = CancellationToken::new();
470
471 self.is_connected.store(false, Ordering::Relaxed);
472 Ok(())
473 }
474
475 fn reset(&mut self) -> anyhow::Result<()> {
476 log::debug!("Resetting");
477 self.is_connected.store(false, Ordering::Relaxed);
478 Ok(())
479 }
480
481 fn dispose(&mut self) -> anyhow::Result<()> {
482 log::debug!("Disposing");
483 self.stop()
484 }
485
486 async fn connect(&mut self) -> anyhow::Result<()> {
487 log::debug!("Connecting...");
488
489 if self.cancellation_token.is_cancelled() {
490 self.cancellation_token = CancellationToken::new();
491 }
492
493 self.is_connected.store(true, Ordering::Relaxed);
494
495 log::info!("Connected");
496 Ok(())
497 }
498
499 async fn disconnect(&mut self) -> anyhow::Result<()> {
500 log::debug!("Disconnecting...");
501
502 self.send_close_to_active_feeds();
503 self.clear_feed_channels();
504
505 for handle in self.task_handles.take_all() {
506 if let Err(e) = handle.await
507 && !e.is_cancelled()
508 {
509 log::error!("Task join error: {e}");
510 }
511 }
512
513 self.is_connected.store(false, Ordering::Relaxed);
514 self.cancellation_token = CancellationToken::new();
515
516 log::info!("Disconnected");
517 Ok(())
518 }
519
520 fn is_connected(&self) -> bool {
522 self.is_connected.load(Ordering::Relaxed)
523 }
524
525 fn is_disconnected(&self) -> bool {
526 !self.is_connected()
527 }
528
529 fn subscribe_instrument(&mut self, cmd: SubscribeInstrument) -> anyhow::Result<()> {
535 let dataset = self.get_dataset_for_venue(cmd.instrument_id.venue)?;
536 let start_after_subscribe = self.get_or_create_feed_handler(&dataset);
537
538 self.symbol_venue_map
539 .insert(cmd.instrument_id.symbol, cmd.instrument_id.venue);
540 let symbol = cmd.instrument_id.symbol.to_string();
541
542 let subscription = Subscription::builder()
543 .schema(databento::dbn::Schema::Definition)
544 .symbols(symbol)
545 .build();
546
547 self.send_subscription_to_dataset(&dataset, None, subscription, start_after_subscribe)?;
548
549 Ok(())
550 }
551
552 fn subscribe_quotes(&mut self, cmd: SubscribeQuotes) -> anyhow::Result<()> {
558 let dataset = self.get_dataset_for_venue(cmd.instrument_id.venue)?;
559 let symbol = cmd.instrument_id.symbol.to_string();
560 let price_precision = price_precision_from_params(cmd.params.as_ref())?
561 .map(|precision| (cmd.instrument_id.symbol, precision));
562 let schema = schema_from_params(cmd.params.as_ref(), dbn::Schema::Mbp1, QUOTE_SCHEMAS)?;
563
564 let subscription = Subscription::builder()
565 .schema(schema)
566 .symbols(symbol)
567 .build();
568
569 let start_after_subscribe = self.get_or_create_feed_handler(&dataset);
570 self.symbol_venue_map
571 .insert(cmd.instrument_id.symbol, cmd.instrument_id.venue);
572
573 self.send_subscription_to_dataset(
574 &dataset,
575 price_precision,
576 subscription,
577 start_after_subscribe,
578 )?;
579
580 Ok(())
581 }
582
583 fn subscribe_trades(&mut self, cmd: SubscribeTrades) -> anyhow::Result<()> {
589 let dataset = self.get_dataset_for_venue(cmd.instrument_id.venue)?;
590 let symbol = cmd.instrument_id.symbol.to_string();
591 let price_precision = price_precision_from_params(cmd.params.as_ref())?
592 .map(|precision| (cmd.instrument_id.symbol, precision));
593 let schema = schema_from_params(cmd.params.as_ref(), dbn::Schema::Trades, TRADE_SCHEMAS)?;
594
595 let subscription = Subscription::builder()
596 .schema(schema)
597 .symbols(symbol)
598 .build();
599
600 let start_after_subscribe = self.get_or_create_feed_handler(&dataset);
601 self.symbol_venue_map
602 .insert(cmd.instrument_id.symbol, cmd.instrument_id.venue);
603
604 self.send_subscription_to_dataset(
605 &dataset,
606 price_precision,
607 subscription,
608 start_after_subscribe,
609 )?;
610
611 Ok(())
612 }
613
614 fn subscribe_book_deltas(&mut self, cmd: SubscribeBookDeltas) -> anyhow::Result<()> {
620 let dataset = self.get_dataset_for_venue(cmd.instrument_id.venue)?;
621 let start_after_subscribe = self.get_or_create_feed_handler(&dataset);
622
623 self.symbol_venue_map
624 .insert(cmd.instrument_id.symbol, cmd.instrument_id.venue);
625 let symbol = cmd.instrument_id.symbol.to_string();
626
627 let subscription = Subscription::builder()
628 .schema(databento::dbn::Schema::Mbo) .symbols(symbol)
630 .build();
631
632 self.send_subscription_to_dataset(&dataset, None, subscription, start_after_subscribe)?;
633
634 Ok(())
635 }
636
637 fn subscribe_instrument_status(
643 &mut self,
644 cmd: SubscribeInstrumentStatus,
645 ) -> anyhow::Result<()> {
646 let dataset = self.get_dataset_for_venue(cmd.instrument_id.venue)?;
647 let start_after_subscribe = self.get_or_create_feed_handler(&dataset);
648
649 self.symbol_venue_map
650 .insert(cmd.instrument_id.symbol, cmd.instrument_id.venue);
651 let symbol = cmd.instrument_id.symbol.to_string();
652
653 let subscription = Subscription::builder()
654 .schema(databento::dbn::Schema::Status)
655 .symbols(symbol)
656 .build();
657
658 self.send_subscription_to_dataset(&dataset, None, subscription, start_after_subscribe)?;
659
660 Ok(())
661 }
662
663 fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
665 log::warn!(
669 "Databento does not support granular unsubscribing - ignoring unsubscribe request for {}",
670 cmd.instrument_id
671 );
672
673 Ok(())
674 }
675
676 fn unsubscribe_trades(&mut self, cmd: &UnsubscribeTrades) -> anyhow::Result<()> {
677 log::warn!(
681 "Databento does not support granular unsubscribing - ignoring unsubscribe request for {}",
682 cmd.instrument_id
683 );
684
685 Ok(())
686 }
687
688 fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> anyhow::Result<()> {
689 log::warn!(
693 "Databento does not support granular unsubscribing - ignoring unsubscribe request for {}",
694 cmd.instrument_id
695 );
696
697 Ok(())
698 }
699
700 fn unsubscribe_instrument_status(
701 &mut self,
702 cmd: &UnsubscribeInstrumentStatus,
703 ) -> anyhow::Result<()> {
704 log::warn!(
708 "Databento does not support granular unsubscribing - ignoring unsubscribe request for {}",
709 cmd.instrument_id
710 );
711
712 Ok(())
713 }
714
715 fn request_instruments(&self, request: RequestInstruments) -> anyhow::Result<()> {
716 log::debug!("Request instruments: {request:?}");
717
718 let historical_client = self.historical.clone();
719 let data_sender = self.data_sender.clone();
720 let dataset = request
721 .venue
722 .map(|venue| self.get_dataset_for_venue(venue))
723 .transpose()?
724 .unwrap_or_else(|| "GLBX.MDP3".to_string());
725 let request_id = request.request_id;
726 let client_id = request.client_id.unwrap_or(self.client_id);
727 let venue = request.venue.unwrap_or(*DATABENTO_VENUE);
728 let start_nanos = datetime_to_unix_nanos(request.start);
729 let end_nanos = datetime_to_unix_nanos(request.end);
730 let request_params = request.params;
731 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
732
733 get_runtime().spawn(async move {
734 let query_params = instruments_query_params(dataset, query_start, query_end);
735
736 match historical_client.get_range_instruments(query_params).await {
737 Ok(instruments) => {
738 log::debug!("Retrieved {} instruments", instruments.len());
739
740 let response = DataResponse::Instruments(InstrumentsResponse::new(
741 request_id,
742 client_id,
743 venue,
744 instruments,
745 start_nanos,
746 end_nanos,
747 get_atomic_clock_realtime().get_time_ns(),
748 request_params,
749 ));
750
751 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
752 log::error!("Failed to send instruments response: {e}");
753 }
754 }
755 Err(e) => {
756 log::error!("Failed to request instruments: {e}");
757 let response = DataResponse::Instruments(InstrumentsResponse::new(
758 request_id,
759 client_id,
760 venue,
761 Vec::new(),
762 start_nanos,
763 end_nanos,
764 get_atomic_clock_realtime().get_time_ns(),
765 request_params,
766 ));
767
768 send_data_response(&data_sender, response, "empty instruments");
769 }
770 }
771 });
772
773 Ok(())
774 }
775
776 fn request_instrument(&self, request: RequestInstrument) -> anyhow::Result<()> {
777 log::debug!("Request instrument: {request:?}");
778
779 let dataset = self.get_dataset_for_venue(request.instrument_id.venue)?;
780 let historical_client = self.historical.clone();
781 let data_sender = self.data_sender.clone();
782 let instrument_id = request.instrument_id;
783 let request_id = request.request_id;
784 let client_id = request.client_id.unwrap_or(self.client_id);
785 let start_nanos = datetime_to_unix_nanos(request.start);
786 let end_nanos = datetime_to_unix_nanos(request.end);
787 let request_params = request.params;
788 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
789
790 get_runtime().spawn(async move {
791 let query_params =
792 instrument_query_params(dataset, instrument_id, query_start, query_end);
793
794 match historical_client.get_range_instruments(query_params).await {
795 Ok(instruments) => {
796 let instrument = requested_instrument(instruments, instrument_id);
797
798 let Some(instrument) = instrument else {
799 log::error!("Instrument not found: {instrument_id}");
800 return;
801 };
802
803 let response = DataResponse::Instrument(Box::new(InstrumentResponse::new(
804 request_id,
805 client_id,
806 instrument.id(),
807 instrument,
808 start_nanos,
809 end_nanos,
810 get_atomic_clock_realtime().get_time_ns(),
811 request_params,
812 )));
813
814 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
815 log::error!("Failed to send instrument response: {e}");
816 }
817 }
818 Err(e) => {
819 log::error!("Failed to request instrument {instrument_id}: {e}");
820 }
821 }
822 });
823
824 Ok(())
825 }
826
827 fn request_quotes(&self, request: RequestQuotes) -> anyhow::Result<()> {
828 log::debug!("Request quotes: {request:?}");
829
830 let historical_client = self.historical.clone();
831 let data_sender = self.data_sender.clone();
832 let dataset = self.get_dataset_for_venue(request.instrument_id.venue)?;
833 let instrument_id = request.instrument_id;
834 let symbols = historical_client.prepare_symbols_from_instrument_ids(&[instrument_id]);
835 let request_id = request.request_id;
836 let client_id = request.client_id.unwrap_or(self.client_id);
837 let start_nanos = datetime_to_unix_nanos(request.start);
838 let end_nanos = datetime_to_unix_nanos(request.end);
839 let limit = request.limit.map(|limit| limit.get() as u64);
840 let request_params = request.params;
841 let price_precision = price_precision_from_params(request_params.as_ref())?;
842 let schema = schema_from_params(request_params.as_ref(), dbn::Schema::Mbp1, QUOTE_SCHEMAS)?
843 .to_string();
844 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
845
846 get_runtime().spawn(async move {
847 seed_price_precision_if_needed(
848 &historical_client,
849 dataset.as_str(),
850 instrument_id,
851 query_start,
852 query_end,
853 price_precision,
854 )
855 .await;
856
857 let params = RangeQueryParams {
858 dataset,
859 symbols,
860 start: query_start,
861 end: query_end,
862 limit,
863 price_precision,
864 };
865
866 match historical_client
867 .get_range_quotes(params, Some(schema))
868 .await
869 {
870 Ok(quotes) => {
871 log::debug!("Retrieved {} quotes", quotes.len());
872 let response = DataResponse::Quotes(QuotesResponse::new(
873 request_id,
874 client_id,
875 instrument_id,
876 quotes,
877 start_nanos,
878 end_nanos,
879 get_atomic_clock_realtime().get_time_ns(),
880 request_params,
881 ));
882
883 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
884 log::error!("Failed to send quotes response: {e}");
885 }
886 }
887 Err(e) => {
888 log::error!("Failed to request quotes: {e}");
889 let response = DataResponse::Quotes(QuotesResponse::new(
890 request_id,
891 client_id,
892 instrument_id,
893 Vec::new(),
894 start_nanos,
895 end_nanos,
896 get_atomic_clock_realtime().get_time_ns(),
897 request_params,
898 ));
899
900 send_data_response(&data_sender, response, "empty quotes");
901 }
902 }
903 });
904
905 Ok(())
906 }
907
908 fn request_trades(&self, request: RequestTrades) -> anyhow::Result<()> {
909 log::debug!("Request trades: {request:?}");
910
911 let historical_client = self.historical.clone();
912 let data_sender = self.data_sender.clone();
913 let dataset = self.get_dataset_for_venue(request.instrument_id.venue)?;
914 let instrument_id = request.instrument_id;
915 let symbols = historical_client.prepare_symbols_from_instrument_ids(&[instrument_id]);
916 let request_id = request.request_id;
917 let client_id = request.client_id.unwrap_or(self.client_id);
918 let start_nanos = datetime_to_unix_nanos(request.start);
919 let end_nanos = datetime_to_unix_nanos(request.end);
920 let limit = request.limit.map(|limit| limit.get() as u64);
921 let request_params = request.params;
922 let price_precision = price_precision_from_params(request_params.as_ref())?;
923 let schema =
924 schema_from_params(request_params.as_ref(), dbn::Schema::Trades, TRADE_SCHEMAS)?
925 .to_string();
926 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
927
928 get_runtime().spawn(async move {
929 seed_price_precision_if_needed(
930 &historical_client,
931 dataset.as_str(),
932 instrument_id,
933 query_start,
934 query_end,
935 price_precision,
936 )
937 .await;
938
939 let params = RangeQueryParams {
940 dataset,
941 symbols,
942 start: query_start,
943 end: query_end,
944 limit,
945 price_precision,
946 };
947
948 match historical_client
949 .get_range_trades(params, Some(schema))
950 .await
951 {
952 Ok(trades) => {
953 log::debug!("Retrieved {} trades", trades.len());
954 let response = DataResponse::Trades(TradesResponse::new(
955 request_id,
956 client_id,
957 instrument_id,
958 trades,
959 start_nanos,
960 end_nanos,
961 get_atomic_clock_realtime().get_time_ns(),
962 request_params,
963 ));
964
965 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
966 log::error!("Failed to send trades response: {e}");
967 }
968 }
969 Err(e) => {
970 log::error!("Failed to request trades: {e}");
971 let response = DataResponse::Trades(TradesResponse::new(
972 request_id,
973 client_id,
974 instrument_id,
975 Vec::new(),
976 start_nanos,
977 end_nanos,
978 get_atomic_clock_realtime().get_time_ns(),
979 request_params,
980 ));
981
982 send_data_response(&data_sender, response, "empty trades");
983 }
984 }
985 });
986
987 Ok(())
988 }
989
990 fn request_bars(&self, request: RequestBars) -> anyhow::Result<()> {
991 log::debug!("Request bars: {request:?}");
992
993 let historical_client = self.historical.clone();
994 let data_sender = self.data_sender.clone();
995 let instrument_id = request.bar_type.instrument_id();
996 let dataset = self.get_dataset_for_venue(instrument_id.venue)?;
997 let symbols = historical_client.prepare_symbols_from_instrument_ids(&[instrument_id]);
998 let request_id = request.request_id;
999 let client_id = request.client_id.unwrap_or(self.client_id);
1000 let bar_type = request.bar_type;
1001 let start_nanos = datetime_to_unix_nanos(request.start);
1002 let end_nanos = datetime_to_unix_nanos(request.end);
1003 let limit = request.limit.map(|limit| limit.get() as u64);
1004 let request_params = request.params;
1005 let price_precision = price_precision_from_params(request_params.as_ref())?;
1006 let timestamp_on_close = self.config.bars_timestamp_on_close;
1007 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
1008
1009 get_runtime().spawn(async move {
1010 seed_price_precision_if_needed(
1011 &historical_client,
1012 dataset.as_str(),
1013 instrument_id,
1014 query_start,
1015 query_end,
1016 price_precision,
1017 )
1018 .await;
1019
1020 let params = RangeQueryParams {
1021 dataset,
1022 symbols,
1023 start: query_start,
1024 end: query_end,
1025 limit,
1026 price_precision,
1027 };
1028
1029 let aggregation = match bar_type.spec().aggregation {
1030 BarAggregation::Second => BarAggregation::Second,
1031 BarAggregation::Minute => BarAggregation::Minute,
1032 BarAggregation::Hour => BarAggregation::Hour,
1033 BarAggregation::Day => BarAggregation::Day,
1034 _ => {
1035 log::error!(
1036 "Unsupported bar aggregation: {:?}",
1037 bar_type.spec().aggregation
1038 );
1039 let response = DataResponse::Bars(BarsResponse::new(
1040 request_id,
1041 client_id,
1042 bar_type,
1043 Vec::new(),
1044 start_nanos,
1045 end_nanos,
1046 get_atomic_clock_realtime().get_time_ns(),
1047 request_params,
1048 ));
1049
1050 send_data_response(&data_sender, response, "empty bars");
1051 return;
1052 }
1053 };
1054
1055 match historical_client
1056 .get_range_bars(params, aggregation, timestamp_on_close)
1057 .await
1058 {
1059 Ok(bars) => {
1060 log::debug!("Retrieved {} bars", bars.len());
1061 let response = DataResponse::Bars(BarsResponse::new(
1062 request_id,
1063 client_id,
1064 bar_type,
1065 bars,
1066 start_nanos,
1067 end_nanos,
1068 get_atomic_clock_realtime().get_time_ns(),
1069 request_params,
1070 ));
1071
1072 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
1073 log::error!("Failed to send bars response: {e}");
1074 }
1075 }
1076 Err(e) => {
1077 log::error!("Failed to request bars: {e}");
1078 let response = DataResponse::Bars(BarsResponse::new(
1079 request_id,
1080 client_id,
1081 bar_type,
1082 Vec::new(),
1083 start_nanos,
1084 end_nanos,
1085 get_atomic_clock_realtime().get_time_ns(),
1086 request_params,
1087 ));
1088
1089 send_data_response(&data_sender, response, "empty bars");
1090 }
1091 }
1092 });
1093
1094 Ok(())
1095 }
1096
1097 fn request_book_depth(&self, request: RequestBookDepth) -> anyhow::Result<()> {
1098 log::debug!("Request book depth: {request:?}");
1099
1100 let historical_client = self.historical.clone();
1101 let data_sender = self.data_sender.clone();
1102 let dataset = self.get_dataset_for_venue(request.instrument_id.venue)?;
1103 let instrument_id = request.instrument_id;
1104 let symbols = historical_client.prepare_symbols_from_instrument_ids(&[instrument_id]);
1105 let request_id = request.request_id;
1106 let client_id = request.client_id.unwrap_or(self.client_id);
1107 let start_nanos = datetime_to_unix_nanos(request.start);
1108 let end_nanos = datetime_to_unix_nanos(request.end);
1109 let limit = request.limit.map(|limit| limit.get() as u64);
1110 let depth = request.depth.map(|depth| depth.get());
1111 let request_params = request.params;
1112 let price_precision = price_precision_from_params(request_params.as_ref())?;
1113 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
1114
1115 get_runtime().spawn(async move {
1116 seed_price_precision_if_needed(
1117 &historical_client,
1118 dataset.as_str(),
1119 instrument_id,
1120 query_start,
1121 query_end,
1122 price_precision,
1123 )
1124 .await;
1125
1126 let params = RangeQueryParams {
1127 dataset,
1128 symbols,
1129 start: query_start,
1130 end: query_end,
1131 limit,
1132 price_precision,
1133 };
1134
1135 match historical_client
1136 .get_range_order_book_depth10(params, depth)
1137 .await
1138 {
1139 Ok(depths) => {
1140 log::debug!("Retrieved {} order book depths", depths.len());
1141 let response = DataResponse::BookDepth(BookDepthResponse::new(
1142 request_id,
1143 client_id,
1144 instrument_id,
1145 depths,
1146 start_nanos,
1147 end_nanos,
1148 get_atomic_clock_realtime().get_time_ns(),
1149 request_params,
1150 ));
1151
1152 send_data_response(&data_sender, response, "book depth");
1153 }
1154 Err(e) => {
1155 log::error!("Failed to request order book depths: {e}");
1156 let response = DataResponse::BookDepth(BookDepthResponse::new(
1157 request_id,
1158 client_id,
1159 instrument_id,
1160 Vec::new(),
1161 start_nanos,
1162 end_nanos,
1163 get_atomic_clock_realtime().get_time_ns(),
1164 request_params,
1165 ));
1166
1167 send_data_response(&data_sender, response, "empty book depth");
1168 }
1169 }
1170 });
1171
1172 Ok(())
1173 }
1174
1175 fn request_book_deltas(&self, request: RequestBookDeltas) -> anyhow::Result<()> {
1176 log::debug!("Request book deltas: {request:?}");
1177
1178 let historical_client = self.historical.clone();
1179 let data_sender = self.data_sender.clone();
1180 let dataset = self.get_dataset_for_venue(request.instrument_id.venue)?;
1181 let instrument_id = request.instrument_id;
1182 let symbols = historical_client.prepare_symbols_from_instrument_ids(&[instrument_id]);
1183 let request_id = request.request_id;
1184 let client_id = request.client_id.unwrap_or(self.client_id);
1185 let start_nanos = datetime_to_unix_nanos(request.start);
1186 let end_nanos = datetime_to_unix_nanos(request.end);
1187 let limit = request.limit.map(|limit| limit.get() as u64);
1188 let request_params = request.params;
1189 let price_precision = price_precision_from_params(request_params.as_ref())?;
1190 let (query_start, query_end) = resolve_request_time_range(start_nanos, end_nanos);
1191
1192 get_runtime().spawn(async move {
1193 seed_price_precision_if_needed(
1194 &historical_client,
1195 dataset.as_str(),
1196 instrument_id,
1197 query_start,
1198 query_end,
1199 price_precision,
1200 )
1201 .await;
1202
1203 let params = RangeQueryParams {
1204 dataset,
1205 symbols,
1206 start: query_start,
1207 end: query_end,
1208 limit,
1209 price_precision,
1210 };
1211
1212 match historical_client.get_range_order_book_deltas(params).await {
1213 Ok(deltas) => {
1214 log::debug!("Retrieved {} order book deltas", deltas.len());
1215 let response = DataResponse::BookDeltas(BookDeltasResponse::new(
1216 request_id,
1217 client_id,
1218 instrument_id,
1219 deltas,
1220 start_nanos,
1221 end_nanos,
1222 get_atomic_clock_realtime().get_time_ns(),
1223 request_params,
1224 ));
1225
1226 send_data_response(&data_sender, response, "book deltas");
1227 }
1228 Err(e) => {
1229 log::error!("Failed to request order book deltas: {e}");
1230 let response = DataResponse::BookDeltas(BookDeltasResponse::new(
1231 request_id,
1232 client_id,
1233 instrument_id,
1234 Vec::new(),
1235 start_nanos,
1236 end_nanos,
1237 get_atomic_clock_realtime().get_time_ns(),
1238 request_params,
1239 ));
1240
1241 send_data_response(&data_sender, response, "empty book deltas");
1242 }
1243 }
1244 });
1245
1246 Ok(())
1247 }
1248}
1249
1250fn instruments_query_params(
1251 dataset: String,
1252 start_nanos: UnixNanos,
1253 end_nanos: Option<UnixNanos>,
1254) -> RangeQueryParams {
1255 RangeQueryParams {
1256 dataset,
1257 symbols: vec!["ALL_SYMBOLS".to_string()],
1258 start: start_nanos,
1259 end: end_nanos,
1260 limit: None,
1261 price_precision: None,
1262 }
1263}
1264
1265fn instrument_query_params(
1266 dataset: String,
1267 instrument_id: InstrumentId,
1268 start_nanos: UnixNanos,
1269 end_nanos: Option<UnixNanos>,
1270) -> RangeQueryParams {
1271 RangeQueryParams {
1272 dataset,
1273 symbols: vec![instrument_id_to_symbol_string(
1274 instrument_id,
1275 &mut AHashMap::new(),
1276 )],
1277 start: start_nanos,
1278 end: end_nanos,
1279 limit: None,
1280 price_precision: None,
1281 }
1282}
1283
1284fn resolve_request_time_range(
1285 start_nanos: Option<UnixNanos>,
1286 end_nanos: Option<UnixNanos>,
1287) -> (UnixNanos, Option<UnixNanos>) {
1288 let mut end = end_nanos.unwrap_or_else(|| get_atomic_clock_realtime().get_time_ns());
1289 let mut start = start_nanos.unwrap_or_else(|| start_of_utc_day(end));
1290
1291 if start > end {
1292 start = end;
1293 }
1294
1295 if start == end {
1296 if end.as_u64() > 0 {
1297 start = UnixNanos::from(end.as_u64() - 1);
1298 } else {
1299 end = UnixNanos::from(1);
1300 }
1301 }
1302
1303 (start, Some(end))
1304}
1305
1306fn start_of_utc_day(timestamp: UnixNanos) -> UnixNanos {
1307 UnixNanos::from((timestamp.as_u64() / NANOSECONDS_IN_DAY) * NANOSECONDS_IN_DAY)
1308}
1309
1310async fn seed_price_precision_if_needed(
1311 historical_client: &DatabentoHistoricalClient,
1312 dataset: &str,
1313 instrument_id: InstrumentId,
1314 start_nanos: UnixNanos,
1315 end_nanos: Option<UnixNanos>,
1316 price_precision: Option<u8>,
1317) {
1318 if price_precision.is_some()
1319 || historical_client
1320 .price_precision(instrument_id.symbol)
1321 .is_some()
1322 {
1323 return;
1324 }
1325
1326 let query_params =
1327 instrument_query_params(dataset.to_string(), instrument_id, start_nanos, end_nanos);
1328
1329 if let Err(e) = historical_client.get_range_instruments(query_params).await {
1330 log::warn!("Failed to seed price precision for {instrument_id}: {e}");
1331 }
1332}
1333
1334fn send_data_response(
1335 data_sender: &tokio::sync::mpsc::UnboundedSender<DataEvent>,
1336 response: DataResponse,
1337 label: &str,
1338) {
1339 if let Err(e) = data_sender.send(DataEvent::Response(response)) {
1340 log::error!("Failed to send {label} response: {e}");
1341 }
1342}
1343
1344fn requested_instrument(
1345 instruments: Vec<InstrumentAny>,
1346 instrument_id: InstrumentId,
1347) -> Option<InstrumentAny> {
1348 instruments
1349 .into_iter()
1350 .rev()
1351 .find(|instrument| instrument.id() == instrument_id)
1352}
1353
1354fn price_precision_from_params(params: Option<&Params>) -> anyhow::Result<Option<u8>> {
1355 let Some(price_precision) = params.and_then(|params| params.get_u64(PRICE_PRECISION_PARAM))
1356 else {
1357 return Ok(None);
1358 };
1359
1360 Ok(Some(u8::try_from(price_precision).map_err(|_| {
1361 anyhow::anyhow!(
1362 "`{PRICE_PRECISION_PARAM}` must be less than or equal to {}",
1363 u8::MAX
1364 )
1365 })?))
1366}
1367
1368fn schema_from_params(
1369 params: Option<&Params>,
1370 default_schema: dbn::Schema,
1371 allowed_schemas: &[dbn::Schema],
1372) -> anyhow::Result<dbn::Schema> {
1373 let schema = if let Some(schema) = params.and_then(|params| params.get_str(SCHEMA_PARAM)) {
1374 dbn::Schema::from_str(schema)?
1375 } else {
1376 default_schema
1377 };
1378
1379 if allowed_schemas.contains(&schema) {
1380 return Ok(schema);
1381 }
1382
1383 let allowed = allowed_schemas
1384 .iter()
1385 .map(dbn::Schema::as_str)
1386 .collect::<Vec<_>>()
1387 .join(", ");
1388 anyhow::bail!(
1389 "Invalid `{SCHEMA_PARAM}` '{}'. Must be one of: {allowed}",
1390 schema.as_str()
1391 );
1392}
1393
1394fn send_subscription_commands(
1395 tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1396 dataset: &str,
1397 price_precision: Option<(Symbol, u8)>,
1398 subscription: Subscription,
1399 start_after_subscribe: bool,
1400) -> anyhow::Result<()> {
1401 if let Some((symbol, precision)) = price_precision {
1402 tx.send(HandlerCommand::SetPricePrecision(symbol, precision))
1403 .map_err(|e| anyhow::anyhow!("Failed to send command to dataset {dataset}: {e}"))?;
1404 }
1405
1406 tx.send(HandlerCommand::Subscribe(subscription))
1407 .map_err(|e| anyhow::anyhow!("Failed to send command to dataset {dataset}: {e}"))?;
1408
1409 if start_after_subscribe {
1410 tx.send(HandlerCommand::Start)
1411 .map_err(|e| anyhow::anyhow!("Failed to send command to dataset {dataset}: {e}"))?;
1412 }
1413
1414 Ok(())
1415}
1416
1417#[cfg(test)]
1418mod tests {
1419 use std::path::PathBuf;
1420
1421 use nautilus_common::live::runner::replace_data_event_sender;
1422 use nautilus_core::UUID4;
1423 use nautilus_model::{
1424 identifiers::{ClientId, InstrumentId},
1425 instruments::{CurrencyPair, InstrumentAny},
1426 types::{Currency, Price, Quantity},
1427 };
1428 use rstest::rstest;
1429 use serde_json::json;
1430
1431 use super::*;
1432
1433 #[derive(Clone, Copy)]
1434 enum SubscribeKind {
1435 Quotes,
1436 Trades,
1437 }
1438
1439 fn currency_pair(instrument_id: &str) -> InstrumentAny {
1440 currency_pair_with_ts_init(instrument_id, UnixNanos::default())
1441 }
1442
1443 fn test_data_client() -> DatabentoDataClient {
1444 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
1445 replace_data_event_sender(sender);
1446
1447 let config = DatabentoDataClientConfig::new(
1448 "32-character-with-lots-of-filler",
1449 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("publishers.json"),
1450 true,
1451 true,
1452 );
1453 DatabentoDataClient::new(
1454 ClientId::from("DATABENTO-TEST"),
1455 config,
1456 get_atomic_clock_realtime(),
1457 )
1458 .expect("test client should initialize")
1459 }
1460
1461 #[rstest]
1462 #[tokio::test]
1463 async fn test_stop_aborts_active_tasks_and_marks_disconnected() {
1464 let mut client = test_data_client();
1465
1466 let handle = tokio::spawn(async { std::future::pending::<()>().await });
1467 client.task_handles.push(handle);
1468 client.is_connected.store(true, Ordering::Relaxed);
1469
1470 client.stop().unwrap();
1471
1472 assert!(client.task_handles.is_empty());
1473 assert!(client.is_disconnected());
1474 }
1475
1476 #[rstest]
1477 #[case("EQUS", "EQUS.PLUS")] #[case("GLBX", "EQUS.MINI")] fn test_venue_dataset_map_overrides_default(#[case] venue: &str, #[case] dataset: &str) {
1480 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel::<DataEvent>();
1481 replace_data_event_sender(sender);
1482
1483 let mut config = DatabentoDataClientConfig::new(
1484 "32-character-with-lots-of-filler",
1485 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("publishers.json"),
1486 true,
1487 true,
1488 );
1489 config.venue_dataset_map = IndexMap::from([(venue.to_string(), dataset.to_string())]);
1490
1491 let client = DatabentoDataClient::new(
1492 ClientId::from("DATABENTO-TEST"),
1493 config,
1494 get_atomic_clock_realtime(),
1495 )
1496 .expect("test client should initialize");
1497
1498 assert_eq!(
1499 client.get_dataset_for_venue(Venue::from(venue)).unwrap(),
1500 dataset
1501 );
1502
1503 assert_eq!(
1505 client.get_dataset_for_venue(Venue::from("XCBO")).unwrap(),
1506 "OPRA.PILLAR"
1507 );
1508 }
1509
1510 fn subscribe_quotes_cmd(params: Option<Params>) -> SubscribeQuotes {
1511 SubscribeQuotes::new(
1512 InstrumentId::from("ESM4.GLBX"),
1513 Some(ClientId::from("DATABENTO-TEST")),
1514 None,
1515 UUID4::new(),
1516 UnixNanos::default(),
1517 None,
1518 params,
1519 )
1520 }
1521
1522 fn subscribe_trades_cmd(params: Option<Params>) -> SubscribeTrades {
1523 SubscribeTrades::new(
1524 InstrumentId::from("ESM4.GLBX"),
1525 Some(ClientId::from("DATABENTO-TEST")),
1526 None,
1527 UUID4::new(),
1528 UnixNanos::default(),
1529 None,
1530 params,
1531 )
1532 }
1533
1534 fn currency_pair_with_ts_init(instrument_id: &str, ts_init: UnixNanos) -> InstrumentAny {
1535 let instrument_id = InstrumentId::from(instrument_id);
1536 InstrumentAny::CurrencyPair(CurrencyPair::new(
1537 instrument_id,
1538 instrument_id.symbol,
1539 Currency::from("BTC"),
1540 Currency::from("USDT"),
1541 2,
1542 6,
1543 Price::from("0.01"),
1544 Quantity::from("0.000001"),
1545 None,
1546 None,
1547 None,
1548 None,
1549 None,
1550 None,
1551 None,
1552 None,
1553 None,
1554 None,
1555 None,
1556 None,
1557 None,
1558 None,
1559 UnixNanos::default(),
1560 ts_init,
1561 ))
1562 }
1563
1564 #[rstest]
1565 fn test_instruments_query_params_requests_all_symbols() {
1566 let start = UnixNanos::from(1_000_000_000);
1567 let end = UnixNanos::from(2_000_000_000);
1568
1569 let params = instruments_query_params("GLBX.MDP3".to_string(), start, Some(end));
1570
1571 assert_eq!(params.dataset, "GLBX.MDP3");
1572 assert_eq!(params.symbols, vec!["ALL_SYMBOLS"]);
1573 assert_eq!(params.start, start);
1574 assert_eq!(params.end, Some(end));
1575 assert_eq!(params.limit, None);
1576 assert_eq!(params.price_precision, None);
1577 }
1578
1579 #[rstest]
1580 fn test_instrument_query_params_requests_single_symbol() {
1581 let instrument_id = InstrumentId::from("ESM4.GLBX");
1582
1583 let start = UnixNanos::from(1_000_000_000);
1584 let end = UnixNanos::from(2_000_000_000);
1585
1586 let params =
1587 instrument_query_params("GLBX.MDP3".to_string(), instrument_id, start, Some(end));
1588
1589 assert_eq!(params.dataset, "GLBX.MDP3");
1590 assert_eq!(params.symbols, vec!["ESM4"]);
1591 assert_eq!(params.start, start);
1592 assert_eq!(params.end, Some(end));
1593 assert_eq!(params.limit, None);
1594 assert_eq!(params.price_precision, None);
1595 }
1596
1597 #[rstest]
1598 fn test_resolve_request_time_range_defaults_to_end_day() {
1599 let end = UnixNanos::from(1_706_443_200_000_000_001);
1600
1601 let (start, resolved_end) = resolve_request_time_range(None, Some(end));
1602
1603 assert_eq!(start, UnixNanos::from(1_706_400_000_000_000_000));
1604 assert_eq!(resolved_end, Some(end));
1605 }
1606
1607 #[rstest]
1608 fn test_resolve_request_time_range_makes_empty_interval_non_empty() {
1609 let end = UnixNanos::from(1_706_443_200_000_000_001);
1610
1611 let (start, resolved_end) = resolve_request_time_range(Some(end), Some(end));
1612
1613 assert_eq!(start, UnixNanos::from(end.as_u64() - 1));
1614 assert_eq!(resolved_end, Some(end));
1615 }
1616
1617 #[rstest]
1618 fn test_requested_instrument_filters_exact_id() {
1619 let requested_id = InstrumentId::from("BTCUSDT.BINANCE");
1620 let instruments = vec![
1621 currency_pair("ETHUSDT.BINANCE"),
1622 currency_pair("BTCUSDT.BINANCE"),
1623 ];
1624
1625 let instrument = requested_instrument(instruments, requested_id).expect("instrument");
1626
1627 assert_eq!(instrument.id(), requested_id);
1628 }
1629
1630 #[rstest]
1631 fn test_requested_instrument_returns_latest_matching_id() {
1632 let requested_id = InstrumentId::from("BTCUSDT.BINANCE");
1633 let instruments = vec![
1634 currency_pair_with_ts_init("BTCUSDT.BINANCE", UnixNanos::from(1)),
1635 currency_pair_with_ts_init("BTCUSDT.BINANCE", UnixNanos::from(2)),
1636 ];
1637
1638 let instrument = requested_instrument(instruments, requested_id).expect("instrument");
1639
1640 assert_eq!(instrument.ts_init(), UnixNanos::from(2));
1641 }
1642
1643 #[rstest]
1644 fn test_requested_instrument_returns_none_on_miss() {
1645 let instruments = vec![currency_pair("ETHUSDT.BINANCE")];
1646
1647 let instrument = requested_instrument(instruments, InstrumentId::from("BTCUSDT.BINANCE"));
1648
1649 assert!(instrument.is_none());
1650 }
1651
1652 #[rstest]
1653 fn test_price_precision_from_params() {
1654 let mut params = Params::new();
1655 params.insert(PRICE_PRECISION_PARAM.to_string(), json!(5));
1656
1657 let price_precision = price_precision_from_params(Some(¶ms)).unwrap();
1658
1659 assert_eq!(price_precision, Some(5));
1660 }
1661
1662 #[rstest]
1663 fn test_price_precision_from_params_rejects_out_of_range_value() {
1664 let mut params = Params::new();
1665 params.insert(
1666 PRICE_PRECISION_PARAM.to_string(),
1667 json!(u64::from(u8::MAX) + 1),
1668 );
1669
1670 let result = price_precision_from_params(Some(¶ms));
1671
1672 assert!(result.is_err());
1673 }
1674
1675 #[rstest]
1676 fn test_schema_from_params_returns_default() {
1677 let schema = schema_from_params(None, dbn::Schema::Mbp1, QUOTE_SCHEMAS).unwrap();
1678
1679 assert_eq!(schema, dbn::Schema::Mbp1);
1680 }
1681
1682 #[rstest]
1683 fn test_schema_from_params_accepts_allowed_value() {
1684 let mut params = Params::new();
1685 params.insert(SCHEMA_PARAM.to_string(), json!("tbbo"));
1686
1687 let schema = schema_from_params(Some(¶ms), dbn::Schema::Mbp1, QUOTE_SCHEMAS).unwrap();
1688
1689 assert_eq!(schema, dbn::Schema::Tbbo);
1690 }
1691
1692 #[rstest]
1693 fn test_schema_from_params_rejects_disallowed_value() {
1694 let mut params = Params::new();
1695 params.insert(SCHEMA_PARAM.to_string(), json!("mbo"));
1696
1697 let result = schema_from_params(Some(¶ms), dbn::Schema::Mbp1, QUOTE_SCHEMAS);
1698
1699 assert!(result.is_err());
1700 }
1701
1702 #[rstest]
1703 #[case::quotes(SubscribeKind::Quotes)]
1704 #[case::trades(SubscribeKind::Trades)]
1705 fn test_invalid_subscribe_params_do_not_create_feed_handler(#[case] kind: SubscribeKind) {
1706 let mut client = test_data_client();
1707 let mut params = Params::new();
1708 params.insert(SCHEMA_PARAM.to_string(), json!("definition"));
1709
1710 let result = match kind {
1711 SubscribeKind::Quotes => client.subscribe_quotes(subscribe_quotes_cmd(Some(params))),
1712 SubscribeKind::Trades => client.subscribe_trades(subscribe_trades_cmd(Some(params))),
1713 };
1714
1715 assert!(result.is_err());
1716 assert!(client.cmd_channels.lock().expect(MUTEX_POISONED).is_empty());
1717 }
1718
1719 #[rstest]
1720 fn test_send_subscription_commands_starts_after_subscribe() {
1721 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
1722 let subscription = Subscription::builder()
1723 .schema(dbn::Schema::Mbp1)
1724 .symbols(vec!["ESM4"])
1725 .build();
1726
1727 send_subscription_commands(
1728 &tx,
1729 "GLBX.MDP3",
1730 Some((Symbol::from("ESM4"), 2)),
1731 subscription,
1732 true,
1733 )
1734 .unwrap();
1735
1736 assert!(matches!(
1737 rx.try_recv().unwrap(),
1738 HandlerCommand::SetPricePrecision(symbol, 2) if symbol == Symbol::from("ESM4")
1739 ));
1740 assert!(matches!(
1741 rx.try_recv().unwrap(),
1742 HandlerCommand::Subscribe(sub) if sub.schema == dbn::Schema::Mbp1
1743 ));
1744 assert!(matches!(rx.try_recv().unwrap(), HandlerCommand::Start));
1745 }
1746
1747 #[rstest]
1748 fn test_send_subscription_commands_without_precision_or_start() {
1749 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
1750 let subscription = Subscription::builder()
1751 .schema(dbn::Schema::Mbp1)
1752 .symbols(vec!["ESM4"])
1753 .build();
1754
1755 send_subscription_commands(&tx, "GLBX.MDP3", None, subscription, false).unwrap();
1756
1757 assert!(matches!(
1758 rx.try_recv().unwrap(),
1759 HandlerCommand::Subscribe(sub) if sub.schema == dbn::Schema::Mbp1
1760 ));
1761 assert!(matches!(
1762 rx.try_recv(),
1763 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
1764 ));
1765 }
1766}