Skip to main content

nautilus_data/engine/
dispatch.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16#[cfg(feature = "defi")]
17use nautilus_common::defi::cache::IntegrityError;
18
19use super::{
20    Bar, Cache, CustomData, DataEngine, Display, FundingRateUpdate, IndexPriceUpdate, Instrument,
21    InstrumentAny, InstrumentClose, InstrumentStatus, MarkPriceUpdate, MarketStatusAction,
22    OptionGreeks, OrderBookDelta, OrderBookDeltas, OrderBookDepth, QuoteTick, Rc, RecordFlag,
23    RefCell, TradeTick, book, mem, msgbus, switchboard,
24};
25
26impl DataEngine {
27    #[inline]
28    fn pipeline_cache_writes_allowed(&self) -> bool {
29        !self.config.disable_historical_cache
30    }
31
32    pub(crate) fn handle_instrument(&mut self, instrument: &InstrumentAny) {
33        if self.cache_instrument(instrument) {
34            self.publish_instrument(instrument);
35        }
36    }
37
38    pub(crate) fn cache_instrument(&self, instrument: &InstrumentAny) -> bool {
39        log::debug!("Handling instrument: {}", instrument.id());
40
41        if let Err(e) = self
42            .cache
43            .as_ref()
44            .borrow_mut()
45            .add_instrument(instrument.clone())
46        {
47            log_error_on_cache_insert(&e);
48
49            #[cfg(feature = "defi")]
50            if e.is::<IntegrityError>()
51                || nautilus_model::defi::currency::instrument_codes(instrument)
52                    .map_or(true, |codes| !codes.is_empty())
53            {
54                return false;
55            }
56        }
57
58        true
59    }
60
61    pub(crate) fn publish_instrument(&mut self, instrument: &InstrumentAny) {
62        let topic = switchboard::get_instrument_topic(instrument.id());
63        log::debug!("Publishing instrument to topic: {topic}");
64        msgbus::publish_instrument(topic, instrument);
65
66        self.update_option_chains(instrument);
67    }
68
69    pub(super) fn handle_delta(&mut self, delta: OrderBookDelta) {
70        let mut deltas = if self.config.buffer_deltas {
71            self.buffer_delta(delta);
72
73            if !RecordFlag::F_LAST.matches(delta.flags) {
74                return; // Not the last delta for event
75            }
76
77            self.buffered_deltas_map
78                .remove(&delta.instrument_id)
79                .expect("buffered deltas exist")
80        } else {
81            self.single_delta_batch(delta)
82        };
83
84        let topic = switchboard::get_book_deltas_topic(deltas.instrument_id);
85        msgbus::publish_deltas(topic, &deltas);
86        self.reclaim_deltas_frame(mem::take(&mut deltas.deltas));
87    }
88
89    pub(super) fn handle_deltas(&mut self, deltas: &OrderBookDeltas) {
90        if self.config.buffer_deltas {
91            let instrument_id = deltas.instrument_id;
92
93            for delta in &deltas.deltas {
94                let is_last = RecordFlag::F_LAST.matches(delta.flags);
95                self.buffer_delta(*delta);
96
97                if is_last {
98                    let mut deltas_to_publish = self
99                        .buffered_deltas_map
100                        .remove(&instrument_id)
101                        .expect("buffered deltas exist");
102                    let topic = switchboard::get_book_deltas_topic(instrument_id);
103                    msgbus::publish_deltas(topic, &deltas_to_publish);
104                    self.reclaim_deltas_frame(mem::take(&mut deltas_to_publish.deltas));
105                }
106            }
107        } else {
108            let topic = switchboard::get_book_deltas_topic(deltas.instrument_id);
109            msgbus::publish_deltas(topic, deltas);
110        }
111    }
112
113    pub(super) fn handle_depth(&self, depth: &OrderBookDepth) {
114        let topic = switchboard::get_book_depth_topic(depth.instrument_id);
115        msgbus::publish_depth(topic, depth);
116
117        if self.config.emit_quotes_from_book_depths
118            && let Some(quote) = derive_quote_from_depth(depth)
119        {
120            book::publish_quote_if_changed(&self.cache, quote);
121        }
122    }
123
124    pub(super) fn handle_quote(&self, quote: QuoteTick) {
125        if let Err(e) = self.cache.as_ref().borrow_mut().add_quote(quote) {
126            log_error_on_cache_insert(&e);
127        }
128
129        for synthetic_quote in self.synthetic_quotes_from_quote(quote) {
130            let topic = switchboard::get_quotes_topic(synthetic_quote.instrument_id);
131            msgbus::publish_quote(topic, &synthetic_quote);
132        }
133
134        let topic = switchboard::get_quotes_topic(quote.instrument_id);
135        msgbus::publish_quote(topic, &quote);
136    }
137
138    pub(super) fn handle_trade(&self, trade: TradeTick) {
139        if let Err(e) = self.cache.as_ref().borrow_mut().add_trade(trade) {
140            log_error_on_cache_insert(&e);
141        }
142
143        for synthetic_trade in self.synthetic_trades_from_trade(trade) {
144            let topic = switchboard::get_trades_topic(synthetic_trade.instrument_id);
145            msgbus::publish_trade(topic, &synthetic_trade);
146        }
147
148        let topic = switchboard::get_trades_topic(trade.instrument_id);
149        msgbus::publish_trade(topic, &trade);
150    }
151
152    pub(super) fn handle_bar(&self, bar: Bar) {
153        process_engine_bar(&self.cache, self.config.validate_data_sequence, true, bar);
154    }
155
156    pub(super) fn handle_mark_price(&self, mark_price: MarkPriceUpdate) {
157        if let Err(e) = self.cache.as_ref().borrow_mut().add_mark_price(mark_price) {
158            log_error_on_cache_insert(&e);
159        }
160
161        let topic = switchboard::get_mark_price_topic(mark_price.instrument_id);
162        msgbus::publish_mark_price(topic, &mark_price);
163    }
164
165    pub(super) fn handle_index_price(&self, index_price: IndexPriceUpdate) {
166        if let Err(e) = self
167            .cache
168            .as_ref()
169            .borrow_mut()
170            .add_index_price(index_price)
171        {
172            log_error_on_cache_insert(&e);
173        }
174
175        let topic = switchboard::get_index_price_topic(index_price.instrument_id);
176        msgbus::publish_index_price(topic, &index_price);
177    }
178
179    /// Handles a funding rate update by adding it to the cache and publishing to the message bus.
180    pub fn handle_funding_rate(&mut self, funding_rate: FundingRateUpdate) {
181        if let Err(e) = self
182            .cache
183            .as_ref()
184            .borrow_mut()
185            .add_funding_rate(funding_rate)
186        {
187            log_error_on_cache_insert(&e);
188        }
189
190        let topic = switchboard::get_funding_rate_topic(funding_rate.instrument_id);
191        msgbus::publish_funding_rate(topic, &funding_rate);
192    }
193
194    pub(super) fn handle_instrument_status(&mut self, status: InstrumentStatus) {
195        if let Err(e) = self
196            .cache
197            .as_ref()
198            .borrow_mut()
199            .add_instrument_status(status)
200        {
201            log_error_on_cache_insert(&e);
202        }
203
204        let topic = switchboard::get_instrument_status_topic(status.instrument_id);
205        msgbus::publish_any(topic, &status);
206
207        if self
208            .option_chain_instrument_index
209            .contains_key(&status.instrument_id)
210            && matches!(
211                status.action,
212                MarketStatusAction::Close | MarketStatusAction::NotAvailableForTrading
213            )
214        {
215            self.expire_option_chain_instrument(status.instrument_id);
216        }
217    }
218
219    pub(super) fn handle_instrument_close(&self, close: InstrumentClose) {
220        let topic = switchboard::get_instrument_close_topic(close.instrument_id);
221        msgbus::publish_any(topic, &close);
222    }
223
224    pub(super) fn handle_custom_data(&self, custom: &CustomData) {
225        log::debug!("Processing custom data: {}", custom.data.type_name());
226        let topic = switchboard::get_custom_topic(&custom.data_type);
227        msgbus::publish_any(topic, custom);
228    }
229
230    pub(super) fn handle_delta_pipeline(&mut self, delta: OrderBookDelta) {
231        // Pipeline deltas are not buffered; replays arrive pre-batched
232        let mut deltas = self.single_delta_batch(delta);
233        let topic = switchboard::get_pipeline_book_deltas_topic(deltas.instrument_id);
234        msgbus::publish_deltas(topic, &deltas);
235        self.reclaim_deltas_frame(mem::take(&mut deltas.deltas));
236    }
237
238    fn buffer_delta(&mut self, delta: OrderBookDelta) {
239        if let Some(buffered_deltas) = self.buffered_deltas_map.get_mut(&delta.instrument_id) {
240            buffered_deltas.deltas.push(delta);
241            buffered_deltas.flags = delta.flags;
242            buffered_deltas.sequence = delta.sequence;
243            buffered_deltas.ts_event = delta.ts_event;
244            buffered_deltas.ts_init = delta.ts_init;
245            return;
246        }
247
248        let instrument_id = delta.instrument_id;
249        let buffered_deltas = self.single_delta_batch(delta);
250        self.buffered_deltas_map
251            .insert(instrument_id, buffered_deltas);
252    }
253
254    fn single_delta_batch(&mut self, delta: OrderBookDelta) -> OrderBookDeltas {
255        let instrument_id = delta.instrument_id;
256        let mut frame = mem::take(&mut self.deltas_frame);
257        frame.clear();
258        frame.push(delta);
259        OrderBookDeltas::new(instrument_id, frame)
260    }
261
262    fn reclaim_deltas_frame(&mut self, mut frame: Vec<OrderBookDelta>) {
263        frame.clear();
264        self.deltas_frame = frame;
265    }
266
267    pub(super) fn handle_deltas_pipeline(&self, deltas: &OrderBookDeltas) {
268        let topic = switchboard::get_pipeline_book_deltas_topic(deltas.instrument_id);
269        msgbus::publish_deltas(topic, deltas);
270    }
271
272    pub(super) fn handle_depth_pipeline(&self, depth: &OrderBookDepth) {
273        let topic = switchboard::get_pipeline_book_depth_topic(depth.instrument_id);
274        msgbus::publish_depth(topic, depth);
275    }
276
277    pub(super) fn handle_quote_pipeline(&self, quote: QuoteTick) {
278        if self.pipeline_cache_writes_allowed()
279            && let Err(e) = self.cache.as_ref().borrow_mut().add_quote(quote)
280        {
281            log_error_on_cache_insert(&e);
282        }
283
284        let topic = switchboard::get_pipeline_quotes_topic(quote.instrument_id);
285        msgbus::publish_quote(topic, &quote);
286    }
287
288    pub(super) fn handle_trade_pipeline(&self, trade: TradeTick) {
289        if self.pipeline_cache_writes_allowed()
290            && let Err(e) = self.cache.as_ref().borrow_mut().add_trade(trade)
291        {
292            log_error_on_cache_insert(&e);
293        }
294
295        let topic = switchboard::get_pipeline_trades_topic(trade.instrument_id);
296        msgbus::publish_trade(topic, &trade);
297    }
298
299    pub(super) fn handle_bar_pipeline(&self, bar: Bar) {
300        if !validate_bar_sequence(&self.cache, self.config.validate_data_sequence, &bar) {
301            return;
302        }
303
304        if self.pipeline_cache_writes_allowed()
305            && let Err(e) = self.cache.as_ref().borrow_mut().add_bar(bar)
306        {
307            log_error_on_cache_insert(&e);
308        }
309
310        let topic = switchboard::get_pipeline_bars_topic(bar.bar_type);
311        msgbus::publish_bar(topic, &bar);
312    }
313
314    pub(super) fn handle_mark_price_pipeline(&self, mark_price: MarkPriceUpdate) {
315        if self.pipeline_cache_writes_allowed()
316            && let Err(e) = self.cache.as_ref().borrow_mut().add_mark_price(mark_price)
317        {
318            log_error_on_cache_insert(&e);
319        }
320
321        let topic = switchboard::get_pipeline_mark_price_topic(mark_price.instrument_id);
322        msgbus::publish_mark_price(topic, &mark_price);
323    }
324
325    pub(super) fn handle_index_price_pipeline(&self, index_price: IndexPriceUpdate) {
326        if self.pipeline_cache_writes_allowed()
327            && let Err(e) = self
328                .cache
329                .as_ref()
330                .borrow_mut()
331                .add_index_price(index_price)
332        {
333            log_error_on_cache_insert(&e);
334        }
335
336        let topic = switchboard::get_pipeline_index_price_topic(index_price.instrument_id);
337        msgbus::publish_index_price(topic, &index_price);
338    }
339
340    pub(super) fn handle_funding_rate_pipeline(&self, funding_rate: FundingRateUpdate) {
341        if self.pipeline_cache_writes_allowed()
342            && let Err(e) = self
343                .cache
344                .as_ref()
345                .borrow_mut()
346                .add_funding_rate(funding_rate)
347        {
348            log_error_on_cache_insert(&e);
349        }
350
351        let topic = switchboard::get_pipeline_funding_rate_topic(funding_rate.instrument_id);
352        msgbus::publish_funding_rate(topic, &funding_rate);
353    }
354
355    pub(super) fn handle_instrument_status_pipeline(&self, status: InstrumentStatus) {
356        if self.pipeline_cache_writes_allowed()
357            && let Err(e) = self
358                .cache
359                .as_ref()
360                .borrow_mut()
361                .add_instrument_status(status)
362        {
363            log_error_on_cache_insert(&e);
364        }
365
366        let topic = switchboard::get_pipeline_instrument_status_topic(status.instrument_id);
367        msgbus::publish_any(topic, &status);
368    }
369
370    pub(super) fn handle_option_greeks_pipeline(&self, greeks: OptionGreeks) {
371        if self.pipeline_cache_writes_allowed() {
372            self.cache.borrow_mut().add_option_greeks(greeks);
373        }
374
375        let topic = switchboard::get_pipeline_option_greeks_topic(greeks.instrument_id);
376        msgbus::publish_option_greeks(topic, &greeks);
377    }
378
379    pub(super) fn handle_instrument_close_pipeline(&self, close: InstrumentClose) {
380        let topic = switchboard::get_pipeline_instrument_close_topic(close.instrument_id);
381        msgbus::publish_any(topic, &close);
382    }
383
384    pub(super) fn handle_custom_data_pipeline(&self, custom: &CustomData) {
385        log::debug!("Pipeline custom data: {}", custom.data.type_name());
386        let topic = switchboard::get_pipeline_custom_topic(&custom.data_type);
387        msgbus::publish_any(topic, custom);
388    }
389
390    pub(super) fn handle_quotes(&self, quotes: &[QuoteTick]) {
391        if let Err(e) = self.cache.as_ref().borrow_mut().add_quotes(quotes) {
392            log_error_on_cache_insert(&e);
393        }
394    }
395
396    pub(super) fn handle_trades(&self, trades: &[TradeTick]) {
397        if let Err(e) = self.cache.as_ref().borrow_mut().add_trades(trades) {
398            log_error_on_cache_insert(&e);
399        }
400    }
401
402    pub(super) fn handle_funding_rates(&self, funding_rates: &[FundingRateUpdate]) {
403        if let Err(e) = self
404            .cache
405            .as_ref()
406            .borrow_mut()
407            .add_funding_rates(funding_rates)
408        {
409            log_error_on_cache_insert(&e);
410        }
411    }
412
413    pub(super) fn handle_bars(&self, bars: &[Bar]) {
414        if let Err(e) = self.cache.as_ref().borrow_mut().add_bars(bars) {
415            log_error_on_cache_insert(&e);
416        }
417    }
418}
419
420#[inline(always)]
421pub(super) fn log_error_on_cache_insert<T: Display>(e: &T) {
422    log::error!("Error on cache insert: {e}");
423}
424
425// Top-of-book `QuoteTick` from an `OrderBookDepth`. Returns `None` for
426// missing-side padding or zero size.
427fn derive_quote_from_depth(depth: &OrderBookDepth) -> Option<QuoteTick> {
428    let bid = depth.bids.first()?;
429    let ask = depth.asks.first()?;
430
431    if bid.side.is_none() || ask.side.is_none() || bid.size.is_zero() || ask.size.is_zero() {
432        return None;
433    }
434
435    Some(QuoteTick::new(
436        depth.instrument_id,
437        bid.price,
438        ask.price,
439        bid.size,
440        ask.size,
441        depth.ts_event,
442        depth.ts_init,
443    ))
444}
445
446// Validates a bar against `last_bar` before writing and (optionally) publishing.
447// Live bars and aggregator emissions honor `validate_data_sequence`;
448// request-generated bars use `Cache::add_bar_historical`.
449pub(super) fn process_engine_bar(
450    cache: &Rc<RefCell<Cache>>,
451    validate_sequence: bool,
452    publish: bool,
453    bar: Bar,
454) {
455    debug_assert!(
456        bar.bar_type.is_standard(),
457        "bars must be published and cached under the standard bar type"
458    );
459
460    if !validate_bar_sequence(cache, validate_sequence, &bar) {
461        return;
462    }
463
464    if let Err(e) = cache.as_ref().borrow_mut().add_bar(bar) {
465        log_error_on_cache_insert(&e);
466    }
467
468    if publish {
469        let topic = switchboard::get_bars_topic(bar.bar_type);
470        msgbus::publish_bar(topic, &bar);
471    }
472}
473
474fn validate_bar_sequence(cache: &Rc<RefCell<Cache>>, validate_sequence: bool, bar: &Bar) -> bool {
475    if !validate_sequence {
476        return true;
477    }
478
479    let Some(last_bar) = cache.as_ref().borrow().bar(&bar.bar_type).copied() else {
480        return true;
481    };
482
483    if bar.ts_event < last_bar.ts_event {
484        log::warn!(
485            "Bar {bar} was prior to last bar `ts_event` {}",
486            last_bar.ts_event,
487        );
488        return false;
489    }
490
491    if bar.ts_init < last_bar.ts_init {
492        log::warn!(
493            "Bar {bar} was prior to last bar `ts_init` {}",
494            last_bar.ts_init,
495        );
496        return false;
497    }
498
499    true
500}