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