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