Skip to main content

nautilus_databento/
data.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Provides a unified data client that combines Databento's live streaming and historical data capabilities.
17//!
18//! This module implements a data client that manages connections to multiple Databento datasets,
19//! handles live market data subscriptions, and provides access to historical data on demand.
20
21use 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/// Configuration for the Databento data client.
96#[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    /// Databento API credential.
107    pub(crate) credential: Credential,
108    /// Path to publishers.json file.
109    pub publishers_filepath: PathBuf,
110    /// Override for the Databento Historical API base URL.
111    pub historical_base_url: Option<String>,
112    /// Override for the Databento live gateway address.
113    pub live_gateway_addr: Option<String>,
114    /// Venue-to-dataset overrides applied on top of the publishers.json mappings.
115    pub venue_dataset_map: IndexMap<String, String>,
116    /// Whether to use exchange as venue for GLBX instruments.
117    pub use_exchange_as_venue: bool,
118    /// Whether to timestamp bars on close.
119    pub bars_timestamp_on_close: bool,
120    /// Reconnection timeout in minutes (None for infinite retries).
121    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    /// Creates a new [`DatabentoDataClientConfig`] instance.
136    #[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), // Default: 10 minutes
150            historical_base_url: None,
151            live_gateway_addr: None,
152        }
153    }
154
155    /// Returns the API key associated with this config.
156    #[must_use]
157    pub fn api_key(&self) -> &str {
158        self.credential.api_key()
159    }
160
161    /// Returns a masked version of the API key for logging purposes.
162    #[must_use]
163    pub fn api_key_masked(&self) -> String {
164        self.credential.api_key_masked()
165    }
166}
167
168/// A Databento data client that combines live streaming and historical data functionality.
169///
170/// This client uses the existing `DatabentoFeedHandler` for live data subscriptions
171/// and `DatabentoHistoricalClient` for historical data requests. It supports multiple
172/// datasets simultaneously, with separate feed handlers per dataset.
173#[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    /// Creates a new [`DatabentoDataClient`] instance.
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if client creation or publisher configuration loading fails.
199    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        // Create data loader for venue-to-dataset mapping
221        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        // Load publisher configuration
230        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    /// Returns the API key associated with this client.
259    #[must_use]
260    pub fn api_key(&self) -> &str {
261        self.config.api_key()
262    }
263
264    /// Returns a masked version of the API key for logging purposes.
265    #[must_use]
266    pub fn api_key_masked(&self) -> String {
267        self.config.api_key_masked()
268    }
269
270    /// Gets the dataset for a given venue using the data loader.
271    ///
272    /// # Errors
273    ///
274    /// Returns an error if the venue-to-dataset mapping cannot be found.
275    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    /// Gets or creates a feed handler for the specified dataset.
283    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        // Records resolve to the underlying contract, so a continuous or parent symbol would
329        // publish on topics that its subscriber does not listen on
330        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    /// Initializes the live feed handler for streaming data.
378    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            // run() logs its terminal error and forwards it as `DatabentoMessage::Error`
405            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        // Spawn message processing task with cancellation support
413        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    /// Returns the client identifier.
495    fn client_id(&self) -> ClientId {
496        self.client_id
497    }
498
499    /// Returns the venue associated with this client (None for multi-venue clients).
500    fn venue(&self) -> Option<Venue> {
501        None
502    }
503
504    /// Starts the data client.
505    ///
506    /// # Errors
507    ///
508    /// Returns an error if the client fails to start.
509    fn start(&mut self) -> anyhow::Result<()> {
510        log::debug!("Starting");
511        Ok(())
512    }
513
514    /// Stops the data client and cancels all active subscriptions.
515    ///
516    /// # Errors
517    ///
518    /// Returns an error if the client fails to stop cleanly.
519    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    /// Returns whether the client is currently connected.
585    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    /// Subscribes to instrument definition data for the specified instrument.
594    ///
595    /// # Errors
596    ///
597    /// Returns an error if the subscription request fails.
598    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    /// Subscribes to quote tick data for the specified instruments.
617    ///
618    /// # Errors
619    ///
620    /// Returns an error if the subscription request fails.
621    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    /// Subscribes to trade tick data for the specified instruments.
648    ///
649    /// # Errors
650    ///
651    /// Returns an error if the subscription request fails.
652    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    /// Subscribes to order book delta updates for the specified instruments.
679    ///
680    /// # Errors
681    ///
682    /// Returns an error if the subscription request fails.
683    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) // Market by order for book deltas
693            .symbols(symbol)
694            .build();
695
696        self.send_subscription_to_dataset(&dataset, None, subscription, start_after_subscribe)?;
697
698        Ok(())
699    }
700
701    /// Subscribes to `DatabentoStatistics` or `DatabentoImbalance` for the instrument given by the
702    /// identifier of the data type.
703    ///
704    /// # Errors
705    ///
706    /// Returns an error if the data type is not supported, has metadata, has a missing or invalid
707    /// instrument ID identifier, or the subscription request fails.
708    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    /// Subscribes to 10-level order book depth snapshots for the specified instrument.
715    ///
716    /// # Errors
717    ///
718    /// Returns an error if the requested depth is not 10, the symbol is not a raw symbol, or the
719    /// subscription request fails.
720    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    /// Subscribes to OHLCV bars for the specified bar type.
732    ///
733    /// # Errors
734    ///
735    /// Returns an error if the bar type has no matching OHLCV schema or the subscription request
736    /// fails.
737    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    /// Subscribes to instrument status updates for the specified instruments.
743    ///
744    /// # Errors
745    ///
746    /// Returns an error if the subscription request fails.
747    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    // Unsubscribe methods
769    fn unsubscribe_quotes(&mut self, cmd: &UnsubscribeQuotes) -> anyhow::Result<()> {
770        // Note: Databento live API doesn't support granular unsubscribing.
771        // The feed handler manages subscriptions and can handle reconnections
772        // with the appropriate subscription state.
773        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        // Note: Databento live API doesn't support granular unsubscribing.
783        // The feed handler manages subscriptions and can handle reconnections
784        // with the appropriate subscription state.
785        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        // Note: Databento live API doesn't support granular unsubscribing.
795        // The feed handler manages subscriptions and can handle reconnections
796        // with the appropriate subscription state.
797        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        // Note: Databento live API doesn't support granular unsubscribing.
807        // The feed handler manages subscriptions and can handle reconnections
808        // with the appropriate subscription state.
809        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        // Note: Databento live API doesn't support granular unsubscribing.
819        // The feed handler manages subscriptions and can handle reconnections
820        // with the appropriate subscription state.
821        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        // Note: Databento live API doesn't support granular unsubscribing.
831        // The feed handler manages subscriptions and can handle reconnections
832        // with the appropriate subscription state.
833        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        // Note: Databento live API doesn't support granular unsubscribing.
846        // The feed handler manages subscriptions and can handle reconnections
847        // with the appropriate subscription state.
848        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    // Records are published without metadata, so metadata would change the subscribed topic
1559    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")] // overrides the apply_default EQUS -> EQUS.MINI mapping
1724    #[case("GLBX", "EQUS.MINI")] // overrides the apply_default GLBX -> GLBX.MDP3 mapping
1725    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        // The override is targeted: an unrelated venue keeps its default.
1750        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(&params)).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(&params));
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(&params), 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(&params), 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}