1use std::{fmt::Debug, fs, num::NonZeroU64, path::PathBuf, str::FromStr, sync::Arc};
19
20use ahash::AHashMap;
21use databento::{
22 dbn::{self, decode::DbnMetadata},
23 historical::timeseries::GetRangeParams,
24};
25use indexmap::IndexMap;
26use nautilus_core::{AtomicMap, UnixNanos, consts::NAUTILUS_USER_AGENT, time::AtomicTime};
27use nautilus_model::{
28 data::{Bar, Data, InstrumentStatus, OrderBookDelta, OrderBookDepth10, QuoteTick, TradeTick},
29 enums::BarAggregation,
30 identifiers::{InstrumentId, Symbol, Venue},
31 instruments::{Instrument, InstrumentAny},
32};
33
34use crate::{
35 common::{Credential, get_date_time_range},
36 decode::{
37 decode_imbalance_msg, decode_instrument_def_msg, decode_mbo_msg, decode_mbp10_msg,
38 decode_record, decode_statistics_msg, decode_status_msg, is_supported_stat_type,
39 },
40 symbology::{
41 MetadataCache, check_consistent_symbology, decode_nautilus_instrument_id,
42 infer_symbology_type,
43 },
44 types::{DatabentoImbalance, DatabentoPublisher, DatabentoStatistics, PublisherId},
45};
46
47#[derive(Clone)]
52pub struct DatabentoHistoricalClient {
53 credential: Credential,
54 clock: &'static AtomicTime,
55 inner: Arc<databento::HistoricalClient>,
56 publisher_venue_map: Arc<IndexMap<PublisherId, Venue>>,
57 symbol_venue_map: Arc<AtomicMap<Symbol, Venue>>,
58 price_precisions: Arc<AtomicMap<Symbol, u8>>,
59 use_exchange_as_venue: bool,
60}
61
62#[derive(Debug)]
64pub struct RangeQueryParams {
65 pub dataset: String,
66 pub symbols: Vec<String>,
67 pub start: UnixNanos,
68 pub end: Option<UnixNanos>,
69 pub limit: Option<u64>,
70 pub price_precision: Option<u8>,
71}
72
73#[derive(Debug, Clone)]
75pub struct DatasetRange {
76 pub start: String,
77 pub end: String,
78}
79
80impl Debug for DatabentoHistoricalClient {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct(stringify!(DatabentoHistoricalClient))
83 .field("credential", &self.credential)
84 .finish()
85 }
86}
87
88impl DatabentoHistoricalClient {
89 #[must_use]
91 pub fn api_key(&self) -> &str {
92 self.credential.api_key()
93 }
94
95 pub fn new(
101 credential: Credential,
102 publishers_filepath: PathBuf,
103 clock: &'static AtomicTime,
104 use_exchange_as_venue: bool,
105 ) -> anyhow::Result<Self> {
106 let client = databento::HistoricalClient::builder()
107 .user_agent_extension(NAUTILUS_USER_AGENT.into())
108 .key(credential.api_key())
109 .map_err(|e| anyhow::anyhow!("Failed to create client builder: {e}"))?
110 .build()
111 .map_err(|e| anyhow::anyhow!("Failed to build client: {e}"))?;
112
113 Self::from_client(
114 credential,
115 publishers_filepath,
116 clock,
117 use_exchange_as_venue,
118 client,
119 )
120 }
121
122 pub fn new_with_base_url(
131 credential: Credential,
132 publishers_filepath: PathBuf,
133 clock: &'static AtomicTime,
134 use_exchange_as_venue: bool,
135 base_url: &str,
136 ) -> anyhow::Result<Self> {
137 let client = databento::HistoricalClient::builder()
138 .user_agent_extension(NAUTILUS_USER_AGENT.into())
139 .base_url(base_url.parse().map_err(|e| {
140 anyhow::anyhow!("Failed to parse Databento Historical API base URL: {e}")
141 })?)
142 .key(credential.api_key())
143 .map_err(|e| anyhow::anyhow!("Failed to create client builder: {e}"))?
144 .build()
145 .map_err(|e| anyhow::anyhow!("Failed to build client: {e}"))?;
146
147 Self::from_client(
148 credential,
149 publishers_filepath,
150 clock,
151 use_exchange_as_venue,
152 client,
153 )
154 }
155
156 fn from_client(
157 credential: Credential,
158 publishers_filepath: PathBuf,
159 clock: &'static AtomicTime,
160 use_exchange_as_venue: bool,
161 client: databento::HistoricalClient,
162 ) -> anyhow::Result<Self> {
163 let file_content = fs::read_to_string(publishers_filepath)?;
164 let publishers_vec: Vec<DatabentoPublisher> = serde_json::from_str(&file_content)?;
165
166 let publisher_venue_map = publishers_vec
167 .into_iter()
168 .map(|p| (p.publisher_id, Venue::from(p.venue.as_str())))
169 .collect::<IndexMap<u16, Venue>>();
170
171 Ok(Self {
172 clock,
173 inner: Arc::new(client),
174 publisher_venue_map: Arc::new(publisher_venue_map),
175 symbol_venue_map: Arc::new(AtomicMap::new()),
176 price_precisions: Arc::new(AtomicMap::new()),
177 credential,
178 use_exchange_as_venue,
179 })
180 }
181
182 pub fn set_price_precision(&self, symbol: Symbol, price_precision: u8) {
188 self.price_precisions.insert(symbol, price_precision);
189 }
190
191 #[must_use]
193 pub fn price_precision(&self, symbol: Symbol) -> Option<u8> {
194 self.price_precisions.load().get(&symbol).copied()
195 }
196
197 fn resolve_price_precision(
207 &self,
208 instrument_id: &InstrumentId,
209 price_precision: Option<u8>,
210 ) -> anyhow::Result<u8> {
211 if let Some(precision) = price_precision {
212 return Ok(precision);
213 }
214
215 let precisions = self.price_precisions.load();
216 precisions
217 .get(&instrument_id.symbol)
218 .copied()
219 .ok_or_else(|| {
220 anyhow::anyhow!(
221 "Could not resolve `price_precision` for {instrument_id}: \
222 pass `price_precision` explicitly, call `set_price_precision`, \
223 or fetch the instrument definitions first via `get_range_instruments`"
224 )
225 })
226 }
227
228 fn resolve_cached_price_precision(
229 &self,
230 instrument_id: &InstrumentId,
231 price_precision: Option<u8>,
232 precision_cache: &mut AHashMap<InstrumentId, u8>,
233 ) -> anyhow::Result<u8> {
234 if let Some(precision) = price_precision {
235 return Ok(precision);
236 }
237
238 if let Some(precision) = precision_cache.get(instrument_id) {
239 return Ok(*precision);
240 }
241
242 let precision = self.resolve_price_precision(instrument_id, None)?;
243 precision_cache.insert(*instrument_id, precision);
244 Ok(precision)
245 }
246
247 pub async fn get_dataset_range(&self, dataset: &str) -> anyhow::Result<DatasetRange> {
253 let mut client = (*self.inner).clone();
254 let response = client
255 .metadata()
256 .get_dataset_range(dataset)
257 .await
258 .map_err(|e| anyhow::anyhow!("Failed to get dataset range: {e}"))?;
259
260 Ok(DatasetRange {
261 start: response.start.to_string(),
262 end: response.end.to_string(),
263 })
264 }
265
266 pub async fn get_range_instruments(
272 &self,
273 params: RangeQueryParams,
274 ) -> anyhow::Result<Vec<InstrumentAny>> {
275 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
276 check_consistent_symbology(&symbols)?;
277
278 let first_symbol = params
279 .symbols
280 .first()
281 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
282 let stype_in = infer_symbology_type(first_symbol);
283 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
284 let time_range = get_date_time_range(params.start, end)?;
285
286 let range_params = GetRangeParams::builder()
287 .dataset(params.dataset)
288 .date_time_range(time_range)
289 .symbols(symbols)
290 .stype_in(stype_in)
291 .schema(dbn::Schema::Definition)
292 .maybe_limit(params.limit.and_then(NonZeroU64::new))
293 .build();
294
295 let mut client = (*self.inner).clone();
296 let mut decoder = client
297 .timeseries()
298 .get_range(&range_params)
299 .await
300 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
301
302 let metadata = decoder.metadata().clone();
303 let mut metadata_cache = MetadataCache::new(metadata);
304 let mut instruments = Vec::new();
305
306 while let Some(msg) = decoder.decode_record::<dbn::InstrumentDefMsg>().await? {
307 let record = dbn::RecordRef::from(msg);
308 let sym_map = self.symbol_venue_map.load();
309 let mut instrument_id = decode_nautilus_instrument_id(
310 &record,
311 &mut metadata_cache,
312 &self.publisher_venue_map,
313 &sym_map,
314 )?;
315
316 if self.use_exchange_as_venue && instrument_id.venue == Venue::GLBX() {
317 let exchange = msg
318 .exchange()
319 .map_err(|e| anyhow::anyhow!("Missing exchange in record: {e}"))?;
320 let venue = Venue::from_code(exchange)
321 .map_err(|e| anyhow::anyhow!("Venue not found for exchange {exchange}: {e}"))?;
322 instrument_id.venue = venue;
323 }
324
325 match decode_instrument_def_msg(msg, instrument_id, None, None) {
326 Ok(Some(instrument)) => instruments.push(instrument),
327 Ok(None) => {} Err(e) => anyhow::bail!("Failed to decode instrument {instrument_id}: {e}"),
329 }
330 }
331
332 for instrument in &instruments {
333 self.price_precisions
334 .insert(instrument.id().symbol, instrument.price_precision());
335 }
336
337 Ok(instruments)
338 }
339
340 pub async fn get_range_quotes(
346 &self,
347 params: RangeQueryParams,
348 schema: Option<String>,
349 ) -> anyhow::Result<Vec<QuoteTick>> {
350 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
351 check_consistent_symbology(&symbols)?;
352
353 let first_symbol = params
354 .symbols
355 .first()
356 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
357 let stype_in = infer_symbology_type(first_symbol);
358 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
359 let time_range = get_date_time_range(params.start, end)?;
360 let schema = schema.unwrap_or_else(|| "mbp-1".to_string());
361 let dbn_schema = dbn::Schema::from_str(&schema)?;
362
363 match dbn_schema {
364 dbn::Schema::Mbp1
365 | dbn::Schema::Tbbo
366 | dbn::Schema::Bbo1S
367 | dbn::Schema::Bbo1M
368 | dbn::Schema::Cmbp1
369 | dbn::Schema::Tcbbo
370 | dbn::Schema::Cbbo1S
371 | dbn::Schema::Cbbo1M => (),
372 _ => anyhow::bail!(
373 "Invalid schema. Must be one of: mbp-1, tbbo, bbo-1s, bbo-1m, cmbp-1, tcbbo, cbbo-1s, cbbo-1m"
374 ),
375 }
376
377 let range_params = GetRangeParams::builder()
378 .dataset(params.dataset)
379 .date_time_range(time_range)
380 .symbols(symbols)
381 .stype_in(stype_in)
382 .schema(dbn_schema)
383 .maybe_limit(params.limit.and_then(NonZeroU64::new))
384 .build();
385
386 let price_precision_arg = params.price_precision;
387
388 let mut client = (*self.inner).clone();
389 let mut decoder = client
390 .timeseries()
391 .get_range(&range_params)
392 .await
393 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
394
395 let metadata = decoder.metadata().clone();
396 let mut metadata_cache = MetadataCache::new(metadata);
397 let mut precision_cache = AHashMap::new();
398 let mut result: Vec<QuoteTick> = Vec::new();
399
400 let mut process_record = |record: dbn::RecordRef| -> anyhow::Result<()> {
401 let sym_map = self.symbol_venue_map.load();
402 let instrument_id = decode_nautilus_instrument_id(
403 &record,
404 &mut metadata_cache,
405 &self.publisher_venue_map,
406 &sym_map,
407 )?;
408 let price_precision = self.resolve_cached_price_precision(
409 &instrument_id,
410 price_precision_arg,
411 &mut precision_cache,
412 )?;
413
414 let (data, _) = decode_record(
415 &record,
416 instrument_id,
417 price_precision,
418 None,
419 false, true,
421 )?;
422
423 match data {
424 Some(Data::Quote(quote)) => result.push(quote),
425 None => {} _ => anyhow::bail!("Invalid data element not `QuoteTick`, was {data:?}"),
427 }
428 Ok(())
429 };
430
431 match dbn_schema {
432 dbn::Schema::Mbp1 => {
433 while let Some(msg) = decoder.decode_record::<dbn::Mbp1Msg>().await? {
434 process_record(dbn::RecordRef::from(msg))?;
435 }
436 }
437 dbn::Schema::Tbbo => {
438 while let Some(msg) = decoder.decode_record::<dbn::TbboMsg>().await? {
439 process_record(dbn::RecordRef::from(msg))?;
440 }
441 }
442 dbn::Schema::Cmbp1 => {
443 while let Some(msg) = decoder.decode_record::<dbn::Cmbp1Msg>().await? {
444 process_record(dbn::RecordRef::from(msg))?;
445 }
446 }
447 dbn::Schema::Tcbbo => {
448 while let Some(msg) = decoder.decode_record::<dbn::TcbboMsg>().await? {
449 process_record(dbn::RecordRef::from(msg))?;
450 }
451 }
452 dbn::Schema::Bbo1M => {
453 while let Some(msg) = decoder.decode_record::<dbn::Bbo1MMsg>().await? {
454 process_record(dbn::RecordRef::from(msg))?;
455 }
456 }
457 dbn::Schema::Bbo1S => {
458 while let Some(msg) = decoder.decode_record::<dbn::Bbo1SMsg>().await? {
459 process_record(dbn::RecordRef::from(msg))?;
460 }
461 }
462 dbn::Schema::Cbbo1S | dbn::Schema::Cbbo1M => {
463 while let Some(msg) = decoder.decode_record::<dbn::CbboMsg>().await? {
464 process_record(dbn::RecordRef::from(msg))?;
465 }
466 }
467 _ => anyhow::bail!("Invalid schema {dbn_schema}"),
468 }
469
470 Ok(result)
471 }
472
473 pub async fn get_range_order_book_depth10(
479 &self,
480 params: RangeQueryParams,
481 depth: Option<usize>,
482 ) -> anyhow::Result<Vec<OrderBookDepth10>> {
483 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
484 check_consistent_symbology(&symbols)?;
485
486 let first_symbol = params
487 .symbols
488 .first()
489 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
490 let stype_in = infer_symbology_type(first_symbol);
491 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
492 let time_range = get_date_time_range(params.start, end)?;
493
494 let _depth = depth.unwrap_or(10);
496 if _depth != 10 {
497 anyhow::bail!("Only depth=10 is currently supported for order book depths");
498 }
499
500 let range_params = GetRangeParams::builder()
501 .dataset(params.dataset)
502 .date_time_range(time_range)
503 .symbols(symbols)
504 .stype_in(stype_in)
505 .schema(dbn::Schema::Mbp10)
506 .maybe_limit(params.limit.and_then(NonZeroU64::new))
507 .build();
508
509 let price_precision_arg = params.price_precision;
510
511 let mut client = (*self.inner).clone();
512 let mut decoder = client
513 .timeseries()
514 .get_range(&range_params)
515 .await
516 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
517
518 let metadata = decoder.metadata().clone();
519 let mut metadata_cache = MetadataCache::new(metadata);
520 let mut precision_cache = AHashMap::new();
521 let mut result: Vec<OrderBookDepth10> = Vec::new();
522
523 let mut process_record = |record: dbn::RecordRef| -> anyhow::Result<()> {
524 let sym_map = self.symbol_venue_map.load();
525 let instrument_id = decode_nautilus_instrument_id(
526 &record,
527 &mut metadata_cache,
528 &self.publisher_venue_map,
529 &sym_map,
530 )?;
531 let price_precision = self.resolve_cached_price_precision(
532 &instrument_id,
533 price_precision_arg,
534 &mut precision_cache,
535 )?;
536
537 if let Some(msg) = record.get::<dbn::Mbp10Msg>() {
538 let depth = decode_mbp10_msg(msg, instrument_id, price_precision, None)?;
539 result.push(depth);
540 }
541
542 Ok(())
543 };
544
545 while let Some(msg) = decoder.decode_record::<dbn::Mbp10Msg>().await? {
546 process_record(dbn::RecordRef::from(msg))?;
547 }
548
549 Ok(result)
550 }
551
552 pub async fn get_range_order_book_deltas(
558 &self,
559 params: RangeQueryParams,
560 ) -> anyhow::Result<Vec<OrderBookDelta>> {
561 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
562 check_consistent_symbology(&symbols)?;
563
564 let first_symbol = params
565 .symbols
566 .first()
567 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
568 let stype_in = infer_symbology_type(first_symbol);
569 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
570 let time_range = get_date_time_range(params.start, end)?;
571
572 let range_params = GetRangeParams::builder()
573 .dataset(params.dataset)
574 .date_time_range(time_range)
575 .symbols(symbols)
576 .stype_in(stype_in)
577 .schema(dbn::Schema::Mbo)
578 .maybe_limit(params.limit.and_then(NonZeroU64::new))
579 .build();
580
581 let price_precision_arg = params.price_precision;
582
583 let mut client = (*self.inner).clone();
584 let mut decoder = client
585 .timeseries()
586 .get_range(&range_params)
587 .await
588 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
589
590 let metadata = decoder.metadata().clone();
591 let mut metadata_cache = MetadataCache::new(metadata);
592 let mut precision_cache = AHashMap::new();
593 let mut result: Vec<OrderBookDelta> = Vec::new();
594
595 let mut process_record = |record: dbn::RecordRef| -> anyhow::Result<()> {
596 let sym_map = self.symbol_venue_map.load();
597 let instrument_id = decode_nautilus_instrument_id(
598 &record,
599 &mut metadata_cache,
600 &self.publisher_venue_map,
601 &sym_map,
602 )?;
603 let price_precision = self.resolve_cached_price_precision(
604 &instrument_id,
605 price_precision_arg,
606 &mut precision_cache,
607 )?;
608
609 if let Some(msg) = record.get::<dbn::MboMsg>() {
610 let (delta, _trade) =
611 decode_mbo_msg(msg, instrument_id, price_precision, None, false)?;
612
613 if let Some(delta) = delta {
614 result.push(delta);
615 }
616 }
617
618 Ok(())
619 };
620
621 while let Some(msg) = decoder.decode_record::<dbn::MboMsg>().await? {
622 process_record(dbn::RecordRef::from(msg))?;
623 }
624
625 Ok(result)
626 }
627
628 pub async fn get_range_trades(
634 &self,
635 params: RangeQueryParams,
636 schema: Option<String>,
637 ) -> anyhow::Result<Vec<TradeTick>> {
638 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
639 check_consistent_symbology(&symbols)?;
640
641 let first_symbol = params
642 .symbols
643 .first()
644 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
645 let stype_in = infer_symbology_type(first_symbol);
646 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
647 let time_range = get_date_time_range(params.start, end)?;
648 let schema = schema.unwrap_or_else(|| "trades".to_string());
649 let dbn_schema = dbn::Schema::from_str(&schema)?;
650
651 match dbn_schema {
652 dbn::Schema::Trades
653 | dbn::Schema::Tbbo
654 | dbn::Schema::Tcbbo
655 | dbn::Schema::Mbp1
656 | dbn::Schema::Cmbp1 => (),
657 _ => {
658 anyhow::bail!("Invalid schema. Must be one of: trades, tbbo, tcbbo, mbp-1, cmbp-1")
659 }
660 }
661
662 let range_params = GetRangeParams::builder()
663 .dataset(params.dataset)
664 .date_time_range(time_range)
665 .symbols(symbols)
666 .stype_in(stype_in)
667 .schema(dbn_schema)
668 .maybe_limit(params.limit.and_then(NonZeroU64::new))
669 .build();
670
671 let price_precision_arg = params.price_precision;
672
673 let mut client = (*self.inner).clone();
674 let mut decoder = client
675 .timeseries()
676 .get_range(&range_params)
677 .await
678 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
679
680 let metadata = decoder.metadata().clone();
681 let mut metadata_cache = MetadataCache::new(metadata);
682 let mut precision_cache = AHashMap::new();
683 let mut result: Vec<TradeTick> = Vec::new();
684
685 let mut process_record = |record: dbn::RecordRef| -> anyhow::Result<()> {
686 let sym_map = self.symbol_venue_map.load();
687 let instrument_id = decode_nautilus_instrument_id(
688 &record,
689 &mut metadata_cache,
690 &self.publisher_venue_map,
691 &sym_map,
692 )?;
693 let price_precision = self.resolve_cached_price_precision(
694 &instrument_id,
695 price_precision_arg,
696 &mut precision_cache,
697 )?;
698
699 let (data, data2) =
700 decode_record(&record, instrument_id, price_precision, None, true, true)?;
701
702 match (data, data2) {
703 (Some(Data::Trade(trade)), _) | (_, Some(Data::Trade(trade))) => result.push(trade),
704 (Some(_) | None, None) => {}
705 (None, Some(data)) => {
706 anyhow::bail!("Invalid data element not `TradeTick`, was {data:?}")
707 }
708 (Some(data), Some(_)) => {
709 anyhow::bail!("Invalid data element not `TradeTick`, was {data:?}")
710 }
711 }
712 Ok(())
713 };
714
715 match dbn_schema {
716 dbn::Schema::Trades => {
717 while let Some(msg) = decoder.decode_record::<dbn::TradeMsg>().await? {
718 process_record(dbn::RecordRef::from(msg))?;
719 }
720 }
721 dbn::Schema::Mbp1 => {
722 while let Some(msg) = decoder.decode_record::<dbn::Mbp1Msg>().await? {
723 process_record(dbn::RecordRef::from(msg))?;
724 }
725 }
726 dbn::Schema::Tbbo => {
727 while let Some(msg) = decoder.decode_record::<dbn::TbboMsg>().await? {
728 process_record(dbn::RecordRef::from(msg))?;
729 }
730 }
731 dbn::Schema::Cmbp1 => {
732 while let Some(msg) = decoder.decode_record::<dbn::Cmbp1Msg>().await? {
733 process_record(dbn::RecordRef::from(msg))?;
734 }
735 }
736 dbn::Schema::Tcbbo => {
737 while let Some(msg) = decoder.decode_record::<dbn::TcbboMsg>().await? {
738 process_record(dbn::RecordRef::from(msg))?;
739 }
740 }
741 _ => anyhow::bail!("Invalid schema {dbn_schema}"),
742 }
743
744 Ok(result)
745 }
746
747 pub async fn get_range_bars(
753 &self,
754 params: RangeQueryParams,
755 aggregation: BarAggregation,
756 timestamp_on_close: bool,
757 ) -> anyhow::Result<Vec<Bar>> {
758 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
759 check_consistent_symbology(&symbols)?;
760
761 let first_symbol = params
762 .symbols
763 .first()
764 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
765 let stype_in = infer_symbology_type(first_symbol);
766 let schema = match aggregation {
767 BarAggregation::Second => dbn::Schema::Ohlcv1S,
768 BarAggregation::Minute => dbn::Schema::Ohlcv1M,
769 BarAggregation::Hour => dbn::Schema::Ohlcv1H,
770 BarAggregation::Day => dbn::Schema::Ohlcv1D,
771 _ => anyhow::bail!("Invalid `BarAggregation` for request, was {aggregation}"),
772 };
773
774 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
775 let time_range = get_date_time_range(params.start, end)?;
776
777 let range_params = GetRangeParams::builder()
778 .dataset(params.dataset)
779 .date_time_range(time_range)
780 .symbols(symbols)
781 .stype_in(stype_in)
782 .schema(schema)
783 .maybe_limit(params.limit.and_then(NonZeroU64::new))
784 .build();
785
786 let price_precision_arg = params.price_precision;
787
788 let mut client = (*self.inner).clone();
789 let mut decoder = client
790 .timeseries()
791 .get_range(&range_params)
792 .await
793 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
794
795 let metadata = decoder.metadata().clone();
796 let mut metadata_cache = MetadataCache::new(metadata);
797 let mut precision_cache = AHashMap::new();
798 let mut result: Vec<Bar> = Vec::new();
799
800 while let Some(msg) = decoder.decode_record::<dbn::OhlcvMsg>().await? {
801 let record = dbn::RecordRef::from(msg);
802 let sym_map = self.symbol_venue_map.load();
803 let instrument_id = decode_nautilus_instrument_id(
804 &record,
805 &mut metadata_cache,
806 &self.publisher_venue_map,
807 &sym_map,
808 )?;
809 let price_precision = self.resolve_cached_price_precision(
810 &instrument_id,
811 price_precision_arg,
812 &mut precision_cache,
813 )?;
814
815 let (data, _) = decode_record(
816 &record,
817 instrument_id,
818 price_precision,
819 None,
820 false, timestamp_on_close,
822 )?;
823
824 match data {
825 Some(Data::Bar(bar)) => {
826 result.push(bar);
827 }
828 _ => anyhow::bail!("Invalid data element not `Bar`, was {data:?}"),
829 }
830 }
831
832 Ok(result)
833 }
834
835 pub async fn get_range_imbalance(
841 &self,
842 params: RangeQueryParams,
843 ) -> anyhow::Result<Vec<DatabentoImbalance>> {
844 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
845 check_consistent_symbology(&symbols)?;
846
847 let first_symbol = params
848 .symbols
849 .first()
850 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
851 let stype_in = infer_symbology_type(first_symbol);
852 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
853 let time_range = get_date_time_range(params.start, end)?;
854
855 let range_params = GetRangeParams::builder()
856 .dataset(params.dataset)
857 .date_time_range(time_range)
858 .symbols(symbols)
859 .stype_in(stype_in)
860 .schema(dbn::Schema::Imbalance)
861 .maybe_limit(params.limit.and_then(NonZeroU64::new))
862 .build();
863
864 let price_precision_arg = params.price_precision;
865
866 let mut client = (*self.inner).clone();
867 let mut decoder = client
868 .timeseries()
869 .get_range(&range_params)
870 .await
871 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
872
873 let metadata = decoder.metadata().clone();
874 let mut metadata_cache = MetadataCache::new(metadata);
875 let mut precision_cache = AHashMap::new();
876 let mut result: Vec<DatabentoImbalance> = Vec::new();
877
878 while let Some(msg) = decoder.decode_record::<dbn::ImbalanceMsg>().await? {
879 let record = dbn::RecordRef::from(msg);
880 let sym_map = self.symbol_venue_map.load();
881 let instrument_id = decode_nautilus_instrument_id(
882 &record,
883 &mut metadata_cache,
884 &self.publisher_venue_map,
885 &sym_map,
886 )?;
887 let price_precision = self.resolve_cached_price_precision(
888 &instrument_id,
889 price_precision_arg,
890 &mut precision_cache,
891 )?;
892
893 let imbalance = decode_imbalance_msg(msg, instrument_id, price_precision, None)?;
894 result.push(imbalance);
895 }
896
897 Ok(result)
898 }
899
900 pub async fn get_range_statistics(
906 &self,
907 params: RangeQueryParams,
908 ) -> anyhow::Result<Vec<DatabentoStatistics>> {
909 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
910 check_consistent_symbology(&symbols)?;
911
912 let first_symbol = params
913 .symbols
914 .first()
915 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
916 let stype_in = infer_symbology_type(first_symbol);
917 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
918 let time_range = get_date_time_range(params.start, end)?;
919
920 let range_params = GetRangeParams::builder()
921 .dataset(params.dataset)
922 .date_time_range(time_range)
923 .symbols(symbols)
924 .stype_in(stype_in)
925 .schema(dbn::Schema::Statistics)
926 .maybe_limit(params.limit.and_then(NonZeroU64::new))
927 .build();
928
929 let price_precision_arg = params.price_precision;
930
931 let mut client = (*self.inner).clone();
932 let mut decoder = client
933 .timeseries()
934 .get_range(&range_params)
935 .await
936 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
937
938 let metadata = decoder.metadata().clone();
939 let mut metadata_cache = MetadataCache::new(metadata);
940 let mut precision_cache = AHashMap::new();
941 let mut result: Vec<DatabentoStatistics> = Vec::new();
942
943 while let Some(msg) = decoder.decode_record::<dbn::StatMsg>().await? {
944 if !is_supported_stat_type(msg.stat_type) {
946 log::warn!("Skipping unsupported `stat_type` {}", msg.stat_type);
947 continue;
948 }
949
950 let record = dbn::RecordRef::from(msg);
951 let sym_map = self.symbol_venue_map.load();
952 let instrument_id = decode_nautilus_instrument_id(
953 &record,
954 &mut metadata_cache,
955 &self.publisher_venue_map,
956 &sym_map,
957 )?;
958 let price_precision = self.resolve_cached_price_precision(
959 &instrument_id,
960 price_precision_arg,
961 &mut precision_cache,
962 )?;
963
964 if let Some(statistics) =
965 decode_statistics_msg(msg, instrument_id, price_precision, None)?
966 {
967 result.push(statistics);
968 }
969 }
970
971 Ok(result)
972 }
973
974 pub async fn get_range_status(
980 &self,
981 params: RangeQueryParams,
982 ) -> anyhow::Result<Vec<InstrumentStatus>> {
983 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
984 check_consistent_symbology(&symbols)?;
985
986 let first_symbol = params
987 .symbols
988 .first()
989 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
990 let stype_in = infer_symbology_type(first_symbol);
991 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
992 let time_range = get_date_time_range(params.start, end)?;
993
994 let range_params = GetRangeParams::builder()
995 .dataset(params.dataset)
996 .date_time_range(time_range)
997 .symbols(symbols)
998 .stype_in(stype_in)
999 .schema(dbn::Schema::Status)
1000 .maybe_limit(params.limit.and_then(NonZeroU64::new))
1001 .build();
1002
1003 let mut client = (*self.inner).clone();
1004 let mut decoder = client
1005 .timeseries()
1006 .get_range(&range_params)
1007 .await
1008 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
1009
1010 let metadata = decoder.metadata().clone();
1011 let mut metadata_cache = MetadataCache::new(metadata);
1012 let mut result: Vec<InstrumentStatus> = Vec::new();
1013
1014 while let Some(msg) = decoder.decode_record::<dbn::StatusMsg>().await? {
1015 let record = dbn::RecordRef::from(msg);
1016 let sym_map = self.symbol_venue_map.load();
1017 let instrument_id = decode_nautilus_instrument_id(
1018 &record,
1019 &mut metadata_cache,
1020 &self.publisher_venue_map,
1021 &sym_map,
1022 )?;
1023
1024 let status = decode_status_msg(msg, instrument_id, None)?;
1025 result.push(status);
1026 }
1027
1028 Ok(result)
1029 }
1030
1031 pub fn prepare_symbols_from_instrument_ids(
1033 &self,
1034 instrument_ids: &[InstrumentId],
1035 ) -> Vec<String> {
1036 self.symbol_venue_map.rcu(|m| {
1037 for id in instrument_ids {
1038 m.entry(id.symbol).or_insert(id.venue);
1039 }
1040 });
1041
1042 instrument_ids
1043 .iter()
1044 .map(|id| id.symbol.to_string())
1045 .collect()
1046 }
1047}
1048
1049#[cfg(test)]
1050mod tests {
1051 use nautilus_core::time::get_atomic_clock_realtime;
1052 use rstest::{fixture, rstest};
1053
1054 use super::*;
1055
1056 fn test_api_key() -> String {
1057 "test-000000000000000000000000000".to_string()
1058 }
1059
1060 fn publishers_path() -> PathBuf {
1061 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("publishers.json")
1062 }
1063
1064 #[fixture]
1065 fn historical_client() -> DatabentoHistoricalClient {
1066 DatabentoHistoricalClient::new(
1067 Credential::new(test_api_key()),
1068 publishers_path(),
1069 get_atomic_clock_realtime(),
1070 false,
1071 )
1072 .unwrap()
1073 }
1074
1075 #[rstest]
1076 fn test_new_with_base_url_rejects_invalid_url() {
1077 let err = DatabentoHistoricalClient::new_with_base_url(
1078 Credential::new(test_api_key()),
1079 publishers_path(),
1080 get_atomic_clock_realtime(),
1081 false,
1082 "://invalid",
1083 )
1084 .expect_err("expected invalid base URL to fail");
1085 let err_msg = format!("{err}");
1086
1087 assert!(
1088 err_msg.contains("Failed to parse Databento Historical API base URL"),
1089 "unexpected error message: {err_msg}",
1090 );
1091 }
1092
1093 #[rstest]
1094 fn test_set_price_precision_inserts_into_cache(historical_client: DatabentoHistoricalClient) {
1095 let symbol = Symbol::from("ESM4");
1096
1097 assert_eq!(historical_client.price_precision(symbol), None);
1098
1099 historical_client.set_price_precision(symbol, 2);
1100
1101 assert_eq!(historical_client.price_precision(symbol), Some(2));
1102 }
1103
1104 #[rstest]
1105 fn test_resolve_price_precision_explicit_arg(historical_client: DatabentoHistoricalClient) {
1106 let instrument_id = InstrumentId::from("ESM4.GLBX");
1107 historical_client.set_price_precision(Symbol::from("ESM4"), 9);
1109
1110 let precision = historical_client
1111 .resolve_price_precision(&instrument_id, Some(2))
1112 .unwrap();
1113 assert_eq!(precision, 2);
1114 }
1115
1116 #[rstest]
1117 fn test_resolve_price_precision_cache_hit(historical_client: DatabentoHistoricalClient) {
1118 let instrument_id = InstrumentId::from("ESM4.GLBX");
1119 historical_client.set_price_precision(Symbol::from("ESM4"), 2);
1120
1121 let precision = historical_client
1122 .resolve_price_precision(&instrument_id, None)
1123 .unwrap();
1124 assert_eq!(precision, 2);
1125 }
1126
1127 #[rstest]
1128 fn test_resolve_price_precision_cache_miss_errors(
1129 historical_client: DatabentoHistoricalClient,
1130 ) {
1131 let instrument_id = InstrumentId::from("ESM4.GLBX");
1132
1133 let err = historical_client
1134 .resolve_price_precision(&instrument_id, None)
1135 .expect_err("expected cache-miss error");
1136 let err_msg = format!("{err}");
1137 assert!(
1138 err_msg.contains("Could not resolve `price_precision`"),
1139 "unexpected error message: {err_msg}",
1140 );
1141 assert!(
1142 err_msg.contains("ESM4.GLBX"),
1143 "error should name the instrument: {err_msg}",
1144 );
1145 }
1146}