1use ahash::AHashMap;
17use jiff::Timestamp;
18use nautilus_common::messages::data::{
19 BarsResponse, BookDeltasResponse, BookDepthResponse, CustomDataResponse, DataResponse,
20 FundingRatesResponse, InstrumentResponse, InstrumentsResponse, QuotesResponse, RequestBars,
21 RequestBookDeltas, RequestBookDepth, RequestCommand, RequestCustomData, RequestFundingRates,
22 RequestInstrument, RequestInstruments, RequestQuotes, RequestTrades, SubscribeBars,
23 SubscribeCommand, SubscribeCustomData, SubscribeQuotes, SubscribeTrades, TradesResponse,
24};
25use nautilus_core::{
26 Params, UUID4, UnixNanos,
27 correctness::{FAILED, check_key_not_in_map},
28};
29use nautilus_model::{
30 data::{
31 Bar, CustomData, DataBatch, FromDataBatch, FundingRateUpdate, NautilusDataType,
32 OrderBookDelta, OrderBookDepth, QuoteTick, TradeTick,
33 },
34 identifiers::{ClientId, Venue},
35 instruments::{Instrument, InstrumentAny},
36};
37use nautilus_persistence::catalog::traits::{CatalogInstrumentQuery, CatalogQuery, DataCatalog};
38use serde_json::Value;
39use ustr::Ustr;
40
41use super::{DataEngine, requests::request_params};
42
43const PARAM_SKIP_CATALOG_DATA: &str = "skip_catalog_data";
44const PARAM_UPDATE_CATALOG: &str = "update_catalog";
45const PARAM_FORCE_INSTRUMENT_UPDATE: &str = "force_instrument_update";
46const PARAM_SUBSCRIPTION_NAME: &str = "subscription_name";
47const PARAM_FROM_DAY_START: &str = "from_day_start";
48const CATALOG_CLIENT_ID: &str = "CATALOG";
49
50pub(crate) type CatalogMap = AHashMap<Ustr, DataCatalog>;
51
52impl DataEngine {
53 pub fn register_catalog(&mut self, catalog: DataCatalog, name: Option<&str>) {
59 let name = Ustr::from(name.unwrap_or("catalog_0"));
60
61 check_key_not_in_map(&name, &self.catalogs, "name", "catalogs").expect(FAILED);
62
63 self.catalogs.insert(name, catalog);
64 log::info!("Registered catalog <{name}>");
65 }
66
67 pub(super) fn subscribe_command_with_prefilled_start_ns(
68 &mut self,
69 cmd: SubscribeCommand,
70 ) -> anyhow::Result<SubscribeCommand> {
71 match cmd {
72 SubscribeCommand::Quotes(cmd) if Self::is_start_ns_missing(cmd.params.as_ref()) => {
73 let identifier = cmd.instrument_id.to_string();
74 let params = self.params_with_prefilled_start_ns(
75 cmd.params.as_ref(),
76 &NautilusDataType::QuoteTick,
77 &identifier,
78 )?;
79 Ok(SubscribeCommand::Quotes(SubscribeQuotes { params, ..cmd }))
80 }
81 SubscribeCommand::Trades(cmd) if Self::is_start_ns_missing(cmd.params.as_ref()) => {
82 let identifier = cmd.instrument_id.to_string();
83 let params = self.params_with_prefilled_start_ns(
84 cmd.params.as_ref(),
85 &NautilusDataType::TradeTick,
86 &identifier,
87 )?;
88 Ok(SubscribeCommand::Trades(SubscribeTrades { params, ..cmd }))
89 }
90 SubscribeCommand::Bars(cmd)
91 if cmd.bar_type.is_externally_aggregated()
92 && Self::is_start_ns_missing(cmd.params.as_ref()) =>
93 {
94 let identifier = cmd.bar_type.to_string();
95 let params = self.params_with_prefilled_start_ns(
96 cmd.params.as_ref(),
97 &NautilusDataType::Bar,
98 &identifier,
99 )?;
100 Ok(SubscribeCommand::Bars(SubscribeBars { params, ..cmd }))
101 }
102 SubscribeCommand::Data(cmd) if Self::is_start_ns_missing(cmd.params.as_ref()) => {
103 let type_name = cmd.data_type.type_name().to_string();
104 let identifier = cmd.data_type.identifier().map(String::from);
105 let params = self.params_with_custom_data_prefilled_start_ns(
106 cmd.params.as_ref(),
107 &type_name,
108 identifier.as_deref(),
109 )?;
110 Ok(SubscribeCommand::Data(SubscribeCustomData {
111 params,
112 ..cmd
113 }))
114 }
115 _ => Ok(cmd),
116 }
117 }
118
119 fn is_start_ns_missing(params: Option<&Params>) -> bool {
120 params.is_none_or(|params| !params.contains_key("start_ns"))
121 }
122
123 fn params_with_prefilled_start_ns(
124 &mut self,
125 params: Option<&Params>,
126 data_type: &NautilusDataType,
127 identifier: &str,
128 ) -> anyhow::Result<Option<Params>> {
129 let last_timestamp = self.catalog_last_timestamp(data_type, identifier)?;
130
131 Ok(Some(Self::params_with_start_ns(params, last_timestamp)))
132 }
133
134 fn params_with_custom_data_prefilled_start_ns(
135 &mut self,
136 params: Option<&Params>,
137 type_name: &str,
138 identifier: Option<&str>,
139 ) -> anyhow::Result<Option<Params>> {
140 let last_timestamp = self.catalog_custom_data_last_timestamp(type_name, identifier)?;
141
142 Ok(Some(Self::params_with_start_ns(params, last_timestamp)))
143 }
144
145 fn params_with_start_ns(params: Option<&Params>, last_timestamp: Option<u64>) -> Params {
146 let start_ns = last_timestamp.map_or(Value::Null, |last_timestamp| {
147 Value::from(last_timestamp.saturating_add(1))
148 });
149 let mut params = params.cloned().unwrap_or_else(Params::new);
150
151 params.insert("start_ns".to_string(), start_ns);
152
153 params
154 }
155
156 fn catalog_last_timestamp(
157 &mut self,
158 data_type: &NautilusDataType,
159 identifier: &str,
160 ) -> anyhow::Result<Option<u64>> {
161 for catalog in self.catalogs.values_mut() {
162 if let Some(last_timestamp) =
163 catalog.query_last_timestamp(data_type.clone(), Some(identifier))?
164 {
165 return Ok(Some(last_timestamp));
166 }
167 }
168
169 Ok(None)
170 }
171
172 fn catalog_custom_data_last_timestamp(
173 &mut self,
174 type_name: &str,
175 identifier: Option<&str>,
176 ) -> anyhow::Result<Option<u64>> {
177 let data_type = NautilusDataType::Custom {
182 type_name: type_name.to_string(),
183 };
184
185 for catalog in self.catalogs.values_mut() {
186 if let Some(last_timestamp) =
187 catalog.query_last_timestamp(data_type.clone(), identifier)?
188 {
189 return Ok(Some(last_timestamp));
190 }
191 }
192
193 Ok(None)
194 }
195
196 pub(super) fn catalogs_registered(&self) -> bool {
197 !self.catalogs.is_empty()
198 }
199
200 pub(super) fn dispatch_date_range_request(
206 &mut self,
207 req: RequestCommand,
208 ) -> anyhow::Result<()> {
209 if matches!(
210 req,
211 RequestCommand::Instrument(_) | RequestCommand::Instruments(_)
212 ) {
213 return self.dispatch_instrument_catalog_request(req);
214 }
215
216 let Some(key) = request_identifier(&req) else {
217 return self.dispatch_request_to_client(req).map(|_| ());
218 };
219
220 let now_ns = self.clock.borrow().timestamp_ns();
221 let now_dt = now_ns.to_datetime_utc();
222 let query_past_data = request_params(&req)
223 .and_then(|p| p.get(PARAM_SUBSCRIPTION_NAME))
224 .is_none();
225
226 let (start_dt, end_dt) = bound_request_dates(
227 request_start(&req),
228 request_end(&req),
229 now_dt,
230 query_past_data,
231 );
232 let start_ns = datetime_to_unix_nanos_or_zero(start_dt);
233 let end_ns = datetime_to_unix_nanos_or_zero(end_dt);
234
235 if start_ns > end_ns {
236 anyhow::bail!(
237 "Cannot dispatch request, start {start_ns} was greater than end {end_ns}"
238 );
239 }
240
241 let client_id = req.client_id().copied();
242 let venue = req.venue().copied();
243 let used_client_id = self
244 .get_client(client_id.as_ref(), venue.as_ref())
245 .map(|client| client.client_id());
246
247 let (catalog_start_dt, catalog_start_ns) = if matches!(req, RequestCommand::BookDeltas(_))
251 && request_params(&req)
252 .and_then(|p| p.get_bool(PARAM_FROM_DAY_START))
253 .unwrap_or(true)
254 {
255 let floored = floor_to_utc_day(start_dt);
256 (floored, datetime_to_unix_nanos_or_zero(floored))
257 } else {
258 (start_dt, start_ns)
259 };
260
261 let query_interval = vec![(start_ns.as_u64(), end_ns.as_u64())];
262 let catalog_query_interval = vec![(catalog_start_ns.as_u64(), end_ns.as_u64())];
263 let mut missing_intervals = query_interval.clone();
264 let mut has_catalog_data = false;
265 let mut winning_catalog: Option<Ustr> = None;
266
267 for (name, catalog) in &mut self.catalogs {
268 let catalog_intervals = catalog_missing_intervals(
269 catalog,
270 catalog_start_ns.as_u64(),
271 end_ns.as_u64(),
272 &key,
273 )?;
274
275 if catalog_intervals != catalog_query_interval {
276 has_catalog_data = true;
277 winning_catalog = Some(*name);
278 missing_intervals = if catalog_start_ns == start_ns {
280 catalog_intervals
281 } else {
282 catalog_missing_intervals(catalog, start_ns.as_u64(), end_ns.as_u64(), &key)?
283 };
284 break;
285 }
286 }
287
288 let skip_catalog_data = request_params(&req)
289 .and_then(|p| p.get_bool(PARAM_SKIP_CATALOG_DATA))
290 .unwrap_or(false);
291
292 if skip_catalog_data {
296 missing_intervals = query_interval;
297 }
298
299 let n_client_requests = if used_client_id.is_some() {
300 missing_intervals.len()
301 } else {
302 0
303 };
304 let n_catalog_requests = usize::from(has_catalog_data && !skip_catalog_data);
305 let n_requests = n_client_requests + n_catalog_requests;
306
307 if n_requests == 0 {
308 let empty = build_empty_response(&req, start_ns, end_ns, used_client_id, now_ns)?;
309 self.response(empty);
310 return Ok(());
311 }
312
313 let parent_id = *req.request_id();
314 self.new_request_pipeline(req.clone(), n_requests);
315
316 if n_catalog_requests == 1
317 && let Some(catalog_name) = winning_catalog
318 {
319 let leg = with_dates_for_pipeline(&req, Some(catalog_start_dt), Some(end_dt), now_ns);
320 let leg_id = *leg.request_id();
321 self.register_request_pipeline_leg(leg_id, parent_id);
322
323 match self.query_catalog_leg(
324 &leg,
325 catalog_name,
326 catalog_start_ns,
327 end_ns,
328 used_client_id,
329 now_ns,
330 ) {
331 Ok(resp) => self.response(resp),
332 Err(e) => {
333 log::error!(
334 "Catalog leg query failed for parent {parent_id} (catalog {catalog_name}): {e}"
335 );
336 let empty = match build_empty_response(
337 &leg,
338 start_ns,
339 end_ns,
340 used_client_id,
341 now_ns,
342 ) {
343 Ok(empty) => empty,
344 Err(e) => {
345 self.abort_request_pipeline(parent_id);
346 return Err(e);
347 }
348 };
349 self.response(empty);
350 }
351 }
352 }
353
354 if n_client_requests > 0 {
355 for (leg_start_ns, leg_end_ns) in &missing_intervals {
356 let leg_start_dt = UnixNanos::from(*leg_start_ns).to_datetime_utc();
357 let leg_end_dt = UnixNanos::from(*leg_end_ns).to_datetime_utc();
358 let leg =
359 with_dates_for_pipeline(&req, Some(leg_start_dt), Some(leg_end_dt), now_ns);
360 let leg_id = *leg.request_id();
361 self.register_request_pipeline_leg(leg_id, parent_id);
362
363 if let Err(e) = self.dispatch_request_to_client(leg) {
364 log::error!("Client leg dispatch failed for parent {parent_id}: {e}");
368 self.abort_request_pipeline(parent_id);
369 return Err(e);
370 }
371 }
372 }
373
374 Ok(())
375 }
376
377 fn abort_request_pipeline(&mut self, parent_id: UUID4) {
378 self.request_pipeline_n_components.remove(&parent_id);
379 self.request_pipeline_parent_request.remove(&parent_id);
380 self.request_pipeline_responses.remove(&parent_id);
381 self.request_pipeline_parent_request_id
382 .retain(|_, p_id| *p_id != parent_id);
383 }
384
385 fn query_catalog_leg(
386 &mut self,
387 leg: &RequestCommand,
388 catalog_name: Ustr,
389 start_ns: UnixNanos,
390 end_ns: UnixNanos,
391 used_client_id: Option<ClientId>,
392 ts_init: UnixNanos,
393 ) -> anyhow::Result<DataResponse> {
394 let catalog = self.catalogs.get_mut(&catalog_name).ok_or_else(|| {
395 anyhow::anyhow!("Catalog {catalog_name} disappeared between intervals query and read")
396 })?;
397
398 match leg {
399 RequestCommand::Quotes(cmd) => {
400 let data = quote_ticks_from_query_result(
401 catalog.query_batch(
402 &CatalogQuery::new(NautilusDataType::QuoteTick)
403 .with_identifiers(Some(vec![cmd.instrument_id.to_string()]))
404 .with_range(Some(start_ns), Some(end_ns)),
405 )?,
406 )?;
407 Ok(build_quotes_catalog_response(
408 cmd,
409 data,
410 start_ns,
411 end_ns,
412 used_client_id,
413 ts_init,
414 ))
415 }
416 RequestCommand::Trades(cmd) => {
417 let data = trade_ticks_from_query_result(
418 catalog.query_batch(
419 &CatalogQuery::new(NautilusDataType::TradeTick)
420 .with_identifiers(Some(vec![cmd.instrument_id.to_string()]))
421 .with_range(Some(start_ns), Some(end_ns)),
422 )?,
423 )?;
424 Ok(build_trades_catalog_response(
425 cmd,
426 data,
427 start_ns,
428 end_ns,
429 used_client_id,
430 ts_init,
431 ))
432 }
433 RequestCommand::FundingRates(cmd) => {
434 let data = funding_rates_from_query_result(
435 catalog.query_batch(
436 &CatalogQuery::new(NautilusDataType::FundingRateUpdate)
437 .with_identifiers(Some(vec![cmd.instrument_id.to_string()]))
438 .with_range(Some(start_ns), Some(end_ns)),
439 )?,
440 )?;
441 Ok(build_funding_rates_catalog_response(
442 cmd,
443 data,
444 start_ns,
445 end_ns,
446 used_client_id,
447 ts_init,
448 ))
449 }
450 RequestCommand::Bars(cmd) => {
451 let data = bars_from_query_result(
452 catalog.query_batch(
453 &CatalogQuery::new(NautilusDataType::Bar)
454 .with_identifiers(Some(vec![cmd.bar_type.to_string()]))
455 .with_range(Some(start_ns), Some(end_ns)),
456 )?,
457 )?;
458 Ok(build_bars_catalog_response(
459 cmd,
460 data,
461 start_ns,
462 end_ns,
463 used_client_id,
464 ts_init,
465 ))
466 }
467 RequestCommand::Data(cmd) => {
468 let identifiers = cmd
469 .data_type
470 .identifier()
471 .map(|identifier| vec![identifier.to_string()]);
472 let where_clause = request_filter_expr(cmd.params.as_ref());
473 let data = catalog.query_batch(
474 &CatalogQuery::new(NautilusDataType::Custom {
475 type_name: cmd.data_type.type_name().to_string(),
476 })
477 .with_identifiers(identifiers)
478 .with_range(Some(start_ns), Some(end_ns))
479 .with_where_clause(where_clause),
480 )?;
481 Ok(build_custom_data_catalog_response(
482 cmd,
483 custom_data_from_query_result(data)?,
484 start_ns,
485 end_ns,
486 ts_init,
487 ))
488 }
489 RequestCommand::BookDeltas(cmd) => {
490 let data = order_book_deltas_from_query_result(
491 catalog.query_batch(
492 &CatalogQuery::new(NautilusDataType::OrderBookDelta)
493 .with_identifiers(Some(vec![cmd.instrument_id.to_string()]))
494 .with_range(Some(start_ns), Some(end_ns)),
495 )?,
496 )?;
497 Ok(build_book_deltas_catalog_response(
498 cmd,
499 data,
500 start_ns,
501 end_ns,
502 used_client_id,
503 ts_init,
504 ))
505 }
506 RequestCommand::BookDepth(cmd) => {
507 let data = order_book_depths_from_query_result(
508 catalog.query_batch(
509 &CatalogQuery::new(NautilusDataType::OrderBookDepth)
510 .with_identifiers(Some(vec![cmd.instrument_id.to_string()]))
511 .with_range(Some(start_ns), Some(end_ns)),
512 )?,
513 )?;
514 Ok(build_book_depth_catalog_response(
515 cmd,
516 data,
517 start_ns,
518 end_ns,
519 used_client_id,
520 ts_init,
521 ))
522 }
523 _ => {
524 anyhow::bail!("query_catalog_leg called with non-catalog-eligible variant {leg:?}")
525 }
526 }
527 }
528
529 fn dispatch_instrument_catalog_request(&mut self, req: RequestCommand) -> anyhow::Result<()> {
530 match req {
531 RequestCommand::Instrument(cmd) => self.dispatch_instrument_request(cmd),
532 RequestCommand::Instruments(cmd) => self.dispatch_instruments_request(cmd),
533 _ => self.dispatch_request_to_client(req).map(|_| ()),
534 }
535 }
536
537 fn dispatch_instrument_request(&mut self, cmd: RequestInstrument) -> anyhow::Result<()> {
538 let force_instrument_update = cmd
539 .params
540 .as_ref()
541 .and_then(|params| params.get_bool(PARAM_FORCE_INSTRUMENT_UPDATE))
542 .unwrap_or(false);
543
544 if force_instrument_update {
545 return self
546 .dispatch_request_to_client(RequestCommand::Instrument(cmd))
547 .map(|_| ());
548 }
549
550 let identifier = cmd.instrument_id.to_string();
551 let now_ns = self.clock.borrow().timestamp_ns();
552 let used_client_id = self
553 .get_client(cmd.client_id.as_ref(), Some(&cmd.instrument_id.venue))
554 .map(|client| client.client_id());
555 let (start_dt, end_dt) =
556 bound_request_dates(cmd.start, cmd.end, now_ns.to_datetime_utc(), true);
557 let start_ns = datetime_to_unix_nanos_or_zero(start_dt);
558 let end_ns = datetime_to_unix_nanos_or_zero(end_dt);
559 let start = Some(start_ns);
560 let end = cmd.end.map(datetime_to_unix_nanos_or_zero);
561
562 for catalog in self.catalogs.values_mut() {
563 let data = latest_instruments(
564 catalog.instruments(
565 &CatalogInstrumentQuery::new()
566 .with_instrument_ids(Some(vec![identifier.clone()]))
567 .with_range(start, end),
568 )?,
569 );
570
571 if let Some(instrument) = data.into_iter().next() {
572 let response = DataResponse::Instrument(Box::new(InstrumentResponse::new(
573 cmd.request_id,
574 resolve_response_client_id(cmd.client_id, used_client_id),
575 cmd.instrument_id,
576 instrument,
577 Some(start_ns),
578 Some(end_ns),
579 now_ns,
580 Some(catalog_response_params(cmd.params.as_ref())),
581 )));
582 self.response(response);
583 return Ok(());
584 }
585 }
586
587 self.dispatch_request_to_client(RequestCommand::Instrument(cmd))
588 .map(|_| ())
589 }
590
591 fn dispatch_instruments_request(&mut self, cmd: RequestInstruments) -> anyhow::Result<()> {
592 let update_catalog = cmd
593 .params
594 .as_ref()
595 .and_then(|params| params.get_bool(PARAM_UPDATE_CATALOG))
596 .unwrap_or(false);
597 let force_instrument_update = cmd
598 .params
599 .as_ref()
600 .and_then(|params| params.get_bool(PARAM_FORCE_INSTRUMENT_UPDATE))
601 .unwrap_or(false);
602
603 if update_catalog || force_instrument_update {
604 return self
605 .dispatch_request_to_client(RequestCommand::Instruments(cmd))
606 .map(|_| ());
607 }
608
609 let now_ns = self.clock.borrow().timestamp_ns();
610 let used_client_id = self
611 .get_client(cmd.client_id.as_ref(), cmd.venue.as_ref())
612 .map(|client| client.client_id());
613 let (start_dt, end_dt) =
614 bound_request_dates(cmd.start, cmd.end, now_ns.to_datetime_utc(), true);
615 let start_ns = datetime_to_unix_nanos_or_zero(start_dt);
616 let end_ns = datetime_to_unix_nanos_or_zero(end_dt);
617 let start = Some(start_ns);
618 let end = cmd.end.map(datetime_to_unix_nanos_or_zero);
619 let mut data = Vec::new();
620
621 for catalog in self.catalogs.values_mut() {
622 data.extend(
623 catalog.instruments(
624 &CatalogInstrumentQuery::new()
625 .with_range(start, end)
626 .with_where_clause(request_filter_expr(cmd.params.as_ref())),
627 )?,
628 );
629 }
630
631 if let Some(venue) = cmd.venue {
632 data.retain(|instrument| instrument.venue() == venue);
633 }
634
635 if instrument_only_last(cmd.params.as_ref()) {
636 data = latest_instruments(data);
637 }
638
639 let response = DataResponse::Instruments(InstrumentsResponse::new(
640 cmd.request_id,
641 resolve_response_client_id(cmd.client_id, used_client_id),
642 instrument_response_venue(cmd.venue, &data),
643 data,
644 Some(start_ns),
645 Some(end_ns),
646 now_ns,
647 Some(catalog_response_params(cmd.params.as_ref())),
648 ));
649 self.response(response);
650 Ok(())
651 }
652}
653
654struct RequestCatalogKey {
655 data_type: NautilusDataType,
656 identifier: Option<String>,
657}
658
659pub(super) fn is_date_range_variant(req: &RequestCommand) -> bool {
660 matches!(
661 req,
662 RequestCommand::Data(_)
663 | RequestCommand::Instrument(_)
664 | RequestCommand::Instruments(_)
665 | RequestCommand::Quotes(_)
666 | RequestCommand::Trades(_)
667 | RequestCommand::FundingRates(_)
668 | RequestCommand::Bars(_)
669 | RequestCommand::BookDeltas(_)
670 | RequestCommand::BookDepth(_)
671 )
672}
673
674fn request_identifier(req: &RequestCommand) -> Option<RequestCatalogKey> {
675 match req {
676 RequestCommand::Data(cmd) => Some(RequestCatalogKey {
677 data_type: NautilusDataType::Custom {
678 type_name: cmd.data_type.type_name().to_string(),
679 },
680 identifier: cmd.data_type.identifier().map(String::from),
681 }),
682 RequestCommand::Quotes(cmd) => Some(RequestCatalogKey::new(
683 NautilusDataType::QuoteTick,
684 Some(cmd.instrument_id.to_string()),
685 )),
686 RequestCommand::Trades(cmd) => Some(RequestCatalogKey::new(
687 NautilusDataType::TradeTick,
688 Some(cmd.instrument_id.to_string()),
689 )),
690 RequestCommand::FundingRates(cmd) => Some(RequestCatalogKey::new(
691 NautilusDataType::FundingRateUpdate,
692 Some(cmd.instrument_id.to_string()),
693 )),
694 RequestCommand::Bars(cmd) => Some(RequestCatalogKey::new(
695 NautilusDataType::Bar,
696 Some(cmd.bar_type.to_string()),
697 )),
698 RequestCommand::BookDeltas(cmd) => Some(RequestCatalogKey::new(
699 NautilusDataType::OrderBookDelta,
700 Some(cmd.instrument_id.to_string()),
701 )),
702 RequestCommand::BookDepth(cmd) => Some(RequestCatalogKey::new(
703 NautilusDataType::OrderBookDepth,
704 Some(cmd.instrument_id.to_string()),
705 )),
706 _ => None,
707 }
708}
709
710impl RequestCatalogKey {
711 fn new(data_type: NautilusDataType, identifier: Option<String>) -> Self {
712 Self {
713 data_type,
714 identifier,
715 }
716 }
717
718 fn catalog_data_type(&self) -> NautilusDataType {
719 self.data_type.clone()
720 }
721}
722
723fn catalog_missing_intervals(
724 catalog: &mut DataCatalog,
725 start: u64,
726 end: u64,
727 key: &RequestCatalogKey,
728) -> anyhow::Result<Vec<(u64, u64)>> {
729 catalog.get_missing_intervals_for_request(
730 UnixNanos::from(start),
731 UnixNanos::from(end),
732 key.catalog_data_type(),
733 key.identifier.as_deref(),
734 )
735}
736
737fn quote_ticks_from_query_result(data: DataBatch) -> anyhow::Result<Vec<QuoteTick>> {
738 QuoteTick::from_batch(data)
739}
740
741fn trade_ticks_from_query_result(data: DataBatch) -> anyhow::Result<Vec<TradeTick>> {
742 TradeTick::from_batch(data)
743}
744
745fn funding_rates_from_query_result(data: DataBatch) -> anyhow::Result<Vec<FundingRateUpdate>> {
746 FundingRateUpdate::from_batch(data)
747}
748
749fn bars_from_query_result(data: DataBatch) -> anyhow::Result<Vec<Bar>> {
750 Bar::from_batch(data)
751}
752
753fn order_book_deltas_from_query_result(data: DataBatch) -> anyhow::Result<Vec<OrderBookDelta>> {
754 OrderBookDelta::from_batch(data)
755}
756
757fn order_book_depths_from_query_result(data: DataBatch) -> anyhow::Result<Vec<OrderBookDepth>> {
758 OrderBookDepth::from_batch(data)
759}
760
761fn request_start(req: &RequestCommand) -> Option<Timestamp> {
762 match req {
763 RequestCommand::Data(cmd) => cmd.start,
764 RequestCommand::Instrument(cmd) => cmd.start,
765 RequestCommand::Instruments(cmd) => cmd.start,
766 RequestCommand::Quotes(cmd) => cmd.start,
767 RequestCommand::Trades(cmd) => cmd.start,
768 RequestCommand::FundingRates(cmd) => cmd.start,
769 RequestCommand::Bars(cmd) => cmd.start,
770 RequestCommand::BookDeltas(cmd) => cmd.start,
771 RequestCommand::BookDepth(cmd) => cmd.start,
772 _ => None,
773 }
774}
775
776fn request_end(req: &RequestCommand) -> Option<Timestamp> {
777 match req {
778 RequestCommand::Data(cmd) => cmd.end,
779 RequestCommand::Instrument(cmd) => cmd.end,
780 RequestCommand::Instruments(cmd) => cmd.end,
781 RequestCommand::Quotes(cmd) => cmd.end,
782 RequestCommand::Trades(cmd) => cmd.end,
783 RequestCommand::FundingRates(cmd) => cmd.end,
784 RequestCommand::Bars(cmd) => cmd.end,
785 RequestCommand::BookDeltas(cmd) => cmd.end,
786 RequestCommand::BookDepth(cmd) => cmd.end,
787 _ => None,
788 }
789}
790
791fn request_filter_expr(params: Option<&Params>) -> Option<String> {
792 params
793 .and_then(|params| params.get_str("filter_expr"))
794 .filter(|filter_expr| !filter_expr.is_empty())
795 .map(ToString::to_string)
796}
797
798fn bound_request_dates(
799 start: Option<Timestamp>,
800 end: Option<Timestamp>,
801 now: Timestamp,
802 query_past_data: bool,
803) -> (Timestamp, Timestamp) {
804 let zero = Timestamp::UNIX_EPOCH;
805 let mut start = start.unwrap_or(zero);
806 let mut end = end.unwrap_or(now);
807
808 if query_past_data {
809 if start > now {
810 start = now;
811 }
812
813 if end > now {
814 end = now;
815 }
816 }
817
818 (start, end)
819}
820
821fn datetime_to_unix_nanos_or_zero(dt: Timestamp) -> UnixNanos {
822 UnixNanos::from(u64::try_from(dt.as_nanosecond().max(0)).unwrap_or(0))
823}
824
825fn floor_to_utc_day(dt: Timestamp) -> Timestamp {
826 let midnight = jiff::tz::Offset::UTC.to_datetime(dt).date().at(0, 0, 0, 0);
827 jiff::tz::Offset::UTC
828 .to_timestamp(midnight)
829 .expect("midnight UTC is always valid")
830}
831
832fn with_dates_for_pipeline(
833 req: &RequestCommand,
834 start: Option<Timestamp>,
835 end: Option<Timestamp>,
836 ts_init: UnixNanos,
837) -> RequestCommand {
838 let new_id = UUID4::new();
839
840 match req {
841 RequestCommand::Quotes(cmd) => RequestCommand::Quotes(RequestQuotes {
842 instrument_id: cmd.instrument_id,
843 start,
844 end,
845 limit: cmd.limit,
846 client_id: cmd.client_id,
847 request_id: new_id,
848 ts_init,
849 params: cmd.params.clone(),
850 }),
851 RequestCommand::Trades(cmd) => RequestCommand::Trades(RequestTrades {
852 instrument_id: cmd.instrument_id,
853 start,
854 end,
855 limit: cmd.limit,
856 client_id: cmd.client_id,
857 request_id: new_id,
858 ts_init,
859 params: cmd.params.clone(),
860 }),
861 RequestCommand::FundingRates(cmd) => RequestCommand::FundingRates(RequestFundingRates {
862 instrument_id: cmd.instrument_id,
863 start,
864 end,
865 limit: cmd.limit,
866 client_id: cmd.client_id,
867 request_id: new_id,
868 ts_init,
869 params: cmd.params.clone(),
870 }),
871 RequestCommand::BookDeltas(cmd) => RequestCommand::BookDeltas(RequestBookDeltas {
872 instrument_id: cmd.instrument_id,
873 start,
874 end,
875 limit: cmd.limit,
876 client_id: cmd.client_id,
877 request_id: new_id,
878 ts_init,
879 params: cmd.params.clone(),
880 }),
881 RequestCommand::BookDepth(cmd) => RequestCommand::BookDepth(RequestBookDepth {
882 instrument_id: cmd.instrument_id,
883 start,
884 end,
885 limit: cmd.limit,
886 depth: cmd.depth,
887 client_id: cmd.client_id,
888 request_id: new_id,
889 ts_init,
890 params: cmd.params.clone(),
891 }),
892 RequestCommand::Data(cmd) => RequestCommand::Data(RequestCustomData {
893 client_id: cmd.client_id,
894 data_type: cmd.data_type.clone(),
895 start,
896 end,
897 limit: cmd.limit,
898 request_id: new_id,
899 ts_init,
900 params: cmd.params.clone(),
901 }),
902 RequestCommand::Bars(cmd) => RequestCommand::Bars(RequestBars {
903 bar_type: cmd.bar_type,
904 start,
905 end,
906 limit: cmd.limit,
907 client_id: cmd.client_id,
908 request_id: new_id,
909 ts_init,
910 params: cmd.params.clone(),
911 }),
912 _ => req.clone(),
916 }
917}
918
919fn build_empty_response(
920 req: &RequestCommand,
921 start: UnixNanos,
922 end: UnixNanos,
923 used_client_id: Option<ClientId>,
924 ts_init: UnixNanos,
925) -> anyhow::Result<DataResponse> {
926 let response = match req {
927 RequestCommand::Data(cmd) => DataResponse::Data(CustomDataResponse::new(
928 cmd.request_id,
929 cmd.client_id,
930 None,
931 cmd.data_type.clone(),
932 Vec::<CustomData>::new(),
933 Some(start),
934 Some(end),
935 ts_init,
936 cmd.params.clone(),
937 )),
938 RequestCommand::Quotes(cmd) => DataResponse::Quotes(QuotesResponse::new(
939 cmd.request_id,
940 resolve_response_client_id(cmd.client_id, used_client_id),
941 cmd.instrument_id,
942 Vec::new(),
943 Some(start),
944 Some(end),
945 ts_init,
946 cmd.params.clone(),
947 )),
948 RequestCommand::Trades(cmd) => DataResponse::Trades(TradesResponse::new(
949 cmd.request_id,
950 resolve_response_client_id(cmd.client_id, used_client_id),
951 cmd.instrument_id,
952 Vec::new(),
953 Some(start),
954 Some(end),
955 ts_init,
956 cmd.params.clone(),
957 )),
958 RequestCommand::FundingRates(cmd) => DataResponse::FundingRates(FundingRatesResponse::new(
959 cmd.request_id,
960 resolve_response_client_id(cmd.client_id, used_client_id),
961 cmd.instrument_id,
962 Vec::new(),
963 Some(start),
964 Some(end),
965 ts_init,
966 cmd.params.clone(),
967 )),
968 RequestCommand::Bars(cmd) => DataResponse::Bars(BarsResponse::new(
969 cmd.request_id,
970 resolve_response_client_id(cmd.client_id, used_client_id),
971 cmd.bar_type,
972 Vec::new(),
973 Some(start),
974 Some(end),
975 ts_init,
976 cmd.params.clone(),
977 )),
978 RequestCommand::BookDeltas(cmd) => DataResponse::BookDeltas(BookDeltasResponse::new(
979 cmd.request_id,
980 resolve_response_client_id(cmd.client_id, used_client_id),
981 cmd.instrument_id,
982 Vec::new(),
983 Some(start),
984 Some(end),
985 ts_init,
986 cmd.params.clone(),
987 )),
988 RequestCommand::BookDepth(cmd) => DataResponse::BookDepth(BookDepthResponse::new(
989 cmd.request_id,
990 resolve_response_client_id(cmd.client_id, used_client_id),
991 cmd.instrument_id,
992 Vec::new(),
993 Some(start),
994 Some(end),
995 ts_init,
996 cmd.params.clone(),
997 )),
998 _ => {
999 anyhow::bail!("Cannot build empty catalog response for non-catalog-eligible request")
1000 }
1001 };
1002
1003 Ok(response)
1004}
1005
1006fn build_quotes_catalog_response(
1007 cmd: &RequestQuotes,
1008 data: Vec<QuoteTick>,
1009 start: UnixNanos,
1010 end: UnixNanos,
1011 used_client_id: Option<ClientId>,
1012 ts_init: UnixNanos,
1013) -> DataResponse {
1014 let params = catalog_response_params(cmd.params.as_ref());
1015 DataResponse::Quotes(QuotesResponse::new(
1016 cmd.request_id,
1017 resolve_response_client_id(cmd.client_id, used_client_id),
1018 cmd.instrument_id,
1019 data,
1020 Some(start),
1021 Some(end),
1022 ts_init,
1023 Some(params),
1024 ))
1025}
1026
1027fn build_trades_catalog_response(
1028 cmd: &RequestTrades,
1029 data: Vec<TradeTick>,
1030 start: UnixNanos,
1031 end: UnixNanos,
1032 used_client_id: Option<ClientId>,
1033 ts_init: UnixNanos,
1034) -> DataResponse {
1035 let params = catalog_response_params(cmd.params.as_ref());
1036 DataResponse::Trades(TradesResponse::new(
1037 cmd.request_id,
1038 resolve_response_client_id(cmd.client_id, used_client_id),
1039 cmd.instrument_id,
1040 data,
1041 Some(start),
1042 Some(end),
1043 ts_init,
1044 Some(params),
1045 ))
1046}
1047
1048fn build_funding_rates_catalog_response(
1049 cmd: &RequestFundingRates,
1050 data: Vec<FundingRateUpdate>,
1051 start: UnixNanos,
1052 end: UnixNanos,
1053 used_client_id: Option<ClientId>,
1054 ts_init: UnixNanos,
1055) -> DataResponse {
1056 let params = catalog_response_params(cmd.params.as_ref());
1057 DataResponse::FundingRates(FundingRatesResponse::new(
1058 cmd.request_id,
1059 resolve_response_client_id(cmd.client_id, used_client_id),
1060 cmd.instrument_id,
1061 data,
1062 Some(start),
1063 Some(end),
1064 ts_init,
1065 Some(params),
1066 ))
1067}
1068
1069fn build_bars_catalog_response(
1070 cmd: &RequestBars,
1071 data: Vec<Bar>,
1072 start: UnixNanos,
1073 end: UnixNanos,
1074 used_client_id: Option<ClientId>,
1075 ts_init: UnixNanos,
1076) -> DataResponse {
1077 let params = catalog_response_params(cmd.params.as_ref());
1078 DataResponse::Bars(BarsResponse::new(
1079 cmd.request_id,
1080 resolve_response_client_id(cmd.client_id, used_client_id),
1081 cmd.bar_type,
1082 data,
1083 Some(start),
1084 Some(end),
1085 ts_init,
1086 Some(params),
1087 ))
1088}
1089
1090fn build_custom_data_catalog_response(
1091 cmd: &RequestCustomData,
1092 data: Vec<CustomData>,
1093 start: UnixNanos,
1094 end: UnixNanos,
1095 ts_init: UnixNanos,
1096) -> DataResponse {
1097 let params = catalog_response_params(cmd.params.as_ref());
1098 DataResponse::Data(CustomDataResponse::new(
1099 cmd.request_id,
1100 cmd.client_id,
1101 None,
1102 cmd.data_type.clone(),
1103 data,
1104 Some(start),
1105 Some(end),
1106 ts_init,
1107 Some(params),
1108 ))
1109}
1110
1111fn build_book_deltas_catalog_response(
1112 cmd: &RequestBookDeltas,
1113 data: Vec<OrderBookDelta>,
1114 start: UnixNanos,
1115 end: UnixNanos,
1116 used_client_id: Option<ClientId>,
1117 ts_init: UnixNanos,
1118) -> DataResponse {
1119 let params = catalog_response_params(cmd.params.as_ref());
1120 DataResponse::BookDeltas(BookDeltasResponse::new(
1121 cmd.request_id,
1122 resolve_response_client_id(cmd.client_id, used_client_id),
1123 cmd.instrument_id,
1124 data,
1125 Some(start),
1126 Some(end),
1127 ts_init,
1128 Some(params),
1129 ))
1130}
1131
1132fn build_book_depth_catalog_response(
1133 cmd: &RequestBookDepth,
1134 data: Vec<OrderBookDepth>,
1135 start: UnixNanos,
1136 end: UnixNanos,
1137 used_client_id: Option<ClientId>,
1138 ts_init: UnixNanos,
1139) -> DataResponse {
1140 let params = catalog_response_params(cmd.params.as_ref());
1141 DataResponse::BookDepth(BookDepthResponse::new(
1142 cmd.request_id,
1143 resolve_response_client_id(cmd.client_id, used_client_id),
1144 cmd.instrument_id,
1145 data,
1146 Some(start),
1147 Some(end),
1148 ts_init,
1149 Some(params),
1150 ))
1151}
1152
1153fn catalog_response_params(existing: Option<&Params>) -> Params {
1154 let mut params = existing.cloned().unwrap_or_else(Params::new);
1155 params.insert(PARAM_UPDATE_CATALOG.to_string(), Value::Bool(false));
1156 params
1157}
1158
1159fn custom_data_from_query_result(data: DataBatch) -> anyhow::Result<Vec<CustomData>> {
1160 CustomData::from_batch(data)
1161}
1162
1163fn instrument_only_last(params: Option<&Params>) -> bool {
1164 params
1165 .and_then(|params| params.get_bool("only_last"))
1166 .unwrap_or(true)
1167}
1168
1169fn latest_instruments(data: Vec<InstrumentAny>) -> Vec<InstrumentAny> {
1170 let mut instruments: AHashMap<_, InstrumentAny> = AHashMap::new();
1171
1172 for instrument in data {
1173 let id = instrument.id();
1174 match instruments.get(&id) {
1175 Some(existing) if existing.ts_init() >= instrument.ts_init() => {}
1176 _ => {
1177 instruments.insert(id, instrument);
1178 }
1179 }
1180 }
1181
1182 let mut data: Vec<_> = instruments.into_values().collect();
1183 data.sort_by_key(|instrument| instrument.id().to_string());
1184 data
1185}
1186
1187fn instrument_response_venue(request_venue: Option<Venue>, data: &[InstrumentAny]) -> Venue {
1188 request_venue.unwrap_or_else(|| {
1189 data.iter()
1190 .map(Instrument::venue)
1191 .min_by_key(std::string::ToString::to_string)
1192 .unwrap_or_else(|| Venue::from(CATALOG_CLIENT_ID))
1193 })
1194}
1195
1196fn resolve_response_client_id(
1197 request_client_id: Option<ClientId>,
1198 used_client_id: Option<ClientId>,
1199) -> ClientId {
1200 request_client_id
1201 .or(used_client_id)
1202 .unwrap_or_else(|| ClientId::new(CATALOG_CLIENT_ID))
1203}
1204
1205#[cfg(test)]
1206mod tests {
1207 use nautilus_common::messages::data::RequestJoin;
1208 use rstest::rstest;
1209
1210 use super::*;
1211
1212 #[rstest]
1213 fn test_build_empty_response_rejects_non_catalog_variant() {
1214 let request = RequestCommand::Join(RequestJoin::new(
1215 vec![UUID4::new()],
1216 None,
1217 None,
1218 UUID4::new(),
1219 UnixNanos::default(),
1220 None,
1221 None,
1222 ));
1223
1224 let result = build_empty_response(
1225 &request,
1226 UnixNanos::from(1u64),
1227 UnixNanos::from(2u64),
1228 None,
1229 UnixNanos::from(3u64),
1230 );
1231
1232 assert_eq!(
1233 result.unwrap_err().to_string(),
1234 "Cannot build empty catalog response for non-catalog-eligible request"
1235 );
1236 }
1237}