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