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 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 for instrument in &instruments {
334 self.price_precisions
335 .insert(instrument.id().symbol, instrument.price_precision());
336 }
337
338 Ok(instruments)
339 }
340
341 pub async fn get_range_quotes(
347 &self,
348 params: RangeQueryParams,
349 schema: Option<String>,
350 ) -> anyhow::Result<Vec<QuoteTick>> {
351 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
352 check_consistent_symbology(&symbols)?;
353
354 let first_symbol = params
355 .symbols
356 .first()
357 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
358 let stype_in = infer_symbology_type(first_symbol);
359 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
360 let time_range = get_date_time_range(params.start, end)?;
361 let schema = schema.unwrap_or_else(|| "mbp-1".to_string());
362 let dbn_schema = dbn::Schema::from_str(&schema)?;
363
364 match dbn_schema {
365 dbn::Schema::Mbp1
366 | dbn::Schema::Tbbo
367 | dbn::Schema::Bbo1S
368 | dbn::Schema::Bbo1M
369 | dbn::Schema::Cmbp1
370 | dbn::Schema::Tcbbo
371 | dbn::Schema::Cbbo1S
372 | dbn::Schema::Cbbo1M => (),
373 _ => anyhow::bail!(
374 "Invalid schema. Must be one of: mbp-1, tbbo, bbo-1s, bbo-1m, cmbp-1, tcbbo, cbbo-1s, cbbo-1m"
375 ),
376 }
377
378 let range_params = GetRangeParams::builder()
379 .dataset(params.dataset)
380 .date_time_range(time_range)
381 .symbols(symbols)
382 .stype_in(stype_in)
383 .schema(dbn_schema)
384 .maybe_limit(params.limit.and_then(NonZeroU64::new))
385 .build();
386
387 let price_precision_arg = params.price_precision;
388
389 let mut client = (*self.inner).clone();
390 let mut decoder = client
391 .timeseries()
392 .get_range(&range_params)
393 .await
394 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
395
396 let metadata = decoder.metadata().clone();
397 let mut metadata_cache = MetadataCache::new(metadata);
398 let mut precision_cache = AHashMap::new();
399 let mut result: Vec<QuoteTick> = Vec::new();
400
401 let mut process_record = |record: dbn::RecordRef| -> anyhow::Result<()> {
402 let sym_map = self.symbol_venue_map.load();
403 let instrument_id = decode_nautilus_instrument_id(
404 &record,
405 &mut metadata_cache,
406 &self.publisher_venue_map,
407 &sym_map,
408 )?;
409 let price_precision = self.resolve_cached_price_precision(
410 &instrument_id,
411 price_precision_arg,
412 &mut precision_cache,
413 )?;
414
415 let (data, _) = decode_record(
416 &record,
417 instrument_id,
418 price_precision,
419 None,
420 false, true,
422 )?;
423
424 match data {
425 Some(Data::Quote(quote)) => result.push(quote),
426 None => {} _ => anyhow::bail!("Invalid data element not `QuoteTick`, was {data:?}"),
428 }
429 Ok(())
430 };
431
432 match dbn_schema {
433 dbn::Schema::Mbp1 => {
434 while let Some(msg) = decoder.decode_record::<dbn::Mbp1Msg>().await? {
435 process_record(dbn::RecordRef::from(msg))?;
436 }
437 }
438 dbn::Schema::Tbbo => {
439 while let Some(msg) = decoder.decode_record::<dbn::TbboMsg>().await? {
440 process_record(dbn::RecordRef::from(msg))?;
441 }
442 }
443 dbn::Schema::Cmbp1 => {
444 while let Some(msg) = decoder.decode_record::<dbn::Cmbp1Msg>().await? {
445 process_record(dbn::RecordRef::from(msg))?;
446 }
447 }
448 dbn::Schema::Tcbbo => {
449 while let Some(msg) = decoder.decode_record::<dbn::TcbboMsg>().await? {
450 process_record(dbn::RecordRef::from(msg))?;
451 }
452 }
453 dbn::Schema::Bbo1M => {
454 while let Some(msg) = decoder.decode_record::<dbn::Bbo1MMsg>().await? {
455 process_record(dbn::RecordRef::from(msg))?;
456 }
457 }
458 dbn::Schema::Bbo1S => {
459 while let Some(msg) = decoder.decode_record::<dbn::Bbo1SMsg>().await? {
460 process_record(dbn::RecordRef::from(msg))?;
461 }
462 }
463 dbn::Schema::Cbbo1S | dbn::Schema::Cbbo1M => {
464 while let Some(msg) = decoder.decode_record::<dbn::CbboMsg>().await? {
465 process_record(dbn::RecordRef::from(msg))?;
466 }
467 }
468 _ => anyhow::bail!("Invalid schema {dbn_schema}"),
469 }
470
471 Ok(result)
472 }
473
474 pub async fn get_range_order_book_depth10(
480 &self,
481 params: RangeQueryParams,
482 depth: Option<usize>,
483 ) -> anyhow::Result<Vec<OrderBookDepth10>> {
484 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
485 check_consistent_symbology(&symbols)?;
486
487 let first_symbol = params
488 .symbols
489 .first()
490 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
491 let stype_in = infer_symbology_type(first_symbol);
492 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
493 let time_range = get_date_time_range(params.start, end)?;
494
495 let _depth = depth.unwrap_or(10);
497 if _depth != 10 {
498 anyhow::bail!("Only depth=10 is currently supported for order book depths");
499 }
500
501 let range_params = GetRangeParams::builder()
502 .dataset(params.dataset)
503 .date_time_range(time_range)
504 .symbols(symbols)
505 .stype_in(stype_in)
506 .schema(dbn::Schema::Mbp10)
507 .maybe_limit(params.limit.and_then(NonZeroU64::new))
508 .build();
509
510 let price_precision_arg = params.price_precision;
511
512 let mut client = (*self.inner).clone();
513 let mut decoder = client
514 .timeseries()
515 .get_range(&range_params)
516 .await
517 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
518
519 let metadata = decoder.metadata().clone();
520 let mut metadata_cache = MetadataCache::new(metadata);
521 let mut precision_cache = AHashMap::new();
522 let mut result: Vec<OrderBookDepth10> = Vec::new();
523
524 let mut process_record = |record: dbn::RecordRef| -> anyhow::Result<()> {
525 let sym_map = self.symbol_venue_map.load();
526 let instrument_id = decode_nautilus_instrument_id(
527 &record,
528 &mut metadata_cache,
529 &self.publisher_venue_map,
530 &sym_map,
531 )?;
532 let price_precision = self.resolve_cached_price_precision(
533 &instrument_id,
534 price_precision_arg,
535 &mut precision_cache,
536 )?;
537
538 if let Some(msg) = record.get::<dbn::Mbp10Msg>() {
539 let depth = decode_mbp10_msg(msg, instrument_id, price_precision, None)?;
540 result.push(depth);
541 }
542
543 Ok(())
544 };
545
546 while let Some(msg) = decoder.decode_record::<dbn::Mbp10Msg>().await? {
547 process_record(dbn::RecordRef::from(msg))?;
548 }
549
550 Ok(result)
551 }
552
553 pub async fn get_range_order_book_deltas(
559 &self,
560 params: RangeQueryParams,
561 ) -> anyhow::Result<Vec<OrderBookDelta>> {
562 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
563 check_consistent_symbology(&symbols)?;
564
565 let first_symbol = params
566 .symbols
567 .first()
568 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
569 let stype_in = infer_symbology_type(first_symbol);
570 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
571 let time_range = get_date_time_range(params.start, end)?;
572
573 let range_params = GetRangeParams::builder()
574 .dataset(params.dataset)
575 .date_time_range(time_range)
576 .symbols(symbols)
577 .stype_in(stype_in)
578 .schema(dbn::Schema::Mbo)
579 .maybe_limit(params.limit.and_then(NonZeroU64::new))
580 .build();
581
582 let price_precision_arg = params.price_precision;
583
584 let mut client = (*self.inner).clone();
585 let mut decoder = client
586 .timeseries()
587 .get_range(&range_params)
588 .await
589 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
590
591 let metadata = decoder.metadata().clone();
592 let mut metadata_cache = MetadataCache::new(metadata);
593 let mut precision_cache = AHashMap::new();
594 let mut result: Vec<OrderBookDelta> = Vec::new();
595 let mut delta_buffer = MboDeltaBuffer::default();
596
597 let mut process_record = |record: dbn::RecordRef| -> anyhow::Result<()> {
598 let sym_map = self.symbol_venue_map.load();
599 let instrument_id = decode_nautilus_instrument_id(
600 &record,
601 &mut metadata_cache,
602 &self.publisher_venue_map,
603 &sym_map,
604 )?;
605 let price_precision = self.resolve_cached_price_precision(
606 &instrument_id,
607 price_precision_arg,
608 &mut precision_cache,
609 )?;
610
611 if let Some(msg) = record.get::<dbn::MboMsg>() {
612 let (delta, _trade) =
613 decode_mbo_msg(msg, instrument_id, price_precision, None, false)?;
614 delta_buffer.push(msg, instrument_id, delta);
615 while let Some(delta) = delta_buffer.pop_ready() {
616 result.push(delta);
617 }
618 }
619
620 Ok(())
621 };
622
623 while let Some(msg) = decoder.decode_record::<dbn::MboMsg>().await? {
624 process_record(dbn::RecordRef::from(msg))?;
625 }
626
627 delta_buffer.finish();
628 while let Some(delta) = delta_buffer.pop_ready() {
629 result.push(delta);
630 }
631
632 Ok(result)
633 }
634
635 pub async fn get_range_trades(
641 &self,
642 params: RangeQueryParams,
643 schema: Option<String>,
644 ) -> anyhow::Result<Vec<TradeTick>> {
645 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
646 check_consistent_symbology(&symbols)?;
647
648 let first_symbol = params
649 .symbols
650 .first()
651 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
652 let stype_in = infer_symbology_type(first_symbol);
653 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
654 let time_range = get_date_time_range(params.start, end)?;
655 let schema = schema.unwrap_or_else(|| "trades".to_string());
656 let dbn_schema = dbn::Schema::from_str(&schema)?;
657
658 match dbn_schema {
659 dbn::Schema::Trades
660 | dbn::Schema::Tbbo
661 | dbn::Schema::Tcbbo
662 | dbn::Schema::Mbp1
663 | dbn::Schema::Cmbp1 => (),
664 _ => {
665 anyhow::bail!("Invalid schema. Must be one of: trades, tbbo, tcbbo, mbp-1, cmbp-1")
666 }
667 }
668
669 let range_params = GetRangeParams::builder()
670 .dataset(params.dataset)
671 .date_time_range(time_range)
672 .symbols(symbols)
673 .stype_in(stype_in)
674 .schema(dbn_schema)
675 .maybe_limit(params.limit.and_then(NonZeroU64::new))
676 .build();
677
678 let price_precision_arg = params.price_precision;
679
680 let mut client = (*self.inner).clone();
681 let mut decoder = client
682 .timeseries()
683 .get_range(&range_params)
684 .await
685 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
686
687 let metadata = decoder.metadata().clone();
688 let mut metadata_cache = MetadataCache::new(metadata);
689 let mut precision_cache = AHashMap::new();
690 let mut result: Vec<TradeTick> = Vec::new();
691
692 let mut process_record = |record: dbn::RecordRef| -> anyhow::Result<()> {
693 let sym_map = self.symbol_venue_map.load();
694 let instrument_id = decode_nautilus_instrument_id(
695 &record,
696 &mut metadata_cache,
697 &self.publisher_venue_map,
698 &sym_map,
699 )?;
700 let price_precision = self.resolve_cached_price_precision(
701 &instrument_id,
702 price_precision_arg,
703 &mut precision_cache,
704 )?;
705
706 let (data, data2) =
707 decode_record(&record, instrument_id, price_precision, None, true, true)?;
708
709 match (data, data2) {
710 (Some(Data::Trade(trade)), _) | (_, Some(Data::Trade(trade))) => result.push(trade),
711 (Some(_) | None, None) => {}
712 (None, Some(data)) => {
713 anyhow::bail!("Invalid data element not `TradeTick`, was {data:?}")
714 }
715 (Some(data), Some(_)) => {
716 anyhow::bail!("Invalid data element not `TradeTick`, was {data:?}")
717 }
718 }
719 Ok(())
720 };
721
722 match dbn_schema {
723 dbn::Schema::Trades => {
724 while let Some(msg) = decoder.decode_record::<dbn::TradeMsg>().await? {
725 process_record(dbn::RecordRef::from(msg))?;
726 }
727 }
728 dbn::Schema::Mbp1 => {
729 while let Some(msg) = decoder.decode_record::<dbn::Mbp1Msg>().await? {
730 process_record(dbn::RecordRef::from(msg))?;
731 }
732 }
733 dbn::Schema::Tbbo => {
734 while let Some(msg) = decoder.decode_record::<dbn::TbboMsg>().await? {
735 process_record(dbn::RecordRef::from(msg))?;
736 }
737 }
738 dbn::Schema::Cmbp1 => {
739 while let Some(msg) = decoder.decode_record::<dbn::Cmbp1Msg>().await? {
740 process_record(dbn::RecordRef::from(msg))?;
741 }
742 }
743 dbn::Schema::Tcbbo => {
744 while let Some(msg) = decoder.decode_record::<dbn::TcbboMsg>().await? {
745 process_record(dbn::RecordRef::from(msg))?;
746 }
747 }
748 _ => anyhow::bail!("Invalid schema {dbn_schema}"),
749 }
750
751 Ok(result)
752 }
753
754 pub async fn get_range_bars(
760 &self,
761 params: RangeQueryParams,
762 aggregation: BarAggregation,
763 timestamp_on_close: bool,
764 ) -> anyhow::Result<Vec<Bar>> {
765 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
766 check_consistent_symbology(&symbols)?;
767
768 let first_symbol = params
769 .symbols
770 .first()
771 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
772 let stype_in = infer_symbology_type(first_symbol);
773 let schema = match aggregation {
774 BarAggregation::Second => dbn::Schema::Ohlcv1S,
775 BarAggregation::Minute => dbn::Schema::Ohlcv1M,
776 BarAggregation::Hour => dbn::Schema::Ohlcv1H,
777 BarAggregation::Day => dbn::Schema::Ohlcv1D,
778 _ => anyhow::bail!("Invalid `BarAggregation` for request, was {aggregation}"),
779 };
780
781 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
782 let time_range = get_date_time_range(params.start, end)?;
783
784 let range_params = GetRangeParams::builder()
785 .dataset(params.dataset)
786 .date_time_range(time_range)
787 .symbols(symbols)
788 .stype_in(stype_in)
789 .schema(schema)
790 .maybe_limit(params.limit.and_then(NonZeroU64::new))
791 .build();
792
793 let price_precision_arg = params.price_precision;
794
795 let mut client = (*self.inner).clone();
796 let mut decoder = client
797 .timeseries()
798 .get_range(&range_params)
799 .await
800 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
801
802 let metadata = decoder.metadata().clone();
803 let mut metadata_cache = MetadataCache::new(metadata);
804 let mut precision_cache = AHashMap::new();
805 let mut result: Vec<Bar> = Vec::new();
806
807 while let Some(msg) = decoder.decode_record::<dbn::OhlcvMsg>().await? {
808 let record = dbn::RecordRef::from(msg);
809 let sym_map = self.symbol_venue_map.load();
810 let instrument_id = decode_nautilus_instrument_id(
811 &record,
812 &mut metadata_cache,
813 &self.publisher_venue_map,
814 &sym_map,
815 )?;
816 let price_precision = self.resolve_cached_price_precision(
817 &instrument_id,
818 price_precision_arg,
819 &mut precision_cache,
820 )?;
821
822 let (data, _) = decode_record(
823 &record,
824 instrument_id,
825 price_precision,
826 None,
827 false, timestamp_on_close,
829 )?;
830
831 match data {
832 Some(Data::Bar(bar)) => {
833 result.push(bar);
834 }
835 _ => anyhow::bail!("Invalid data element not `Bar`, was {data:?}"),
836 }
837 }
838
839 Ok(result)
840 }
841
842 pub async fn get_range_imbalance(
848 &self,
849 params: RangeQueryParams,
850 ) -> anyhow::Result<Vec<DatabentoImbalance>> {
851 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
852 check_consistent_symbology(&symbols)?;
853
854 let first_symbol = params
855 .symbols
856 .first()
857 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
858 let stype_in = infer_symbology_type(first_symbol);
859 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
860 let time_range = get_date_time_range(params.start, end)?;
861
862 let range_params = GetRangeParams::builder()
863 .dataset(params.dataset)
864 .date_time_range(time_range)
865 .symbols(symbols)
866 .stype_in(stype_in)
867 .schema(dbn::Schema::Imbalance)
868 .maybe_limit(params.limit.and_then(NonZeroU64::new))
869 .build();
870
871 let price_precision_arg = params.price_precision;
872
873 let mut client = (*self.inner).clone();
874 let mut decoder = client
875 .timeseries()
876 .get_range(&range_params)
877 .await
878 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
879
880 let metadata = decoder.metadata().clone();
881 let mut metadata_cache = MetadataCache::new(metadata);
882 let mut precision_cache = AHashMap::new();
883 let mut result: Vec<DatabentoImbalance> = Vec::new();
884
885 while let Some(msg) = decoder.decode_record::<dbn::ImbalanceMsg>().await? {
886 let record = dbn::RecordRef::from(msg);
887 let sym_map = self.symbol_venue_map.load();
888 let instrument_id = decode_nautilus_instrument_id(
889 &record,
890 &mut metadata_cache,
891 &self.publisher_venue_map,
892 &sym_map,
893 )?;
894 let price_precision = self.resolve_cached_price_precision(
895 &instrument_id,
896 price_precision_arg,
897 &mut precision_cache,
898 )?;
899
900 let imbalance = decode_imbalance_msg(msg, instrument_id, price_precision, None)?;
901 result.push(imbalance);
902 }
903
904 Ok(result)
905 }
906
907 pub async fn get_range_statistics(
913 &self,
914 params: RangeQueryParams,
915 ) -> anyhow::Result<Vec<DatabentoStatistics>> {
916 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
917 check_consistent_symbology(&symbols)?;
918
919 let first_symbol = params
920 .symbols
921 .first()
922 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
923 let stype_in = infer_symbology_type(first_symbol);
924 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
925 let time_range = get_date_time_range(params.start, end)?;
926
927 let range_params = GetRangeParams::builder()
928 .dataset(params.dataset)
929 .date_time_range(time_range)
930 .symbols(symbols)
931 .stype_in(stype_in)
932 .schema(dbn::Schema::Statistics)
933 .maybe_limit(params.limit.and_then(NonZeroU64::new))
934 .build();
935
936 let price_precision_arg = params.price_precision;
937
938 let mut client = (*self.inner).clone();
939 let mut decoder = client
940 .timeseries()
941 .get_range(&range_params)
942 .await
943 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
944
945 let metadata = decoder.metadata().clone();
946 let mut metadata_cache = MetadataCache::new(metadata);
947 let mut precision_cache = AHashMap::new();
948 let mut result: Vec<DatabentoStatistics> = Vec::new();
949
950 while let Some(msg) = decoder.decode_record::<dbn::StatMsg>().await? {
951 if !is_supported_stat_type(msg.stat_type) {
953 log::warn!("Skipping unsupported `stat_type` {}", msg.stat_type);
954 continue;
955 }
956
957 let record = dbn::RecordRef::from(msg);
958 let sym_map = self.symbol_venue_map.load();
959 let instrument_id = decode_nautilus_instrument_id(
960 &record,
961 &mut metadata_cache,
962 &self.publisher_venue_map,
963 &sym_map,
964 )?;
965 let price_precision = self.resolve_cached_price_precision(
966 &instrument_id,
967 price_precision_arg,
968 &mut precision_cache,
969 )?;
970
971 if let Some(statistics) =
972 decode_statistics_msg(msg, instrument_id, price_precision, None)?
973 {
974 result.push(statistics);
975 }
976 }
977
978 Ok(result)
979 }
980
981 pub async fn get_range_status(
987 &self,
988 params: RangeQueryParams,
989 ) -> anyhow::Result<Vec<InstrumentStatus>> {
990 let symbols: Vec<&str> = params.symbols.iter().map(String::as_str).collect();
991 check_consistent_symbology(&symbols)?;
992
993 let first_symbol = params
994 .symbols
995 .first()
996 .ok_or_else(|| anyhow::anyhow!("No symbols provided"))?;
997 let stype_in = infer_symbology_type(first_symbol);
998 let end = params.end.unwrap_or_else(|| self.clock.get_time_ns());
999 let time_range = get_date_time_range(params.start, end)?;
1000
1001 let range_params = GetRangeParams::builder()
1002 .dataset(params.dataset)
1003 .date_time_range(time_range)
1004 .symbols(symbols)
1005 .stype_in(stype_in)
1006 .schema(dbn::Schema::Status)
1007 .maybe_limit(params.limit.and_then(NonZeroU64::new))
1008 .build();
1009
1010 let mut client = (*self.inner).clone();
1011 let mut decoder = client
1012 .timeseries()
1013 .get_range(&range_params)
1014 .await
1015 .map_err(|e| anyhow::anyhow!("Failed to get range: {e}"))?;
1016
1017 let metadata = decoder.metadata().clone();
1018 let mut metadata_cache = MetadataCache::new(metadata);
1019 let mut result: Vec<InstrumentStatus> = Vec::new();
1020
1021 while let Some(msg) = decoder.decode_record::<dbn::StatusMsg>().await? {
1022 let record = dbn::RecordRef::from(msg);
1023 let sym_map = self.symbol_venue_map.load();
1024 let instrument_id = decode_nautilus_instrument_id(
1025 &record,
1026 &mut metadata_cache,
1027 &self.publisher_venue_map,
1028 &sym_map,
1029 )?;
1030
1031 let status = decode_status_msg(msg, instrument_id, None)?;
1032 result.push(status);
1033 }
1034
1035 Ok(result)
1036 }
1037
1038 pub fn prepare_symbols_from_instrument_ids(
1040 &self,
1041 instrument_ids: &[InstrumentId],
1042 ) -> Vec<String> {
1043 self.symbol_venue_map.rcu(|m| {
1044 for id in instrument_ids {
1045 m.entry(id.symbol).or_insert(id.venue);
1046 }
1047 });
1048
1049 instrument_ids
1050 .iter()
1051 .map(|id| id.symbol.to_string())
1052 .collect()
1053 }
1054}
1055
1056#[cfg(test)]
1057mod tests {
1058 use std::ffi::c_char;
1059
1060 use databento::dbn::{MappingInterval, SymbolMapping, encode::EncodeRecord};
1061 use nautilus_core::time::get_atomic_clock_realtime;
1062 use rstest::{fixture, rstest};
1063 use time::macros::date;
1064 use tokio::{
1065 io::{AsyncReadExt, AsyncWriteExt},
1066 net::TcpListener,
1067 };
1068
1069 use super::*;
1070
1071 fn test_api_key() -> String {
1072 "test-000000000000000000000000000".to_string()
1073 }
1074
1075 fn publishers_path() -> PathBuf {
1076 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("publishers.json")
1077 }
1078
1079 fn mbo_record(action: c_char, flags: u8, sequence: u32) -> dbn::MboMsg {
1080 let ts_event = 1_609_160_400_000_000_000;
1081 dbn::MboMsg {
1082 hd: dbn::RecordHeader::new::<dbn::MboMsg>(dbn::rtype::MBO, 1, 1, ts_event),
1083 order_id: u64::from(sequence),
1084 price: 4_800_250_000_000,
1085 size: 2,
1086 flags: dbn::FlagSet::new(flags),
1087 channel_id: 1,
1088 action,
1089 side: 'A' as c_char,
1090 ts_recv: ts_event,
1091 ts_in_delta: 0,
1092 sequence,
1093 }
1094 }
1095
1096 fn encode_mbo_response(records: &[dbn::MboMsg]) -> Vec<u8> {
1097 let metadata = dbn::Metadata::builder()
1098 .dataset("GLBX.MDP3")
1099 .schema(Some(dbn::Schema::Mbo))
1100 .start(1_609_160_400_000_000_000)
1101 .stype_in(Some(dbn::SType::RawSymbol))
1102 .stype_out(dbn::SType::InstrumentId)
1103 .symbols(vec!["ESM4".to_string()])
1104 .mappings(vec![SymbolMapping {
1105 raw_symbol: "ESM4".to_string(),
1106 intervals: vec![MappingInterval {
1107 start_date: date!(2020 - 12 - 28),
1108 end_date: date!(2020 - 12 - 29),
1109 symbol: "1".to_string(),
1110 }],
1111 }])
1112 .build();
1113 let mut body = Vec::new();
1114 {
1115 let mut encoder = dbn::encode::dbn::Encoder::with_zstd(&mut body, &metadata).unwrap();
1116 encoder.encode_records(records).unwrap();
1117 encoder.flush().unwrap();
1118 }
1119 body
1120 }
1121
1122 async fn serve_response(listener: TcpListener, body: Vec<u8>) {
1123 let (mut stream, _) = listener.accept().await.unwrap();
1124 let mut request = Vec::new();
1125 let mut buffer = [0_u8; 1024];
1126 loop {
1127 let read = stream.read(&mut buffer).await.unwrap();
1128 if read == 0 {
1129 break;
1130 }
1131 request.extend_from_slice(&buffer[..read]);
1132 if request.windows(4).any(|window| window == b"\r\n\r\n") {
1133 break;
1134 }
1135 }
1136
1137 let headers = format!(
1138 "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1139 body.len(),
1140 );
1141 stream.write_all(headers.as_bytes()).await.unwrap();
1142 stream.write_all(&body).await.unwrap();
1143 stream.shutdown().await.unwrap();
1144 }
1145
1146 #[fixture]
1147 fn historical_client() -> DatabentoHistoricalClient {
1148 DatabentoHistoricalClient::new(
1149 Credential::new(test_api_key()),
1150 publishers_path(),
1151 get_atomic_clock_realtime(),
1152 false,
1153 )
1154 .unwrap()
1155 }
1156
1157 #[rstest]
1158 fn test_new_with_base_url_rejects_invalid_url() {
1159 let err = DatabentoHistoricalClient::new_with_base_url(
1160 Credential::new(test_api_key()),
1161 publishers_path(),
1162 get_atomic_clock_realtime(),
1163 false,
1164 "://invalid",
1165 )
1166 .expect_err("expected invalid base URL to fail");
1167 let err_msg = format!("{err}");
1168
1169 assert!(
1170 err_msg.contains("Failed to parse Databento Historical API base URL"),
1171 "unexpected error message: {err_msg}",
1172 );
1173 }
1174
1175 #[rstest]
1176 #[tokio::test]
1177 async fn test_get_range_order_book_deltas_preserves_boundaries_and_drains_end() {
1178 let last = dbn::flags::LAST;
1179 let body = encode_mbo_response(&[
1180 mbo_record('C' as c_char, 0, 1),
1181 mbo_record('N' as c_char, last, 2),
1182 mbo_record('C' as c_char, 0, 3),
1183 ]);
1184 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1185 let base_url = format!("http://{}/", listener.local_addr().unwrap());
1186 let server = tokio::spawn(serve_response(listener, body));
1187 let client = DatabentoHistoricalClient::new_with_base_url(
1188 Credential::new(test_api_key()),
1189 publishers_path(),
1190 get_atomic_clock_realtime(),
1191 false,
1192 &base_url,
1193 )
1194 .unwrap();
1195
1196 let deltas = client
1197 .get_range_order_book_deltas(RangeQueryParams {
1198 dataset: "GLBX.MDP3".to_string(),
1199 symbols: vec!["ESM4".to_string()],
1200 start: 1_609_160_000_000_000_000.into(),
1201 end: Some(1_609_161_000_000_000_000.into()),
1202 limit: None,
1203 price_precision: Some(2),
1204 })
1205 .await
1206 .unwrap();
1207 server.await.unwrap();
1208
1209 assert_eq!(deltas.len(), 2);
1210 assert_eq!(deltas[0].instrument_id, InstrumentId::from("ESM4.GLBX"));
1211 assert_eq!(deltas[0].order.order_id, 1);
1212 assert_eq!(deltas[0].flags, last);
1213 assert_eq!(deltas[0].sequence, 1);
1214 assert_eq!(deltas[1].instrument_id, InstrumentId::from("ESM4.GLBX"));
1215 assert_eq!(deltas[1].order.order_id, 3);
1216 assert_eq!(deltas[1].flags, 0);
1217 assert_eq!(deltas[1].sequence, 3);
1218 }
1219
1220 #[rstest]
1221 fn test_set_price_precision_inserts_into_cache(historical_client: DatabentoHistoricalClient) {
1222 let symbol = Symbol::from("ESM4");
1223
1224 assert_eq!(historical_client.price_precision(symbol), None);
1225
1226 historical_client.set_price_precision(symbol, 2);
1227
1228 assert_eq!(historical_client.price_precision(symbol), Some(2));
1229 }
1230
1231 #[rstest]
1232 fn test_resolve_price_precision_explicit_arg(historical_client: DatabentoHistoricalClient) {
1233 let instrument_id = InstrumentId::from("ESM4.GLBX");
1234 historical_client.set_price_precision(Symbol::from("ESM4"), 9);
1236
1237 let precision = historical_client
1238 .resolve_price_precision(&instrument_id, Some(2))
1239 .unwrap();
1240 assert_eq!(precision, 2);
1241 }
1242
1243 #[rstest]
1244 fn test_resolve_price_precision_cache_hit(historical_client: DatabentoHistoricalClient) {
1245 let instrument_id = InstrumentId::from("ESM4.GLBX");
1246 historical_client.set_price_precision(Symbol::from("ESM4"), 2);
1247
1248 let precision = historical_client
1249 .resolve_price_precision(&instrument_id, None)
1250 .unwrap();
1251 assert_eq!(precision, 2);
1252 }
1253
1254 #[rstest]
1255 fn test_resolve_price_precision_cache_miss_errors(
1256 historical_client: DatabentoHistoricalClient,
1257 ) {
1258 let instrument_id = InstrumentId::from("ESM4.GLBX");
1259
1260 let err = historical_client
1261 .resolve_price_precision(&instrument_id, None)
1262 .expect_err("expected cache-miss error");
1263 let err_msg = format!("{err}");
1264 assert!(
1265 err_msg.contains("Could not resolve `price_precision`"),
1266 "unexpected error message: {err_msg}",
1267 );
1268 assert!(
1269 err_msg.contains("ESM4.GLBX"),
1270 "error should name the instrument: {err_msg}",
1271 );
1272 }
1273}