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, OrderBookDepth, 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, ohlcv_schema_from_aggregation},
36 decode::{
37 MboDeltaBuffer, decode_imbalance_msg, decode_instrument_def_msg, decode_mbo_msg,
38 decode_mbp10_msg, decode_record, decode_statistics_msg, decode_status_msg,
39 is_supported_stat_type,
40 },
41 symbology::{
42 MetadataCache, check_consistent_symbology, decode_nautilus_instrument_id,
43 infer_symbology_type,
44 },
45 types::{DatabentoImbalance, DatabentoPublisher, DatabentoStatistics, PublisherId},
46};
47
48#[derive(Clone)]
53pub struct DatabentoHistoricalClient {
54 credential: Credential,
55 clock: &'static AtomicTime,
56 inner: Arc<databento::HistoricalClient>,
57 publisher_venue_map: Arc<IndexMap<PublisherId, Venue>>,
58 symbol_venue_map: Arc<AtomicMap<Symbol, Venue>>,
59 price_precisions: Arc<AtomicMap<Symbol, u8>>,
60 use_exchange_as_venue: bool,
61}
62
63#[derive(Debug)]
65pub struct RangeQueryParams {
66 pub dataset: String,
67 pub symbols: Vec<String>,
68 pub start: UnixNanos,
69 pub end: Option<UnixNanos>,
70 pub limit: Option<u64>,
71 pub price_precision: Option<u8>,
72}
73
74#[derive(Debug, Clone)]
76pub struct DatasetRange {
77 pub start: String,
78 pub end: String,
79}
80
81impl Debug for DatabentoHistoricalClient {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct(stringify!(DatabentoHistoricalClient))
84 .field("credential", &self.credential)
85 .finish()
86 }
87}
88
89impl DatabentoHistoricalClient {
90 #[must_use]
92 pub fn api_key(&self) -> &str {
93 self.credential.api_key()
94 }
95
96 pub fn new(
102 credential: Credential,
103 publishers_filepath: PathBuf,
104 clock: &'static AtomicTime,
105 use_exchange_as_venue: bool,
106 ) -> anyhow::Result<Self> {
107 let client = databento::HistoricalClient::builder()
108 .user_agent_extension(NAUTILUS_USER_AGENT.into())
109 .key(credential.api_key())
110 .map_err(|e| anyhow::anyhow!("Failed to create client builder: {e}"))?
111 .build()
112 .map_err(|e| anyhow::anyhow!("Failed to build client: {e}"))?;
113
114 Self::from_client(
115 credential,
116 publishers_filepath,
117 clock,
118 use_exchange_as_venue,
119 client,
120 )
121 }
122
123 pub fn new_with_base_url(
132 credential: Credential,
133 publishers_filepath: PathBuf,
134 clock: &'static AtomicTime,
135 use_exchange_as_venue: bool,
136 base_url: &str,
137 ) -> anyhow::Result<Self> {
138 let client = databento::HistoricalClient::builder()
139 .user_agent_extension(NAUTILUS_USER_AGENT.into())
140 .base_url(base_url.parse().map_err(|e| {
141 anyhow::anyhow!("Failed to parse Databento Historical API base URL: {e}")
142 })?)
143 .key(credential.api_key())
144 .map_err(|e| anyhow::anyhow!("Failed to create client builder: {e}"))?
145 .build()
146 .map_err(|e| anyhow::anyhow!("Failed to build client: {e}"))?;
147
148 Self::from_client(
149 credential,
150 publishers_filepath,
151 clock,
152 use_exchange_as_venue,
153 client,
154 )
155 }
156
157 fn from_client(
158 credential: Credential,
159 publishers_filepath: PathBuf,
160 clock: &'static AtomicTime,
161 use_exchange_as_venue: bool,
162 client: databento::HistoricalClient,
163 ) -> anyhow::Result<Self> {
164 let file_content = fs::read_to_string(publishers_filepath)?;
165 let publishers_vec: Vec<DatabentoPublisher> = serde_json::from_str(&file_content)?;
166
167 let publisher_venue_map = publishers_vec
168 .into_iter()
169 .map(|p| (p.publisher_id, Venue::from(p.venue.as_str())))
170 .collect::<IndexMap<u16, Venue>>();
171
172 Ok(Self {
173 clock,
174 inner: Arc::new(client),
175 publisher_venue_map: Arc::new(publisher_venue_map),
176 symbol_venue_map: Arc::new(AtomicMap::new()),
177 price_precisions: Arc::new(AtomicMap::new()),
178 credential,
179 use_exchange_as_venue,
180 })
181 }
182
183 pub fn set_price_precision(&self, symbol: Symbol, price_precision: u8) {
189 self.price_precisions.insert(symbol, price_precision);
190 }
191
192 #[must_use]
194 pub fn price_precision(&self, symbol: Symbol) -> Option<u8> {
195 self.price_precisions.load().get(&symbol).copied()
196 }
197
198 fn resolve_price_precision(
208 &self,
209 instrument_id: &InstrumentId,
210 price_precision: Option<u8>,
211 ) -> anyhow::Result<u8> {
212 if let Some(precision) = price_precision {
213 return Ok(precision);
214 }
215
216 let precisions = self.price_precisions.load();
217 precisions
218 .get(&instrument_id.symbol)
219 .copied()
220 .ok_or_else(|| {
221 anyhow::anyhow!(
222 "Could not resolve `price_precision` for {instrument_id}: \
223 pass `price_precision` explicitly, call `set_price_precision`, \
224 or fetch the instrument definitions first via `get_range_instruments`"
225 )
226 })
227 }
228
229 fn resolve_cached_price_precision(
230 &self,
231 instrument_id: &InstrumentId,
232 price_precision: Option<u8>,
233 precision_cache: &mut AHashMap<InstrumentId, u8>,
234 ) -> anyhow::Result<u8> {
235 if let Some(precision) = price_precision {
236 return Ok(precision);
237 }
238
239 if let Some(precision) = precision_cache.get(instrument_id) {
240 return Ok(*precision);
241 }
242
243 let precision = self.resolve_price_precision(instrument_id, None)?;
244 precision_cache.insert(*instrument_id, precision);
245 Ok(precision)
246 }
247
248 pub async fn get_dataset_range(&self, dataset: &str) -> anyhow::Result<DatasetRange> {
254 let mut client = (*self.inner).clone();
255 let response = client
256 .metadata()
257 .get_dataset_range(dataset)
258 .await
259 .map_err(|e| anyhow::anyhow!("Failed to get dataset range: {e}"))?;
260
261 Ok(DatasetRange {
262 start: response.start.to_string(),
263 end: response.end.to_string(),
264 })
265 }
266
267 pub async fn get_range_instruments(
273 &self,
274 params: RangeQueryParams,
275 ) -> anyhow::Result<Vec<InstrumentAny>> {
276 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
277 check_consistent_symbology(&symbols)?;
278
279 let first_symbol = params
280 .symbols
281 .first()
282 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
283 let stype_in = infer_symbology_type(first_symbol);
284 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
285 let time_range = get_date_time_range(params.start, end)?;
286
287 let range_params = GetRangeParams::builder()
288 .dataset(params.dataset)
289 .date_time_range(time_range)
290 .symbols(symbols)
291 .stype_in(stype_in)
292 .schema(dbn::Schema::Definition)
293 .maybe_limit(params.limit.and_then(NonZeroU64::new))
294 .build();
295
296 let mut client = (*self.inner).clone();
297 let mut decoder = client
298 .timeseries()
299 .get_range(&range_params)
300 .await
301 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
302
303 let metadata = decoder.metadata().clone();
304 let mut metadata_cache = MetadataCache::new(metadata);
305 let mut instruments = Vec::new();
306
307 while let Some(msg) = decoder.decode_record::<dbn::InstrumentDefMsg>().await? {
308 let record = dbn::RecordRef::from(msg);
309 let sym_map = self.symbol_venue_map.load();
310 let mut instrument_id = decode_nautilus_instrument_id(
311 &record,
312 &mut metadata_cache,
313 &self.publisher_venue_map,
314 &sym_map,
315 )?;
316
317 if self.use_exchange_as_venue && instrument_id.venue == Venue::GLBX() {
318 let exchange = msg
319 .exchange()
320 .map_err(|e| anyhow::anyhow!("Missing exchange in record: {e}"))?;
321 let venue = Venue::from_code(exchange)
322 .map_err(|e| anyhow::anyhow!("Venue not found for exchange {exchange}: {e}"))?;
323 instrument_id.venue = venue;
324 }
325
326 match decode_instrument_def_msg(msg, instrument_id, None, None) {
327 Ok(Some(instrument)) => instruments.push(instrument),
328 Ok(None) => {} Err(e) => anyhow::bail!("Failed to decode instrument {instrument_id}: {e}"),
330 }
331 }
332
333 self.cache_price_precisions(&instruments);
334
335 Ok(instruments)
336 }
337
338 fn cache_price_precisions(&self, instruments: &[InstrumentAny]) {
339 if instruments.is_empty() {
340 return;
341 }
342
343 self.price_precisions.rcu(|precisions| {
344 for instrument in instruments {
345 precisions.insert(instrument.id().symbol, instrument.price_precision());
346 }
347 });
348 }
349
350 pub async fn get_range_quotes(
356 &self,
357 params: RangeQueryParams,
358 schema: Option<String>,
359 ) -> anyhow::Result<Vec<QuoteTick>> {
360 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
361 check_consistent_symbology(&symbols)?;
362
363 let first_symbol = params
364 .symbols
365 .first()
366 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
367 let stype_in = infer_symbology_type(first_symbol);
368 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
369 let time_range = get_date_time_range(params.start, end)?;
370 let schema = schema.unwrap_or_else(|| "mbp-1".to_string());
371 let dbn_schema = dbn::Schema::from_str(&schema)?;
372
373 match dbn_schema {
374 dbn::Schema::Mbp1
375 | dbn::Schema::Tbbo
376 | dbn::Schema::Bbo1S
377 | dbn::Schema::Bbo1M
378 | dbn::Schema::Cmbp1
379 | dbn::Schema::Tcbbo
380 | dbn::Schema::Cbbo1S
381 | dbn::Schema::Cbbo1M => (),
382 _ => anyhow::bail!(
383 "Invalid schema. Must be one of: mbp-1, tbbo, bbo-1s, bbo-1m, cmbp-1, tcbbo, cbbo-1s, cbbo-1m"
384 ),
385 }
386
387 let range_params = GetRangeParams::builder()
388 .dataset(params.dataset)
389 .date_time_range(time_range)
390 .symbols(symbols)
391 .stype_in(stype_in)
392 .schema(dbn_schema)
393 .maybe_limit(params.limit.and_then(NonZeroU64::new))
394 .build();
395
396 let price_precision_arg = params.price_precision;
397
398 let mut client = (*self.inner).clone();
399 let mut decoder = client
400 .timeseries()
401 .get_range(&range_params)
402 .await
403 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
404
405 let metadata = decoder.metadata().clone();
406 let mut metadata_cache = MetadataCache::new(metadata);
407 let mut precision_cache = AHashMap::new();
408 let mut result: Vec<QuoteTick> = Vec::new();
409
410 let mut process_record = |record: dbn::RecordRef| -> anyhow::Result<()> {
411 let sym_map = self.symbol_venue_map.load();
412 let instrument_id = decode_nautilus_instrument_id(
413 &record,
414 &mut metadata_cache,
415 &self.publisher_venue_map,
416 &sym_map,
417 )?;
418 let price_precision = self.resolve_cached_price_precision(
419 &instrument_id,
420 price_precision_arg,
421 &mut precision_cache,
422 )?;
423
424 let (data, _) = decode_record(
425 &record,
426 instrument_id,
427 price_precision,
428 None,
429 false, true,
431 )?;
432
433 match data {
434 Some(Data::Quote(quote)) => result.push(quote),
435 None => {} _ => anyhow::bail!("Invalid data element not `QuoteTick`, was {data:?}"),
437 }
438 Ok(())
439 };
440
441 match dbn_schema {
442 dbn::Schema::Mbp1 => {
443 while let Some(msg) = decoder.decode_record::<dbn::Mbp1Msg>().await? {
444 process_record(dbn::RecordRef::from(msg))?;
445 }
446 }
447 dbn::Schema::Tbbo => {
448 while let Some(msg) = decoder.decode_record::<dbn::TbboMsg>().await? {
449 process_record(dbn::RecordRef::from(msg))?;
450 }
451 }
452 dbn::Schema::Cmbp1 => {
453 while let Some(msg) = decoder.decode_record::<dbn::Cmbp1Msg>().await? {
454 process_record(dbn::RecordRef::from(msg))?;
455 }
456 }
457 dbn::Schema::Tcbbo => {
458 while let Some(msg) = decoder.decode_record::<dbn::TcbboMsg>().await? {
459 process_record(dbn::RecordRef::from(msg))?;
460 }
461 }
462 dbn::Schema::Bbo1M => {
463 while let Some(msg) = decoder.decode_record::<dbn::Bbo1MMsg>().await? {
464 process_record(dbn::RecordRef::from(msg))?;
465 }
466 }
467 dbn::Schema::Bbo1S => {
468 while let Some(msg) = decoder.decode_record::<dbn::Bbo1SMsg>().await? {
469 process_record(dbn::RecordRef::from(msg))?;
470 }
471 }
472 dbn::Schema::Cbbo1S | dbn::Schema::Cbbo1M => {
473 while let Some(msg) = decoder.decode_record::<dbn::CbboMsg>().await? {
474 process_record(dbn::RecordRef::from(msg))?;
475 }
476 }
477 _ => anyhow::bail!("Invalid schema {dbn_schema}"),
478 }
479
480 Ok(result)
481 }
482
483 pub async fn get_range_order_book_depth(
489 &self,
490 params: RangeQueryParams,
491 depth: Option<usize>,
492 ) -> anyhow::Result<Vec<OrderBookDepth>> {
493 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
494 check_consistent_symbology(&symbols)?;
495
496 let first_symbol = params
497 .symbols
498 .first()
499 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
500 let stype_in = infer_symbology_type(first_symbol);
501 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
502 let time_range = get_date_time_range(params.start, end)?;
503
504 let _depth = depth.unwrap_or(10);
506 if _depth != 10 {
507 anyhow::bail!("Only depth=10 is currently supported for order book depths");
508 }
509
510 let range_params = GetRangeParams::builder()
511 .dataset(params.dataset)
512 .date_time_range(time_range)
513 .symbols(symbols)
514 .stype_in(stype_in)
515 .schema(dbn::Schema::Mbp10)
516 .maybe_limit(params.limit.and_then(NonZeroU64::new))
517 .build();
518
519 let price_precision_arg = params.price_precision;
520
521 let mut client = (*self.inner).clone();
522 let mut decoder = client
523 .timeseries()
524 .get_range(&range_params)
525 .await
526 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
527
528 let metadata = decoder.metadata().clone();
529 let mut metadata_cache = MetadataCache::new(metadata);
530 let mut precision_cache = AHashMap::new();
531 let mut result: Vec<OrderBookDepth> = Vec::new();
532
533 let mut process_record = |record: dbn::RecordRef| -> anyhow::Result<()> {
534 let sym_map = self.symbol_venue_map.load();
535 let instrument_id = decode_nautilus_instrument_id(
536 &record,
537 &mut metadata_cache,
538 &self.publisher_venue_map,
539 &sym_map,
540 )?;
541 let price_precision = self.resolve_cached_price_precision(
542 &instrument_id,
543 price_precision_arg,
544 &mut precision_cache,
545 )?;
546
547 if let Some(msg) = record.get::<dbn::Mbp10Msg>() {
548 let depth = decode_mbp10_msg(msg, instrument_id, price_precision, None)?;
549 result.push(depth);
550 }
551
552 Ok(())
553 };
554
555 while let Some(msg) = decoder.decode_record::<dbn::Mbp10Msg>().await? {
556 process_record(dbn::RecordRef::from(msg))?;
557 }
558
559 Ok(result)
560 }
561
562 pub async fn get_range_order_book_deltas(
568 &self,
569 params: RangeQueryParams,
570 ) -> anyhow::Result<Vec<OrderBookDelta>> {
571 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
572 check_consistent_symbology(&symbols)?;
573
574 let first_symbol = params
575 .symbols
576 .first()
577 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
578 let stype_in = infer_symbology_type(first_symbol);
579 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
580 let time_range = get_date_time_range(params.start, end)?;
581
582 let range_params = GetRangeParams::builder()
583 .dataset(params.dataset)
584 .date_time_range(time_range)
585 .symbols(symbols)
586 .stype_in(stype_in)
587 .schema(dbn::Schema::Mbo)
588 .maybe_limit(params.limit.and_then(NonZeroU64::new))
589 .build();
590
591 let price_precision_arg = params.price_precision;
592
593 let mut client = (*self.inner).clone();
594 let mut decoder = client
595 .timeseries()
596 .get_range(&range_params)
597 .await
598 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
599
600 let metadata = decoder.metadata().clone();
601 let mut metadata_cache = MetadataCache::new(metadata);
602 let mut precision_cache = AHashMap::new();
603 let mut result: Vec<OrderBookDelta> = Vec::new();
604 let mut delta_buffer = MboDeltaBuffer::default();
605
606 let mut process_record = |record: dbn::RecordRef| -> anyhow::Result<()> {
607 let sym_map = self.symbol_venue_map.load();
608 let instrument_id = decode_nautilus_instrument_id(
609 &record,
610 &mut metadata_cache,
611 &self.publisher_venue_map,
612 &sym_map,
613 )?;
614 let price_precision = self.resolve_cached_price_precision(
615 &instrument_id,
616 price_precision_arg,
617 &mut precision_cache,
618 )?;
619
620 if let Some(msg) = record.get::<dbn::MboMsg>() {
621 let (delta, _trade) =
622 decode_mbo_msg(msg, instrument_id, price_precision, None, false)?;
623 delta_buffer.push(msg, instrument_id, delta);
624 while let Some(delta) = delta_buffer.pop_ready() {
625 result.push(delta);
626 }
627 }
628
629 Ok(())
630 };
631
632 while let Some(msg) = decoder.decode_record::<dbn::MboMsg>().await? {
633 process_record(dbn::RecordRef::from(msg))?;
634 }
635
636 delta_buffer.finish();
637 while let Some(delta) = delta_buffer.pop_ready() {
638 result.push(delta);
639 }
640
641 Ok(result)
642 }
643
644 pub async fn get_range_trades(
650 &self,
651 params: RangeQueryParams,
652 schema: Option<String>,
653 ) -> anyhow::Result<Vec<TradeTick>> {
654 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
655 check_consistent_symbology(&symbols)?;
656
657 let first_symbol = params
658 .symbols
659 .first()
660 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
661 let stype_in = infer_symbology_type(first_symbol);
662 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
663 let time_range = get_date_time_range(params.start, end)?;
664 let schema = schema.unwrap_or_else(|| "trades".to_string());
665 let dbn_schema = dbn::Schema::from_str(&schema)?;
666
667 match dbn_schema {
668 dbn::Schema::Trades
669 | dbn::Schema::Tbbo
670 | dbn::Schema::Tcbbo
671 | dbn::Schema::Mbp1
672 | dbn::Schema::Cmbp1 => (),
673 _ => {
674 anyhow::bail!("Invalid schema. Must be one of: trades, tbbo, tcbbo, mbp-1, cmbp-1")
675 }
676 }
677
678 let range_params = GetRangeParams::builder()
679 .dataset(params.dataset)
680 .date_time_range(time_range)
681 .symbols(symbols)
682 .stype_in(stype_in)
683 .schema(dbn_schema)
684 .maybe_limit(params.limit.and_then(NonZeroU64::new))
685 .build();
686
687 let price_precision_arg = params.price_precision;
688
689 let mut client = (*self.inner).clone();
690 let mut decoder = client
691 .timeseries()
692 .get_range(&range_params)
693 .await
694 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
695
696 let metadata = decoder.metadata().clone();
697 let mut metadata_cache = MetadataCache::new(metadata);
698 let mut precision_cache = AHashMap::new();
699 let mut result: Vec<TradeTick> = Vec::new();
700
701 let mut process_record = |record: dbn::RecordRef| -> anyhow::Result<()> {
702 let sym_map = self.symbol_venue_map.load();
703 let instrument_id = decode_nautilus_instrument_id(
704 &record,
705 &mut metadata_cache,
706 &self.publisher_venue_map,
707 &sym_map,
708 )?;
709 let price_precision = self.resolve_cached_price_precision(
710 &instrument_id,
711 price_precision_arg,
712 &mut precision_cache,
713 )?;
714
715 let (data, data2) =
716 decode_record(&record, instrument_id, price_precision, None, true, true)?;
717
718 match (data, data2) {
719 (Some(Data::Trade(trade)), _) | (_, Some(Data::Trade(trade))) => result.push(trade),
720 (Some(_) | None, None) => {}
721 (None, Some(data)) => {
722 anyhow::bail!("Invalid data element not `TradeTick`, was {data:?}")
723 }
724 (Some(data), Some(_)) => {
725 anyhow::bail!("Invalid data element not `TradeTick`, was {data:?}")
726 }
727 }
728 Ok(())
729 };
730
731 match dbn_schema {
732 dbn::Schema::Trades => {
733 while let Some(msg) = decoder.decode_record::<dbn::TradeMsg>().await? {
734 process_record(dbn::RecordRef::from(msg))?;
735 }
736 }
737 dbn::Schema::Mbp1 => {
738 while let Some(msg) = decoder.decode_record::<dbn::Mbp1Msg>().await? {
739 process_record(dbn::RecordRef::from(msg))?;
740 }
741 }
742 dbn::Schema::Tbbo => {
743 while let Some(msg) = decoder.decode_record::<dbn::TbboMsg>().await? {
744 process_record(dbn::RecordRef::from(msg))?;
745 }
746 }
747 dbn::Schema::Cmbp1 => {
748 while let Some(msg) = decoder.decode_record::<dbn::Cmbp1Msg>().await? {
749 process_record(dbn::RecordRef::from(msg))?;
750 }
751 }
752 dbn::Schema::Tcbbo => {
753 while let Some(msg) = decoder.decode_record::<dbn::TcbboMsg>().await? {
754 process_record(dbn::RecordRef::from(msg))?;
755 }
756 }
757 _ => anyhow::bail!("Invalid schema {dbn_schema}"),
758 }
759
760 Ok(result)
761 }
762
763 pub async fn get_range_bars(
769 &self,
770 params: RangeQueryParams,
771 aggregation: BarAggregation,
772 timestamp_on_close: bool,
773 ) -> anyhow::Result<Vec<Bar>> {
774 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
775 check_consistent_symbology(&symbols)?;
776
777 let first_symbol = params
778 .symbols
779 .first()
780 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
781 let stype_in = infer_symbology_type(first_symbol);
782 let schema = ohlcv_schema_from_aggregation(aggregation)?;
783
784 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
785 let time_range = get_date_time_range(params.start, end)?;
786
787 let range_params = GetRangeParams::builder()
788 .dataset(params.dataset)
789 .date_time_range(time_range)
790 .symbols(symbols)
791 .stype_in(stype_in)
792 .schema(schema)
793 .maybe_limit(params.limit.and_then(NonZeroU64::new))
794 .build();
795
796 let price_precision_arg = params.price_precision;
797
798 let mut client = (*self.inner).clone();
799 let mut decoder = client
800 .timeseries()
801 .get_range(&range_params)
802 .await
803 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
804
805 let metadata = decoder.metadata().clone();
806 let mut metadata_cache = MetadataCache::new(metadata);
807 let mut precision_cache = AHashMap::new();
808 let mut result: Vec<Bar> = Vec::new();
809
810 while let Some(msg) = decoder.decode_record::<dbn::OhlcvMsg>().await? {
811 let record = dbn::RecordRef::from(msg);
812 let sym_map = self.symbol_venue_map.load();
813 let instrument_id = decode_nautilus_instrument_id(
814 &record,
815 &mut metadata_cache,
816 &self.publisher_venue_map,
817 &sym_map,
818 )?;
819 let price_precision = self.resolve_cached_price_precision(
820 &instrument_id,
821 price_precision_arg,
822 &mut precision_cache,
823 )?;
824
825 let (data, _) = decode_record(
826 &record,
827 instrument_id,
828 price_precision,
829 None,
830 false, timestamp_on_close,
832 )?;
833
834 match data {
835 Some(Data::Bar(bar)) => {
836 result.push(bar);
837 }
838 _ => anyhow::bail!("Invalid data element not `Bar`, was {data:?}"),
839 }
840 }
841
842 Ok(result)
843 }
844
845 pub async fn get_range_imbalance(
851 &self,
852 params: RangeQueryParams,
853 ) -> anyhow::Result<Vec<DatabentoImbalance>> {
854 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
855 check_consistent_symbology(&symbols)?;
856
857 let first_symbol = params
858 .symbols
859 .first()
860 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
861 let stype_in = infer_symbology_type(first_symbol);
862 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
863 let time_range = get_date_time_range(params.start, end)?;
864
865 let range_params = GetRangeParams::builder()
866 .dataset(params.dataset)
867 .date_time_range(time_range)
868 .symbols(symbols)
869 .stype_in(stype_in)
870 .schema(dbn::Schema::Imbalance)
871 .maybe_limit(params.limit.and_then(NonZeroU64::new))
872 .build();
873
874 let price_precision_arg = params.price_precision;
875
876 let mut client = (*self.inner).clone();
877 let mut decoder = client
878 .timeseries()
879 .get_range(&range_params)
880 .await
881 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
882
883 let metadata = decoder.metadata().clone();
884 let mut metadata_cache = MetadataCache::new(metadata);
885 let mut precision_cache = AHashMap::new();
886 let mut result: Vec<DatabentoImbalance> = Vec::new();
887
888 while let Some(msg) = decoder.decode_record::<dbn::ImbalanceMsg>().await? {
889 let record = dbn::RecordRef::from(msg);
890 let sym_map = self.symbol_venue_map.load();
891 let instrument_id = decode_nautilus_instrument_id(
892 &record,
893 &mut metadata_cache,
894 &self.publisher_venue_map,
895 &sym_map,
896 )?;
897 let price_precision = self.resolve_cached_price_precision(
898 &instrument_id,
899 price_precision_arg,
900 &mut precision_cache,
901 )?;
902
903 let imbalance = decode_imbalance_msg(msg, instrument_id, price_precision, None)?;
904 result.push(imbalance);
905 }
906
907 Ok(result)
908 }
909
910 pub async fn get_range_statistics(
916 &self,
917 params: RangeQueryParams,
918 ) -> anyhow::Result<Vec<DatabentoStatistics>> {
919 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
920 check_consistent_symbology(&symbols)?;
921
922 let first_symbol = params
923 .symbols
924 .first()
925 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
926 let stype_in = infer_symbology_type(first_symbol);
927 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
928 let time_range = get_date_time_range(params.start, end)?;
929
930 let range_params = GetRangeParams::builder()
931 .dataset(params.dataset)
932 .date_time_range(time_range)
933 .symbols(symbols)
934 .stype_in(stype_in)
935 .schema(dbn::Schema::Statistics)
936 .maybe_limit(params.limit.and_then(NonZeroU64::new))
937 .build();
938
939 let price_precision_arg = params.price_precision;
940
941 let mut client = (*self.inner).clone();
942 let mut decoder = client
943 .timeseries()
944 .get_range(&range_params)
945 .await
946 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
947
948 let metadata = decoder.metadata().clone();
949 let mut metadata_cache = MetadataCache::new(metadata);
950 let mut precision_cache = AHashMap::new();
951 let mut result: Vec<DatabentoStatistics> = Vec::new();
952
953 while let Some(msg) = decoder.decode_record::<dbn::StatMsg>().await? {
954 if !is_supported_stat_type(msg.stat_type) {
956 log::warn!("Skipping unsupported `stat_type` {}", msg.stat_type);
957 continue;
958 }
959
960 let record = dbn::RecordRef::from(msg);
961 let sym_map = self.symbol_venue_map.load();
962 let instrument_id = decode_nautilus_instrument_id(
963 &record,
964 &mut metadata_cache,
965 &self.publisher_venue_map,
966 &sym_map,
967 )?;
968 let price_precision = self.resolve_cached_price_precision(
969 &instrument_id,
970 price_precision_arg,
971 &mut precision_cache,
972 )?;
973
974 if let Some(statistics) =
975 decode_statistics_msg(msg, instrument_id, price_precision, None)?
976 {
977 result.push(statistics);
978 }
979 }
980
981 Ok(result)
982 }
983
984 pub async fn get_range_status(
990 &self,
991 params: RangeQueryParams,
992 ) -> anyhow::Result<Vec<InstrumentStatus>> {
993 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
994 check_consistent_symbology(&symbols)?;
995
996 let first_symbol = params
997 .symbols
998 .first()
999 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
1000 let stype_in = infer_symbology_type(first_symbol);
1001 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
1002 let time_range = get_date_time_range(params.start, end)?;
1003
1004 let range_params = GetRangeParams::builder()
1005 .dataset(params.dataset)
1006 .date_time_range(time_range)
1007 .symbols(symbols)
1008 .stype_in(stype_in)
1009 .schema(dbn::Schema::Status)
1010 .maybe_limit(params.limit.and_then(NonZeroU64::new))
1011 .build();
1012
1013 let mut client = (*self.inner).clone();
1014 let mut decoder = client
1015 .timeseries()
1016 .get_range(&range_params)
1017 .await
1018 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
1019
1020 let metadata = decoder.metadata().clone();
1021 let mut metadata_cache = MetadataCache::new(metadata);
1022 let mut result: Vec<InstrumentStatus> = Vec::new();
1023
1024 while let Some(msg) = decoder.decode_record::<dbn::StatusMsg>().await? {
1025 let record = dbn::RecordRef::from(msg);
1026 let sym_map = self.symbol_venue_map.load();
1027 let instrument_id = decode_nautilus_instrument_id(
1028 &record,
1029 &mut metadata_cache,
1030 &self.publisher_venue_map,
1031 &sym_map,
1032 )?;
1033
1034 let status = decode_status_msg(msg, instrument_id, None)?;
1035 result.push(status);
1036 }
1037
1038 Ok(result)
1039 }
1040
1041 pub fn prepare_symbols_from_instrument_ids(
1043 &self,
1044 instrument_ids: &[InstrumentId],
1045 ) -> Vec<String> {
1046 self.symbol_venue_map.rcu(|m| {
1047 for id in instrument_ids {
1048 m.entry(id.symbol).or_insert(id.venue);
1049 }
1050 });
1051
1052 instrument_ids
1053 .iter()
1054 .map(|id| id.symbol.to_string())
1055 .collect()
1056 }
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061 use std::ffi::c_char;
1062
1063 use databento::dbn::{MappingInterval, SymbolMapping, encode::EncodeRecord};
1064 use nautilus_core::time::get_atomic_clock_realtime;
1065 use rstest::{fixture, rstest};
1066 use time::macros::date;
1067 use tokio::{
1068 io::{AsyncReadExt, AsyncWriteExt},
1069 net::TcpListener,
1070 };
1071
1072 use super::*;
1073
1074 fn test_api_key() -> String {
1075 "test-000000000000000000000000000".to_string()
1076 }
1077
1078 fn publishers_path() -> PathBuf {
1079 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("publishers.json")
1080 }
1081
1082 fn mbo_record(action: c_char, flags: u8, sequence: u32) -> dbn::MboMsg {
1083 let ts_event = 1_609_160_400_000_000_000;
1084 dbn::MboMsg {
1085 hd: dbn::RecordHeader::new::<dbn::MboMsg>(dbn::rtype::MBO, 1, 1, ts_event),
1086 order_id: u64::from(sequence),
1087 price: 4_800_250_000_000,
1088 size: 2,
1089 flags: dbn::FlagSet::new(flags),
1090 channel_id: 1,
1091 action,
1092 side: 'A' as c_char,
1093 ts_recv: ts_event,
1094 ts_in_delta: 0,
1095 sequence,
1096 }
1097 }
1098
1099 fn encode_mbo_response(records: &[dbn::MboMsg]) -> Vec<u8> {
1100 let metadata = dbn::Metadata::builder()
1101 .dataset("GLBX.MDP3")
1102 .schema(Some(dbn::Schema::Mbo))
1103 .start(1_609_160_400_000_000_000)
1104 .stype_in(Some(dbn::SType::RawSymbol))
1105 .stype_out(dbn::SType::InstrumentId)
1106 .symbols(vec!["ESM4".to_string()])
1107 .mappings(vec![SymbolMapping {
1108 raw_symbol: "ESM4".to_string(),
1109 intervals: vec![MappingInterval {
1110 start_date: date!(2020 - 12 - 28),
1111 end_date: date!(2020 - 12 - 29),
1112 symbol: "1".to_string(),
1113 }],
1114 }])
1115 .build();
1116 let mut body = Vec::new();
1117 {
1118 let mut encoder = dbn::encode::dbn::Encoder::with_zstd(&mut body, &metadata).unwrap();
1119 encoder.encode_records(records).unwrap();
1120 encoder.flush().unwrap();
1121 }
1122 body
1123 }
1124
1125 async fn serve_response(listener: TcpListener, body: Vec<u8>) {
1126 let (mut stream, _) = listener.accept().await.unwrap();
1127 let mut request = Vec::new();
1128 let mut buffer = [0_u8; 1024];
1129 loop {
1130 let read = stream.read(&mut buffer).await.unwrap();
1131 if read == 0 {
1132 break;
1133 }
1134 request.extend_from_slice(&buffer[..read]);
1135 if request.windows(4).any(|window| window == b"\r\n\r\n") {
1136 break;
1137 }
1138 }
1139
1140 let headers = format!(
1141 "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1142 body.len(),
1143 );
1144 stream.write_all(headers.as_bytes()).await.unwrap();
1145 stream.write_all(&body).await.unwrap();
1146 stream.shutdown().await.unwrap();
1147 }
1148
1149 #[fixture]
1150 fn historical_client() -> DatabentoHistoricalClient {
1151 DatabentoHistoricalClient::new(
1152 Credential::new(test_api_key()),
1153 publishers_path(),
1154 get_atomic_clock_realtime(),
1155 false,
1156 )
1157 .unwrap()
1158 }
1159
1160 #[rstest]
1161 fn test_new_with_base_url_rejects_invalid_url() {
1162 let err = DatabentoHistoricalClient::new_with_base_url(
1163 Credential::new(test_api_key()),
1164 publishers_path(),
1165 get_atomic_clock_realtime(),
1166 false,
1167 "://invalid",
1168 )
1169 .expect_err("expected invalid base URL to fail");
1170 let err_msg = format!("{err}");
1171
1172 assert!(
1173 err_msg.contains("Failed to parse Databento Historical API base URL"),
1174 "unexpected error message: {err_msg}",
1175 );
1176 }
1177
1178 #[rstest]
1179 #[tokio::test]
1180 async fn test_get_range_order_book_deltas_preserves_boundaries_and_drains_end() {
1181 let last = dbn::flags::LAST;
1182 let body = encode_mbo_response(&[
1183 mbo_record('C' as c_char, 0, 1),
1184 mbo_record('N' as c_char, last, 2),
1185 mbo_record('C' as c_char, 0, 3),
1186 ]);
1187 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1188 let base_url = format!("http://{}/", listener.local_addr().unwrap());
1189 let server = tokio::spawn(serve_response(listener, body));
1190 let client = DatabentoHistoricalClient::new_with_base_url(
1191 Credential::new(test_api_key()),
1192 publishers_path(),
1193 get_atomic_clock_realtime(),
1194 false,
1195 &base_url,
1196 )
1197 .unwrap();
1198
1199 let deltas = client
1200 .get_range_order_book_deltas(RangeQueryParams {
1201 dataset: "GLBX.MDP3".to_string(),
1202 symbols: vec!["ESM4".to_string()],
1203 start: 1_609_160_000_000_000_000.into(),
1204 end: Some(1_609_161_000_000_000_000.into()),
1205 limit: None,
1206 price_precision: Some(2),
1207 })
1208 .await
1209 .unwrap();
1210 server.await.unwrap();
1211
1212 assert_eq!(deltas.len(), 2);
1213 assert_eq!(deltas[0].instrument_id, InstrumentId::from("ESM4.GLBX"));
1214 assert_eq!(deltas[0].order.order_id, 1);
1215 assert_eq!(deltas[0].flags, last);
1216 assert_eq!(deltas[0].sequence, 1);
1217 assert_eq!(deltas[1].instrument_id, InstrumentId::from("ESM4.GLBX"));
1218 assert_eq!(deltas[1].order.order_id, 3);
1219 assert_eq!(deltas[1].flags, 0);
1220 assert_eq!(deltas[1].sequence, 3);
1221 }
1222
1223 #[rstest]
1224 fn test_definition_precision_batch_merges_and_last_symbol_wins(
1225 historical_client: DatabentoHistoricalClient,
1226 ) {
1227 let mut first = nautilus_model::instruments::stubs::crypto_perpetual_ethusdt();
1228 first.id = "FIRST.GLBX".into();
1229 first.price_precision = 3;
1230 let mut second = first.clone();
1231 second.id = "SECOND.GLBX".into();
1232 second.price_precision = 5;
1233 let mut latest = first.clone();
1234 latest.id = "FIRST.XNAS".into();
1235 latest.price_precision = 7;
1236 historical_client.set_price_precision("RETAINED".into(), 9);
1237 historical_client.set_price_precision("FIRST".into(), 1);
1238
1239 historical_client.cache_price_precisions(&[
1240 InstrumentAny::CryptoPerpetual(first),
1241 InstrumentAny::CryptoPerpetual(second),
1242 InstrumentAny::CryptoPerpetual(latest),
1243 ]);
1244
1245 assert_eq!(historical_client.price_precisions.len(), 3);
1246
1247 for (symbol, precision) in [("RETAINED", 9), ("FIRST", 7), ("SECOND", 5)] {
1248 assert_eq!(
1249 historical_client
1250 .resolve_price_precision(&InstrumentId::new(symbol.into(), "GLBX".into()), None)
1251 .unwrap(),
1252 precision
1253 );
1254 }
1255 }
1256
1257 #[rstest]
1258 fn test_set_price_precision_inserts_into_cache(historical_client: DatabentoHistoricalClient) {
1259 let symbol = Symbol::from("ESM4");
1260
1261 assert_eq!(historical_client.price_precision(symbol), None);
1262
1263 historical_client.set_price_precision(symbol, 2);
1264
1265 assert_eq!(historical_client.price_precision(symbol), Some(2));
1266 }
1267
1268 #[rstest]
1269 fn test_resolve_price_precision_explicit_arg(historical_client: DatabentoHistoricalClient) {
1270 let instrument_id = InstrumentId::from("ESM4.GLBX");
1271 historical_client.set_price_precision(Symbol::from("ESM4"), 9);
1273
1274 let precision = historical_client
1275 .resolve_price_precision(&instrument_id, Some(2))
1276 .unwrap();
1277 assert_eq!(precision, 2);
1278 }
1279
1280 #[rstest]
1281 fn test_resolve_price_precision_cache_hit(historical_client: DatabentoHistoricalClient) {
1282 let instrument_id = InstrumentId::from("ESM4.GLBX");
1283 historical_client.set_price_precision(Symbol::from("ESM4"), 2);
1284
1285 let precision = historical_client
1286 .resolve_price_precision(&instrument_id, None)
1287 .unwrap();
1288 assert_eq!(precision, 2);
1289 }
1290
1291 #[rstest]
1292 fn test_resolve_price_precision_cache_miss_errors(
1293 historical_client: DatabentoHistoricalClient,
1294 ) {
1295 let instrument_id = InstrumentId::from("ESM4.GLBX");
1296
1297 let err = historical_client
1298 .resolve_price_precision(&instrument_id, None)
1299 .expect_err("expected cache-miss error");
1300 let err_msg = format!("{err}");
1301 assert!(
1302 err_msg.contains("Could not resolve `price_precision`"),
1303 "unexpected error message: {err_msg}",
1304 );
1305 assert!(
1306 err_msg.contains("ESM4.GLBX"),
1307 "error should name the instrument: {err_msg}",
1308 );
1309 }
1310}