1use std::fmt::Debug;
17
18use anyhow::Context;
19use jiff::tz::TimeZone;
20use nautilus_common::{
21 messages::data::{SubscribeBars, SubscribeCommand},
22 msgbus::{self, MStr, Topic, TypedHandler},
23};
24use nautilus_core::{UUID4, datetime::get_timezone};
25use nautilus_model::data::{Bar, BarType, QuoteTick, TradeTick};
26
27use super::{
28 AggregationSource, BAR_AGGREGATOR_PRIORITY, BarAggregation, BarAggregator, BarBarHandler,
29 BarQuoteHandler, BarTradeHandler, DataCommand, DataEngine, DataResponse, Instrument,
30 InstrumentAny, PriceType, Rc, RefCell, RenkoBarAggregator, RequestBarAggregation,
31 RequestCommand, SubscribeQuotes, SubscribeTrades, TickBarAggregator,
32 TickImbalanceBarAggregator, TickRunsBarAggregator, TimeBarAggregator, UnsubscribeBars,
33 UnsubscribeCommand, UnsubscribeQuotes, UnsubscribeTrades, ValueBarAggregator,
34 ValueImbalanceBarAggregator, ValueRunsBarAggregator, VolumeBarAggregator,
35 VolumeImbalanceBarAggregator, VolumeRunsBarAggregator, log_error_on_cache_insert,
36 process_engine_bar, request_bar_aggregation_from_params, request_params,
37 requests::time_zone_from_params, switchboard,
38};
39
40impl DataEngine {
41 pub(super) fn prepare_request_bar_aggregators_from_state(
42 &mut self,
43 request_id: UUID4,
44 state: &RequestBarAggregation,
45 ) -> anyhow::Result<()> {
46 if !self.can_start_request_bar_aggregators(request_id, state) {
47 anyhow::bail!(
48 "Cannot request aggregated bars: one of the aggregators in `bar_types` is already running"
49 );
50 }
51
52 self.request_bar_aggregations
53 .insert(request_id, state.clone());
54
55 if let Err(e) = self.init_request_bar_aggregators(request_id, state) {
56 self.cleanup_request_bar_aggregators(&request_id);
57 return Err(e);
58 }
59
60 Ok(())
61 }
62
63 pub(super) fn prepare_request_bar_aggregators(
64 &mut self,
65 req: &RequestCommand,
66 ) -> anyhow::Result<()> {
67 let request_id = *req.request_id();
68
69 let Some(state) = request_bar_aggregation_from_params(request_params(req))? else {
70 return Ok(());
71 };
72
73 self.prepare_request_bar_aggregators_from_state(request_id, &state)
74 }
75
76 fn can_start_request_bar_aggregators(
77 &self,
78 request_id: UUID4,
79 state: &RequestBarAggregation,
80 ) -> bool {
81 let aggregator_request_id = state.aggregator_request_id(request_id);
82 state.bar_types.iter().all(|bar_type| {
83 let key = bar_aggregator_key(*bar_type, aggregator_request_id);
84 self.bar_aggregators
85 .get(&key)
86 .is_none_or(|aggregator| !aggregator.borrow().is_running())
87 })
88 }
89
90 fn init_request_bar_aggregators(
91 &mut self,
92 request_id: UUID4,
93 state: &RequestBarAggregation,
94 ) -> anyhow::Result<()> {
95 let aggregator_request_id = state.aggregator_request_id(request_id);
96
97 for bar_type in &state.bar_types {
98 self.create_bar_aggregator_for_key(
99 *bar_type,
100 aggregator_request_id,
101 state.skip_first_non_full_bar,
102 state.time_zone.clone(),
103 )?;
104 self.setup_bar_aggregator(*bar_type, true, aggregator_request_id)?;
105
106 let key = bar_aggregator_key(*bar_type, aggregator_request_id);
107 if let Some(aggregator) = self.bar_aggregators.get(&key) {
108 if state.disable_build_with_no_updates {
109 aggregator.borrow_mut().set_build_with_no_updates(false);
110 }
111
112 aggregator.borrow_mut().set_is_running(true);
113 }
114 }
115
116 self.set_request_bar_aggregator_chain_handlers(request_id, state);
117
118 Ok(())
119 }
120
121 fn set_request_bar_aggregator_chain_handlers(
122 &self,
123 request_id: UUID4,
124 state: &RequestBarAggregation,
125 ) {
126 let aggregator_request_id = state.aggregator_request_id(request_id);
127
128 for bar_type in &state.bar_types {
129 let key = bar_aggregator_key(*bar_type, aggregator_request_id);
130
131 let Some(aggregator) = self.bar_aggregators.get(&key).cloned() else {
132 continue;
133 };
134
135 let downstream: Vec<_> = state
136 .bar_types
137 .iter()
138 .filter(|candidate| {
139 candidate.is_composite()
140 && candidate.composite().standard() == bar_type.standard()
141 })
142 .filter_map(|candidate| {
143 let key = bar_aggregator_key(*candidate, aggregator_request_id);
144 self.bar_aggregators.get(&key).cloned()
145 })
146 .collect();
147
148 let cache = Rc::clone(&self.cache);
149
150 let handler: Box<dyn FnMut(Bar)> = Box::new(move |bar: Bar| {
151 if let Err(e) = cache.as_ref().borrow_mut().add_bar_historical(bar) {
153 log_error_on_cache_insert(&e);
154 }
155
156 for aggregator in &downstream {
157 aggregator.borrow_mut().handle_bar(bar);
158 }
159 });
160
161 aggregator.borrow_mut().set_historical_mode(true, handler);
162 }
163 }
164
165 pub(super) fn cleanup_request_bar_aggregators(&mut self, request_id: &UUID4) -> bool {
166 let Some(state) = self.request_bar_aggregations.remove(request_id) else {
167 return false;
168 };
169
170 let aggregator_request_id = state.aggregator_request_id(*request_id);
171
172 for bar_type in state.bar_types {
173 let key = bar_aggregator_key(bar_type, aggregator_request_id);
174 let has_live_handlers =
175 state.update_subscriptions && self.bar_aggregator_handlers.contains_key(&key);
176
177 let keep_running = if has_live_handlers {
178 match self.setup_bar_aggregator(bar_type, false, aggregator_request_id) {
179 Ok(()) => true,
180 Err(e) => {
181 log::error!(
182 "Error starting live request bar aggregator for {bar_type}: {e}"
183 );
184 false
185 }
186 }
187 } else {
188 false
189 };
190
191 if let Some(aggregator) = self.bar_aggregators.get(&key) {
192 aggregator.borrow_mut().set_is_running(keep_running);
193 }
194
195 if !state.update_subscriptions
196 && let Err(e) = self.stop_bar_aggregator(bar_type, aggregator_request_id)
197 {
198 log::error!("Error stopping request bar aggregator for {bar_type}: {e}");
199 }
200 }
201
202 true
203 }
204
205 pub(super) fn process_request_bar_aggregation_response(&mut self, resp: &DataResponse) {
206 let correlation_id = *resp.correlation_id();
207
208 let Some(state) = self.request_bar_aggregations.get(&correlation_id).cloned() else {
209 return;
210 };
211
212 match resp {
213 DataResponse::Quotes(r) => {
214 for quote in &r.data {
215 self.update_request_bar_aggregators_from_quote(&state, correlation_id, *quote);
216 }
217 }
218 DataResponse::Trades(r) => {
219 for trade in &r.data {
220 self.update_request_bar_aggregators_from_trade(&state, correlation_id, *trade);
221 }
222 }
223 DataResponse::Bars(r) => {
224 for bar in &r.data {
225 self.update_request_bar_aggregators_from_bar(&state, correlation_id, *bar);
226 }
227 }
228 _ => {}
229 }
230
231 self.cleanup_request_bar_aggregators(&correlation_id);
232 }
233
234 pub(super) fn update_request_bar_aggregators_from_quote(
235 &self,
236 state: &RequestBarAggregation,
237 request_id: UUID4,
238 quote: QuoteTick,
239 ) {
240 let aggregator_request_id = state.aggregator_request_id(request_id);
241
242 for bar_type in &state.bar_types {
243 if bar_type.is_composite()
244 || bar_type.instrument_id() != quote.instrument_id
245 || bar_type.spec().price_type == PriceType::Last
246 {
247 continue;
248 }
249
250 self.update_request_bar_aggregator(*bar_type, aggregator_request_id, |aggregator| {
251 aggregator.handle_quote(quote);
252 });
253 }
254 }
255
256 pub(super) fn update_request_bar_aggregators_from_trade(
257 &self,
258 state: &RequestBarAggregation,
259 request_id: UUID4,
260 trade: TradeTick,
261 ) {
262 let aggregator_request_id = state.aggregator_request_id(request_id);
263
264 for bar_type in &state.bar_types {
265 if bar_type.is_composite()
266 || bar_type.instrument_id() != trade.instrument_id
267 || bar_type.spec().price_type != PriceType::Last
268 {
269 continue;
270 }
271
272 self.update_request_bar_aggregator(*bar_type, aggregator_request_id, |aggregator| {
273 aggregator.handle_trade(trade);
274 });
275 }
276 }
277
278 pub(super) fn update_request_bar_aggregators_from_bar(
279 &self,
280 state: &RequestBarAggregation,
281 request_id: UUID4,
282 bar: Bar,
283 ) {
284 let aggregator_request_id = state.aggregator_request_id(request_id);
285
286 for bar_type in &state.bar_types {
287 if !bar_type.is_composite()
288 || bar_type.composite().standard() != bar.bar_type.standard()
289 {
290 continue;
291 }
292
293 self.update_request_bar_aggregator(*bar_type, aggregator_request_id, |aggregator| {
294 aggregator.handle_bar(bar);
295 });
296 }
297 }
298
299 pub(super) fn update_request_bar_aggregator<F>(
300 &self,
301 bar_type: BarType,
302 request_id: Option<UUID4>,
303 update: F,
304 ) where
305 F: FnOnce(&mut dyn BarAggregator),
306 {
307 let key = bar_aggregator_key(bar_type, request_id);
308
309 let Some(aggregator) = self.bar_aggregators.get(&key) else {
310 log::error!("Cannot update request bar aggregator: no aggregator found for {bar_type}");
311 return;
312 };
313
314 update(aggregator.borrow_mut().as_mut());
315 }
316
317 pub(super) fn subscribe_bars(&mut self, cmd: &SubscribeBars) -> anyhow::Result<()> {
318 match cmd.bar_type.aggregation_source() {
319 AggregationSource::Internal => self.start_bar_aggregation(cmd)?,
320 AggregationSource::External => {
321 if cmd.bar_type.instrument_id().is_synthetic() {
322 anyhow::bail!(
323 "Cannot subscribe for externally aggregated synthetic instrument bar data"
324 );
325 }
326 }
327 }
328
329 Ok(())
330 }
331
332 pub(super) fn unsubscribe_bars(&mut self, cmd: &UnsubscribeBars) {
333 let bar_type = cmd.bar_type;
334
335 let topic = switchboard::get_bars_topic(bar_type.standard());
337 if msgbus::exact_subscriber_count_bars(topic) > 0 {
338 return;
339 }
340
341 let retained = self
342 .subscriptions_bar_aggregation
343 .get(&bar_type.standard())
344 .map(|subscription| subscription.command.clone());
345
346 let command = retained.map_or_else(
347 || cmd.clone(),
348 |subscribe| {
349 UnsubscribeBars::new(
350 subscribe.bar_type,
351 subscribe.client_id,
352 subscribe.venue,
353 cmd.command_id,
354 cmd.ts_init,
355 Some(subscribe.command_id),
356 subscribe.params,
357 )
358 },
359 );
360
361 if self
362 .bar_aggregators
363 .contains_key(&bar_aggregator_key(bar_type, None))
364 {
365 match self.stop_bar_aggregator(bar_type, None) {
366 Ok(()) => {
367 self.subscriptions_bar_aggregation
368 .remove(&bar_type.standard());
369 self.unsubscribe_bar_aggregator(&command);
370 }
371 Err(e) => log::error!("Error stopping bar aggregator for {bar_type}: {e}"),
372 }
373 }
374
375 if command.bar_type.is_composite() {
378 let source_type = command.bar_type.composite();
379
380 if self
381 .bar_aggregators
382 .contains_key(&bar_aggregator_key(source_type, None))
383 {
384 self.unsubscribe_bars(&UnsubscribeBars::new(
385 source_type,
386 command.client_id,
387 command.venue,
388 UUID4::new(),
389 command.ts_init,
390 Some(command.command_id),
391 command.params.clone(),
392 ));
393 }
394 }
395 }
396
397 fn create_bar_aggregator(
398 &self,
399 instrument: &InstrumentAny,
400 bar_type: BarType,
401 skip_first_non_full_bar: Option<bool>,
402 time_zone: TimeZone,
403 ) -> Box<dyn BarAggregator> {
404 let cache = Rc::clone(&self.cache);
405 let validate_sequence = self.config.validate_data_sequence;
406
407 let handler = move |bar: Bar| {
408 process_engine_bar(&cache, validate_sequence, true, bar);
409 };
410
411 let clock = Rc::clone(&self.clock);
412 let config = self.config.clone();
413
414 let price_precision = instrument.price_precision();
415 let size_precision = instrument.size_precision();
416
417 if bar_type.spec().is_time_aggregated() {
418 let time_bars_origin_offset = config
419 .time_bars_origin_offset
420 .get(&bar_type.spec().aggregation)
421 .map(|duration| jiff::SignedDuration::try_from(*duration).unwrap_or_default());
422
423 Box::new(TimeBarAggregator::new(
424 bar_type,
425 price_precision,
426 size_precision,
427 clock,
428 handler,
429 config.time_bars_build_with_no_updates,
430 config.time_bars_timestamp_on_close,
431 config.time_bars_interval_type,
432 time_bars_origin_offset,
433 time_zone,
434 config.time_bars_build_delay,
435 skip_first_non_full_bar.unwrap_or(config.time_bars_skip_first_non_full_bar),
436 ))
437 } else {
438 match bar_type.spec().aggregation {
439 BarAggregation::Tick => Box::new(TickBarAggregator::new(
440 bar_type,
441 price_precision,
442 size_precision,
443 handler,
444 )) as Box<dyn BarAggregator>,
445 BarAggregation::TickImbalance => Box::new(TickImbalanceBarAggregator::new(
446 bar_type,
447 price_precision,
448 size_precision,
449 handler,
450 )) as Box<dyn BarAggregator>,
451 BarAggregation::TickRuns => Box::new(TickRunsBarAggregator::new(
452 bar_type,
453 price_precision,
454 size_precision,
455 handler,
456 )) as Box<dyn BarAggregator>,
457 BarAggregation::Volume => Box::new(VolumeBarAggregator::new(
458 bar_type,
459 price_precision,
460 size_precision,
461 handler,
462 )) as Box<dyn BarAggregator>,
463 BarAggregation::VolumeImbalance => Box::new(VolumeImbalanceBarAggregator::new(
464 bar_type,
465 price_precision,
466 size_precision,
467 handler,
468 )) as Box<dyn BarAggregator>,
469 BarAggregation::VolumeRuns => Box::new(VolumeRunsBarAggregator::new(
470 bar_type,
471 price_precision,
472 size_precision,
473 handler,
474 )) as Box<dyn BarAggregator>,
475 BarAggregation::Value => Box::new(ValueBarAggregator::new(
476 bar_type,
477 price_precision,
478 size_precision,
479 handler,
480 )) as Box<dyn BarAggregator>,
481 BarAggregation::ValueImbalance => Box::new(ValueImbalanceBarAggregator::new(
482 bar_type,
483 price_precision,
484 size_precision,
485 handler,
486 )) as Box<dyn BarAggregator>,
487 BarAggregation::ValueRuns => Box::new(ValueRunsBarAggregator::new(
488 bar_type,
489 price_precision,
490 size_precision,
491 handler,
492 )) as Box<dyn BarAggregator>,
493 BarAggregation::Renko => Box::new(RenkoBarAggregator::new(
494 bar_type,
495 price_precision,
496 size_precision,
497 instrument.price_increment(),
498 handler,
499 )) as Box<dyn BarAggregator>,
500 other => unreachable!(
501 "Unsupported internal bar aggregation dispatch for {other:?}; update `create_bar_aggregator`"
502 ),
503 }
504 }
505 }
506
507 pub(super) fn create_bar_aggregator_for_key(
508 &mut self,
509 bar_type: BarType,
510 request_id: Option<UUID4>,
511 skip_first_non_full_bar: Option<bool>,
512 time_zone: Option<TimeZone>,
513 ) -> anyhow::Result<()> {
514 let key = bar_aggregator_key(bar_type, request_id);
515
516 let time_zone = match time_zone {
517 Some(zone) => zone,
518 None => get_timezone(self.config.time_bars_time_zone.as_deref().unwrap_or("UTC"))
519 .context("invalid `time_bars_time_zone` configuration")?,
520 };
521
522 if let Some(aggregator) = self.bar_aggregators.get(&key) {
523 let aggregator = aggregator.borrow();
524 if let Some(aggregator) = aggregator.as_any().downcast_ref::<TimeBarAggregator>() {
525 anyhow::ensure!(
526 aggregator.time_zone() == &time_zone,
527 "Cannot reuse bar aggregator {bar_type} with a different time zone"
528 );
529 }
530
531 return Ok(());
532 }
533
534 let instrument = {
535 let cache = self.cache.borrow();
536 cache
537 .instrument(&bar_type.instrument_id())
538 .ok_or_else(|| {
539 anyhow::anyhow!(
540 "Cannot start bar aggregation: no instrument found for {}",
541 bar_type.instrument_id(),
542 )
543 })?
544 .clone()
545 };
546
547 let aggregator =
548 self.create_bar_aggregator(&instrument, bar_type, skip_first_non_full_bar, time_zone);
549 debug_assert_eq!(
550 aggregator.bar_type(),
551 key.0,
552 "aggregator bar type must match its standardized key"
553 );
554 self.bar_aggregators
555 .insert(key, Rc::new(RefCell::new(aggregator)));
556
557 Ok(())
558 }
559
560 fn start_bar_aggregation(&mut self, cmd: &SubscribeBars) -> anyhow::Result<()> {
561 let time_zone = time_zone_from_params(cmd.params.as_ref())?;
562 let skip_first_non_full_bar = cmd
563 .params
564 .as_ref()
565 .and_then(|params| params.get_bool("skip_first_non_full_bar"));
566 self.create_bar_aggregator_for_key(
567 cmd.bar_type,
568 None,
569 skip_first_non_full_bar,
570 time_zone.clone(),
571 )?;
572 let key = bar_aggregator_key(cmd.bar_type, None);
573
574 if self
575 .bar_aggregators
576 .get(&key)
577 .is_some_and(|aggregator| aggregator.borrow().is_running())
578 && self.bar_aggregator_handlers.contains_key(&key)
579 {
580 if let Some(source_command) = self
581 .subscriptions_bar_aggregation
582 .get(&cmd.bar_type.standard())
583 .and_then(|subscription| subscription.source.clone())
584 {
585 self.execute(DataCommand::Subscribe(source_command));
586 }
587
588 log::warn!(
589 "Aggregator for {} is currently in use, subscription can't be started",
590 cmd.bar_type,
591 );
592 return Ok(());
593 }
594
595 self.start_bar_aggregator(cmd.bar_type, None, skip_first_non_full_bar, time_zone)?;
596 let source = self.subscribe_bar_aggregator(cmd);
597 self.subscriptions_bar_aggregation.insert(
598 cmd.bar_type.standard(),
599 BarAggregationSubscription {
600 command: cmd.clone(),
601 source,
602 },
603 );
604
605 Ok(())
606 }
607
608 fn start_bar_aggregator(
609 &mut self,
610 bar_type: BarType,
611 request_id: Option<UUID4>,
612 skip_first_non_full_bar: Option<bool>,
613 time_zone: Option<TimeZone>,
614 ) -> anyhow::Result<()> {
615 let key = bar_aggregator_key(bar_type, request_id);
616 let bar_type_std = bar_type.standard();
617
618 self.create_bar_aggregator_for_key(
619 bar_type,
620 request_id,
621 skip_first_non_full_bar,
622 time_zone,
623 )?;
624 let aggregator = self
625 .bar_aggregators
626 .get(&key)
627 .map(Rc::clone)
628 .ok_or_else(|| anyhow::anyhow!("Cannot start bar aggregation for {bar_type}"))?;
629 let defer_subscription_activation = request_id.is_none()
630 && aggregator.borrow().is_running()
631 && !self.bar_aggregator_handlers.contains_key(&key);
632
633 if !self.bar_aggregator_handlers.contains_key(&key) {
634 let mut subscriptions = Vec::new();
636
637 if bar_type.is_composite() {
638 let topic = switchboard::get_bars_topic(bar_type.composite());
639 let handler = TypedHandler::new(BarBarHandler::new(&aggregator, bar_type_std));
640 msgbus::subscribe_bars(topic.into(), handler.clone(), None);
641 subscriptions.push(BarAggregatorSubscription::Bar { topic, handler });
642 } else if bar_type.spec().price_type == PriceType::Last {
643 let topic = switchboard::get_trades_topic(bar_type.instrument_id());
644 let handler = TypedHandler::new(BarTradeHandler::new(&aggregator, bar_type_std));
645 msgbus::subscribe_trades(
646 topic.into(),
647 handler.clone(),
648 Some(BAR_AGGREGATOR_PRIORITY),
649 );
650 subscriptions.push(BarAggregatorSubscription::Trade { topic, handler });
651 } else {
652 if matches!(
654 bar_type.spec().aggregation,
655 BarAggregation::TickImbalance
656 | BarAggregation::VolumeImbalance
657 | BarAggregation::ValueImbalance
658 | BarAggregation::TickRuns
659 | BarAggregation::VolumeRuns
660 | BarAggregation::ValueRuns
661 ) {
662 log::warn!(
663 "Bar type {bar_type} uses imbalance/runs aggregation which requires trade \
664 data with `aggressor_side`, but `price_type` is not LAST so it will receive \
665 quote data: bars will not emit correctly",
666 );
667 }
668
669 let topic = switchboard::get_quotes_topic(bar_type.instrument_id());
670 let handler = TypedHandler::new(BarQuoteHandler::new(&aggregator, bar_type_std));
671 msgbus::subscribe_quotes(
672 topic.into(),
673 handler.clone(),
674 Some(BAR_AGGREGATOR_PRIORITY),
675 );
676 subscriptions.push(BarAggregatorSubscription::Quote { topic, handler });
677 }
678
679 self.bar_aggregator_handlers.insert(key, subscriptions);
680 }
681
682 if defer_subscription_activation {
683 return Ok(());
684 }
685
686 self.setup_bar_aggregator(bar_type, false, request_id)?;
687
688 aggregator.borrow_mut().set_is_running(true);
689
690 Ok(())
691 }
692
693 fn subscribe_bar_aggregator(&mut self, cmd: &SubscribeBars) -> Option<SubscribeCommand> {
694 let subscribe = self.bar_aggregator_source_command(cmd)?;
695 self.execute(DataCommand::Subscribe(subscribe.clone()));
696 Some(subscribe)
697 }
698
699 fn bar_aggregator_source_command(&self, cmd: &SubscribeBars) -> Option<SubscribeCommand> {
700 let key = bar_aggregator_key(cmd.bar_type, None);
701 if !self.bar_aggregators.contains_key(&key) {
702 log::error!(
703 "Cannot subscribe bar aggregator: no aggregator found for {}",
704 cmd.bar_type,
705 );
706 return None;
707 }
708
709 if cmd.bar_type.is_composite() {
710 let composite_bar_type = cmd.bar_type.composite();
711 if composite_bar_type.is_externally_aggregated() {
712 let subscribe = SubscribeBars::new(
713 composite_bar_type,
714 cmd.client_id,
715 cmd.venue,
716 UUID4::new(),
717 cmd.ts_init,
718 Some(cmd.command_id),
719 cmd.params.clone(),
720 );
721 return Some(SubscribeCommand::Bars(subscribe));
722 }
723 } else if cmd.bar_type.spec().price_type == PriceType::Last {
724 let subscribe = SubscribeTrades::new(
725 cmd.bar_type.instrument_id(),
726 cmd.client_id,
727 cmd.venue,
728 UUID4::new(),
729 cmd.ts_init,
730 Some(cmd.command_id),
731 cmd.params.clone(),
732 );
733 return Some(SubscribeCommand::Trades(subscribe));
734 } else {
735 let subscribe = SubscribeQuotes::new(
736 cmd.bar_type.instrument_id(),
737 cmd.client_id,
738 cmd.venue,
739 UUID4::new(),
740 cmd.ts_init,
741 Some(cmd.command_id),
742 cmd.params.clone(),
743 );
744 return Some(SubscribeCommand::Quotes(subscribe));
745 }
746
747 None
748 }
749
750 pub(super) fn setup_bar_aggregator(
754 &self,
755 bar_type: BarType,
756 historical: bool,
757 request_id: Option<UUID4>,
758 ) -> anyhow::Result<()> {
759 let key = bar_aggregator_key(bar_type, request_id);
760
761 let aggregator = self.bar_aggregators.get(&key).ok_or_else(|| {
762 anyhow::anyhow!("Cannot setup bar aggregator: no aggregator found for {bar_type}")
763 })?;
764
765 let cache = Rc::clone(&self.cache);
767 let validate_sequence = self.config.validate_data_sequence;
768 let publish = !historical;
769
770 let handler: Box<dyn FnMut(Bar)> = Box::new(move |bar: Bar| {
771 process_engine_bar(&cache, validate_sequence, publish, bar);
772 });
773
774 aggregator
775 .borrow_mut()
776 .set_historical_mode(historical, handler);
777
778 if bar_type.spec().is_time_aggregated() {
780 use nautilus_common::clock::VirtualClock;
781
782 if historical {
783 let test_clock = Rc::new(RefCell::new(VirtualClock::new()));
785 aggregator.borrow_mut().set_clock(test_clock);
786 let aggregator_weak = Rc::downgrade(aggregator);
789 aggregator.borrow_mut().set_aggregator_weak(aggregator_weak);
790 } else {
791 aggregator.borrow_mut().set_clock(Rc::clone(&self.clock));
792 aggregator
793 .borrow_mut()
794 .start_timer(Some(Rc::clone(aggregator)));
795 }
796 }
797
798 Ok(())
799 }
800
801 fn unsubscribe_bar_aggregator(&mut self, cmd: &UnsubscribeBars) {
802 if cmd.bar_type.is_composite() {
803 let composite_bar_type = cmd.bar_type.composite();
804 if composite_bar_type.is_externally_aggregated() {
805 let unsubscribe = UnsubscribeBars::new(
806 composite_bar_type,
807 cmd.client_id,
808 cmd.venue,
809 UUID4::new(),
810 cmd.ts_init,
811 Some(cmd.command_id),
812 cmd.params.clone(),
813 );
814 self.execute(DataCommand::Unsubscribe(UnsubscribeCommand::Bars(
815 unsubscribe,
816 )));
817 }
818 } else if cmd.bar_type.spec().price_type == PriceType::Last {
819 let unsubscribe = UnsubscribeTrades::new(
820 cmd.bar_type.instrument_id(),
821 cmd.client_id,
822 cmd.venue,
823 UUID4::new(),
824 cmd.ts_init,
825 Some(cmd.command_id),
826 cmd.params.clone(),
827 );
828 self.execute(DataCommand::Unsubscribe(UnsubscribeCommand::Trades(
829 unsubscribe,
830 )));
831 } else {
832 let unsubscribe = UnsubscribeQuotes::new(
833 cmd.bar_type.instrument_id(),
834 cmd.client_id,
835 cmd.venue,
836 UUID4::new(),
837 cmd.ts_init,
838 Some(cmd.command_id),
839 cmd.params.clone(),
840 );
841 self.execute(DataCommand::Unsubscribe(UnsubscribeCommand::Quotes(
842 unsubscribe,
843 )));
844 }
845 }
846
847 pub(super) fn stop_bar_aggregator(
848 &mut self,
849 bar_type: BarType,
850 request_id: Option<UUID4>,
851 ) -> anyhow::Result<()> {
852 let key = bar_aggregator_key(bar_type, request_id);
853
854 let aggregator = self.bar_aggregators.shift_remove(&key).ok_or_else(|| {
855 anyhow::anyhow!("Cannot stop bar aggregator: no aggregator to stop for {bar_type}")
856 })?;
857
858 aggregator.borrow_mut().stop();
859
860 if let Some(subs) = self.bar_aggregator_handlers.remove(&key) {
862 for sub in subs {
863 match sub {
864 BarAggregatorSubscription::Bar { topic, handler } => {
865 msgbus::unsubscribe_bars(topic.into(), &handler);
866 }
867 BarAggregatorSubscription::Trade { topic, handler } => {
868 msgbus::unsubscribe_trades(topic.into(), &handler);
869 }
870 BarAggregatorSubscription::Quote { topic, handler } => {
871 msgbus::unsubscribe_quotes(topic.into(), &handler);
872 }
873 }
874 }
875 }
876
877 Ok(())
878 }
879}
880
881#[derive(Clone, Debug)]
882pub(super) struct BarAggregationSubscription {
883 pub(super) command: SubscribeBars,
884 pub(super) source: Option<SubscribeCommand>,
885}
886
887pub(crate) type BarAggregatorKey = (BarType, Option<UUID4>);
893
894#[inline]
895pub(crate) fn bar_aggregator_key(bar_type: BarType, request_id: Option<UUID4>) -> BarAggregatorKey {
896 (bar_type.standard(), request_id)
897}
898
899#[derive(Clone)]
904pub enum BarAggregatorSubscription {
905 Bar {
906 topic: MStr<Topic>,
907 handler: TypedHandler<Bar>,
908 },
909 Trade {
910 topic: MStr<Topic>,
911 handler: TypedHandler<TradeTick>,
912 },
913 Quote {
914 topic: MStr<Topic>,
915 handler: TypedHandler<QuoteTick>,
916 },
917}
918
919impl Debug for BarAggregatorSubscription {
920 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
921 match self {
922 Self::Bar { topic, handler } => f
923 .debug_struct(stringify!(Bar))
924 .field("topic", topic)
925 .field("handler_id", &handler.id())
926 .finish(),
927 Self::Trade { topic, handler } => f
928 .debug_struct(stringify!(Trade))
929 .field("topic", topic)
930 .field("handler_id", &handler.id())
931 .finish(),
932 Self::Quote { topic, handler } => f
933 .debug_struct(stringify!(Quote))
934 .field("topic", topic)
935 .field("handler_id", &handler.id())
936 .finish(),
937 }
938 }
939}
940
941#[cfg(test)]
942mod tests {
943 use std::{cell::RefCell, rc::Rc};
944
945 use jiff::Timestamp;
946 use nautilus_common::{
947 cache::Cache,
948 clock::{Clock, VirtualClock},
949 messages::data::SubscribeBars,
950 msgbus::MessageBus,
951 };
952 use nautilus_core::{UUID4, UnixNanos, datetime::get_timezone};
953 use nautilus_model::{
954 data::{Bar, BarType},
955 identifiers::TraderId,
956 instruments::{InstrumentAny, stubs::audusd_sim},
957 types::{Price, Quantity},
958 };
959 use rstest::rstest;
960
961 use super::{bar_aggregator_key, request_bar_aggregation_from_params, time_zone_from_params};
962 use crate::engine::{DataEngine, config::DataEngineConfig};
963
964 #[rstest]
965 #[case::utc_default(None, None, "2026-11-02T00:00:00Z")]
966 #[case::config(Some("America/New_York"), None, "2026-11-02T05:00:00Z")]
967 #[case::utc_override(Some("America/New_York"), Some("UTC"), "2026-11-02T00:00:00Z")]
968 fn test_calendar_zone_precedence_and_reuse(
969 #[case] config_zone: Option<&str>,
970 #[case] override_zone: Option<&str>,
971 #[case] expected: &str,
972 ) {
973 let _msgbus = MessageBus::new(TraderId::new("TESTER-001"), UUID4::new(), None, None)
974 .register_message_bus();
975 let clock = Rc::new(RefCell::new(VirtualClock::new()));
976 clock.borrow_mut().set_time(UnixNanos::from(
977 "2026-11-01T12:00:00Z".parse::<Timestamp>().unwrap(),
978 ));
979 let cache = Rc::new(RefCell::new(Cache::default()));
980 cache
981 .borrow_mut()
982 .add_instrument(InstrumentAny::CurrencyPair(audusd_sim()))
983 .unwrap();
984 let config = DataEngineConfig::builder()
985 .maybe_time_bars_time_zone(config_zone.map(str::to_owned))
986 .time_bars_skip_first_non_full_bar(true)
987 .build();
988 let mut engine = DataEngine::new(
989 Rc::clone(&clock) as Rc<RefCell<dyn Clock>>,
990 Rc::clone(&cache),
991 Some(config),
992 );
993 let bar_type = BarType::from("AUD/USD.SIM-1-DAY-LAST-INTERNAL");
994 let mut params = nautilus_core::Params::default();
995 params.insert(
996 "skip_first_non_full_bar".to_owned(),
997 serde_json::json!(false),
998 );
999
1000 if let Some(zone) = override_zone {
1001 params.insert("time_zone".to_owned(), serde_json::json!(zone));
1002 }
1003
1004 let mut cmd = SubscribeBars::new(
1005 bar_type,
1006 None,
1007 Some(bar_type.instrument_id().venue),
1008 UUID4::new(),
1009 UnixNanos::default(),
1010 None,
1011 Some(params),
1012 );
1013 engine.subscribe_bars(&cmd).unwrap();
1014 engine.subscribe_bars(&cmd).unwrap();
1015 cmd.params
1016 .as_mut()
1017 .unwrap()
1018 .insert("time_zone".to_owned(), serde_json::json!("Asia/Tokyo"));
1019 let e = engine.subscribe_bars(&cmd).unwrap_err();
1020
1021 engine
1023 .create_bar_aggregator_for_key(
1024 bar_type,
1025 Some(UUID4::new()),
1026 None,
1027 Some(get_timezone("Asia/Tokyo").unwrap()),
1028 )
1029 .unwrap();
1030 assert_eq!(
1031 e.to_string(),
1032 format!("Cannot reuse bar aggregator {bar_type} with a different time zone")
1033 );
1034 assert_eq!(
1035 clock.borrow().next_time_ns(&format!("TIME_BAR_{bar_type}")),
1036 Some(UnixNanos::from(expected.parse::<Timestamp>().unwrap()))
1037 );
1038 let now = clock.borrow().timestamp_ns();
1039 engine
1040 .bar_aggregators
1041 .get(&bar_aggregator_key(bar_type, None))
1042 .unwrap()
1043 .borrow_mut()
1044 .update(Price::from("0.65000"), Quantity::from(2), now);
1045 let close = UnixNanos::from(expected.parse::<Timestamp>().unwrap());
1046 let events = clock.borrow_mut().advance_time(close, true);
1047 let handlers = clock.borrow().match_handlers(events);
1048 for handler in handlers {
1049 handler.callback.call(handler.event);
1050 }
1051
1052 assert_eq!(
1053 cache.borrow().bar(&bar_type).copied(),
1054 Some(Bar::new(
1055 bar_type,
1056 Price::from("0.65000"),
1057 Price::from("0.65000"),
1058 Price::from("0.65000"),
1059 Price::from("0.65000"),
1060 Quantity::from(2),
1061 close,
1062 close
1063 ))
1064 );
1065 }
1066
1067 #[rstest]
1068 #[case::wrong_type(serde_json::json!(17), "`time_zone` parameter must be a string")]
1069 #[case::unknown_zone(serde_json::json!("Not/A_Zone"), "invalid `time_zone` parameter \"Not/A_Zone\"")]
1070 fn test_calendar_zone_parameter_rejects_invalid_value(
1071 #[case] value: serde_json::Value,
1072 #[case] expected: &str,
1073 ) {
1074 let mut params = nautilus_core::Params::default();
1075 params.insert("time_zone".to_owned(), value);
1076 assert_eq!(
1077 time_zone_from_params(Some(¶ms))
1078 .unwrap_err()
1079 .to_string(),
1080 expected
1081 );
1082 }
1083
1084 #[rstest]
1085 fn test_calendar_request_zone_checks_shared_instance() {
1086 let clock = Rc::new(RefCell::new(VirtualClock::new()));
1087 let cache = Rc::new(RefCell::new(Cache::default()));
1088 cache
1089 .borrow_mut()
1090 .add_instrument(InstrumentAny::CurrencyPair(audusd_sim()))
1091 .unwrap();
1092 let mut engine = DataEngine::new(clock, cache, None);
1093 let bar_type = BarType::from("AUD/USD.SIM-1-MONTH-LAST-INTERNAL");
1094 engine
1095 .create_bar_aggregator_for_key(bar_type, None, None, None)
1096 .unwrap();
1097 let mut params: nautilus_core::Params = serde_json::from_value(serde_json::json!({
1098 "bar_types": [bar_type.to_string()], "update_subscriptions": true,
1099 "time_zone": "America/New_York"
1100 }))
1101 .unwrap();
1102 let state = request_bar_aggregation_from_params(Some(¶ms))
1103 .unwrap()
1104 .unwrap();
1105 let id = UUID4::new();
1106 let error = engine
1107 .prepare_request_bar_aggregators_from_state(id, &state)
1108 .unwrap_err();
1109 params.insert("update_subscriptions".to_owned(), serde_json::json!(false));
1110 let state = request_bar_aggregation_from_params(Some(¶ms))
1111 .unwrap()
1112 .unwrap();
1113 engine
1114 .prepare_request_bar_aggregators_from_state(id, &state)
1115 .unwrap();
1116 let instance = engine
1117 .bar_aggregators
1118 .get(&bar_aggregator_key(bar_type, Some(id)))
1119 .unwrap()
1120 .borrow();
1121 let request_zone = instance
1122 .as_any()
1123 .downcast_ref::<crate::aggregation::TimeBarAggregator>()
1124 .unwrap()
1125 .time_zone()
1126 .clone();
1127 let live = engine
1128 .bar_aggregators
1129 .get(&bar_aggregator_key(bar_type, None))
1130 .unwrap()
1131 .borrow();
1132 let live_zone = live
1133 .as_any()
1134 .downcast_ref::<crate::aggregation::TimeBarAggregator>()
1135 .unwrap()
1136 .time_zone()
1137 .clone();
1138 assert_eq!(
1139 error.to_string(),
1140 format!("Cannot reuse bar aggregator {bar_type} with a different time zone")
1141 );
1142 assert_eq!(request_zone, get_timezone("America/New_York").unwrap());
1143 assert_eq!(live_zone, jiff::tz::TimeZone::UTC);
1144 assert_eq!(engine.bar_aggregators.len(), 2);
1145 }
1146}