Skip to main content

nautilus_common/actor/
indicators.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 = "indicators")]
17use std::cell::RefCell;
18use std::{any::Any, fmt::Debug, rc::Rc};
19
20use ahash::AHashMap;
21#[cfg(feature = "indicators")]
22use nautilus_indicators::indicator::Indicator;
23use nautilus_model::{
24    data::{Bar, BarSpecification, BarType, QuoteTick, TradeTick},
25    identifiers::InstrumentId,
26};
27
28/// Shared indicator handle used by actor and strategy registration.
29pub type SharedActorIndicator = Rc<dyn ActorIndicator>;
30
31/// Indicator callback interface used by the actor core.
32pub trait ActorIndicator: Any {
33    /// Returns a stable key used to de-duplicate indicator registrations.
34    fn key(&self) -> usize;
35
36    /// Returns this indicator as [`Any`] for adapter-specific downcasting.
37    fn as_any(&self) -> &dyn Any;
38
39    /// Checks if the indicator is initialized.
40    ///
41    /// # Errors
42    ///
43    /// Returns an error if the indicator cannot report readiness.
44    fn initialized(&self) -> anyhow::Result<bool>;
45
46    /// Handles a quote tick.
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if the indicator cannot handle the quote tick.
51    fn handle_quote(&self, quote: &QuoteTick) -> anyhow::Result<()>;
52
53    /// Handles a trade tick.
54    ///
55    /// # Errors
56    ///
57    /// Returns an error if the indicator cannot handle the trade tick.
58    fn handle_trade(&self, trade: &TradeTick) -> anyhow::Result<()>;
59
60    /// Handles a bar.
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if the indicator cannot handle the bar.
65    fn handle_bar(&self, bar: &Bar) -> anyhow::Result<()>;
66}
67
68#[cfg(feature = "indicators")]
69impl<T> ActorIndicator for RefCell<T>
70where
71    T: Indicator + 'static,
72{
73    fn key(&self) -> usize {
74        std::ptr::from_ref(self).cast::<()>() as usize
75    }
76
77    fn as_any(&self) -> &dyn Any {
78        self
79    }
80
81    fn initialized(&self) -> anyhow::Result<bool> {
82        Ok(self.borrow().initialized())
83    }
84
85    fn handle_quote(&self, quote: &QuoteTick) -> anyhow::Result<()> {
86        self.borrow_mut().handle_quote(quote);
87        Ok(())
88    }
89
90    fn handle_trade(&self, trade: &TradeTick) -> anyhow::Result<()> {
91        self.borrow_mut().handle_trade(trade);
92        Ok(())
93    }
94
95    fn handle_bar(&self, bar: &Bar) -> anyhow::Result<()> {
96        self.borrow_mut().handle_bar(bar);
97        Ok(())
98    }
99}
100
101/// Registry for actor and strategy indicator callbacks.
102#[derive(Clone, Default)]
103#[allow(
104    clippy::struct_field_names,
105    reason = "indicator-prefixed fields denote distinct indicator collections"
106)]
107pub struct Indicators {
108    indicators: Vec<SharedActorIndicator>,
109    indicators_for_quotes: AHashMap<InstrumentId, Vec<SharedActorIndicator>>,
110    indicators_for_trades: AHashMap<InstrumentId, Vec<SharedActorIndicator>>,
111    indicators_for_bars: AHashMap<(InstrumentId, BarSpecification), Vec<SharedActorIndicator>>,
112}
113
114impl Debug for Indicators {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.debug_struct(stringify!(Indicators))
117            .field("indicators", &self.indicators.len())
118            .field("indicators_for_quotes", &self.indicators_for_quotes.len())
119            .field("indicators_for_trades", &self.indicators_for_trades.len())
120            .field("indicators_for_bars", &self.indicators_for_bars.len())
121            .finish()
122    }
123}
124
125impl Indicators {
126    /// Returns the registered indicators.
127    #[must_use]
128    pub fn registered_indicators(&self) -> Vec<SharedActorIndicator> {
129        self.indicators.clone()
130    }
131
132    /// Returns whether all registered indicators are initialized.
133    ///
134    /// # Errors
135    ///
136    /// Returns an error if a registered indicator cannot report readiness.
137    pub fn initialized(&self) -> anyhow::Result<bool> {
138        if self.indicators.is_empty() {
139            return Ok(false);
140        }
141
142        for indicator in &self.indicators {
143            if !indicator.initialized()? {
144                return Ok(false);
145            }
146        }
147
148        Ok(true)
149    }
150
151    /// Registers an indicator to receive quote ticks for an instrument.
152    pub fn register_indicator_for_quote_ticks(
153        &mut self,
154        instrument_id: InstrumentId,
155        indicator: SharedActorIndicator,
156    ) {
157        self.register_indicator(indicator.clone());
158        self.register_by_key(instrument_id, indicator, IndicatorKind::Quote);
159    }
160
161    /// Registers an indicator to receive trade ticks for an instrument.
162    pub fn register_indicator_for_trade_ticks(
163        &mut self,
164        instrument_id: InstrumentId,
165        indicator: SharedActorIndicator,
166    ) {
167        self.register_indicator(indicator.clone());
168        self.register_by_key(instrument_id, indicator, IndicatorKind::Trade);
169    }
170
171    /// Registers an indicator to receive bars for a bar type.
172    pub fn register_indicator_for_bars(
173        &mut self,
174        bar_type: BarType,
175        indicator: SharedActorIndicator,
176    ) {
177        self.register_indicator(indicator.clone());
178        self.register_bar(bar_type.id_spec_key(), indicator);
179    }
180
181    /// Handles a quote tick with registered indicators.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if a registered indicator cannot handle the quote tick.
186    pub fn handle_quote(&self, quote: &QuoteTick) -> anyhow::Result<()> {
187        if let Some(indicators) = self.indicators_for_quotes.get(&quote.instrument_id) {
188            for indicator in indicators {
189                indicator.handle_quote(quote)?;
190            }
191        }
192
193        Ok(())
194    }
195
196    /// Handles quote ticks with registered indicators.
197    ///
198    /// # Errors
199    ///
200    /// Returns an error if a registered indicator cannot handle a quote tick.
201    pub fn handle_quotes(&self, quotes: &[QuoteTick]) -> anyhow::Result<()> {
202        for quote in quotes {
203            self.handle_quote(quote)?;
204        }
205
206        Ok(())
207    }
208
209    /// Handles a trade tick with registered indicators.
210    ///
211    /// # Errors
212    ///
213    /// Returns an error if a registered indicator cannot handle the trade tick.
214    pub fn handle_trade(&self, trade: &TradeTick) -> anyhow::Result<()> {
215        if let Some(indicators) = self.indicators_for_trades.get(&trade.instrument_id) {
216            for indicator in indicators {
217                indicator.handle_trade(trade)?;
218            }
219        }
220
221        Ok(())
222    }
223
224    /// Handles trade ticks with registered indicators.
225    ///
226    /// # Errors
227    ///
228    /// Returns an error if a registered indicator cannot handle a trade tick.
229    pub fn handle_trades(&self, trades: &[TradeTick]) -> anyhow::Result<()> {
230        for trade in trades {
231            self.handle_trade(trade)?;
232        }
233
234        Ok(())
235    }
236
237    /// Handles a bar with registered indicators.
238    ///
239    /// # Errors
240    ///
241    /// Returns an error if a registered indicator cannot handle the bar.
242    pub fn handle_bar(&self, bar: &Bar) -> anyhow::Result<()> {
243        if let Some(indicators) = self.indicators_for_bars.get(&bar.bar_type.id_spec_key()) {
244            for indicator in indicators {
245                indicator.handle_bar(bar)?;
246            }
247        }
248
249        Ok(())
250    }
251
252    /// Handles bars with registered indicators.
253    ///
254    /// # Errors
255    ///
256    /// Returns an error if a registered indicator cannot handle a bar.
257    pub fn handle_bars(&self, bars: &[Bar]) -> anyhow::Result<()> {
258        for bar in bars {
259            self.handle_bar(bar)?;
260        }
261
262        Ok(())
263    }
264
265    fn register_indicator(&mut self, indicator: SharedActorIndicator) {
266        if !contains_indicator(&self.indicators, &indicator) {
267            self.indicators.push(indicator);
268        }
269    }
270
271    fn register_by_key(
272        &mut self,
273        instrument_id: InstrumentId,
274        indicator: SharedActorIndicator,
275        kind: IndicatorKind,
276    ) {
277        let indicators = match kind {
278            IndicatorKind::Quote => self.indicators_for_quotes.entry(instrument_id).or_default(),
279            IndicatorKind::Trade => self.indicators_for_trades.entry(instrument_id).or_default(),
280        };
281
282        if !contains_indicator(indicators, &indicator) {
283            indicators.push(indicator);
284        }
285    }
286
287    fn register_bar(
288        &mut self,
289        bar_key: (InstrumentId, BarSpecification),
290        indicator: SharedActorIndicator,
291    ) {
292        let indicators = self.indicators_for_bars.entry(bar_key).or_default();
293
294        if !contains_indicator(indicators, &indicator) {
295            indicators.push(indicator);
296        }
297    }
298}
299
300#[derive(Clone, Copy)]
301enum IndicatorKind {
302    Quote,
303    Trade,
304}
305
306fn contains_indicator(
307    indicators: &[SharedActorIndicator],
308    indicator: &SharedActorIndicator,
309) -> bool {
310    let indicator_key = indicator.key();
311    indicators
312        .iter()
313        .any(|registered| registered.key() == indicator_key)
314}
315
316#[cfg(test)]
317mod tests {
318    use std::{
319        any::Any,
320        cell::Cell,
321        rc::Rc,
322        str::FromStr,
323        sync::atomic::{AtomicUsize, Ordering},
324    };
325
326    use nautilus_model::data::{Bar, BarType, QuoteTick, TradeTick};
327    use rstest::rstest;
328
329    use super::{ActorIndicator, Indicators, SharedActorIndicator};
330
331    static NEXT_KEY: AtomicUsize = AtomicUsize::new(1);
332
333    #[derive(Debug)]
334    struct TrackingIndicator {
335        key: usize,
336        initialized: Cell<bool>,
337        quotes: Cell<usize>,
338        trades: Cell<usize>,
339        bars: Cell<usize>,
340    }
341
342    impl TrackingIndicator {
343        fn new() -> Self {
344            Self {
345                key: NEXT_KEY.fetch_add(1, Ordering::Relaxed),
346                initialized: Cell::new(false),
347                quotes: Cell::new(0),
348                trades: Cell::new(0),
349                bars: Cell::new(0),
350            }
351        }
352
353        fn set_initialized(&self, initialized: bool) {
354            self.initialized.set(initialized);
355        }
356    }
357
358    impl ActorIndicator for TrackingIndicator {
359        fn key(&self) -> usize {
360            self.key
361        }
362
363        fn as_any(&self) -> &dyn Any {
364            self
365        }
366
367        fn initialized(&self) -> anyhow::Result<bool> {
368            Ok(self.initialized.get())
369        }
370
371        fn handle_quote(&self, _quote: &QuoteTick) -> anyhow::Result<()> {
372            self.quotes.set(self.quotes.get() + 1);
373            Ok(())
374        }
375
376        fn handle_trade(&self, _trade: &TradeTick) -> anyhow::Result<()> {
377            self.trades.set(self.trades.get() + 1);
378            Ok(())
379        }
380
381        fn handle_bar(&self, _bar: &Bar) -> anyhow::Result<()> {
382            self.bars.set(self.bars.get() + 1);
383            Ok(())
384        }
385    }
386
387    #[derive(Debug)]
388    struct ErrorIndicator {
389        key: usize,
390    }
391
392    impl ErrorIndicator {
393        fn new() -> Self {
394            Self {
395                key: NEXT_KEY.fetch_add(1, Ordering::Relaxed),
396            }
397        }
398    }
399
400    impl ActorIndicator for ErrorIndicator {
401        fn key(&self) -> usize {
402            self.key
403        }
404
405        fn as_any(&self) -> &dyn Any {
406            self
407        }
408
409        fn initialized(&self) -> anyhow::Result<bool> {
410            Ok(true)
411        }
412
413        fn handle_quote(&self, _quote: &QuoteTick) -> anyhow::Result<()> {
414            anyhow::bail!("indicator failed");
415        }
416
417        fn handle_trade(&self, _trade: &TradeTick) -> anyhow::Result<()> {
418            Ok(())
419        }
420
421        fn handle_bar(&self, _bar: &Bar) -> anyhow::Result<()> {
422            Ok(())
423        }
424    }
425
426    #[rstest]
427    fn test_register_indicator_routes_quote_trade_and_bar_with_deduplication() {
428        let mut indicators = Indicators::default();
429        let indicator = Rc::new(TrackingIndicator::new());
430        let registered: SharedActorIndicator = indicator.clone();
431        let quote = QuoteTick::default();
432        let trade = TradeTick::default();
433        let bar = Bar::default();
434        let external_bar_type = BarType::from_str(&format!(
435            "{}-1-MINUTE-LAST-EXTERNAL",
436            bar.bar_type.instrument_id()
437        ))
438        .unwrap();
439
440        indicators.register_indicator_for_quote_ticks(quote.instrument_id, registered.clone());
441        indicators.register_indicator_for_quote_ticks(quote.instrument_id, registered.clone());
442        indicators.register_indicator_for_trade_ticks(trade.instrument_id, registered.clone());
443        indicators.register_indicator_for_trade_ticks(trade.instrument_id, registered.clone());
444        indicators.register_indicator_for_bars(external_bar_type, registered.clone());
445        indicators.register_indicator_for_bars(external_bar_type, registered);
446
447        indicators.handle_quote(&quote).unwrap();
448        indicators.handle_trade(&trade).unwrap();
449        indicators.handle_bar(&bar).unwrap();
450
451        assert_eq!(indicators.registered_indicators().len(), 1);
452        assert_eq!(indicator.quotes.get(), 1);
453        assert_eq!(indicator.trades.get(), 1);
454        assert_eq!(indicator.bars.get(), 1);
455    }
456
457    #[rstest]
458    fn test_initialized_requires_all_registered_indicators() {
459        let mut indicators = Indicators::default();
460        let first = Rc::new(TrackingIndicator::new());
461        let second = Rc::new(TrackingIndicator::new());
462        let quote = QuoteTick::default();
463
464        indicators.register_indicator_for_quote_ticks(quote.instrument_id, first.clone());
465        indicators.register_indicator_for_quote_ticks(quote.instrument_id, second.clone());
466
467        first.set_initialized(true);
468
469        assert!(!indicators.initialized().unwrap());
470
471        second.set_initialized(true);
472
473        assert!(indicators.initialized().unwrap());
474    }
475
476    #[rstest]
477    fn test_handle_quote_propagates_indicator_error() {
478        let mut indicators = Indicators::default();
479        let indicator = Rc::new(ErrorIndicator::new());
480        let quote = QuoteTick::default();
481
482        indicators.register_indicator_for_quote_ticks(quote.instrument_id, indicator);
483
484        let err = indicators.handle_quote(&quote).unwrap_err();
485
486        assert_eq!(err.to_string(), "indicator failed");
487    }
488}