1use std::{cell::RefCell, num::NonZeroUsize, rc::Rc};
17
18use indexmap::IndexMap;
19use nautilus_common::{
20 cache::Cache,
21 msgbus::{self, Handler, MStr, Topic, switchboard},
22 timer::TimeEvent,
23};
24use nautilus_model::{
25 data::{OrderBookDeltas, OrderBookDepth10, QuoteTick},
26 enums::InstrumentClass,
27 identifiers::{ClientId, InstrumentId, Venue},
28 instruments::Instrument,
29 orderbook::OrderBook,
30};
31use ustr::Ustr;
32
33#[derive(Clone, Debug)]
35pub struct BookSnapshotInfo {
36 pub instrument_id: InstrumentId,
37 pub venue: Venue,
38 pub parent: Option<(Ustr, InstrumentClass)>,
41 pub topic: MStr<Topic>,
42 pub interval_ms: NonZeroUsize,
43}
44
45pub(crate) type BookSnapshotInfos = Rc<RefCell<IndexMap<InstrumentId, BookSnapshotInfo>>>;
50
51pub(crate) type BookSnapshotKey = (InstrumentId, NonZeroUsize);
53
54pub(crate) enum BookSnapshotUnsubscribeResult {
56 NotSubscribed,
58 Decremented,
60 Removed,
62}
63
64pub(crate) type BookDeltasKey = (InstrumentId, Option<ClientId>, Option<Venue>);
66
67pub(crate) enum BookDeltasUnsubscribeResult {
69 NotSubscribed,
71 Decremented,
73 Removed,
75}
76
77#[derive(Debug)]
83pub struct BookUpdater {
84 pub id: Ustr,
85 pub instrument_id: InstrumentId,
86 pub cache: Rc<RefCell<Cache>>,
87 pub emit_quotes_from_book: bool,
88}
89
90impl BookUpdater {
91 pub fn new(
93 instrument_id: &InstrumentId,
94 cache: Rc<RefCell<Cache>>,
95 emit_quotes_from_book: bool,
96 ) -> Self {
97 Self {
98 id: Ustr::from(&format!("{}-{}", stringify!(BookUpdater), instrument_id)),
99 instrument_id: *instrument_id,
100 cache,
101 emit_quotes_from_book,
102 }
103 }
104}
105
106impl Handler<OrderBookDeltas> for BookUpdater {
107 fn id(&self) -> Ustr {
108 self.id
109 }
110
111 fn handle(&self, deltas: &OrderBookDeltas) {
112 let mut emit: Option<QuoteTick> = None;
113 {
114 let mut cache = self.cache.borrow_mut();
115 if let Some(book) = cache.order_book_mut(&deltas.instrument_id) {
116 if let Err(e) = book.apply_deltas(deltas) {
117 log::error!("Failed to apply deltas: {e}");
118 return;
119 }
120
121 if self.emit_quotes_from_book {
122 emit = derive_quote_from_book(book);
123 }
124 }
125 }
126
127 if let Some(quote) = emit {
128 publish_quote_if_changed(&self.cache, quote);
129 }
130 }
131}
132
133impl Handler<OrderBookDepth10> for BookUpdater {
134 fn id(&self) -> Ustr {
135 self.id
136 }
137
138 fn handle(&self, depth: &OrderBookDepth10) {
139 let mut emit: Option<QuoteTick> = None;
140 {
141 let mut cache = self.cache.borrow_mut();
142 if let Some(book) = cache.order_book_mut(&depth.instrument_id) {
143 if let Err(e) = book.apply_depth(depth) {
144 log::error!("Failed to apply depth: {e}");
145 return;
146 }
147
148 if self.emit_quotes_from_book {
149 emit = derive_quote_from_book(book);
150 }
151 }
152 }
153
154 if let Some(quote) = emit {
155 publish_quote_if_changed(&self.cache, quote);
156 }
157 }
158}
159
160fn derive_quote_from_book(book: &OrderBook) -> Option<QuoteTick> {
161 let bid_price = book.best_bid_price()?;
162 let ask_price = book.best_ask_price()?;
163 let bid_size = book.best_bid_size()?;
164 let ask_size = book.best_ask_size()?;
165
166 if bid_size.is_zero() || ask_size.is_zero() {
167 return None;
168 }
169
170 Some(QuoteTick::new(
171 book.instrument_id,
172 bid_price,
173 ask_price,
174 bid_size,
175 ask_size,
176 book.ts_last,
177 book.ts_last,
178 ))
179}
180
181pub(crate) fn publish_quote_if_changed(cache: &Rc<RefCell<Cache>>, quote: QuoteTick) {
186 let publish = {
187 let cache_ref = cache.borrow();
188 match cache_ref.quote("e.instrument_id) {
189 None => true,
190 Some(last) => {
191 last.bid_price != quote.bid_price
192 || last.ask_price != quote.ask_price
193 || last.bid_size != quote.bid_size
194 || last.ask_size != quote.ask_size
195 }
196 }
197 };
198
199 if !publish {
200 return;
201 }
202
203 if let Err(e) = cache.borrow_mut().add_quote(quote) {
204 log::error!("Error on cache insert: {e}");
205 }
206
207 let topic = switchboard::get_quotes_topic(quote.instrument_id);
208 msgbus::publish_quote(topic, "e);
209}
210
211#[derive(Debug)]
217pub struct BookSnapshotter {
218 pub timer_name: Ustr,
219 pub interval_ms: NonZeroUsize,
220 pub snapshot_infos: Rc<RefCell<IndexMap<InstrumentId, BookSnapshotInfo>>>,
221 pub cache: Rc<RefCell<Cache>>,
222}
223
224impl BookSnapshotter {
225 pub fn new(
227 interval_ms: NonZeroUsize,
228 snapshot_infos: Rc<RefCell<IndexMap<InstrumentId, BookSnapshotInfo>>>,
229 cache: Rc<RefCell<Cache>>,
230 ) -> Self {
231 let timer_name = format!("OrderBookSnapshots|{interval_ms}");
232
233 Self {
234 timer_name: Ustr::from(&timer_name),
235 interval_ms,
236 snapshot_infos,
237 cache,
238 }
239 }
240
241 pub fn snapshot(&self, _event: TimeEvent) {
247 let snapshot_infos: Vec<BookSnapshotInfo> =
248 self.snapshot_infos.borrow().values().cloned().collect();
249
250 log::debug!(
251 "BookSnapshotter.snapshot called for {} subscriptions at {}ms",
252 snapshot_infos.len(),
253 self.interval_ms,
254 );
255
256 let books: Vec<(MStr<Topic>, OrderBook)> = {
257 let cache = self.cache.borrow();
258 let mut books = Vec::new();
259
260 for snap_info in &snapshot_infos {
261 self.collect_snapshot(snap_info, &cache, &mut books);
262 }
263
264 books
265 };
266
267 for (topic, book) in books {
268 msgbus::publish_book(topic, &book);
269 }
270 }
271
272 fn collect_snapshot(
273 &self,
274 snap_info: &BookSnapshotInfo,
275 cache: &Cache,
276 books: &mut Vec<(MStr<Topic>, OrderBook)>,
277 ) {
278 if let Some((root, class)) = snap_info.parent {
279 let topic = snap_info.topic;
280 for instrument in cache.instruments_by_parent(&snap_info.venue, &root, class) {
281 self.collect_order_book(&instrument.id(), topic, cache, books);
282 }
283 } else {
284 self.collect_order_book(&snap_info.instrument_id, snap_info.topic, cache, books);
285 }
286 }
287
288 fn collect_order_book(
289 &self,
290 instrument_id: &InstrumentId,
291 topic: MStr<Topic>,
292 cache: &Cache,
293 books: &mut Vec<(MStr<Topic>, OrderBook)>,
294 ) {
295 let book = match cache.try_order_book(instrument_id) {
296 Ok(book) => book,
297 Err(e) => {
298 log::error!("Cannot publish OrderBook snapshot: {e}");
299 return;
300 }
301 };
302
303 if book.update_count == 0 {
304 log::debug!("OrderBook not yet updated for snapshot: {instrument_id}");
305 return;
306 }
307 log::debug!(
308 "Publishing OrderBook snapshot for {instrument_id} (update_count={})",
309 book.update_count
310 );
311
312 books.push((topic, book.clone()));
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use nautilus_common::msgbus::TypedHandler;
319 use nautilus_core::{UUID4, UnixNanos};
320 use nautilus_model::{
321 data::BookOrder,
322 enums::{BookType, OrderSide},
323 types::{Price, Quantity},
324 };
325 use rstest::rstest;
326
327 use super::*;
328
329 #[rstest]
330 fn snapshot_skips_missing_order_book() {
331 let instrument_id = InstrumentId::from("AUD/USD.SIM");
332 let interval_ms = NonZeroUsize::new(100).unwrap();
333 let topic = switchboard::get_book_snapshots_topic(instrument_id, interval_ms);
334 let snapshot_infos = Rc::new(RefCell::new(IndexMap::new()));
335
336 snapshot_infos.borrow_mut().insert(
337 instrument_id,
338 BookSnapshotInfo {
339 instrument_id,
340 venue: Venue::new("SIM"),
341 parent: None,
342 topic,
343 interval_ms,
344 },
345 );
346
347 let snapshotter = BookSnapshotter::new(
348 interval_ms,
349 snapshot_infos,
350 Rc::new(RefCell::new(Cache::default())),
351 );
352 let event = TimeEvent::new(
353 Ustr::from("TEST"),
354 UUID4::new(),
355 UnixNanos::default(),
356 UnixNanos::default(),
357 );
358
359 snapshotter.snapshot(event);
360 }
361
362 #[rstest]
363 fn snapshot_allows_subscriber_to_mutably_borrow_cache() {
364 let instrument_id = InstrumentId::from("AUD/USD.SIM");
365 let interval_ms = NonZeroUsize::new(100).unwrap();
366 let topic = switchboard::get_book_snapshots_topic(instrument_id, interval_ms);
367 let snapshot_infos = Rc::new(RefCell::new(IndexMap::new()));
368
369 snapshot_infos.borrow_mut().insert(
370 instrument_id,
371 BookSnapshotInfo {
372 instrument_id,
373 venue: Venue::new("SIM"),
374 parent: None,
375 topic,
376 interval_ms,
377 },
378 );
379
380 let cache = Rc::new(RefCell::new(Cache::default()));
381 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
382 book.add(
383 BookOrder::new(OrderSide::Buy, Price::from("100.00"), Quantity::from(10), 0),
384 0,
385 1,
386 UnixNanos::default(),
387 );
388 cache.borrow_mut().add_order_book(book).unwrap();
389
390 let received = Rc::new(RefCell::new(Vec::new()));
391 let handler = CacheWritingBookHandler {
392 id: Ustr::from("CacheWritingBookHandler"),
393 cache: cache.clone(),
394 received: received.clone(),
395 };
396 msgbus::subscribe_book_snapshots(topic.into(), TypedHandler::new(handler), None);
397
398 let snapshotter = BookSnapshotter::new(interval_ms, snapshot_infos, cache);
399 let event = TimeEvent::new(
400 Ustr::from("TEST"),
401 UUID4::new(),
402 UnixNanos::default(),
403 UnixNanos::default(),
404 );
405
406 snapshotter.snapshot(event);
407
408 let received = received.borrow();
409 assert_eq!(received.len(), 1);
410 assert_eq!(received[0].instrument_id, instrument_id);
411 assert_eq!(received[0].best_bid_price(), Some(Price::from("100.00")));
412 }
413
414 #[rstest]
415 fn snapshot_skips_book_with_no_updates() {
416 let instrument_id = InstrumentId::from("AUD/USD.SIM");
417 let interval_ms = NonZeroUsize::new(100).unwrap();
418 let topic = switchboard::get_book_snapshots_topic(instrument_id, interval_ms);
419 let snapshot_infos = Rc::new(RefCell::new(IndexMap::new()));
420
421 snapshot_infos.borrow_mut().insert(
422 instrument_id,
423 BookSnapshotInfo {
424 instrument_id,
425 venue: Venue::new("SIM"),
426 parent: None,
427 topic,
428 interval_ms,
429 },
430 );
431
432 let cache = Rc::new(RefCell::new(Cache::default()));
433 cache
434 .borrow_mut()
435 .add_order_book(OrderBook::new(instrument_id, BookType::L2_MBP))
436 .unwrap();
437
438 let received = Rc::new(RefCell::new(Vec::new()));
439 let handler = CacheWritingBookHandler {
440 id: Ustr::from("CacheWritingBookHandler-NoUpdates"),
441 cache: cache.clone(),
442 received: received.clone(),
443 };
444 msgbus::subscribe_book_snapshots(topic.into(), TypedHandler::new(handler), None);
445
446 let snapshotter = BookSnapshotter::new(interval_ms, snapshot_infos, cache);
447 let event = TimeEvent::new(
448 Ustr::from("TEST"),
449 UUID4::new(),
450 UnixNanos::default(),
451 UnixNanos::default(),
452 );
453
454 snapshotter.snapshot(event);
455
456 assert!(received.borrow().is_empty());
457 }
458
459 struct CacheWritingBookHandler {
460 id: Ustr,
461 cache: Rc<RefCell<Cache>>,
462 received: Rc<RefCell<Vec<OrderBook>>>,
463 }
464
465 impl Handler<OrderBook> for CacheWritingBookHandler {
466 fn id(&self) -> Ustr {
467 self.id
468 }
469
470 fn handle(&self, book: &OrderBook) {
471 let mut cache = self.cache.borrow_mut();
473 let _ = cache.order_book_mut(&book.instrument_id);
474 self.received.borrow_mut().push(book.clone());
475 }
476 }
477}