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