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
34#[derive(Debug, Default)]
35pub(super) struct BookSubscription {
36 pub(super) owners: Vec<Rc<BookSubscriptionOwner>>,
37}
38
39#[derive(Debug)]
40pub(super) struct BookSubscriptionOwner {
41 pub(super) command: SubscribeCommand,
42 pub(super) client_id: Option<ClientId>,
43 pub(super) targets: Vec<InstrumentId>,
44}
45
46impl BookSubscriptionOwner {
47 pub(super) fn managed(&self) -> bool {
48 match &self.command {
49 SubscribeCommand::BookDeltas(cmd) => cmd.managed,
50 SubscribeCommand::BookDepth(cmd) => cmd.managed,
51 SubscribeCommand::BookSnapshots(_) => true,
52 _ => unreachable!("only book subscriptions are retained"),
53 }
54 }
55
56 pub(super) fn is_depth(&self) -> bool {
57 matches!(self.command, SubscribeCommand::BookDepth(_))
58 }
59
60 pub(super) fn config(&self) -> (BookType, Option<NonZeroUsize>) {
61 match &self.command {
62 SubscribeCommand::BookDeltas(cmd) => (cmd.book_type, cmd.depth),
63 SubscribeCommand::BookDepth(cmd) => (cmd.book_type, cmd.depth),
64 SubscribeCommand::BookSnapshots(cmd) => (cmd.book_type, cmd.depth),
65 _ => unreachable!("only book subscriptions are retained"),
66 }
67 }
68}
69
70#[derive(Clone, Debug)]
71pub(super) struct BookSnapshotSource {
72 pub(super) command: SubscribeBookSnapshots,
73 pub(super) client_command: SubscribeCommand,
74}
75
76#[derive(Clone, Debug)]
78pub struct BookSnapshotInfo {
79 pub instrument_id: InstrumentId,
80 pub venue: Venue,
81 pub parent: Option<(Ustr, InstrumentClass)>,
84 pub topic: MStr<Topic>,
85 pub interval_ms: NonZeroUsize,
86}
87
88pub(crate) type BookSnapshotInfos = Rc<RefCell<IndexMap<InstrumentId, BookSnapshotInfo>>>;
93
94pub(crate) type BookSnapshotKey = (InstrumentId, NonZeroUsize);
96
97pub(crate) enum BookSnapshotUnsubscribeResult {
99 NotSubscribed,
101 Decremented,
103 Removed,
105}
106
107pub(crate) type BookDeltasKey = (InstrumentId, Option<ClientId>, Option<Venue>);
109
110pub(crate) enum BookDeltasUnsubscribeResult {
112 NotSubscribed,
114 Decremented,
116 Removed,
118}
119
120#[derive(Debug)]
126pub struct BookUpdater {
127 pub id: Ustr,
128 pub instrument_id: InstrumentId,
129 pub cache: Rc<RefCell<Cache>>,
130 pub emit_quotes_from_book: bool,
131}
132
133impl BookUpdater {
134 pub fn new(
136 instrument_id: &InstrumentId,
137 cache: Rc<RefCell<Cache>>,
138 emit_quotes_from_book: bool,
139 ) -> Self {
140 Self {
141 id: Ustr::from(&format!("{}-{}", stringify!(BookUpdater), instrument_id)),
142 instrument_id: *instrument_id,
143 cache,
144 emit_quotes_from_book,
145 }
146 }
147}
148
149impl Handler<OrderBookDeltas> for BookUpdater {
150 fn id(&self) -> Ustr {
151 self.id
152 }
153
154 fn handle(&self, deltas: &OrderBookDeltas) {
155 let mut emit: Option<QuoteTick> = None;
156 {
157 let mut cache = self.cache.borrow_mut();
158 if let Some(book) = cache.order_book_mut(&deltas.instrument_id) {
159 if let Err(e) = book.apply_deltas(deltas) {
160 log::error!("Failed to apply deltas: {e}");
161 return;
162 }
163
164 if self.emit_quotes_from_book {
165 emit = derive_quote_from_book(book);
166 }
167 }
168 }
169
170 if let Some(quote) = emit {
171 publish_quote_if_changed(&self.cache, quote);
172 }
173 }
174}
175
176impl Handler<OrderBookDepth> for BookUpdater {
177 fn id(&self) -> Ustr {
178 self.id
179 }
180
181 fn handle(&self, depth: &OrderBookDepth) {
182 let mut emit: Option<QuoteTick> = None;
183 {
184 let mut cache = self.cache.borrow_mut();
185 if let Some(book) = cache.order_book_mut(&depth.instrument_id) {
186 if let Err(e) = book.apply_depth(depth) {
187 log::error!("Failed to apply depth: {e}");
188 return;
189 }
190
191 if self.emit_quotes_from_book {
192 emit = derive_quote_from_book(book);
193 }
194 }
195 }
196
197 if let Some(quote) = emit {
198 publish_quote_if_changed(&self.cache, quote);
199 }
200 }
201}
202
203fn derive_quote_from_book(book: &OrderBook) -> Option<QuoteTick> {
204 let bid_price = book.best_bid_price()?;
205 let ask_price = book.best_ask_price()?;
206 let bid_size = book.best_bid_size()?;
207 let ask_size = book.best_ask_size()?;
208
209 if bid_size.is_zero() || ask_size.is_zero() {
210 return None;
211 }
212
213 Some(QuoteTick::new(
214 book.instrument_id,
215 bid_price,
216 ask_price,
217 bid_size,
218 ask_size,
219 book.ts_last,
220 book.ts_last,
221 ))
222}
223
224pub(crate) fn publish_quote_if_changed(cache: &Rc<RefCell<Cache>>, quote: QuoteTick) {
229 let publish = {
230 let cache_ref = cache.borrow();
231 match cache_ref.quote("e.instrument_id) {
232 None => true,
233 Some(last) => {
234 last.bid_price != quote.bid_price
235 || last.ask_price != quote.ask_price
236 || last.bid_size != quote.bid_size
237 || last.ask_size != quote.ask_size
238 }
239 }
240 };
241
242 if !publish {
243 return;
244 }
245
246 if let Err(e) = cache.borrow_mut().add_quote(quote) {
247 log::error!("Error on cache insert: {e}");
248 }
249
250 let topic = switchboard::get_quotes_topic(quote.instrument_id);
251 msgbus::publish_quote(topic, "e);
252}
253
254#[derive(Debug)]
260pub struct BookSnapshotter {
261 pub timer_name: Ustr,
262 pub interval_ms: NonZeroUsize,
263 pub snapshot_infos: Rc<RefCell<IndexMap<InstrumentId, BookSnapshotInfo>>>,
264 pub cache: Rc<RefCell<Cache>>,
265}
266
267impl BookSnapshotter {
268 pub fn new(
270 interval_ms: NonZeroUsize,
271 snapshot_infos: Rc<RefCell<IndexMap<InstrumentId, BookSnapshotInfo>>>,
272 cache: Rc<RefCell<Cache>>,
273 ) -> Self {
274 let timer_name = format!("OrderBookSnapshots|{interval_ms}");
275
276 Self {
277 timer_name: Ustr::from(&timer_name),
278 interval_ms,
279 snapshot_infos,
280 cache,
281 }
282 }
283
284 pub fn snapshot(&self, _event: TimeEvent) {
290 let snapshot_infos: Vec<BookSnapshotInfo> =
291 self.snapshot_infos.borrow().values().cloned().collect();
292
293 log::debug!(
294 "BookSnapshotter.snapshot called for {} subscriptions at {}ms",
295 snapshot_infos.len(),
296 self.interval_ms,
297 );
298
299 let books: Vec<(MStr<Topic>, OrderBook)> = {
300 let cache = self.cache.borrow();
301 let mut books = Vec::new();
302
303 for snap_info in &snapshot_infos {
304 self.collect_snapshot(snap_info, &cache, &mut books);
305 }
306
307 books
308 };
309
310 for (topic, book) in books {
311 msgbus::publish_book(topic, &book);
312 }
313 }
314
315 fn collect_snapshot(
316 &self,
317 snap_info: &BookSnapshotInfo,
318 cache: &Cache,
319 books: &mut Vec<(MStr<Topic>, OrderBook)>,
320 ) {
321 if let Some((root, class)) = snap_info.parent {
322 let topic = snap_info.topic;
323 for instrument in cache.instruments_by_parent(&snap_info.venue, &root, class) {
324 self.collect_order_book(&instrument.id(), topic, cache, books);
325 }
326 } else {
327 self.collect_order_book(&snap_info.instrument_id, snap_info.topic, cache, books);
328 }
329 }
330
331 fn collect_order_book(
332 &self,
333 instrument_id: &InstrumentId,
334 topic: MStr<Topic>,
335 cache: &Cache,
336 books: &mut Vec<(MStr<Topic>, OrderBook)>,
337 ) {
338 let book = match cache.try_order_book(instrument_id) {
339 Ok(book) => book,
340 Err(e) => {
341 log::error!("Cannot publish OrderBook snapshot: {e}");
342 return;
343 }
344 };
345
346 if book.update_count == 0 {
347 log::debug!("OrderBook not yet updated for snapshot: {instrument_id}");
348 return;
349 }
350 log::debug!(
351 "Publishing OrderBook snapshot for {instrument_id} (update_count={})",
352 book.update_count
353 );
354
355 books.push((topic, book.clone()));
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use nautilus_common::msgbus::TypedHandler;
362 use nautilus_core::{UUID4, UnixNanos};
363 use nautilus_model::{
364 data::BookOrder,
365 enums::{BookType, OrderSide},
366 types::{Price, Quantity},
367 };
368 use rstest::rstest;
369
370 use super::*;
371
372 #[rstest]
373 fn snapshot_skips_missing_order_book() {
374 let instrument_id = InstrumentId::from("AUD/USD.SIM");
375 let interval_ms = NonZeroUsize::new(100).unwrap();
376 let topic = switchboard::get_book_snapshots_topic(instrument_id, interval_ms);
377 let snapshot_infos = Rc::new(RefCell::new(IndexMap::new()));
378
379 snapshot_infos.borrow_mut().insert(
380 instrument_id,
381 BookSnapshotInfo {
382 instrument_id,
383 venue: Venue::new("SIM"),
384 parent: None,
385 topic,
386 interval_ms,
387 },
388 );
389
390 let snapshotter = BookSnapshotter::new(
391 interval_ms,
392 snapshot_infos,
393 Rc::new(RefCell::new(Cache::default())),
394 );
395 let event = TimeEvent::new(
396 Ustr::from("TEST"),
397 UUID4::new(),
398 UnixNanos::default(),
399 UnixNanos::default(),
400 );
401
402 snapshotter.snapshot(event);
403 }
404
405 #[rstest]
406 fn snapshot_allows_subscriber_to_mutably_borrow_cache() {
407 let instrument_id = InstrumentId::from("AUD/USD.SIM");
408 let interval_ms = NonZeroUsize::new(100).unwrap();
409 let topic = switchboard::get_book_snapshots_topic(instrument_id, interval_ms);
410 let snapshot_infos = Rc::new(RefCell::new(IndexMap::new()));
411
412 snapshot_infos.borrow_mut().insert(
413 instrument_id,
414 BookSnapshotInfo {
415 instrument_id,
416 venue: Venue::new("SIM"),
417 parent: None,
418 topic,
419 interval_ms,
420 },
421 );
422
423 let cache = Rc::new(RefCell::new(Cache::default()));
424 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
425 book.add(
426 BookOrder::new(OrderSide::Buy, Price::from("100.00"), Quantity::from(10), 0),
427 0,
428 1,
429 UnixNanos::default(),
430 );
431 cache.borrow_mut().add_order_book(book).unwrap();
432
433 let received = Rc::new(RefCell::new(Vec::new()));
434 let handler = CacheWritingBookHandler {
435 id: Ustr::from("CacheWritingBookHandler"),
436 cache: cache.clone(),
437 received: received.clone(),
438 };
439 msgbus::subscribe_book_snapshots(topic.into(), TypedHandler::new(handler), None);
440
441 let snapshotter = BookSnapshotter::new(interval_ms, snapshot_infos, cache);
442 let event = TimeEvent::new(
443 Ustr::from("TEST"),
444 UUID4::new(),
445 UnixNanos::default(),
446 UnixNanos::default(),
447 );
448
449 snapshotter.snapshot(event);
450
451 let received = received.borrow();
452 assert_eq!(received.len(), 1);
453 assert_eq!(received[0].instrument_id, instrument_id);
454 assert_eq!(received[0].best_bid_price(), Some(Price::from("100.00")));
455 }
456
457 #[rstest]
458 fn snapshot_skips_book_with_no_updates() {
459 let instrument_id = InstrumentId::from("AUD/USD.SIM");
460 let interval_ms = NonZeroUsize::new(100).unwrap();
461 let topic = switchboard::get_book_snapshots_topic(instrument_id, interval_ms);
462 let snapshot_infos = Rc::new(RefCell::new(IndexMap::new()));
463
464 snapshot_infos.borrow_mut().insert(
465 instrument_id,
466 BookSnapshotInfo {
467 instrument_id,
468 venue: Venue::new("SIM"),
469 parent: None,
470 topic,
471 interval_ms,
472 },
473 );
474
475 let cache = Rc::new(RefCell::new(Cache::default()));
476 cache
477 .borrow_mut()
478 .add_order_book(OrderBook::new(instrument_id, BookType::L2_MBP))
479 .unwrap();
480
481 let received = Rc::new(RefCell::new(Vec::new()));
482 let handler = CacheWritingBookHandler {
483 id: Ustr::from("CacheWritingBookHandler-NoUpdates"),
484 cache: cache.clone(),
485 received: received.clone(),
486 };
487 msgbus::subscribe_book_snapshots(topic.into(), TypedHandler::new(handler), None);
488
489 let snapshotter = BookSnapshotter::new(interval_ms, snapshot_infos, cache);
490 let event = TimeEvent::new(
491 Ustr::from("TEST"),
492 UUID4::new(),
493 UnixNanos::default(),
494 UnixNanos::default(),
495 );
496
497 snapshotter.snapshot(event);
498
499 assert!(received.borrow().is_empty());
500 }
501
502 struct CacheWritingBookHandler {
503 id: Ustr,
504 cache: Rc<RefCell<Cache>>,
505 received: Rc<RefCell<Vec<OrderBook>>>,
506 }
507
508 impl Handler<OrderBook> for CacheWritingBookHandler {
509 fn id(&self) -> Ustr {
510 self.id
511 }
512
513 fn handle(&self, book: &OrderBook) {
514 let mut cache = self.cache.borrow_mut();
516 let _ = cache.order_book_mut(&book.instrument_id);
517 self.received.borrow_mut().push(book.clone());
518 }
519 }
520}