1use std::{cell::RefCell, num::NonZeroUsize, rc::Rc};
17
18use indexmap::IndexMap;
19use nautilus_common::{
20 cache::Cache,
21 messages::data::{SubscribeBookSnapshots, SubscribeCommand},
22 msgbus::{self, Handler, MStr, Topic, switchboard},
23 timer::TimeEvent,
24};
25use nautilus_model::{
26 data::{OrderBookDeltas, OrderBookDepth, QuoteTick},
27 enums::{BookType, InstrumentClass},
28 identifiers::{ClientId, InstrumentId, Venue},
29 instruments::Instrument,
30 orderbook::OrderBook,
31};
32use ustr::Ustr;
33
34use super::{
35 BookDeltasResponse, BookDepthResponse, DataEngine, DurationNanos, FAILED, OrderBookDelta,
36 PARAMS_IS_PARENT, Params, RecordFlag, SubscribeBookDeltas, SubscribeBookDepth, SubscriptionKey,
37 TimeEventCallback, TypedHandler, UUID4, UnsubscribeBookDeltas, UnsubscribeBookDepth,
38 UnsubscribeBookSnapshots, UnsubscribeCommand, is_parent_subscription,
39 log_error_on_cache_insert,
40};
41
42impl DataEngine {
43 #[must_use]
45 pub fn subscribed_book_deltas(&self) -> Vec<InstrumentId> {
46 self.collect_subscriptions(|client| &client.subscriptions_book_deltas)
47 }
48
49 #[must_use]
51 pub fn subscribed_book_depth(&self) -> Vec<InstrumentId> {
52 self.collect_subscriptions(|client| &client.subscriptions_book_depth)
53 }
54
55 #[must_use]
57 pub fn subscribed_book_snapshots(&self) -> Vec<InstrumentId> {
58 self.book_snapshot_counts
59 .keys()
60 .map(|(instrument_id, _)| *instrument_id)
61 .collect()
62 }
63
64 pub(super) fn subscribe_book_deltas(
65 &mut self,
66 cmd: &SubscribeBookDeltas,
67 ) -> anyhow::Result<bool> {
68 let instrument_id = cmd.single_instrument_id()?;
69 if instrument_id.is_synthetic() {
70 anyhow::bail!("Cannot subscribe for synthetic instrument `OrderBookDelta` data");
71 }
72
73 let had_deltas =
74 self.has_book_delta_subscription_key(instrument_id, cmd.client_id, cmd.venue);
75
76 self.retain_book_subscription(SubscribeCommand::BookDeltas(cmd.clone()))?;
77
78 self.increment_book_delta_subscription(instrument_id, cmd.client_id, cmd.venue);
79
80 Ok(!had_deltas)
81 }
82
83 pub(super) fn subscribe_book_depth(
84 &mut self,
85 cmd: &SubscribeBookDepth,
86 ) -> anyhow::Result<bool> {
87 let instrument_id = cmd.single_instrument_id()?;
88 if instrument_id.is_synthetic() {
89 anyhow::bail!("Cannot subscribe for synthetic instrument `OrderBookDepth` data");
90 }
91
92 let had_depth =
93 self.has_book_depth_subscription_key(instrument_id, cmd.client_id, cmd.venue);
94
95 self.retain_book_subscription(SubscribeCommand::BookDepth(cmd.clone()))?;
96
97 self.increment_book_depth_subscription(instrument_id, cmd.client_id, cmd.venue);
98
99 Ok(!had_depth)
100 }
101
102 pub(super) fn subscribe_book_snapshots(
103 &mut self,
104 cmd: &SubscribeBookSnapshots,
105 ) -> anyhow::Result<()> {
106 let instrument_id = cmd.single_instrument_id()?;
107 if instrument_id.is_synthetic() {
108 anyhow::bail!("Cannot subscribe for synthetic instrument `OrderBookDelta` data");
109 }
110
111 let parent = resolve_parent_components(&instrument_id, cmd.params.as_ref())?;
112
113 let had_snapshots = self.has_book_snapshot_subscriptions(&instrument_id);
114
115 self.retain_book_subscription(SubscribeCommand::BookSnapshots(cmd.clone()))?;
116
117 self.increment_book_snapshot_subscription(cmd, parent);
118
119 if !had_snapshots {
120 self.book_snapshot_sources.insert(
121 instrument_id,
122 BookSnapshotSource {
123 command: cmd.clone(),
124 client_command: SubscribeCommand::BookDeltas(SubscribeBookDeltas::new(
125 instrument_id,
126 cmd.book_type,
127 cmd.client_id,
128 cmd.venue,
129 UUID4::new(),
130 cmd.ts_init,
131 cmd.depth,
132 true, Some(cmd.command_id),
134 cmd.params.clone(),
135 )),
136 },
137 );
138 }
139
140 let source = self
141 .book_snapshot_sources
142 .get(&instrument_id)
143 .cloned()
144 .expect("snapshot source command must exist after increment");
145 self.subscribe_book_snapshot_source(&source.command, source.client_command);
146
147 Ok(())
148 }
149
150 fn subscribe_book_snapshot_source(
151 &mut self,
152 cmd: &SubscribeBookSnapshots,
153 client_command: SubscribeCommand,
154 ) {
155 let instrument_id = cmd
156 .single_instrument_id()
157 .expect("instrument command has one member at this boundary");
158
159 if let Some(client_id) = cmd.client_id.as_ref()
160 && self.is_external_client(*client_id)
161 {
162 if self.config.debug {
163 log::debug!("Skipping subscribe command for external client {client_id}: {cmd:?}");
164 }
165
166 return;
167 }
168
169 log::debug!(
170 "Forwarding BookSnapshots as BookDeltas for {}, client_id={:?}, venue={:?}",
171 instrument_id,
172 cmd.client_id,
173 cmd.venue,
174 );
175
176 if let Some(client) = self.get_command_client(cmd.client_id.as_ref(), cmd.venue.as_ref()) {
177 log::debug!("Calling client.execute_subscribe for BookDeltas: {instrument_id}");
178 client.execute_subscribe(client_command);
179 } else {
180 log::error!(
181 "Cannot handle command: no client found for client_id={:?}, venue={:?}",
182 cmd.client_id,
183 cmd.venue,
184 );
185 }
186 }
187
188 pub(super) fn unsubscribe_book_deltas(&mut self, cmd: &UnsubscribeBookDeltas) -> bool {
189 let instrument_id = cmd
190 .single_instrument_id()
191 .expect("instrument command has one member at this boundary");
192
193 match self.decrement_book_delta_subscription(instrument_id, cmd.client_id, cmd.venue) {
194 BookDeltasUnsubscribeResult::NotSubscribed => {
195 log::warn!("Cannot unsubscribe from `OrderBookDeltas` data: not subscribed");
196 return false;
197 }
198 BookDeltasUnsubscribeResult::Decremented => return false,
199 BookDeltasUnsubscribeResult::Removed => {}
200 }
201
202 true
203 }
204
205 pub(super) fn unsubscribe_book_depth(&mut self, cmd: &UnsubscribeBookDepth) -> bool {
206 let instrument_id = cmd
207 .single_instrument_id()
208 .expect("instrument command has one member at this boundary");
209
210 match self.decrement_book_depth_subscription(instrument_id, cmd.client_id, cmd.venue) {
211 BookDeltasUnsubscribeResult::NotSubscribed => {
212 log::warn!("Cannot unsubscribe from `OrderBookDepth` data: not subscribed");
213 return false;
214 }
215 BookDeltasUnsubscribeResult::Decremented => return false,
216 BookDeltasUnsubscribeResult::Removed => {}
217 }
218
219 true
220 }
221
222 pub(super) fn unsubscribe_book_snapshots(&mut self, cmd: &UnsubscribeBookSnapshots) {
223 let instrument_id = cmd
224 .single_instrument_id()
225 .expect("instrument command has one member at this boundary");
226
227 match self.decrement_book_snapshot_subscription(instrument_id, cmd.interval_ms) {
228 BookSnapshotUnsubscribeResult::NotSubscribed => {
229 log::warn!("Cannot unsubscribe from `OrderBook` snapshots: not subscribed");
230 return;
231 }
232 BookSnapshotUnsubscribeResult::Decremented => return,
233 BookSnapshotUnsubscribeResult::Removed => {}
234 }
235
236 if self.has_book_snapshot_subscriptions(&instrument_id) {
237 return;
238 }
239
240 let Some(source) = self.book_snapshot_sources.remove(&instrument_id) else {
241 log::error!(
242 "Cannot release order book snapshot source for {instrument_id}: command not retained",
243 );
244 return;
245 };
246
247 if let Some(client_id) = source.command.client_id.as_ref()
248 && self.is_external_client(*client_id)
249 {
250 return;
251 }
252
253 if let Some(client) = self.get_command_client(
254 source.command.client_id.as_ref(),
255 source.command.venue.as_ref(),
256 ) {
257 let deltas_cmd = UnsubscribeBookDeltas::new(
258 source
259 .command
260 .single_instrument_id()
261 .expect("book commands are projected onto one instrument"),
262 source.command.client_id,
263 source.command.venue,
264 UUID4::new(),
265 cmd.ts_init,
266 Some(source.command.command_id),
267 source.command.params,
268 );
269 client.execute_unsubscribe(&UnsubscribeCommand::BookDeltas(deltas_cmd));
270 }
271 }
272
273 fn retain_book_subscription(&mut self, command: SubscribeCommand) -> anyhow::Result<()> {
274 let instrument_id = match &command {
275 SubscribeCommand::BookDeltas(cmd) => cmd
276 .single_instrument_id()
277 .expect("book commands are projected onto one instrument"),
278 SubscribeCommand::BookDepth(cmd) => cmd
279 .single_instrument_id()
280 .expect("book commands are projected onto one instrument"),
281 SubscribeCommand::BookSnapshots(cmd) => cmd
282 .single_instrument_id()
283 .expect("book commands are projected onto one instrument"),
284 _ => unreachable!("only book subscriptions are retained"),
285 };
286
287 let parent = resolve_parent_components(&instrument_id, command.params())?;
288
289 let targets = if let Some((root, class)) = parent {
290 self.cache
291 .borrow()
292 .instruments_by_parent(&instrument_id.venue, &root, class)
293 .iter()
294 .map(|instrument| instrument.id())
295 .collect()
296 } else {
297 vec![instrument_id]
298 };
299
300 let client_id = self
301 .get_command_client(command.client_id(), command.venue())
302 .map(|client| client.client_id);
303
304 let subscription = Rc::new(BookSubscriptionOwner {
305 command,
306 client_id,
307 targets,
308 });
309
310 let mut params = subscription
311 .command
312 .params()
313 .map(|params| IndexMap::clone(params))
314 .unwrap_or_default();
315 params.shift_remove(PARAMS_IS_PARENT);
316
317 for active in subscription
318 .targets
319 .iter()
320 .filter_map(|id| self.book_subscriptions.get(id))
321 .flat_map(|book| &book.owners)
322 {
323 if !subscription.managed()
324 && !active.managed()
325 && subscription.client_id != active.client_id
326 {
327 continue;
328 }
329
330 if subscription.is_depth() != active.is_depth() {
331 anyhow::ensure!(
332 !subscription.managed() || !active.managed(),
333 "Conflicting managed book source for {instrument_id}: deltas and depth cannot both manage the same book; use managed=false for the other subscription"
334 );
335 continue;
336 }
337
338 let mut active_params = active
339 .command
340 .params()
341 .map(|params| IndexMap::clone(params))
342 .unwrap_or_default();
343 active_params.shift_remove(PARAMS_IS_PARENT);
344 anyhow::ensure!(
345 subscription.client_id == active.client_id
346 && subscription.config() == active.config()
347 && params == active_params,
348 "Conflicting book subscription for {instrument_id}: shared book sources must use the same client, book type, depth, and parameters"
349 );
350 }
351
352 if subscription.managed() {
353 self.setup_book_updater(
354 &subscription.targets,
355 subscription.config().0,
356 subscription.is_depth(),
357 )?;
358 }
359
360 for target_id in &subscription.targets {
361 self.book_subscriptions
362 .entry(*target_id)
363 .or_default()
364 .owners
365 .push(Rc::clone(&subscription));
366 }
367
368 self.book_subscription_owners
369 .entry(instrument_id)
370 .or_default()
371 .push(subscription);
372 Ok(())
373 }
374
375 pub(super) fn release_book_subscription(&mut self, command: &UnsubscribeCommand) -> bool {
376 let key = SubscriptionKey::from_unsubscribe(command);
377
378 let instrument_id = match command {
379 UnsubscribeCommand::BookDeltas(cmd) => cmd
380 .single_instrument_id()
381 .expect("book commands are projected onto one instrument"),
382 UnsubscribeCommand::BookDepth(cmd) => cmd
383 .single_instrument_id()
384 .expect("book commands are projected onto one instrument"),
385 UnsubscribeCommand::BookSnapshots(cmd) => cmd
386 .single_instrument_id()
387 .expect("book commands are projected onto one instrument"),
388 _ => unreachable!("only book subscriptions are released"),
389 };
390
391 let Some(owners) = self.book_subscription_owners.get_mut(&instrument_id) else {
392 return false;
393 };
394
395 let index = owners.iter().position(|subscription| {
396 SubscriptionKey::from_subscribe(&subscription.command) == key
397 && subscription.command.client_id() == command.client_id()
398 && subscription.command.venue() == command.venue()
399 && command
400 .correlation_id()
401 .is_none_or(|id| subscription.command.command_id() == id)
402 });
403
404 let Some(index) = index else {
405 return false;
406 };
407
408 let subscription = owners.remove(index);
409 if owners.is_empty() {
410 self.book_subscription_owners.remove(&instrument_id);
411 }
412
413 for &target_id in &subscription.targets {
414 let book = self
415 .book_subscriptions
416 .get_mut(&target_id)
417 .expect("retained book subscription");
418 book.owners
419 .retain(|owner| !Rc::ptr_eq(owner, &subscription));
420 let managed = book.owners.iter().any(|owner| owner.managed());
421 if book.owners.is_empty() {
422 self.book_subscriptions.remove(&target_id);
423 }
424
425 if managed {
426 continue;
427 }
428
429 let Some(updater) = self.book_updaters.remove(&target_id) else {
430 continue;
431 };
432
433 let deltas_handler: TypedHandler<OrderBookDeltas> =
434 TypedHandler::new(Rc::clone(&updater));
435 let depth_handler: TypedHandler<OrderBookDepth> = TypedHandler::new(updater);
436 msgbus::unsubscribe_book_deltas(
437 switchboard::get_book_deltas_topic(target_id).into(),
438 &deltas_handler,
439 );
440 msgbus::unsubscribe_book_depth(
441 switchboard::get_book_depth_topic(target_id).into(),
442 &depth_handler,
443 );
444 }
445
446 true
447 }
448
449 fn has_book_snapshot_subscriptions(&self, instrument_id: &InstrumentId) -> bool {
450 self.book_snapshot_counts
451 .keys()
452 .any(|(id, _)| id == instrument_id)
453 }
454
455 fn has_book_delta_subscription_key(
456 &self,
457 instrument_id: InstrumentId,
458 client_id: Option<ClientId>,
459 venue: Option<Venue>,
460 ) -> bool {
461 self.book_deltas_counts
462 .contains_key(&(instrument_id, client_id, venue))
463 }
464
465 fn has_book_depth_subscription_key(
466 &self,
467 instrument_id: InstrumentId,
468 client_id: Option<ClientId>,
469 venue: Option<Venue>,
470 ) -> bool {
471 self.book_depth_counts
472 .contains_key(&(instrument_id, client_id, venue))
473 }
474
475 fn increment_book_delta_subscription(
476 &mut self,
477 instrument_id: InstrumentId,
478 client_id: Option<ClientId>,
479 venue: Option<Venue>,
480 ) {
481 let key = (instrument_id, client_id, venue);
482
483 if let Some(count) = self.book_deltas_counts.get_mut(&key) {
484 *count += 1;
485 } else {
486 self.book_deltas_counts.insert(key, 1);
487 }
488 }
489
490 fn decrement_book_delta_subscription(
491 &mut self,
492 instrument_id: InstrumentId,
493 client_id: Option<ClientId>,
494 venue: Option<Venue>,
495 ) -> BookDeltasUnsubscribeResult {
496 let key = (instrument_id, client_id, venue);
497
498 let Some(count) = self.book_deltas_counts.get_mut(&key) else {
499 return BookDeltasUnsubscribeResult::NotSubscribed;
500 };
501
502 if *count > 1 {
503 *count -= 1;
504 return BookDeltasUnsubscribeResult::Decremented;
505 }
506
507 self.book_deltas_counts.shift_remove(&key);
508 BookDeltasUnsubscribeResult::Removed
509 }
510
511 fn increment_book_depth_subscription(
512 &mut self,
513 instrument_id: InstrumentId,
514 client_id: Option<ClientId>,
515 venue: Option<Venue>,
516 ) {
517 let key = (instrument_id, client_id, venue);
518 *self.book_depth_counts.entry(key).or_insert(0) += 1;
519 }
520
521 fn decrement_book_depth_subscription(
522 &mut self,
523 instrument_id: InstrumentId,
524 client_id: Option<ClientId>,
525 venue: Option<Venue>,
526 ) -> BookDeltasUnsubscribeResult {
527 let key = (instrument_id, client_id, venue);
528
529 let Some(count) = self.book_depth_counts.get_mut(&key) else {
530 return BookDeltasUnsubscribeResult::NotSubscribed;
531 };
532
533 if *count > 1 {
534 *count -= 1;
535 return BookDeltasUnsubscribeResult::Decremented;
536 }
537
538 self.book_depth_counts.shift_remove(&key);
539 BookDeltasUnsubscribeResult::Removed
540 }
541
542 fn increment_book_snapshot_subscription(
543 &mut self,
544 cmd: &SubscribeBookSnapshots,
545 parent: Option<(Ustr, InstrumentClass)>,
546 ) -> bool {
547 let instrument_id = cmd
548 .single_instrument_id()
549 .expect("instrument command has one member at this boundary");
550 let key = (instrument_id, cmd.interval_ms);
551
552 if let Some(count) = self.book_snapshot_counts.get_mut(&key) {
553 *count += 1;
554 return false;
555 }
556
557 self.book_snapshot_counts.insert(key, 1);
558
559 let snapshot_infos = if let Some(snapshot_infos) = self.book_intervals.get(&cmd.interval_ms)
560 {
561 Rc::clone(snapshot_infos)
562 } else {
563 let snapshot_infos = Rc::new(RefCell::new(IndexMap::new()));
564 self.book_intervals
565 .insert(cmd.interval_ms, Rc::clone(&snapshot_infos));
566 self.schedule_book_snapshotter(cmd.interval_ms, Rc::clone(&snapshot_infos));
567 snapshot_infos
568 };
569
570 let topic = switchboard::get_book_snapshots_topic(instrument_id, cmd.interval_ms);
571
572 let snap_info = BookSnapshotInfo {
573 instrument_id,
574 venue: instrument_id.venue,
575 parent,
576 topic,
577 interval_ms: cmd.interval_ms,
578 };
579
580 snapshot_infos.borrow_mut().insert(instrument_id, snap_info);
581
582 true
583 }
584
585 fn decrement_book_snapshot_subscription(
586 &mut self,
587 instrument_id: InstrumentId,
588 interval_ms: NonZeroUsize,
589 ) -> BookSnapshotUnsubscribeResult {
590 let key = (instrument_id, interval_ms);
591
592 let Some(count) = self.book_snapshot_counts.get_mut(&key) else {
593 return BookSnapshotUnsubscribeResult::NotSubscribed;
594 };
595
596 if *count > 1 {
597 *count -= 1;
598 return BookSnapshotUnsubscribeResult::Decremented;
599 }
600
601 self.book_snapshot_counts.shift_remove(&key);
602
603 let remove_interval = if let Some(snapshot_infos) = self.book_intervals.get(&interval_ms) {
604 let mut snapshot_infos = snapshot_infos.borrow_mut();
605 snapshot_infos.shift_remove(&instrument_id);
606 snapshot_infos.is_empty()
607 } else {
608 false
609 };
610
611 if remove_interval {
612 self.book_intervals.remove(&interval_ms);
613
614 if let Some(snapshotter) = self.book_snapshotters.remove(&interval_ms) {
615 let timer_name = snapshotter.timer_name;
616 let mut clock = self.clock.borrow_mut();
617 if clock.timer_exists(&timer_name) {
618 clock.cancel_timer(&timer_name);
619 }
620 }
621 }
622
623 BookSnapshotUnsubscribeResult::Removed
624 }
625
626 fn schedule_book_snapshotter(
627 &mut self,
628 interval_ms: NonZeroUsize,
629 snapshot_infos: BookSnapshotInfos,
630 ) {
631 let interval_ms_u64 =
632 u64::try_from(interval_ms.get()).expect("Snapshot interval exceeds u64");
633 let interval_ns = DurationNanos::from_millis(interval_ms_u64);
634 let now_ns = self.clock.borrow().timestamp_ns();
635 let start_time_ns = now_ns
636 .floor(interval_ns)
637 .checked_add(interval_ns)
638 .expect("Book snapshot timer start exceeds UnixNanos range");
639
640 let snapshotter = Rc::new(BookSnapshotter::new(
641 interval_ms,
642 snapshot_infos,
643 Rc::clone(&self.cache),
644 ));
645 let timer_name = snapshotter.timer_name;
646 let snapshotter_callback = Rc::clone(&snapshotter);
647 let callback_fn: Rc<dyn Fn(TimeEvent)> =
648 Rc::new(move |event| snapshotter_callback.snapshot(event));
649 let callback = TimeEventCallback::from(callback_fn);
650
651 self.clock
652 .borrow_mut()
653 .set_timer_ns(
654 &timer_name,
655 interval_ns,
656 Some(start_time_ns),
657 None,
658 Some(callback),
659 None,
660 None,
661 )
662 .expect(FAILED);
663
664 self.book_snapshotters.insert(interval_ms, snapshotter);
665 }
666
667 fn cache_is_owned_by_live_subscription(&self, instrument_id: &InstrumentId) -> bool {
670 self.book_updaters.contains_key(instrument_id)
671 }
672
673 pub(super) fn handle_book_response(&self, book: &OrderBook) {
674 if self.cache_is_owned_by_live_subscription(&book.instrument_id) {
675 log::debug!(
676 "Skipping cache write for order book {}: live subscription owns the book",
677 book.instrument_id,
678 );
679 return;
680 }
681
682 log::debug!("Adding order book {} to cache", book.instrument_id);
683
684 if let Err(e) = self
685 .cache
686 .as_ref()
687 .borrow_mut()
688 .add_order_book(book.clone())
689 {
690 log_error_on_cache_insert(&e);
691 }
692 }
693
694 pub(super) fn handle_book_deltas_response(&self, resp: &BookDeltasResponse) {
695 if !self.cache_is_owned_by_live_subscription(&resp.instrument_id) {
696 let mut cache = self.cache.as_ref().borrow_mut();
697 if let Some(book) = cache.order_book_mut(&resp.instrument_id) {
698 for delta in resp.data.iter() {
699 if let Err(e) = book.apply_delta(delta) {
700 log::error!("Failed to apply historical delta to cache: {e}");
701 }
702 }
703 } else {
704 log::debug!(
705 "Skipping cache write for {} historical deltas on {}: no cache book yet",
706 resp.data.len(),
707 resp.instrument_id,
708 );
709 }
710 }
711
712 if resp.data.is_empty() {
717 return;
718 }
719
720 let topic = switchboard::get_pipeline_book_deltas_topic(resp.instrument_id);
721 let mut frame: Vec<OrderBookDelta> = Vec::new();
722 for delta in resp.data.iter() {
723 frame.push(*delta);
724 if RecordFlag::F_LAST.matches(delta.flags) {
725 let batch = OrderBookDeltas::new(resp.instrument_id, std::mem::take(&mut frame));
726 msgbus::publish_deltas(topic, &batch);
727 }
728 }
729
730 if !frame.is_empty() {
731 let batch = OrderBookDeltas::new(resp.instrument_id, frame);
732 msgbus::publish_deltas(topic, &batch);
733 }
734 }
735
736 pub(super) fn handle_book_depth_response(&self, resp: &BookDepthResponse) {
737 let topic = switchboard::get_pipeline_book_depth_topic(resp.instrument_id);
738 for depth in resp.data.iter() {
739 msgbus::publish_depth(topic, depth);
740 }
741 }
742
743 fn setup_book_updater(
744 &mut self,
745 target_ids: &[InstrumentId],
746 book_type: BookType,
747 depth: bool,
748 ) -> anyhow::Result<()> {
749 {
750 let mut cache = self.cache.borrow_mut();
751
752 for target_id in target_ids {
753 if !cache.has_order_book(target_id) {
754 cache.add_order_book(OrderBook::new(*target_id, book_type))?;
755 }
756 }
757 }
758
759 for target_id in target_ids {
760 let updater = Rc::clone(self.book_updaters.entry(*target_id).or_insert_with(|| {
761 Rc::new(BookUpdater::new(
762 target_id,
763 Rc::clone(&self.cache),
764 self.config.emit_quotes_from_book,
765 ))
766 }));
767
768 if depth {
769 msgbus::subscribe_book_depth(
770 switchboard::get_book_depth_topic(*target_id).into(),
771 TypedHandler::new(updater),
772 Some(self.msgbus_priority),
773 );
774 } else {
775 msgbus::subscribe_book_deltas(
776 switchboard::get_book_deltas_topic(*target_id).into(),
777 TypedHandler::new(updater),
778 Some(self.msgbus_priority),
779 );
780 }
781 }
782
783 Ok(())
784 }
785}
786
787#[derive(Debug, Default)]
788pub(super) struct BookSubscription {
789 pub(super) owners: Vec<Rc<BookSubscriptionOwner>>,
790}
791
792#[derive(Debug)]
793pub(super) struct BookSubscriptionOwner {
794 pub(super) command: SubscribeCommand,
795 pub(super) client_id: Option<ClientId>,
796 pub(super) targets: Vec<InstrumentId>,
797}
798
799impl BookSubscriptionOwner {
800 pub(super) fn managed(&self) -> bool {
801 match &self.command {
802 SubscribeCommand::BookDeltas(cmd) => cmd.managed,
803 SubscribeCommand::BookDepth(cmd) => cmd.managed,
804 SubscribeCommand::BookSnapshots(_) => true,
805 _ => unreachable!("only book subscriptions are retained"),
806 }
807 }
808
809 pub(super) fn is_depth(&self) -> bool {
810 matches!(self.command, SubscribeCommand::BookDepth(_))
811 }
812
813 pub(super) fn config(&self) -> (BookType, Option<NonZeroUsize>) {
814 match &self.command {
815 SubscribeCommand::BookDeltas(cmd) => (cmd.book_type, cmd.depth),
816 SubscribeCommand::BookDepth(cmd) => (cmd.book_type, cmd.depth),
817 SubscribeCommand::BookSnapshots(cmd) => (cmd.book_type, cmd.depth),
818 _ => unreachable!("only book subscriptions are retained"),
819 }
820 }
821}
822
823#[derive(Clone, Debug)]
824pub(super) struct BookSnapshotSource {
825 pub(super) command: SubscribeBookSnapshots,
826 pub(super) client_command: SubscribeCommand,
827}
828
829#[derive(Clone, Debug)]
831pub struct BookSnapshotInfo {
832 pub instrument_id: InstrumentId,
833 pub venue: Venue,
834 pub parent: Option<(Ustr, InstrumentClass)>,
837 pub topic: MStr<Topic>,
838 pub interval_ms: NonZeroUsize,
839}
840
841pub(crate) type BookSnapshotInfos = Rc<RefCell<IndexMap<InstrumentId, BookSnapshotInfo>>>;
846
847pub(crate) type BookSnapshotKey = (InstrumentId, NonZeroUsize);
849
850pub(crate) enum BookSnapshotUnsubscribeResult {
852 NotSubscribed,
854 Decremented,
856 Removed,
858}
859
860pub(crate) type BookDeltasKey = (InstrumentId, Option<ClientId>, Option<Venue>);
862
863pub(crate) enum BookDeltasUnsubscribeResult {
865 NotSubscribed,
867 Decremented,
869 Removed,
871}
872
873#[derive(Debug)]
879pub struct BookUpdater {
880 pub id: Ustr,
881 pub instrument_id: InstrumentId,
882 pub cache: Rc<RefCell<Cache>>,
883 pub emit_quotes_from_book: bool,
884}
885
886impl BookUpdater {
887 pub fn new(
889 instrument_id: &InstrumentId,
890 cache: Rc<RefCell<Cache>>,
891 emit_quotes_from_book: bool,
892 ) -> Self {
893 Self {
894 id: Ustr::from(&format!("{}-{}", stringify!(BookUpdater), instrument_id)),
895 instrument_id: *instrument_id,
896 cache,
897 emit_quotes_from_book,
898 }
899 }
900}
901
902impl Handler<OrderBookDeltas> for BookUpdater {
903 fn id(&self) -> Ustr {
904 self.id
905 }
906
907 fn handle(&self, deltas: &OrderBookDeltas) {
908 let mut emit: Option<QuoteTick> = None;
909 {
910 let mut cache = self.cache.borrow_mut();
911 if let Some(book) = cache.order_book_mut(&deltas.instrument_id) {
912 if let Err(e) = book.apply_deltas(deltas) {
913 log::error!("Failed to apply deltas: {e}");
914 return;
915 }
916
917 if self.emit_quotes_from_book {
918 emit = derive_quote_from_book(book);
919 }
920 }
921 }
922
923 if let Some(quote) = emit {
924 publish_quote_if_changed(&self.cache, quote);
925 }
926 }
927}
928
929impl Handler<OrderBookDepth> for BookUpdater {
930 fn id(&self) -> Ustr {
931 self.id
932 }
933
934 fn handle(&self, depth: &OrderBookDepth) {
935 let mut emit: Option<QuoteTick> = None;
936 {
937 let mut cache = self.cache.borrow_mut();
938 if let Some(book) = cache.order_book_mut(&depth.instrument_id) {
939 if let Err(e) = book.apply_depth(depth) {
940 log::error!("Failed to apply depth: {e}");
941 return;
942 }
943
944 if self.emit_quotes_from_book {
945 emit = derive_quote_from_book(book);
946 }
947 }
948 }
949
950 if let Some(quote) = emit {
951 publish_quote_if_changed(&self.cache, quote);
952 }
953 }
954}
955
956fn derive_quote_from_book(book: &OrderBook) -> Option<QuoteTick> {
957 let bid_price = book.best_bid_price()?;
958 let ask_price = book.best_ask_price()?;
959 let bid_size = book.best_bid_size()?;
960 let ask_size = book.best_ask_size()?;
961
962 if bid_size.is_zero() || ask_size.is_zero() {
963 return None;
964 }
965
966 Some(QuoteTick::new(
967 book.instrument_id,
968 bid_price,
969 ask_price,
970 bid_size,
971 ask_size,
972 book.ts_last,
973 book.ts_last,
974 ))
975}
976
977pub(crate) fn publish_quote_if_changed(cache: &Rc<RefCell<Cache>>, quote: QuoteTick) {
982 let publish = {
983 let cache_ref = cache.borrow();
984 match cache_ref.quote("e.instrument_id) {
985 None => true,
986 Some(last) => {
987 last.bid_price != quote.bid_price
988 || last.ask_price != quote.ask_price
989 || last.bid_size != quote.bid_size
990 || last.ask_size != quote.ask_size
991 }
992 }
993 };
994
995 if !publish {
996 return;
997 }
998
999 if let Err(e) = cache.borrow_mut().add_quote(quote) {
1000 log::error!("Error on cache insert: {e}");
1001 }
1002
1003 let topic = switchboard::get_quotes_topic(quote.instrument_id);
1004 msgbus::publish_quote(topic, "e);
1005}
1006
1007#[derive(Debug)]
1013pub struct BookSnapshotter {
1014 pub timer_name: Ustr,
1015 pub interval_ms: NonZeroUsize,
1016 pub snapshot_infos: Rc<RefCell<IndexMap<InstrumentId, BookSnapshotInfo>>>,
1017 pub cache: Rc<RefCell<Cache>>,
1018}
1019
1020impl BookSnapshotter {
1021 pub fn new(
1023 interval_ms: NonZeroUsize,
1024 snapshot_infos: Rc<RefCell<IndexMap<InstrumentId, BookSnapshotInfo>>>,
1025 cache: Rc<RefCell<Cache>>,
1026 ) -> Self {
1027 let timer_name = format!("OrderBookSnapshots|{interval_ms}");
1028
1029 Self {
1030 timer_name: Ustr::from(&timer_name),
1031 interval_ms,
1032 snapshot_infos,
1033 cache,
1034 }
1035 }
1036
1037 pub fn snapshot(&self, _event: TimeEvent) {
1043 let snapshot_infos: Vec<BookSnapshotInfo> =
1044 self.snapshot_infos.borrow().values().cloned().collect();
1045
1046 log::debug!(
1047 "BookSnapshotter.snapshot called for {} subscriptions at {}ms",
1048 snapshot_infos.len(),
1049 self.interval_ms,
1050 );
1051
1052 let books: Vec<(MStr<Topic>, OrderBook)> = {
1053 let cache = self.cache.borrow();
1054 let mut books = Vec::new();
1055
1056 for snap_info in &snapshot_infos {
1057 self.collect_snapshot(snap_info, &cache, &mut books);
1058 }
1059
1060 books
1061 };
1062
1063 for (topic, book) in books {
1064 msgbus::publish_book(topic, &book);
1065 }
1066 }
1067
1068 fn collect_snapshot(
1069 &self,
1070 snap_info: &BookSnapshotInfo,
1071 cache: &Cache,
1072 books: &mut Vec<(MStr<Topic>, OrderBook)>,
1073 ) {
1074 if let Some((root, class)) = snap_info.parent {
1075 let topic = snap_info.topic;
1076 for instrument in cache.instruments_by_parent(&snap_info.venue, &root, class) {
1077 self.collect_order_book(&instrument.id(), topic, cache, books);
1078 }
1079 } else {
1080 self.collect_order_book(&snap_info.instrument_id, snap_info.topic, cache, books);
1081 }
1082 }
1083
1084 fn collect_order_book(
1085 &self,
1086 instrument_id: &InstrumentId,
1087 topic: MStr<Topic>,
1088 cache: &Cache,
1089 books: &mut Vec<(MStr<Topic>, OrderBook)>,
1090 ) {
1091 let book = match cache.try_order_book(instrument_id) {
1092 Ok(book) => book,
1093 Err(e) => {
1094 log::error!("Cannot publish OrderBook snapshot: {e}");
1095 return;
1096 }
1097 };
1098
1099 if book.update_count == 0 {
1100 log::debug!("OrderBook not yet updated for snapshot: {instrument_id}");
1101 return;
1102 }
1103 log::debug!(
1104 "Publishing OrderBook snapshot for {instrument_id} (update_count={})",
1105 book.update_count
1106 );
1107
1108 books.push((topic, book.clone()));
1109 }
1110}
1111
1112fn resolve_parent_components(
1120 instrument_id: &InstrumentId,
1121 params: Option<&Params>,
1122) -> anyhow::Result<Option<(Ustr, InstrumentClass)>> {
1123 if !is_parent_subscription(params) {
1124 return Ok(None);
1125 }
1126
1127 let Some((root, class)) = instrument_id.parse_parent_components() else {
1128 anyhow::bail!(
1129 "Cannot expand parent subscription for {instrument_id}: \
1130 symbol does not parse as `<root>.<class>` with a recognized class suffix"
1131 );
1132 };
1133
1134 Ok(Some((Ustr::from(root), class)))
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139 use nautilus_common::msgbus::TypedHandler;
1140 use nautilus_core::{UUID4, UnixNanos};
1141 use nautilus_model::{
1142 data::BookOrder,
1143 enums::{BookType, OrderSide},
1144 types::{Price, Quantity},
1145 };
1146 use rstest::rstest;
1147
1148 use super::*;
1149
1150 #[rstest]
1151 fn snapshot_skips_missing_order_book() {
1152 let instrument_id = InstrumentId::from("AUD/USD.SIM");
1153 let interval_ms = NonZeroUsize::new(100).unwrap();
1154 let topic = switchboard::get_book_snapshots_topic(instrument_id, interval_ms);
1155 let snapshot_infos = Rc::new(RefCell::new(IndexMap::new()));
1156
1157 snapshot_infos.borrow_mut().insert(
1158 instrument_id,
1159 BookSnapshotInfo {
1160 instrument_id,
1161 venue: Venue::new("SIM"),
1162 parent: None,
1163 topic,
1164 interval_ms,
1165 },
1166 );
1167
1168 let snapshotter = BookSnapshotter::new(
1169 interval_ms,
1170 snapshot_infos,
1171 Rc::new(RefCell::new(Cache::default())),
1172 );
1173 let event = TimeEvent::new(
1174 Ustr::from("TEST"),
1175 UUID4::new(),
1176 UnixNanos::default(),
1177 UnixNanos::default(),
1178 );
1179
1180 snapshotter.snapshot(event);
1181 }
1182
1183 #[rstest]
1184 fn snapshot_allows_subscriber_to_mutably_borrow_cache() {
1185 let instrument_id = InstrumentId::from("AUD/USD.SIM");
1186 let interval_ms = NonZeroUsize::new(100).unwrap();
1187 let topic = switchboard::get_book_snapshots_topic(instrument_id, interval_ms);
1188 let snapshot_infos = Rc::new(RefCell::new(IndexMap::new()));
1189
1190 snapshot_infos.borrow_mut().insert(
1191 instrument_id,
1192 BookSnapshotInfo {
1193 instrument_id,
1194 venue: Venue::new("SIM"),
1195 parent: None,
1196 topic,
1197 interval_ms,
1198 },
1199 );
1200
1201 let cache = Rc::new(RefCell::new(Cache::default()));
1202 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
1203 book.add(
1204 BookOrder::new(OrderSide::Buy, Price::from("100.00"), Quantity::from(10), 0),
1205 0,
1206 1,
1207 UnixNanos::default(),
1208 );
1209 cache.borrow_mut().add_order_book(book).unwrap();
1210
1211 let received = Rc::new(RefCell::new(Vec::new()));
1212 let handler = CacheWritingBookHandler {
1213 id: Ustr::from("CacheWritingBookHandler"),
1214 cache: Rc::clone(&cache),
1215 received: Rc::clone(&received),
1216 };
1217 msgbus::subscribe_book_snapshots(topic.into(), TypedHandler::new(handler), None);
1218
1219 let snapshotter = BookSnapshotter::new(interval_ms, snapshot_infos, cache);
1220 let event = TimeEvent::new(
1221 Ustr::from("TEST"),
1222 UUID4::new(),
1223 UnixNanos::default(),
1224 UnixNanos::default(),
1225 );
1226
1227 snapshotter.snapshot(event);
1228
1229 let received = received.borrow();
1230 assert_eq!(received.len(), 1);
1231 assert_eq!(received[0].instrument_id, instrument_id);
1232 assert_eq!(received[0].best_bid_price(), Some(Price::from("100.00")));
1233 }
1234
1235 #[rstest]
1236 fn snapshot_skips_book_with_no_updates() {
1237 let instrument_id = InstrumentId::from("AUD/USD.SIM");
1238 let interval_ms = NonZeroUsize::new(100).unwrap();
1239 let topic = switchboard::get_book_snapshots_topic(instrument_id, interval_ms);
1240 let snapshot_infos = Rc::new(RefCell::new(IndexMap::new()));
1241
1242 snapshot_infos.borrow_mut().insert(
1243 instrument_id,
1244 BookSnapshotInfo {
1245 instrument_id,
1246 venue: Venue::new("SIM"),
1247 parent: None,
1248 topic,
1249 interval_ms,
1250 },
1251 );
1252
1253 let cache = Rc::new(RefCell::new(Cache::default()));
1254 cache
1255 .borrow_mut()
1256 .add_order_book(OrderBook::new(instrument_id, BookType::L2_MBP))
1257 .unwrap();
1258
1259 let received = Rc::new(RefCell::new(Vec::new()));
1260 let handler = CacheWritingBookHandler {
1261 id: Ustr::from("CacheWritingBookHandler-NoUpdates"),
1262 cache: Rc::clone(&cache),
1263 received: Rc::clone(&received),
1264 };
1265 msgbus::subscribe_book_snapshots(topic.into(), TypedHandler::new(handler), None);
1266
1267 let snapshotter = BookSnapshotter::new(interval_ms, snapshot_infos, cache);
1268 let event = TimeEvent::new(
1269 Ustr::from("TEST"),
1270 UUID4::new(),
1271 UnixNanos::default(),
1272 UnixNanos::default(),
1273 );
1274
1275 snapshotter.snapshot(event);
1276
1277 assert!(received.borrow().is_empty());
1278 }
1279
1280 struct CacheWritingBookHandler {
1281 id: Ustr,
1282 cache: Rc<RefCell<Cache>>,
1283 received: Rc<RefCell<Vec<OrderBook>>>,
1284 }
1285
1286 impl Handler<OrderBook> for CacheWritingBookHandler {
1287 fn id(&self) -> Ustr {
1288 self.id
1289 }
1290
1291 fn handle(&self, book: &OrderBook) {
1292 let mut cache = self.cache.borrow_mut();
1294 let _ = cache.order_book_mut(&book.instrument_id);
1295 self.received.borrow_mut().push(book.clone());
1296 }
1297 }
1298}