Skip to main content

nautilus_common/actor/
data_actor.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 std::{
17    any::Any,
18    cell::{Ref, RefCell, RefMut},
19    collections::HashMap,
20    fmt::Debug,
21    num::NonZeroUsize,
22    rc::Rc,
23    sync::Arc,
24};
25
26use ahash::{AHashMap, AHashSet};
27use indexmap::IndexMap;
28use jiff::Timestamp;
29use nautilus_core::{Params, UUID4, UnixNanos, correctness::check_predicate_true};
30#[cfg(feature = "defi")]
31use nautilus_model::defi::{
32    Block, Blockchain, Pool, PoolLiquidityUpdate, PoolSwap, data::PoolFeeCollect, data::PoolFlash,
33};
34use nautilus_model::{
35    data::{
36        Bar, BarType, CustomData, DataType, FundingRateUpdate, IndexPriceUpdate, InstrumentStatus,
37        MarkPriceUpdate, OrderBookDelta, OrderBookDeltas, OrderBookDepth, QuoteTick, TradeTick,
38        close::InstrumentClose,
39        option_chain::{OptionChainSlice, OptionGreeks, StrikeRange},
40    },
41    enums::BookType,
42    identifiers::{ActorId, ClientId, ComponentId, InstrumentId, OptionSeriesId, TraderId, Venue},
43    instruments::{InstrumentAny, SyntheticInstrument},
44    orderbook::OrderBook,
45};
46use serde::{Deserialize, Serialize};
47use ustr::Ustr;
48
49use super::{
50    Actor,
51    binding::DataActorBinding,
52    indicators::{Indicators, SharedActorIndicator},
53    registry::try_get_actor_unchecked,
54};
55#[cfg(feature = "defi")]
56use crate::defi;
57#[cfg(feature = "defi")]
58#[allow(unused_imports)]
59use crate::defi::data_actor as _; // Brings DeFi impl blocks into scope
60#[cfg(feature = "python")]
61use crate::python::msgbus::PyMessageBusScope;
62use crate::{
63    actor::requests::{RequestLifecycle, ResponseScope},
64    cache::{Cache, CacheApi},
65    clock::{Clock, ClockApi},
66    component::{Component, ComponentAccessError},
67    enums::{ComponentState, ComponentTrigger},
68    logging::{CMD, RECV, REQ, SEND},
69    messages::{
70        data::{
71            BarsResponse, BookDeltasResponse, BookDepthResponse, BookResponse, CustomDataResponse,
72            DataCommand, DataResponse, DataResponseEnvelope, FundingRatesResponse,
73            InstrumentResponse, InstrumentsResponse, IntoBarTypes, IntoInstrumentIds,
74            QuotesResponse, RequestBars, RequestBookDeltas, RequestBookDepth, RequestBookSnapshot,
75            RequestCommand, RequestCustomData, RequestFundingRates, RequestInstrument,
76            RequestInstruments, RequestQuotes, RequestTrades, SubscribeBars, SubscribeBookDeltas,
77            SubscribeBookDepth, SubscribeBookSnapshots, SubscribeCommand, SubscribeCustomData,
78            SubscribeFundingRates, SubscribeIndexPrices, SubscribeInstrument,
79            SubscribeInstrumentClose, SubscribeInstrumentStatus, SubscribeInstruments,
80            SubscribeMarkPrices, SubscribeOptionChain, SubscribeOptionGreeks, SubscribeQuotes,
81            SubscribeTrades, TradesResponse, UnsubscribeBars, UnsubscribeBookDeltas,
82            UnsubscribeBookDepth, UnsubscribeBookSnapshots, UnsubscribeCommand,
83            UnsubscribeCustomData, UnsubscribeFundingRates, UnsubscribeIndexPrices,
84            UnsubscribeInstrument, UnsubscribeInstrumentClose, UnsubscribeInstrumentStatus,
85            UnsubscribeInstruments, UnsubscribeMarkPrices, UnsubscribeOptionChain,
86            UnsubscribeOptionGreeks, UnsubscribeQuotes, UnsubscribeTrades,
87            ensure_routable_bar_types, ensure_routable_instrument_ids, ensure_subscription_routes,
88            is_parent_subscription, quote_routing,
89        },
90        system::{QueueStateChanged, ShutdownSystem, SocketStateChanged},
91    },
92    msgbus::{
93        self, MStr, Pattern, ShareableMessageHandler, Topic, TypedHandler, get_message_bus,
94        switchboard::{
95            MessagingSwitchboard, get_bars_topic, get_book_deltas_pattern, get_book_deltas_topic,
96            get_book_depth_pattern, get_book_depth_topic, get_book_snapshots_topic,
97            get_custom_subscription_topics, get_custom_topic, get_funding_rate_topic,
98            get_index_price_topic, get_instrument_close_topic, get_instrument_status_topic,
99            get_instrument_topic, get_instruments_pattern, get_mark_price_topic,
100            get_option_chain_topic, get_option_greeks_topic, get_quotes_topic, get_signal_pattern,
101            get_trades_topic,
102        },
103    },
104    runner::SystemChannel,
105    signal::Signal,
106    timer::{TimeEvent, TimeEventCallback},
107};
108#[cfg(feature = "live")]
109use crate::{
110    live::try_get_system_command_sender,
111    messages::{
112        SystemCommand,
113        system::{ReconnectSocket, socket_endpoint},
114    },
115};
116
117/// Common configuration for [`DataActor`] based components.
118#[derive(Debug, Clone, Deserialize, Serialize)]
119#[serde(default, deny_unknown_fields)]
120#[cfg_attr(
121    feature = "python",
122    pyo3::pyclass(module = "nautilus_trader.common", subclass, from_py_object)
123)]
124#[cfg_attr(
125    feature = "python",
126    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")
127)]
128pub struct DataActorConfig {
129    /// The custom identifier for the Actor.
130    pub actor_id: Option<ActorId>,
131    /// If events should be logged.
132    pub log_events: bool,
133    /// If commands should be logged.
134    pub log_commands: bool,
135}
136
137impl Default for DataActorConfig {
138    fn default() -> Self {
139        Self {
140            actor_id: None,
141            log_events: true,
142            log_commands: true,
143        }
144    }
145}
146
147/// Configuration for creating actors from importable paths.
148#[derive(Debug, Clone, Deserialize, Serialize)]
149#[serde(deny_unknown_fields)]
150#[cfg_attr(
151    feature = "python",
152    pyo3::pyclass(module = "nautilus_trader.common", from_py_object)
153)]
154#[cfg_attr(
155    feature = "python",
156    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")
157)]
158pub struct ImportableActorConfig {
159    /// The fully qualified name of the Actor class.
160    pub actor_path: String,
161    /// The fully qualified name of the Actor config class.
162    pub config_path: String,
163    /// The actor configuration as a dictionary.
164    pub config: HashMap<String, serde_json::Value>,
165}
166
167/// Explicit native-only access for data actor runtime state.
168///
169/// Normal actor and strategy code should use facade methods such as
170/// [`DataActor::clock`] and [`DataActor::cache`]. Import this trait only from
171/// Rust code compiled into the same native binary as the engine, when a
172/// performance-sensitive path or host integration needs access below the facade
173/// API.
174///
175/// Do not import this trait in strategy code intended to run through Python or
176/// the plug-in authoring surface. Native borrows, `Rc<RefCell<_>>`, and core
177/// references do not cross those boundaries.
178pub trait DataActorNative {
179    /// Returns the actor core.
180    fn core(&self) -> &DataActorCore;
181
182    /// Returns the mutable actor core.
183    fn core_mut(&mut self) -> &mut DataActorCore;
184
185    /// Returns the mutable clock borrow for the actor.
186    ///
187    /// # Panics
188    ///
189    /// Panics if the actor is unregistered or the clock is already borrowed.
190    fn clock_mut(&mut self) -> RefMut<'_, dyn Clock> {
191        self.try_clock_mut().unwrap_or_else(|e| panic!("{e}"))
192    }
193
194    /// Returns a mutable clock borrow without panicking on an access conflict.
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if the actor is unregistered or the clock is already borrowed.
199    fn try_clock_mut(&mut self) -> Result<RefMut<'_, dyn Clock>, ComponentAccessError> {
200        self.core_mut()
201            .clock
202            .as_ref()
203            .ok_or(ComponentAccessError::NotRegistered {
204                resource: "clock",
205                operation: "clock_mut",
206            })?
207            .try_borrow_mut()
208            .map_err(|_| ComponentAccessError::WriteConflict {
209                resource: "clock",
210                operation: "clock_mut",
211            })
212    }
213
214    /// Returns a clone of the reference-counted clock.
215    ///
216    /// # Panics
217    ///
218    /// Panics if the actor has not yet been registered.
219    fn clock_rc(&self) -> Rc<RefCell<dyn Clock>> {
220        self.core()
221            .clock
222            .as_ref()
223            .expect("DataActor must be registered before accessing clock")
224            .clone()
225    }
226
227    /// Returns a read-only cache borrow.
228    ///
229    /// # Panics
230    ///
231    /// Panics if the actor is unregistered or the cache is already mutably borrowed.
232    fn cache_ref(&self) -> Ref<'_, Cache> {
233        self.try_cache_ref().unwrap_or_else(|e| panic!("{e}"))
234    }
235
236    /// Returns a cache borrow without panicking on an access conflict.
237    ///
238    /// # Errors
239    ///
240    /// Returns an error if the actor is unregistered or the cache is already mutably borrowed.
241    fn try_cache_ref(&self) -> Result<Ref<'_, Cache>, ComponentAccessError> {
242        self.core()
243            .cache
244            .as_ref()
245            .ok_or(ComponentAccessError::NotRegistered {
246                resource: "cache",
247                operation: "cache_ref",
248            })?
249            .try_borrow()
250            .map_err(|_| ComponentAccessError::ReadConflict {
251                resource: "cache",
252                operation: "cache_ref",
253            })
254    }
255
256    /// Returns a clone of the reference-counted cache.
257    ///
258    /// # Panics
259    ///
260    /// Panics if the actor has not yet been registered.
261    fn cache_rc(&self) -> Rc<RefCell<Cache>> {
262        self.core()
263            .cache
264            .as_ref()
265            .expect("DataActor must be registered before accessing cache")
266            .clone()
267    }
268}
269
270/// Defines lifecycle callbacks, data handlers, and subscription/request
271/// methods for data actors.
272///
273/// Default methods backed only by the native runtime carry explicit
274/// [`DataActorNative`] and [`Component`] bounds. The actor ID and clock facades
275/// use [`DataActorBinding`] to access component state.
276pub trait DataActor {
277    /// Returns the actor ID.
278    ///
279    /// # Panics
280    ///
281    /// Panics if a callback-scoped binding is used outside an active callback.
282    fn actor_id(&self) -> ActorId
283    where
284        Self: DataActorBinding,
285    {
286        self.binding_actor_id()
287    }
288
289    /// Returns the trader ID this actor is registered to.
290    fn trader_id(&self) -> Option<TraderId>
291    where
292        Self: DataActorNative,
293    {
294        self.core().trader_id()
295    }
296
297    /// Returns whether the actor is registered with a trader.
298    fn is_registered(&self) -> bool
299    where
300        Self: DataActorNative,
301    {
302        self.core().is_registered()
303    }
304
305    /// Returns the actor configuration.
306    fn config(&self) -> &DataActorConfig
307    where
308        Self: DataActorNative,
309    {
310        &self.core().config
311    }
312
313    /// Actions to be performed when the actor state is saved.
314    ///
315    /// # Errors
316    ///
317    /// Returns an error if saving the actor state fails.
318    fn on_save(&self) -> anyhow::Result<IndexMap<String, Vec<u8>>> {
319        Ok(IndexMap::new())
320    }
321
322    /// Actions to be performed when the actor state is loaded.
323    ///
324    /// # Errors
325    ///
326    /// Returns an error if loading the actor state fails.
327    #[allow(unused_variables)]
328    fn on_load(&mut self, state: IndexMap<String, Vec<u8>>) -> anyhow::Result<()> {
329        Ok(())
330    }
331
332    /// Actions to be performed on start.
333    ///
334    /// # Errors
335    ///
336    /// Returns an error if starting the actor fails.
337    fn on_start(&mut self) -> anyhow::Result<()> {
338        log::warn!(
339            "The `on_start` handler was called when not overridden, \
340            it's expected that any actions required when starting the actor \
341            occur here, such as subscribing/requesting data"
342        );
343        Ok(())
344    }
345
346    /// Actions to be performed on stop.
347    ///
348    /// # Errors
349    ///
350    /// Returns an error if stopping the actor fails.
351    fn on_stop(&mut self) -> anyhow::Result<()> {
352        log::warn!(
353            "The `on_stop` handler was called when not overridden, \
354            it's expected that any actions required when stopping the actor \
355            occur here, such as unsubscribing from data",
356        );
357        Ok(())
358    }
359
360    /// Actions to be performed on resume.
361    ///
362    /// # Errors
363    ///
364    /// Returns an error if resuming the actor fails.
365    fn on_resume(&mut self) -> anyhow::Result<()> {
366        log::warn!(
367            "The `on_resume` handler was called when not overridden, \
368            it's expected that any actions required when resuming the actor \
369            following a stop occur here"
370        );
371        Ok(())
372    }
373
374    /// Actions to be performed on reset.
375    ///
376    /// # Errors
377    ///
378    /// Returns an error if resetting the actor fails.
379    fn on_reset(&mut self) -> anyhow::Result<()> {
380        log::warn!(
381            "The `on_reset` handler was called when not overridden, \
382            it's expected that any actions required when resetting the actor \
383            occur here, such as resetting indicators and other state"
384        );
385        Ok(())
386    }
387
388    /// Actions to be performed on dispose.
389    ///
390    /// # Errors
391    ///
392    /// Returns an error if disposing the actor fails.
393    fn on_dispose(&mut self) -> anyhow::Result<()> {
394        Ok(())
395    }
396
397    /// Actions to be performed on degrade.
398    ///
399    /// # Errors
400    ///
401    /// Returns an error if degrading the actor fails.
402    fn on_degrade(&mut self) -> anyhow::Result<()> {
403        Ok(())
404    }
405
406    /// Actions to be performed on fault.
407    ///
408    /// # Errors
409    ///
410    /// Returns an error if faulting the actor fails.
411    fn on_fault(&mut self) -> anyhow::Result<()> {
412        Ok(())
413    }
414
415    /// Actions to be performed when receiving a time event.
416    ///
417    /// # Errors
418    ///
419    /// Returns an error if handling the time event fails.
420    #[allow(unused_variables)]
421    fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
422        Ok(())
423    }
424
425    /// Actions to be performed when receiving custom data.
426    ///
427    /// # Errors
428    ///
429    /// Returns an error if handling the data fails.
430    #[allow(unused_variables)]
431    fn on_data(&mut self, data: &CustomData) -> anyhow::Result<()> {
432        Ok(())
433    }
434
435    /// Actions to be performed when receiving a signal.
436    ///
437    /// # Errors
438    ///
439    /// Returns an error if handling the signal fails.
440    #[allow(unused_variables)]
441    fn on_signal(&mut self, signal: &Signal) -> anyhow::Result<()> {
442        Ok(())
443    }
444
445    /// Actions to be performed when receiving a queue state change.
446    ///
447    /// # Errors
448    ///
449    /// Returns an error if handling the queue state change fails.
450    #[allow(unused_variables)]
451    fn on_queue_state(&mut self, event: &QueueStateChanged) -> anyhow::Result<()> {
452        Ok(())
453    }
454
455    /// Actions to be performed when receiving a socket state change.
456    ///
457    /// # Errors
458    ///
459    /// Returns an error if handling the socket state change fails.
460    #[allow(unused_variables)]
461    fn on_socket_state(&mut self, event: &SocketStateChanged) -> anyhow::Result<()> {
462        Ok(())
463    }
464
465    /// Actions to be performed when receiving an instrument.
466    ///
467    /// # Errors
468    ///
469    /// Returns an error if handling the instrument fails.
470    #[allow(unused_variables)]
471    fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
472        Ok(())
473    }
474
475    /// Actions to be performed when receiving order book deltas.
476    ///
477    /// # Errors
478    ///
479    /// Returns an error if handling the book deltas fails.
480    #[allow(unused_variables)]
481    fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
482        Ok(())
483    }
484
485    /// Actions to be performed when receiving an order book depth snapshot.
486    ///
487    /// # Errors
488    ///
489    /// Returns an error if handling the book depth fails.
490    #[allow(unused_variables)]
491    fn on_book_depth(&mut self, depth: &OrderBookDepth) -> anyhow::Result<()> {
492        Ok(())
493    }
494
495    /// Actions to be performed when receiving an order book.
496    ///
497    /// # Errors
498    ///
499    /// Returns an error if handling the book fails.
500    #[allow(unused_variables)]
501    fn on_book(&mut self, order_book: &OrderBook) -> anyhow::Result<()> {
502        Ok(())
503    }
504
505    /// Actions to be performed when receiving a quote.
506    ///
507    /// # Errors
508    ///
509    /// Returns an error if handling the quote fails.
510    #[allow(unused_variables)]
511    fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
512        Ok(())
513    }
514
515    /// Actions to be performed when receiving a trade.
516    ///
517    /// # Errors
518    ///
519    /// Returns an error if handling the trade fails.
520    #[allow(unused_variables)]
521    fn on_trade(&mut self, tick: &TradeTick) -> anyhow::Result<()> {
522        Ok(())
523    }
524
525    /// Actions to be performed when receiving a bar.
526    ///
527    /// # Errors
528    ///
529    /// Returns an error if handling the bar fails.
530    #[allow(unused_variables)]
531    fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
532        Ok(())
533    }
534
535    /// Actions to be performed when receiving a mark price update.
536    ///
537    /// # Errors
538    ///
539    /// Returns an error if handling the mark price update fails.
540    #[allow(unused_variables)]
541    fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
542        Ok(())
543    }
544
545    /// Actions to be performed when receiving an index price update.
546    ///
547    /// # Errors
548    ///
549    /// Returns an error if handling the index price update fails.
550    #[allow(unused_variables)]
551    fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
552        Ok(())
553    }
554
555    /// Actions to be performed when receiving a funding rate update.
556    ///
557    /// # Errors
558    ///
559    /// Returns an error if handling the funding rate update fails.
560    #[allow(unused_variables)]
561    fn on_funding_rate(&mut self, funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
562        Ok(())
563    }
564
565    /// Actions to be performed when receiving exchange-provided option greeks.
566    ///
567    /// # Errors
568    ///
569    /// Returns an error if handling the option greeks fails.
570    #[allow(unused_variables)]
571    fn on_option_greeks(&mut self, greeks: &OptionGreeks) -> anyhow::Result<()> {
572        Ok(())
573    }
574
575    /// Actions to be performed when receiving an option chain slice snapshot.
576    ///
577    /// # Errors
578    ///
579    /// Returns an error if handling the option chain slice fails.
580    #[allow(unused_variables)]
581    fn on_option_chain(&mut self, slice: &OptionChainSlice) -> anyhow::Result<()> {
582        Ok(())
583    }
584
585    /// Actions to be performed when receiving an instrument status update.
586    ///
587    /// # Errors
588    ///
589    /// Returns an error if handling the instrument status update fails.
590    #[allow(unused_variables)]
591    fn on_instrument_status(&mut self, data: &InstrumentStatus) -> anyhow::Result<()> {
592        Ok(())
593    }
594
595    /// Actions to be performed when receiving an instrument close update.
596    ///
597    /// # Errors
598    ///
599    /// Returns an error if handling the instrument close update fails.
600    #[allow(unused_variables)]
601    fn on_instrument_close(&mut self, update: &InstrumentClose) -> anyhow::Result<()> {
602        Ok(())
603    }
604
605    #[cfg(feature = "defi")]
606    /// Actions to be performed when receiving a block.
607    ///
608    /// # Errors
609    ///
610    /// Returns an error if handling the block fails.
611    #[allow(unused_variables)]
612    fn on_block(&mut self, block: &Block) -> anyhow::Result<()> {
613        Ok(())
614    }
615
616    #[cfg(feature = "defi")]
617    /// Actions to be performed when receiving a pool.
618    ///
619    /// # Errors
620    ///
621    /// Returns an error if handling the pool fails.
622    #[allow(unused_variables)]
623    fn on_pool(&mut self, pool: &Pool) -> anyhow::Result<()> {
624        Ok(())
625    }
626
627    #[cfg(feature = "defi")]
628    /// Actions to be performed when receiving a pool swap.
629    ///
630    /// # Errors
631    ///
632    /// Returns an error if handling the pool swap fails.
633    #[allow(unused_variables)]
634    fn on_pool_swap(&mut self, swap: &PoolSwap) -> anyhow::Result<()> {
635        Ok(())
636    }
637
638    #[cfg(feature = "defi")]
639    /// Actions to be performed when receiving a pool liquidity update.
640    ///
641    /// # Errors
642    ///
643    /// Returns an error if handling the pool liquidity update fails.
644    #[allow(unused_variables)]
645    fn on_pool_liquidity_update(&mut self, update: &PoolLiquidityUpdate) -> anyhow::Result<()> {
646        Ok(())
647    }
648
649    #[cfg(feature = "defi")]
650    /// Actions to be performed when receiving a pool fee collect event.
651    ///
652    /// # Errors
653    ///
654    /// Returns an error if handling the pool fee collect fails.
655    #[allow(unused_variables)]
656    fn on_pool_fee_collect(&mut self, collect: &PoolFeeCollect) -> anyhow::Result<()> {
657        Ok(())
658    }
659
660    #[cfg(feature = "defi")]
661    /// Actions to be performed when receiving a pool flash event.
662    ///
663    /// # Errors
664    ///
665    /// Returns an error if handling the pool flash fails.
666    #[allow(unused_variables)]
667    fn on_pool_flash(&mut self, flash: &PoolFlash) -> anyhow::Result<()> {
668        Ok(())
669    }
670
671    /// Actions to be performed when receiving historical custom data.
672    ///
673    /// The callback runs once per response. A scalar [`CustomData`] remains scalar, while a
674    /// `Vec<CustomData>` batch remains intact, including when empty.
675    ///
676    /// # Errors
677    ///
678    /// Returns an error if handling the historical data fails.
679    #[allow(unused_variables)]
680    fn on_historical_data(&mut self, data: &dyn Any) -> anyhow::Result<()> {
681        Ok(())
682    }
683
684    /// Actions to be performed when receiving historical book deltas.
685    ///
686    /// # Errors
687    ///
688    /// Returns an error if handling the historical book deltas fails.
689    #[allow(unused_variables)]
690    fn on_historical_book_deltas(&mut self, deltas: &[OrderBookDelta]) -> anyhow::Result<()> {
691        Ok(())
692    }
693
694    /// Actions to be performed when receiving historical book depth.
695    ///
696    /// # Errors
697    ///
698    /// Returns an error if handling the historical book depth fails.
699    #[allow(unused_variables)]
700    fn on_historical_book_depth(&mut self, depths: &[OrderBookDepth]) -> anyhow::Result<()> {
701        Ok(())
702    }
703
704    /// Actions to be performed when receiving historical quotes.
705    ///
706    /// # Errors
707    ///
708    /// Returns an error if handling the historical quotes fails.
709    #[allow(unused_variables)]
710    fn on_historical_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> {
711        Ok(())
712    }
713
714    /// Actions to be performed when receiving historical trades.
715    ///
716    /// # Errors
717    ///
718    /// Returns an error if handling the historical trades fails.
719    #[allow(unused_variables)]
720    fn on_historical_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> {
721        Ok(())
722    }
723
724    /// Actions to be performed when receiving historical bars.
725    ///
726    /// # Errors
727    ///
728    /// Returns an error if handling the historical bars fails.
729    #[allow(unused_variables)]
730    fn on_historical_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> {
731        Ok(())
732    }
733
734    /// Actions to be performed when receiving historical mark prices.
735    ///
736    /// # Errors
737    ///
738    /// Returns an error if handling the historical mark prices fails.
739    #[allow(unused_variables)]
740    fn on_historical_mark_prices(&mut self, mark_prices: &[MarkPriceUpdate]) -> anyhow::Result<()> {
741        Ok(())
742    }
743
744    /// Actions to be performed when receiving historical index prices.
745    ///
746    /// # Errors
747    ///
748    /// Returns an error if handling the historical index prices fails.
749    #[allow(unused_variables)]
750    fn on_historical_index_prices(
751        &mut self,
752        index_prices: &[IndexPriceUpdate],
753    ) -> anyhow::Result<()> {
754        Ok(())
755    }
756
757    /// Actions to be performed when receiving historical funding rates.
758    ///
759    /// # Errors
760    ///
761    /// Returns an error if handling the historical funding rates fails.
762    #[allow(unused_variables)]
763    fn on_historical_funding_rates(
764        &mut self,
765        funding_rates: &[FundingRateUpdate],
766    ) -> anyhow::Result<()> {
767        Ok(())
768    }
769
770    /// Returns the user-facing clock API.
771    ///
772    /// # Panics
773    ///
774    /// Panics if the native actor is unregistered or a callback-scoped binding is
775    /// used outside an active callback.
776    fn clock(&self) -> ClockApi<'_>
777    where
778        Self: DataActorBinding,
779    {
780        self.binding_clock()
781    }
782
783    /// Returns the user-facing cache API.
784    fn cache(&self) -> CacheApi<'_>
785    where
786        Self: DataActorNative,
787    {
788        self.core().cache_api()
789    }
790
791    /// Sends a shutdown command to the system with an optional reason.
792    ///
793    /// # Panics
794    ///
795    /// Panics if the actor is not registered or has no trader ID.
796    fn shutdown_system(&self, reason: Option<String>)
797    where
798        Self: DataActorNative,
799    {
800        self.core().shutdown_system(reason);
801    }
802
803    /// Publishes `data` on the message bus under the topic derived from `data_type`.
804    ///
805    /// `data_type` is kept as an explicit parameter to allow callers to override the
806    /// routing topic from the payload's intrinsic type.
807    ///
808    /// # Panics
809    ///
810    /// Panics if the actor is not registered with a trader.
811    fn publish_data(&self, data_type: &DataType, data: &CustomData)
812    where
813        Self: DataActorNative,
814    {
815        self.core().publish_data(data_type, data);
816    }
817
818    /// Publishes a [`Signal`] constructed from `name` and `value`.
819    ///
820    /// # Panics
821    ///
822    /// Panics if the actor is not registered with a trader.
823    fn publish_signal(&self, name: &str, value: String, ts_event: UnixNanos)
824    where
825        Self: DataActorNative,
826    {
827        self.core().publish_signal(name, value, ts_event);
828    }
829
830    // panics-doc-ok
831    /// Adds the `synthetic` instrument to the cache.
832    ///
833    /// # Errors
834    ///
835    /// Returns an error if a synthetic with the same ID already exists, or if the
836    /// backing cache fails to persist it.
837    ///
838    /// # Panics
839    ///
840    /// Panics if the actor is not registered with a trader.
841    fn add_synthetic(&self, synthetic: SyntheticInstrument) -> anyhow::Result<()>
842    where
843        Self: DataActorNative,
844    {
845        self.core().add_synthetic(synthetic)
846    }
847
848    // panics-doc-ok
849    /// Updates the `synthetic` instrument in the cache, replacing the existing entry.
850    ///
851    /// # Errors
852    ///
853    /// Returns an error if no synthetic with the same ID already exists, or if the
854    /// backing cache fails to persist the replacement.
855    ///
856    /// # Panics
857    ///
858    /// Panics if the actor is not registered with a trader.
859    fn update_synthetic(&self, synthetic: SyntheticInstrument) -> anyhow::Result<()>
860    where
861        Self: DataActorNative,
862    {
863        self.core().update_synthetic(synthetic)
864    }
865
866    /// Handles a received time event.
867    fn handle_time_event(&mut self, event: &TimeEvent)
868    where
869        Self: Component,
870    {
871        log_received(&event);
872
873        if self.not_running() {
874            log_not_running(&event);
875            return;
876        }
877
878        if let Err(e) = DataActor::on_time_event(self, event) {
879            log_error(&e);
880        }
881    }
882
883    /// Handles a received custom data point.
884    fn handle_data(&mut self, data: &CustomData)
885    where
886        Self: Component,
887    {
888        log_received(&data);
889
890        if self.not_running() {
891            log_not_running(&data);
892            return;
893        }
894
895        if let Err(e) = self.on_data(data) {
896            log_error(&e);
897        }
898    }
899
900    /// Handles a received signal.
901    fn handle_signal(&mut self, signal: &Signal)
902    where
903        Self: Component,
904    {
905        log_received(&signal);
906
907        if self.not_running() {
908            log_not_running(&signal);
909            return;
910        }
911
912        if let Err(e) = self.on_signal(signal) {
913            log_error(&e);
914        }
915    }
916
917    /// Handles a received queue state change.
918    fn handle_queue_state(&mut self, event: &QueueStateChanged)
919    where
920        Self: Component,
921    {
922        log_received(&event);
923
924        if self.not_running() {
925            log_not_running(&event);
926            return;
927        }
928
929        if let Err(e) = self.on_queue_state(event) {
930            log_error(&e);
931        }
932    }
933
934    /// Handles a received socket state change.
935    fn handle_socket_state(&mut self, event: &SocketStateChanged)
936    where
937        Self: Component,
938    {
939        log_received(&event);
940
941        if self.not_running() {
942            log_not_running(&event);
943            return;
944        }
945
946        if let Err(e) = self.on_socket_state(event) {
947            log_error(&e);
948        }
949    }
950
951    /// Handles a received instrument.
952    fn handle_instrument(&mut self, instrument: &InstrumentAny)
953    where
954        Self: Component,
955    {
956        log_received(&instrument);
957
958        if self.not_running() {
959            log_not_running(&instrument);
960            return;
961        }
962
963        if let Err(e) = self.on_instrument(instrument) {
964            log_error(&e);
965        }
966    }
967
968    /// Handles received order book deltas.
969    fn handle_book_deltas(&mut self, deltas: &OrderBookDeltas)
970    where
971        Self: Component,
972    {
973        log_received(&deltas);
974
975        if self.not_running() {
976            log_not_running(&deltas);
977            return;
978        }
979
980        if let Err(e) = self.on_book_deltas(deltas) {
981            log_error(&e);
982        }
983    }
984
985    /// Handles a received order book depth snapshot.
986    fn handle_book_depth(&mut self, depth: &OrderBookDepth)
987    where
988        Self: Component,
989    {
990        log_received(&depth);
991
992        if self.not_running() {
993            log_not_running(&depth);
994            return;
995        }
996
997        if let Err(e) = self.on_book_depth(depth) {
998            log_error(&e);
999        }
1000    }
1001
1002    /// Handles a received order book reference.
1003    fn handle_book(&mut self, book: &OrderBook)
1004    where
1005        Self: Component,
1006    {
1007        log_received(&book);
1008
1009        if self.not_running() {
1010            log_not_running(&book);
1011            return;
1012        }
1013
1014        if let Err(e) = self.on_book(book) {
1015            log_error(&e);
1016        }
1017    }
1018
1019    /// Handles a received quote.
1020    fn handle_quote(&mut self, quote: &QuoteTick)
1021    where
1022        Self: DataActorNative + Component,
1023    {
1024        log_received(&quote);
1025
1026        if let Err(e) = self.core().handle_indicators_for_quote(quote) {
1027            log_error(&e);
1028            return;
1029        }
1030
1031        if self.not_running() {
1032            log_not_running(&quote);
1033            return;
1034        }
1035
1036        if let Err(e) = self.on_quote(quote) {
1037            log_error(&e);
1038        }
1039    }
1040
1041    /// Handles a received trade.
1042    fn handle_trade(&mut self, trade: &TradeTick)
1043    where
1044        Self: DataActorNative + Component,
1045    {
1046        log_received(&trade);
1047
1048        if let Err(e) = self.core().handle_indicators_for_trade(trade) {
1049            log_error(&e);
1050            return;
1051        }
1052
1053        if self.not_running() {
1054            log_not_running(&trade);
1055            return;
1056        }
1057
1058        if let Err(e) = self.on_trade(trade) {
1059            log_error(&e);
1060        }
1061    }
1062
1063    /// Handles a receiving bar.
1064    fn handle_bar(&mut self, bar: &Bar)
1065    where
1066        Self: DataActorNative + Component,
1067    {
1068        log_received(&bar);
1069
1070        if let Err(e) = self.core().handle_indicators_for_bar(bar) {
1071            log_error(&e);
1072            return;
1073        }
1074
1075        if self.not_running() {
1076            log_not_running(&bar);
1077            return;
1078        }
1079
1080        if let Err(e) = self.on_bar(bar) {
1081            log_error(&e);
1082        }
1083    }
1084
1085    /// Handles a received mark price update.
1086    fn handle_mark_price(&mut self, mark_price: &MarkPriceUpdate)
1087    where
1088        Self: Component,
1089    {
1090        log_received(&mark_price);
1091
1092        if self.not_running() {
1093            log_not_running(&mark_price);
1094            return;
1095        }
1096
1097        if let Err(e) = self.on_mark_price(mark_price) {
1098            log_error(&e);
1099        }
1100    }
1101
1102    /// Handles a received index price update.
1103    fn handle_index_price(&mut self, index_price: &IndexPriceUpdate)
1104    where
1105        Self: Component,
1106    {
1107        log_received(&index_price);
1108
1109        if self.not_running() {
1110            log_not_running(&index_price);
1111            return;
1112        }
1113
1114        if let Err(e) = self.on_index_price(index_price) {
1115            log_error(&e);
1116        }
1117    }
1118
1119    /// Handles a received funding rate update.
1120    fn handle_funding_rate(&mut self, funding_rate: &FundingRateUpdate)
1121    where
1122        Self: Component,
1123    {
1124        log_received(&funding_rate);
1125
1126        if self.not_running() {
1127            log_not_running(&funding_rate);
1128            return;
1129        }
1130
1131        if let Err(e) = self.on_funding_rate(funding_rate) {
1132            log_error(&e);
1133        }
1134    }
1135
1136    /// Handles a received option greeks update.
1137    fn handle_option_greeks(&mut self, greeks: &OptionGreeks)
1138    where
1139        Self: Component,
1140    {
1141        log_received(&greeks);
1142
1143        if self.not_running() {
1144            log_not_running(&greeks);
1145            return;
1146        }
1147
1148        if let Err(e) = self.on_option_greeks(greeks) {
1149            log_error(&e);
1150        }
1151    }
1152
1153    /// Handles a received option chain slice snapshot.
1154    fn handle_option_chain(&mut self, slice: &OptionChainSlice)
1155    where
1156        Self: Component,
1157    {
1158        log_received(&slice);
1159
1160        if self.not_running() {
1161            log_not_running(&slice);
1162            return;
1163        }
1164
1165        if let Err(e) = self.on_option_chain(slice) {
1166            log_error(&e);
1167        }
1168    }
1169
1170    /// Handles a received instrument status.
1171    fn handle_instrument_status(&mut self, status: &InstrumentStatus)
1172    where
1173        Self: Component,
1174    {
1175        log_received(&status);
1176
1177        if self.not_running() {
1178            log_not_running(&status);
1179            return;
1180        }
1181
1182        if let Err(e) = self.on_instrument_status(status) {
1183            log_error(&e);
1184        }
1185    }
1186
1187    /// Handles a received instrument close.
1188    fn handle_instrument_close(&mut self, close: &InstrumentClose)
1189    where
1190        Self: Component,
1191    {
1192        log_received(&close);
1193
1194        if self.not_running() {
1195            log_not_running(&close);
1196            return;
1197        }
1198
1199        if let Err(e) = self.on_instrument_close(close) {
1200            log_error(&e);
1201        }
1202    }
1203
1204    #[cfg(feature = "defi")]
1205    /// Handles a received block.
1206    fn handle_block(&mut self, block: &Block)
1207    where
1208        Self: Component,
1209    {
1210        log_received(&block);
1211
1212        if self.not_running() {
1213            log_not_running(&block);
1214            return;
1215        }
1216
1217        if let Err(e) = self.on_block(block) {
1218            log_error(&e);
1219        }
1220    }
1221
1222    #[cfg(feature = "defi")]
1223    /// Handles a received pool definition update.
1224    fn handle_pool(&mut self, pool: &Pool)
1225    where
1226        Self: Component,
1227    {
1228        log_received(&pool);
1229
1230        if self.not_running() {
1231            log_not_running(&pool);
1232            return;
1233        }
1234
1235        if let Err(e) = self.on_pool(pool) {
1236            log_error(&e);
1237        }
1238    }
1239
1240    #[cfg(feature = "defi")]
1241    /// Handles a received pool swap.
1242    fn handle_pool_swap(&mut self, swap: &PoolSwap)
1243    where
1244        Self: Component,
1245    {
1246        log_received(&swap);
1247
1248        if self.not_running() {
1249            log_not_running(&swap);
1250            return;
1251        }
1252
1253        if let Err(e) = self.on_pool_swap(swap) {
1254            log_error(&e);
1255        }
1256    }
1257
1258    #[cfg(feature = "defi")]
1259    /// Handles a received pool liquidity update.
1260    fn handle_pool_liquidity_update(&mut self, update: &PoolLiquidityUpdate)
1261    where
1262        Self: Component,
1263    {
1264        log_received(&update);
1265
1266        if self.not_running() {
1267            log_not_running(&update);
1268            return;
1269        }
1270
1271        if let Err(e) = self.on_pool_liquidity_update(update) {
1272            log_error(&e);
1273        }
1274    }
1275
1276    #[cfg(feature = "defi")]
1277    /// Handles a received pool fee collect.
1278    fn handle_pool_fee_collect(&mut self, collect: &PoolFeeCollect)
1279    where
1280        Self: Component,
1281    {
1282        log_received(&collect);
1283
1284        if self.not_running() {
1285            log_not_running(&collect);
1286            return;
1287        }
1288
1289        if let Err(e) = self.on_pool_fee_collect(collect) {
1290            log_error(&e);
1291        }
1292    }
1293
1294    #[cfg(feature = "defi")]
1295    /// Handles a received pool flash event.
1296    fn handle_pool_flash(&mut self, flash: &PoolFlash)
1297    where
1298        Self: Component,
1299    {
1300        log_received(&flash);
1301
1302        if self.not_running() {
1303            log_not_running(&flash);
1304            return;
1305        }
1306
1307        if let Err(e) = self.on_pool_flash(flash) {
1308            log_error(&e);
1309        }
1310    }
1311
1312    /// Handles received historical data.
1313    fn handle_historical_data(&mut self, data: &dyn Any) {
1314        log_received(&data);
1315
1316        if let Err(e) = self.on_historical_data(data) {
1317            log_error(&e);
1318        }
1319    }
1320
1321    /// Handles a historical payload before its generic completion hook.
1322    fn handle_response(&mut self, envelope: &DataResponseEnvelope)
1323    where
1324        Self: DataActorNative,
1325    {
1326        let Some(scope) = self.core().begin_response(envelope.correlation_id()) else {
1327            return;
1328        };
1329
1330        if let Some(response) = envelope.response() {
1331            match response {
1332                DataResponse::Data(r) => self.handle_data_response(r),
1333                DataResponse::Instrument(r) => self.handle_instrument_response(r),
1334                DataResponse::Instruments(r) => self.handle_instruments_response(r),
1335                DataResponse::Book(r) => self.handle_book_response(r),
1336                DataResponse::BookDeltas(r) => self.handle_book_deltas_response(r),
1337                DataResponse::BookDepth(r) => self.handle_book_depth_response(r),
1338                DataResponse::Quotes(r) => self.handle_quotes_response(r),
1339                DataResponse::Trades(r) => self.handle_trades_response(r),
1340                DataResponse::FundingRates(r) => self.handle_funding_rates_response(r),
1341                DataResponse::Bars(r) => self.handle_bars_response(r),
1342                DataResponse::OptionChainReferencePrice(_) => {}
1343            }
1344        }
1345
1346        if self.core().complete_request(envelope, &scope)
1347            && let Err(e) = self.on_data_response(envelope)
1348        {
1349            log_error(&e);
1350        }
1351
1352        drop(scope);
1353        self.drain_request_groups();
1354    }
1355
1356    /// Delivers ready completion groups after the active response hook returns.
1357    #[doc(hidden)]
1358    fn drain_request_groups(&mut self)
1359    where
1360        Self: DataActorNative,
1361    {
1362        while let Some((envelope, _handler)) = self.core().next_group_response() {
1363            let Some(scope) = self.core().begin_response(envelope.correlation_id()) else {
1364                continue;
1365            };
1366
1367            if self.core().complete_request(&envelope, &scope)
1368                && let Err(e) = self.on_data_response(&envelope)
1369            {
1370                log_error(&e);
1371            }
1372        }
1373    }
1374
1375    /// Receives each historical envelope after its typed payload hook.
1376    ///
1377    /// Terminal envelopes have already cleared pending request state. A failure can include
1378    /// partial data; a payload-free terminal envelope does not invoke a typed payload hook.
1379    ///
1380    /// # Errors
1381    ///
1382    /// Returns an error if the actor's response hook fails.
1383    fn on_data_response(&mut self, _response: &DataResponseEnvelope) -> anyhow::Result<()> {
1384        Ok(())
1385    }
1386
1387    /// Builds the response handler for this actor's binding.
1388    #[doc(hidden)]
1389    fn response_handler(&self) -> TypedHandler<DataResponseEnvelope>
1390    where
1391        Self: DataActorNative + Debug + Sized + 'static,
1392    {
1393        let actor_id = self.core().actor_id().inner();
1394        TypedHandler::from(move |response: &DataResponseEnvelope| {
1395            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1396                actor.handle_response(response);
1397            } else {
1398                log::error!("Actor {actor_id} not found for response handling");
1399            }
1400        })
1401    }
1402
1403    /// Handles a data response.
1404    fn handle_data_response(&mut self, resp: &CustomDataResponse) {
1405        if let Some(data) = resp.data.as_ref().downcast_ref::<Vec<CustomData>>() {
1406            log_received_bulk("CustomDataResponse", &resp.correlation_id, data.len());
1407            log::trace!("{RECV} {resp:?}");
1408        } else {
1409            log_received(&resp);
1410        }
1411
1412        if let Err(e) = self.on_historical_data(resp.data.as_ref()) {
1413            log_error(&e);
1414        }
1415    }
1416
1417    /// Handles an instrument response.
1418    fn handle_instrument_response(&mut self, resp: &InstrumentResponse) {
1419        log_received(&resp);
1420
1421        if let Err(e) = self.on_instrument(&resp.data) {
1422            log_error(&e);
1423        }
1424    }
1425
1426    /// Handles an instruments response.
1427    fn handle_instruments_response(&mut self, resp: &InstrumentsResponse) {
1428        log_received_bulk("InstrumentsResponse", &resp.correlation_id, resp.data.len());
1429        log::trace!("{RECV} {resp:?}");
1430
1431        for inst in &resp.data {
1432            if let Err(e) = self.on_instrument(inst) {
1433                log_error(&e);
1434            }
1435        }
1436    }
1437
1438    /// Handles a book response.
1439    fn handle_book_response(&mut self, resp: &BookResponse) {
1440        log_received(&resp);
1441
1442        if let Err(e) = self.on_book(&resp.data) {
1443            log_error(&e);
1444        }
1445    }
1446
1447    /// Handles a book deltas response.
1448    fn handle_book_deltas_response(&mut self, resp: &BookDeltasResponse) {
1449        log_received_bulk("BookDeltasResponse", &resp.correlation_id, resp.data.len());
1450        log::trace!("{RECV} {resp:?}");
1451
1452        if let Err(e) = self.on_historical_book_deltas(&resp.data) {
1453            log_error(&e);
1454        }
1455    }
1456
1457    /// Handles a book depth response.
1458    fn handle_book_depth_response(&mut self, resp: &BookDepthResponse) {
1459        log_received_bulk("BookDepthResponse", &resp.correlation_id, resp.data.len());
1460        log::trace!("{RECV} {resp:?}");
1461
1462        if let Err(e) = self.on_historical_book_depth(&resp.data) {
1463            log_error(&e);
1464        }
1465    }
1466
1467    /// Handles a quotes response.
1468    fn handle_quotes_response(&mut self, resp: &QuotesResponse)
1469    where
1470        Self: DataActorNative,
1471    {
1472        log_received_bulk("QuotesResponse", &resp.correlation_id, resp.data.len());
1473        log::trace!("{RECV} {resp:?}");
1474
1475        if let Err(e) = self.core().handle_indicators_for_quotes(&resp.data) {
1476            log_error(&e);
1477            return;
1478        }
1479
1480        if let Err(e) = self.on_historical_quotes(&resp.data) {
1481            log_error(&e);
1482        }
1483    }
1484
1485    /// Handles a trades response.
1486    fn handle_trades_response(&mut self, resp: &TradesResponse)
1487    where
1488        Self: DataActorNative,
1489    {
1490        log_received_bulk("TradesResponse", &resp.correlation_id, resp.data.len());
1491        log::trace!("{RECV} {resp:?}");
1492
1493        if let Err(e) = self.core().handle_indicators_for_trades(&resp.data) {
1494            log_error(&e);
1495            return;
1496        }
1497
1498        if let Err(e) = self.on_historical_trades(&resp.data) {
1499            log_error(&e);
1500        }
1501    }
1502
1503    /// Handles a bars response.
1504    fn handle_bars_response(&mut self, resp: &BarsResponse)
1505    where
1506        Self: DataActorNative,
1507    {
1508        log_received_bulk("BarsResponse", &resp.correlation_id, resp.data.len());
1509        log::trace!("{RECV} {resp:?}");
1510
1511        if let Err(e) = self.core().handle_indicators_for_bars(&resp.data) {
1512            log_error(&e);
1513            return;
1514        }
1515
1516        if let Err(e) = self.on_historical_bars(&resp.data) {
1517            log_error(&e);
1518        }
1519    }
1520
1521    /// Handles a funding rates response.
1522    fn handle_funding_rates_response(&mut self, resp: &FundingRatesResponse) {
1523        log_received_bulk(
1524            "FundingRatesResponse",
1525            &resp.correlation_id,
1526            resp.data.len(),
1527        );
1528        log::trace!("{RECV} {resp:?}");
1529
1530        if let Err(e) = self.on_historical_funding_rates(&resp.data) {
1531            log_error(&e);
1532        }
1533    }
1534
1535    /// Subscribe to streaming `data_type` data.
1536    fn subscribe_data(
1537        &mut self,
1538        data_type: DataType,
1539        client_id: Option<ClientId>,
1540        params: Option<Params>,
1541    ) where
1542        Self: DataActorNative,
1543        Self: 'static + Debug + Sized,
1544    {
1545        let actor_id = self.core().actor_id().inner();
1546        let handler = ShareableMessageHandler::from_typed(move |data: &CustomData| {
1547            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1548                actor.handle_data(data);
1549            } else {
1550                log::error!("Actor {actor_id} not found for data handling");
1551            }
1552        });
1553
1554        DataActorCore::subscribe_data(self.core_mut(), handler, data_type, client_id, params);
1555    }
1556
1557    /// Subscribe to [`Signal`] data by `name`.
1558    ///
1559    /// An empty `name` subscribes to every signal.
1560    ///
1561    /// # Parameters
1562    ///
1563    /// - `name`: signal name to subscribe to.
1564    /// - `priority`: optional dispatch priority. Pass `None` for default
1565    ///   ordering (by pattern then handler ID). Pass `Some(p)` when actors
1566    ///   sharing a signal need deterministic ordering: higher-priority
1567    ///   handlers receive the message before lower-priority handlers.
1568    ///
1569    /// Re-subscribing does not update an existing priority; call
1570    /// [`unsubscribe_signal`](Self::unsubscribe_signal) first.
1571    fn subscribe_signal(&mut self, name: &str, priority: Option<u32>)
1572    where
1573        Self: DataActorNative,
1574        Self: 'static + Debug + Sized,
1575    {
1576        let actor_id = self.core().actor_id().inner();
1577        // Signals are published as `CustomData` wrapping a `Signal`; downcast
1578        // the inner value so subscribers receive the typed `Signal` in `on_signal`.
1579        let handler = ShareableMessageHandler::from_typed(move |data: &CustomData| {
1580            if let Some(signal) = data.data.as_any().downcast_ref::<Signal>() {
1581                if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1582                    actor.handle_signal(signal);
1583                } else {
1584                    log::error!("Actor {actor_id} not found for signal handling");
1585                }
1586            }
1587        });
1588
1589        DataActorCore::subscribe_signal(self.core_mut(), handler, name, priority);
1590    }
1591
1592    /// Subscribes to [`QueueStateChanged`] events.
1593    ///
1594    /// `channel=None` matches all runner channels.
1595    ///
1596    /// `priority` controls dispatch order when multiple actors subscribe to the event. Higher
1597    /// values receive the event first. Re-subscribing does not update an existing priority; call
1598    /// [`unsubscribe_queue_state`](Self::unsubscribe_queue_state) first.
1599    fn subscribe_queue_state(&mut self, channel: Option<SystemChannel>, priority: Option<u32>)
1600    where
1601        Self: DataActorNative,
1602        Self: 'static + Debug + Sized,
1603    {
1604        let actor_id = self.core().actor_id().inner();
1605        let handler = ShareableMessageHandler::from_typed(move |event: &QueueStateChanged| {
1606            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1607                actor.handle_queue_state(event);
1608            } else {
1609                log::error!("Actor {actor_id} not found for queue state change handling");
1610            }
1611        });
1612
1613        DataActorCore::subscribe_queue_state(self.core_mut(), handler, channel, priority);
1614    }
1615
1616    /// Subscribes to [`SocketStateChanged`] events.
1617    ///
1618    /// `client_id=None` and `endpoint=None` each match all values of that field.
1619    /// Supplied filters match literal values, including dots and wildcard characters.
1620    ///
1621    /// `priority` controls dispatch order when multiple actors subscribe to the event. Higher
1622    /// values receive the event first. Re-subscribing does not update an existing priority; call
1623    /// [`unsubscribe_socket_state`](Self::unsubscribe_socket_state) first.
1624    fn subscribe_socket_state(
1625        &mut self,
1626        client_id: Option<ClientId>,
1627        endpoint: Option<&str>,
1628        priority: Option<u32>,
1629    ) where
1630        Self: DataActorNative,
1631        Self: 'static + Debug + Sized,
1632    {
1633        let actor_id = self.core().actor_id().inner();
1634        let handler = ShareableMessageHandler::from_typed(move |event: &SocketStateChanged| {
1635            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1636                actor.handle_socket_state(event);
1637            } else {
1638                log::error!("Actor {actor_id} not found for socket state change handling");
1639            }
1640        });
1641
1642        DataActorCore::subscribe_socket_state(
1643            self.core_mut(),
1644            handler,
1645            client_id,
1646            endpoint,
1647            priority,
1648        );
1649    }
1650
1651    /// Subscribe to streaming [`QuoteTick`] data for one or more instruments.
1652    ///
1653    /// Duplicate IDs are removed in first-occurrence order.
1654    ///
1655    /// # Panics
1656    ///
1657    /// Panics if:
1658    /// - The identifier collection is empty.
1659    /// - The IDs span venues without an explicit client.
1660    /// - The client is not external and the list mixes client, synthetic, or spread quote routes.
1661    fn subscribe_quotes(
1662        &mut self,
1663        instrument_ids: impl IntoInstrumentIds,
1664        client_id: Option<ClientId>,
1665        params: Option<Params>,
1666    ) where
1667        Self: DataActorNative,
1668        Self: 'static + Debug + Sized,
1669    {
1670        let actor_id = self.core().actor_id().inner();
1671        let handler = TypedHandler::from(move |quote: &QuoteTick| {
1672            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1673                actor.handle_quote(quote);
1674            } else {
1675                log::error!("Actor {actor_id} not found for quote handling");
1676            }
1677        });
1678
1679        DataActorCore::subscribe_quotes(
1680            self.core_mut(),
1681            handler,
1682            instrument_ids,
1683            client_id,
1684            params,
1685        )
1686        .expect("invalid quote subscription identifier list");
1687    }
1688
1689    /// Subscribe to streaming [`InstrumentAny`] data for the `venue`.
1690    fn subscribe_instruments(
1691        &mut self,
1692        venue: Venue,
1693        client_id: Option<ClientId>,
1694        params: Option<Params>,
1695    ) where
1696        Self: DataActorNative,
1697        Self: 'static + Debug + Sized,
1698    {
1699        let actor_id = self.core().actor_id().inner();
1700        let pattern = get_instruments_pattern(venue);
1701
1702        let handler = TypedHandler::from(move |instrument: &InstrumentAny| {
1703            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1704                actor.handle_instrument(instrument);
1705            } else {
1706                log::error!("Actor {actor_id} not found for instruments handling");
1707            }
1708        });
1709
1710        DataActorCore::subscribe_instruments(
1711            self.core_mut(),
1712            pattern,
1713            handler,
1714            venue,
1715            client_id,
1716            params,
1717        );
1718    }
1719
1720    /// Subscribe to streaming [`InstrumentAny`] data for one or more instruments.
1721    ///
1722    /// Duplicate IDs are removed in first-occurrence order.
1723    ///
1724    /// # Panics
1725    ///
1726    /// Panics if:
1727    /// - The IDs are empty.
1728    /// - The IDs span venues without an explicit client.
1729    /// - The client is not external and the list contains multiple IDs with a synthetic member.
1730    fn subscribe_instrument(
1731        &mut self,
1732        instrument_ids: impl IntoInstrumentIds,
1733        client_id: Option<ClientId>,
1734        params: Option<Params>,
1735    ) where
1736        Self: DataActorNative,
1737        Self: 'static + Debug + Sized,
1738    {
1739        let actor_id = self.core().actor_id().inner();
1740        let handler = TypedHandler::from(move |instrument: &InstrumentAny| {
1741            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1742                actor.handle_instrument(instrument);
1743            } else {
1744                log::error!("Actor {actor_id} not found for instrument handling");
1745            }
1746        });
1747
1748        DataActorCore::subscribe_instrument(
1749            self.core_mut(),
1750            handler,
1751            instrument_ids,
1752            client_id,
1753            params,
1754        )
1755        .expect("invalid instrument subscription identifier list");
1756    }
1757
1758    /// Subscribe to streaming [`OrderBookDeltas`] data for one or more instruments.
1759    ///
1760    /// When `managed` is true, the data engine maintains an [`OrderBook`] in the cache for each
1761    /// instrument the subscription resolves to, applying each batch of deltas as it arrives.
1762    /// A parent subscription resolves to every matching underlying instrument.
1763    ///
1764    /// Duplicate IDs are removed in first-occurrence order.
1765    ///
1766    /// # Panics
1767    ///
1768    /// Panics if:
1769    /// - The IDs are empty.
1770    /// - The IDs span venues without an explicit client.
1771    /// - The client is not external and the list contains multiple IDs with a synthetic member.
1772    fn subscribe_book_deltas(
1773        &mut self,
1774        instrument_ids: impl IntoInstrumentIds,
1775        book_type: BookType,
1776        depth: Option<NonZeroUsize>,
1777        client_id: Option<ClientId>,
1778        managed: bool,
1779        params: Option<Params>,
1780    ) where
1781        Self: DataActorNative,
1782        Self: 'static + Debug + Sized,
1783    {
1784        let actor_id = self.core().actor_id().inner();
1785        let handler = TypedHandler::from(move |deltas: &OrderBookDeltas| {
1786            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1787                actor.handle_book_deltas(deltas);
1788            } else {
1789                log::error!("Actor {actor_id} not found for book deltas handling");
1790            }
1791        });
1792
1793        DataActorCore::subscribe_book_deltas(
1794            self.core_mut(),
1795            handler,
1796            instrument_ids,
1797            book_type,
1798            depth,
1799            client_id,
1800            managed,
1801            params,
1802        )
1803        .expect("invalid instrument subscription identifier list");
1804    }
1805
1806    /// Subscribe to streaming [`OrderBookDepth`] data for one or more instruments.
1807    ///
1808    /// `depth` limits the number of levels per side; `None` uses the adapter default.
1809    /// Venue channel limits still apply.
1810    ///
1811    /// When `managed` is true, the data engine maintains an [`OrderBook`] in the cache for each
1812    /// instrument the subscription resolves to, applying each update as it arrives.
1813    /// A parent subscription resolves to every matching underlying instrument.
1814    ///
1815    /// Duplicate IDs are removed in first-occurrence order.
1816    ///
1817    /// # Panics
1818    ///
1819    /// Panics if:
1820    /// - The IDs are empty.
1821    /// - The IDs span venues without an explicit client.
1822    /// - The client is not external and the list contains multiple IDs with a synthetic member.
1823    fn subscribe_book_depth(
1824        &mut self,
1825        instrument_ids: impl IntoInstrumentIds,
1826        book_type: BookType,
1827        depth: Option<NonZeroUsize>,
1828        client_id: Option<ClientId>,
1829        managed: bool,
1830        params: Option<Params>,
1831    ) where
1832        Self: DataActorNative,
1833        Self: 'static + Debug + Sized,
1834    {
1835        let actor_id = self.core().actor_id().inner();
1836        let handler = TypedHandler::from(move |depth: &OrderBookDepth| {
1837            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1838                actor.handle_book_depth(depth);
1839            } else {
1840                log::error!("Actor {actor_id} not found for book depth handling");
1841            }
1842        });
1843
1844        DataActorCore::subscribe_book_depth(
1845            self.core_mut(),
1846            handler,
1847            instrument_ids,
1848            book_type,
1849            depth,
1850            client_id,
1851            managed,
1852            params,
1853        )
1854        .expect("invalid instrument subscription identifier list");
1855    }
1856
1857    /// Subscribe to [`OrderBook`] snapshots at a specified interval for one or more instruments.
1858    ///
1859    /// Duplicate IDs are removed in first-occurrence order.
1860    ///
1861    /// # Panics
1862    ///
1863    /// Panics if:
1864    /// - The IDs are empty.
1865    /// - The IDs span venues without an explicit client.
1866    /// - The client is not external and the list contains multiple IDs with a synthetic member.
1867    fn subscribe_book_at_interval(
1868        &mut self,
1869        instrument_ids: impl IntoInstrumentIds,
1870        book_type: BookType,
1871        depth: Option<NonZeroUsize>,
1872        interval_ms: NonZeroUsize,
1873        client_id: Option<ClientId>,
1874        params: Option<Params>,
1875    ) where
1876        Self: DataActorNative,
1877        Self: 'static + Debug + Sized,
1878    {
1879        let actor_id = self.core().actor_id().inner();
1880        let handler = TypedHandler::from(move |book: &OrderBook| {
1881            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1882                actor.handle_book(book);
1883            } else {
1884                log::error!("Actor {actor_id} not found for book handling");
1885            }
1886        });
1887
1888        DataActorCore::subscribe_book_at_interval(
1889            self.core_mut(),
1890            handler,
1891            instrument_ids,
1892            book_type,
1893            depth,
1894            interval_ms,
1895            client_id,
1896            params,
1897        )
1898        .expect("invalid instrument subscription identifier list");
1899    }
1900
1901    /// Subscribe to streaming [`TradeTick`] data for one or more instruments.
1902    ///
1903    /// Duplicate IDs are removed in first-occurrence order.
1904    ///
1905    /// # Panics
1906    ///
1907    /// Panics if:
1908    /// - The identifier collection is empty.
1909    /// - The IDs span venues without an explicit client.
1910    fn subscribe_trades(
1911        &mut self,
1912        instrument_ids: impl IntoInstrumentIds,
1913        client_id: Option<ClientId>,
1914        params: Option<Params>,
1915    ) where
1916        Self: DataActorNative,
1917        Self: 'static + Debug + Sized,
1918    {
1919        let actor_id = self.core().actor_id().inner();
1920        let handler = TypedHandler::from(move |trade: &TradeTick| {
1921            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1922                actor.handle_trade(trade);
1923            } else {
1924                log::error!("Actor {actor_id} not found for trade handling");
1925            }
1926        });
1927
1928        DataActorCore::subscribe_trades(
1929            self.core_mut(),
1930            handler,
1931            instrument_ids,
1932            client_id,
1933            params,
1934        )
1935        .expect("invalid trade subscription identifier list");
1936    }
1937
1938    /// Subscribe to streaming [`Bar`] data for one or more bar types.
1939    ///
1940    /// Duplicate types are removed in first-occurrence order.
1941    ///
1942    /// # Panics
1943    ///
1944    /// Panics if the list is empty or spans venues without an explicit client.
1945    fn subscribe_bars(
1946        &mut self,
1947        bar_types: impl IntoBarTypes,
1948        client_id: Option<ClientId>,
1949        params: Option<Params>,
1950    ) where
1951        Self: DataActorNative,
1952        Self: 'static + Debug + Sized,
1953    {
1954        let actor_id = self.core().actor_id().inner();
1955        let handler = TypedHandler::from(move |bar: &Bar| {
1956            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1957                actor.handle_bar(bar);
1958            } else {
1959                log::error!("Actor {actor_id} not found for bar handling");
1960            }
1961        });
1962
1963        DataActorCore::subscribe_bars(self.core_mut(), handler, bar_types, client_id, params)
1964            .expect("invalid bar subscription type list");
1965    }
1966
1967    /// Subscribe to streaming [`MarkPriceUpdate`] data for one or more instruments.
1968    ///
1969    /// Duplicate IDs are removed in first-occurrence order.
1970    ///
1971    /// # Panics
1972    ///
1973    /// Panics if:
1974    /// - The IDs are empty.
1975    /// - The IDs span venues without an explicit client.
1976    fn subscribe_mark_prices(
1977        &mut self,
1978        instrument_ids: impl IntoInstrumentIds,
1979        client_id: Option<ClientId>,
1980        params: Option<Params>,
1981    ) where
1982        Self: DataActorNative,
1983        Self: 'static + Debug + Sized,
1984    {
1985        let actor_id = self.core().actor_id().inner();
1986        let handler = TypedHandler::from(move |mark_price: &MarkPriceUpdate| {
1987            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
1988                actor.handle_mark_price(mark_price);
1989            } else {
1990                log::error!("Actor {actor_id} not found for mark price handling");
1991            }
1992        });
1993
1994        DataActorCore::subscribe_mark_prices(
1995            self.core_mut(),
1996            handler,
1997            instrument_ids,
1998            client_id,
1999            params,
2000        )
2001        .expect("invalid instrument subscription identifier list");
2002    }
2003
2004    /// Subscribe to streaming [`IndexPriceUpdate`] data for one or more instruments.
2005    ///
2006    /// Duplicate IDs are removed in first-occurrence order.
2007    ///
2008    /// # Panics
2009    ///
2010    /// Panics if:
2011    /// - The IDs are empty.
2012    /// - The IDs span venues without an explicit client.
2013    fn subscribe_index_prices(
2014        &mut self,
2015        instrument_ids: impl IntoInstrumentIds,
2016        client_id: Option<ClientId>,
2017        params: Option<Params>,
2018    ) where
2019        Self: DataActorNative,
2020        Self: 'static + Debug + Sized,
2021    {
2022        let actor_id = self.core().actor_id().inner();
2023        let handler = TypedHandler::from(move |index_price: &IndexPriceUpdate| {
2024            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
2025                actor.handle_index_price(index_price);
2026            } else {
2027                log::error!("Actor {actor_id} not found for index price handling");
2028            }
2029        });
2030
2031        DataActorCore::subscribe_index_prices(
2032            self.core_mut(),
2033            handler,
2034            instrument_ids,
2035            client_id,
2036            params,
2037        )
2038        .expect("invalid instrument subscription identifier list");
2039    }
2040
2041    /// Subscribe to streaming [`FundingRateUpdate`] data for one or more instruments.
2042    ///
2043    /// Duplicate IDs are removed in first-occurrence order.
2044    ///
2045    /// # Panics
2046    ///
2047    /// Panics if:
2048    /// - The IDs are empty.
2049    /// - The IDs span venues without an explicit client.
2050    fn subscribe_funding_rates(
2051        &mut self,
2052        instrument_ids: impl IntoInstrumentIds,
2053        client_id: Option<ClientId>,
2054        params: Option<Params>,
2055    ) where
2056        Self: DataActorNative,
2057        Self: 'static + Debug + Sized,
2058    {
2059        let actor_id = self.core().actor_id().inner();
2060        let handler = TypedHandler::from(move |funding_rate: &FundingRateUpdate| {
2061            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
2062                actor.handle_funding_rate(funding_rate);
2063            } else {
2064                log::error!("Actor {actor_id} not found for funding rate handling");
2065            }
2066        });
2067
2068        DataActorCore::subscribe_funding_rates(
2069            self.core_mut(),
2070            handler,
2071            instrument_ids,
2072            client_id,
2073            params,
2074        )
2075        .expect("invalid instrument subscription identifier list");
2076    }
2077
2078    /// Subscribe to streaming [`OptionGreeks`] data for one or more instruments.
2079    ///
2080    /// Duplicate IDs are removed in first-occurrence order.
2081    ///
2082    /// # Panics
2083    ///
2084    /// Panics if:
2085    /// - The IDs are empty.
2086    /// - The IDs span venues without an explicit client.
2087    /// - The client is not external and the list contains multiple IDs with a synthetic member.
2088    fn subscribe_option_greeks(
2089        &mut self,
2090        instrument_ids: impl IntoInstrumentIds,
2091        client_id: Option<ClientId>,
2092        params: Option<Params>,
2093    ) where
2094        Self: DataActorNative,
2095        Self: 'static + Debug + Sized,
2096    {
2097        let actor_id = self.core().actor_id().inner();
2098        let handler = TypedHandler::from(move |option_greeks: &OptionGreeks| {
2099            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
2100                actor.handle_option_greeks(option_greeks);
2101            } else {
2102                log::error!("Actor {actor_id} not found for option greeks handling");
2103            }
2104        });
2105
2106        DataActorCore::subscribe_option_greeks(
2107            self.core_mut(),
2108            handler,
2109            instrument_ids,
2110            client_id,
2111            params,
2112        )
2113        .expect("invalid instrument subscription identifier list");
2114    }
2115
2116    /// Subscribe to streaming [`InstrumentStatus`] data for one or more instruments.
2117    ///
2118    /// Duplicate IDs are removed in first-occurrence order.
2119    ///
2120    /// # Panics
2121    ///
2122    /// Panics if:
2123    /// - The IDs are empty.
2124    /// - The IDs span venues without an explicit client.
2125    /// - The client is not external and the list contains multiple IDs with a synthetic member.
2126    fn subscribe_instrument_status(
2127        &mut self,
2128        instrument_ids: impl IntoInstrumentIds,
2129        client_id: Option<ClientId>,
2130        params: Option<Params>,
2131    ) where
2132        Self: DataActorNative,
2133        Self: 'static + Debug + Sized,
2134    {
2135        let actor_id = self.core().actor_id().inner();
2136        let handler = ShareableMessageHandler::from_typed(move |status: &InstrumentStatus| {
2137            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
2138                actor.handle_instrument_status(status);
2139            } else {
2140                log::error!("Actor {actor_id} not found for instrument status handling");
2141            }
2142        });
2143
2144        DataActorCore::subscribe_instrument_status(
2145            self.core_mut(),
2146            handler,
2147            instrument_ids,
2148            client_id,
2149            params,
2150        )
2151        .expect("invalid instrument subscription identifier list");
2152    }
2153
2154    /// Subscribe to streaming [`InstrumentClose`] data for one or more instruments.
2155    ///
2156    /// Duplicate IDs are removed in first-occurrence order.
2157    ///
2158    /// # Panics
2159    ///
2160    /// Panics if:
2161    /// - The IDs are empty.
2162    /// - The IDs span venues without an explicit client.
2163    /// - The client is not external and the list contains multiple IDs with a synthetic member.
2164    fn subscribe_instrument_close(
2165        &mut self,
2166        instrument_ids: impl IntoInstrumentIds,
2167        client_id: Option<ClientId>,
2168        params: Option<Params>,
2169    ) where
2170        Self: DataActorNative,
2171        Self: 'static + Debug + Sized,
2172    {
2173        let actor_id = self.core().actor_id().inner();
2174        let handler = ShareableMessageHandler::from_typed(move |close: &InstrumentClose| {
2175            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
2176                actor.handle_instrument_close(close);
2177            } else {
2178                log::error!("Actor {actor_id} not found for instrument close handling");
2179            }
2180        });
2181
2182        DataActorCore::subscribe_instrument_close(
2183            self.core_mut(),
2184            handler,
2185            instrument_ids,
2186            client_id,
2187            params,
2188        )
2189        .expect("invalid instrument subscription identifier list");
2190    }
2191
2192    /// Subscribe to streaming [`OptionChainSlice`] snapshots for the option `series_id`.
2193    ///
2194    /// The ATM price is always derived from the exchange-provided forward price
2195    /// embedded in each option greeks/ticker update.
2196    fn subscribe_option_chain(
2197        &mut self,
2198        series_id: OptionSeriesId,
2199        strike_range: StrikeRange,
2200        snapshot_interval_ms: Option<u64>,
2201        client_id: Option<ClientId>,
2202        params: Option<Params>,
2203    ) where
2204        Self: DataActorNative,
2205        Self: 'static + Debug + Sized,
2206    {
2207        let actor_id = self.core().actor_id().inner();
2208        let topic = get_option_chain_topic(series_id);
2209
2210        let handler = TypedHandler::from(move |slice: &OptionChainSlice| {
2211            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
2212                actor.handle_option_chain(slice);
2213            } else {
2214                log::error!("Actor {actor_id} not found for option chain handling");
2215            }
2216        });
2217
2218        DataActorCore::subscribe_option_chain(
2219            self.core_mut(),
2220            topic,
2221            handler,
2222            series_id,
2223            strike_range,
2224            snapshot_interval_ms,
2225            client_id,
2226            params,
2227        );
2228    }
2229
2230    #[cfg(feature = "defi")]
2231    /// Subscribe to streaming [`Block`] data for the `chain`.
2232    fn subscribe_blocks(
2233        &mut self,
2234        chain: Blockchain,
2235        client_id: Option<ClientId>,
2236        params: Option<Params>,
2237    ) where
2238        Self: DataActorNative,
2239        Self: 'static + Debug + Sized,
2240    {
2241        let actor_id = self.core().actor_id().inner();
2242        let topic = defi::switchboard::get_defi_blocks_topic(chain);
2243
2244        let handler = TypedHandler::from(move |block: &Block| {
2245            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
2246                actor.handle_block(block);
2247            } else {
2248                log::error!("Actor {actor_id} not found for block handling");
2249            }
2250        });
2251
2252        DataActorCore::subscribe_blocks(self.core_mut(), topic, handler, chain, client_id, params);
2253    }
2254
2255    #[cfg(feature = "defi")]
2256    /// Subscribe to streaming [`Pool`] definition updates for the AMM pool at the `instrument_id`.
2257    fn subscribe_pool(
2258        &mut self,
2259        instrument_id: InstrumentId,
2260        client_id: Option<ClientId>,
2261        params: Option<Params>,
2262    ) where
2263        Self: DataActorNative,
2264        Self: 'static + Debug + Sized,
2265    {
2266        let actor_id = self.core().actor_id().inner();
2267        let topic = defi::switchboard::get_defi_pool_topic(instrument_id);
2268
2269        let handler = TypedHandler::from(move |pool: &Pool| {
2270            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
2271                actor.handle_pool(pool);
2272            } else {
2273                log::error!("Actor {actor_id} not found for pool handling");
2274            }
2275        });
2276
2277        DataActorCore::subscribe_pool(
2278            self.core_mut(),
2279            topic,
2280            handler,
2281            instrument_id,
2282            client_id,
2283            params,
2284        );
2285    }
2286
2287    #[cfg(feature = "defi")]
2288    /// Subscribe to streaming [`PoolSwap`] data for the `instrument_id`.
2289    fn subscribe_pool_swaps(
2290        &mut self,
2291        instrument_id: InstrumentId,
2292        client_id: Option<ClientId>,
2293        params: Option<Params>,
2294    ) where
2295        Self: DataActorNative,
2296        Self: 'static + Debug + Sized,
2297    {
2298        let actor_id = self.core().actor_id().inner();
2299        let topic = defi::switchboard::get_defi_pool_swaps_topic(instrument_id);
2300
2301        let handler = TypedHandler::from(move |swap: &PoolSwap| {
2302            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
2303                actor.handle_pool_swap(swap);
2304            } else {
2305                log::error!("Actor {actor_id} not found for pool swap handling");
2306            }
2307        });
2308
2309        DataActorCore::subscribe_pool_swaps(
2310            self.core_mut(),
2311            topic,
2312            handler,
2313            instrument_id,
2314            client_id,
2315            params,
2316        );
2317    }
2318
2319    #[cfg(feature = "defi")]
2320    /// Subscribe to streaming [`PoolLiquidityUpdate`] data for the `instrument_id`.
2321    fn subscribe_pool_liquidity_updates(
2322        &mut self,
2323        instrument_id: InstrumentId,
2324        client_id: Option<ClientId>,
2325        params: Option<Params>,
2326    ) where
2327        Self: DataActorNative,
2328        Self: 'static + Debug + Sized,
2329    {
2330        let actor_id = self.core().actor_id().inner();
2331        let topic = defi::switchboard::get_defi_liquidity_topic(instrument_id);
2332
2333        let handler = TypedHandler::from(move |update: &PoolLiquidityUpdate| {
2334            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
2335                actor.handle_pool_liquidity_update(update);
2336            } else {
2337                log::error!("Actor {actor_id} not found for pool liquidity update handling");
2338            }
2339        });
2340
2341        DataActorCore::subscribe_pool_liquidity_updates(
2342            self.core_mut(),
2343            topic,
2344            handler,
2345            instrument_id,
2346            client_id,
2347            params,
2348        );
2349    }
2350
2351    #[cfg(feature = "defi")]
2352    /// Subscribe to streaming [`PoolFeeCollect`] data for the `instrument_id`.
2353    fn subscribe_pool_fee_collects(
2354        &mut self,
2355        instrument_id: InstrumentId,
2356        client_id: Option<ClientId>,
2357        params: Option<Params>,
2358    ) where
2359        Self: DataActorNative,
2360        Self: 'static + Debug + Sized,
2361    {
2362        let actor_id = self.core().actor_id().inner();
2363        let topic = defi::switchboard::get_defi_collect_topic(instrument_id);
2364
2365        let handler = TypedHandler::from(move |collect: &PoolFeeCollect| {
2366            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
2367                actor.handle_pool_fee_collect(collect);
2368            } else {
2369                log::error!("Actor {actor_id} not found for pool fee collect handling");
2370            }
2371        });
2372
2373        DataActorCore::subscribe_pool_fee_collects(
2374            self.core_mut(),
2375            topic,
2376            handler,
2377            instrument_id,
2378            client_id,
2379            params,
2380        );
2381    }
2382
2383    #[cfg(feature = "defi")]
2384    /// Subscribe to streaming [`PoolFlash`] events for the given `instrument_id`.
2385    fn subscribe_pool_flash_events(
2386        &mut self,
2387        instrument_id: InstrumentId,
2388        client_id: Option<ClientId>,
2389        params: Option<Params>,
2390    ) where
2391        Self: DataActorNative,
2392        Self: 'static + Debug + Sized,
2393    {
2394        let actor_id = self.core().actor_id().inner();
2395        let topic = defi::switchboard::get_defi_flash_topic(instrument_id);
2396
2397        let handler = TypedHandler::from(move |flash: &PoolFlash| {
2398            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
2399                actor.handle_pool_flash(flash);
2400            } else {
2401                log::error!("Actor {actor_id} not found for pool flash handling");
2402            }
2403        });
2404
2405        DataActorCore::subscribe_pool_flash_events(
2406            self.core_mut(),
2407            topic,
2408            handler,
2409            instrument_id,
2410            client_id,
2411            params,
2412        );
2413    }
2414
2415    /// Unsubscribe from streaming `data_type` data.
2416    fn unsubscribe_data(
2417        &mut self,
2418        data_type: DataType,
2419        client_id: Option<ClientId>,
2420        params: Option<Params>,
2421    ) where
2422        Self: DataActorNative,
2423        Self: 'static + Debug + Sized,
2424    {
2425        DataActorCore::unsubscribe_data(self.core_mut(), data_type, client_id, params);
2426    }
2427
2428    /// Unsubscribe from [`Signal`] data by `name`.
2429    fn unsubscribe_signal(&mut self, name: &str)
2430    where
2431        Self: DataActorNative,
2432        Self: 'static + Debug + Sized,
2433    {
2434        DataActorCore::unsubscribe_signal(self.core_mut(), name);
2435    }
2436
2437    /// Unsubscribes from [`QueueStateChanged`] events for the same subscription filters.
2438    ///
2439    /// Omitted filters identify the all-values subscription, not every filtered subscription.
2440    fn unsubscribe_queue_state(&mut self, channel: Option<SystemChannel>)
2441    where
2442        Self: DataActorNative,
2443        Self: 'static + Debug + Sized,
2444    {
2445        DataActorCore::unsubscribe_queue_state(self.core_mut(), channel);
2446    }
2447
2448    /// Unsubscribes from [`SocketStateChanged`] events for the same subscription filters.
2449    ///
2450    /// Omitted filters identify the all-values subscription, not every filtered subscription.
2451    fn unsubscribe_socket_state(&mut self, client_id: Option<ClientId>, endpoint: Option<&str>)
2452    where
2453        Self: DataActorNative,
2454        Self: 'static + Debug + Sized,
2455    {
2456        DataActorCore::unsubscribe_socket_state(self.core_mut(), client_id, endpoint);
2457    }
2458
2459    /// Unsubscribe from streaming [`InstrumentAny`] data for the `venue`.
2460    fn unsubscribe_instruments(
2461        &mut self,
2462        venue: Venue,
2463        client_id: Option<ClientId>,
2464        params: Option<Params>,
2465    ) where
2466        Self: DataActorNative,
2467        Self: 'static + Debug + Sized,
2468    {
2469        DataActorCore::unsubscribe_instruments(self.core_mut(), venue, client_id, params);
2470    }
2471
2472    /// Unsubscribe from streaming [`InstrumentAny`] data for one or more instruments.
2473    ///
2474    /// Duplicate IDs are removed in first-occurrence order.
2475    ///
2476    /// # Panics
2477    ///
2478    /// Panics if:
2479    /// - The IDs are empty.
2480    /// - The IDs span venues without an explicit client.
2481    fn unsubscribe_instrument(
2482        &mut self,
2483        instrument_ids: impl IntoInstrumentIds,
2484        client_id: Option<ClientId>,
2485        params: Option<Params>,
2486    ) where
2487        Self: DataActorNative,
2488        Self: 'static + Debug + Sized,
2489    {
2490        DataActorCore::unsubscribe_instrument(self.core_mut(), instrument_ids, client_id, params)
2491            .expect("invalid instrument subscription identifier list");
2492    }
2493
2494    /// Unsubscribe from streaming [`OrderBookDeltas`] data for one or more instruments.
2495    ///
2496    /// Duplicate IDs are removed in first-occurrence order.
2497    ///
2498    /// # Panics
2499    ///
2500    /// Panics if:
2501    /// - The IDs are empty.
2502    /// - The IDs span venues without an explicit client.
2503    fn unsubscribe_book_deltas(
2504        &mut self,
2505        instrument_ids: impl IntoInstrumentIds,
2506        client_id: Option<ClientId>,
2507        params: Option<Params>,
2508    ) where
2509        Self: DataActorNative,
2510        Self: 'static + Debug + Sized,
2511    {
2512        DataActorCore::unsubscribe_book_deltas(self.core_mut(), instrument_ids, client_id, params)
2513            .expect("invalid instrument subscription identifier list");
2514    }
2515
2516    /// Unsubscribe from streaming [`OrderBookDepth`] data for one or more instruments.
2517    ///
2518    /// Duplicate IDs are removed in first-occurrence order.
2519    ///
2520    /// # Panics
2521    ///
2522    /// Panics if:
2523    /// - The IDs are empty.
2524    /// - The IDs span venues without an explicit client.
2525    fn unsubscribe_book_depth(
2526        &mut self,
2527        instrument_ids: impl IntoInstrumentIds,
2528        client_id: Option<ClientId>,
2529        params: Option<Params>,
2530    ) where
2531        Self: DataActorNative,
2532        Self: 'static + Debug + Sized,
2533    {
2534        DataActorCore::unsubscribe_book_depth(self.core_mut(), instrument_ids, client_id, params)
2535            .expect("invalid instrument subscription identifier list");
2536    }
2537
2538    /// Unsubscribe from [`OrderBook`] snapshots at a specified interval for one or more instruments.
2539    ///
2540    /// Duplicate IDs are removed in first-occurrence order.
2541    ///
2542    /// # Panics
2543    ///
2544    /// Panics if:
2545    /// - The IDs are empty.
2546    /// - The IDs span venues without an explicit client.
2547    fn unsubscribe_book_at_interval(
2548        &mut self,
2549        instrument_ids: impl IntoInstrumentIds,
2550        interval_ms: NonZeroUsize,
2551        client_id: Option<ClientId>,
2552        params: Option<Params>,
2553    ) where
2554        Self: DataActorNative,
2555        Self: 'static + Debug + Sized,
2556    {
2557        DataActorCore::unsubscribe_book_at_interval(
2558            self.core_mut(),
2559            instrument_ids,
2560            interval_ms,
2561            client_id,
2562            params,
2563        )
2564        .expect("invalid instrument subscription identifier list");
2565    }
2566
2567    /// Unsubscribe from streaming [`QuoteTick`] data for one or more instruments.
2568    ///
2569    /// Duplicate IDs are removed in first-occurrence order.
2570    ///
2571    /// # Panics
2572    ///
2573    /// Panics if:
2574    /// - The identifier collection is empty.
2575    /// - The IDs span venues without an explicit client.
2576    fn unsubscribe_quotes(
2577        &mut self,
2578        instrument_ids: impl IntoInstrumentIds,
2579        client_id: Option<ClientId>,
2580        params: Option<Params>,
2581    ) where
2582        Self: DataActorNative,
2583        Self: 'static + Debug + Sized,
2584    {
2585        DataActorCore::unsubscribe_quotes(self.core_mut(), instrument_ids, client_id, params)
2586            .expect("invalid quote subscription identifier list");
2587    }
2588
2589    /// Unsubscribe from streaming [`TradeTick`] data for one or more instruments.
2590    ///
2591    /// Duplicate IDs are removed in first-occurrence order.
2592    ///
2593    /// # Panics
2594    ///
2595    /// Panics if:
2596    /// - The identifier collection is empty.
2597    /// - The IDs span venues without an explicit client.
2598    fn unsubscribe_trades(
2599        &mut self,
2600        instrument_ids: impl IntoInstrumentIds,
2601        client_id: Option<ClientId>,
2602        params: Option<Params>,
2603    ) where
2604        Self: DataActorNative,
2605        Self: 'static + Debug + Sized,
2606    {
2607        DataActorCore::unsubscribe_trades(self.core_mut(), instrument_ids, client_id, params)
2608            .expect("invalid trade subscription identifier list");
2609    }
2610
2611    /// Unsubscribe from streaming [`Bar`] data for one or more bar types.
2612    ///
2613    /// Duplicate types are removed in first-occurrence order.
2614    ///
2615    /// # Panics
2616    ///
2617    /// Panics if the list is empty or spans venues without an explicit client.
2618    fn unsubscribe_bars(
2619        &mut self,
2620        bar_types: impl IntoBarTypes,
2621        client_id: Option<ClientId>,
2622        params: Option<Params>,
2623    ) where
2624        Self: DataActorNative,
2625        Self: 'static + Debug + Sized,
2626    {
2627        DataActorCore::unsubscribe_bars(self.core_mut(), bar_types, client_id, params)
2628            .expect("invalid bar subscription type list");
2629    }
2630
2631    /// Unsubscribe from streaming [`MarkPriceUpdate`] data for one or more instruments.
2632    ///
2633    /// Duplicate IDs are removed in first-occurrence order.
2634    ///
2635    /// # Panics
2636    ///
2637    /// Panics if:
2638    /// - The IDs are empty.
2639    /// - The IDs span venues without an explicit client.
2640    fn unsubscribe_mark_prices(
2641        &mut self,
2642        instrument_ids: impl IntoInstrumentIds,
2643        client_id: Option<ClientId>,
2644        params: Option<Params>,
2645    ) where
2646        Self: DataActorNative,
2647        Self: 'static + Debug + Sized,
2648    {
2649        DataActorCore::unsubscribe_mark_prices(self.core_mut(), instrument_ids, client_id, params)
2650            .expect("invalid instrument subscription identifier list");
2651    }
2652
2653    /// Unsubscribe from streaming [`IndexPriceUpdate`] data for one or more instruments.
2654    ///
2655    /// Duplicate IDs are removed in first-occurrence order.
2656    ///
2657    /// # Panics
2658    ///
2659    /// Panics if:
2660    /// - The IDs are empty.
2661    /// - The IDs span venues without an explicit client.
2662    fn unsubscribe_index_prices(
2663        &mut self,
2664        instrument_ids: impl IntoInstrumentIds,
2665        client_id: Option<ClientId>,
2666        params: Option<Params>,
2667    ) where
2668        Self: DataActorNative,
2669        Self: 'static + Debug + Sized,
2670    {
2671        DataActorCore::unsubscribe_index_prices(self.core_mut(), instrument_ids, client_id, params)
2672            .expect("invalid instrument subscription identifier list");
2673    }
2674
2675    /// Unsubscribe from streaming [`FundingRateUpdate`] data for one or more instruments.
2676    ///
2677    /// Duplicate IDs are removed in first-occurrence order.
2678    ///
2679    /// # Panics
2680    ///
2681    /// Panics if:
2682    /// - The IDs are empty.
2683    /// - The IDs span venues without an explicit client.
2684    fn unsubscribe_funding_rates(
2685        &mut self,
2686        instrument_ids: impl IntoInstrumentIds,
2687        client_id: Option<ClientId>,
2688        params: Option<Params>,
2689    ) where
2690        Self: DataActorNative,
2691        Self: 'static + Debug + Sized,
2692    {
2693        DataActorCore::unsubscribe_funding_rates(
2694            self.core_mut(),
2695            instrument_ids,
2696            client_id,
2697            params,
2698        )
2699        .expect("invalid instrument subscription identifier list");
2700    }
2701
2702    /// Unsubscribe from streaming [`OptionGreeks`] data for one or more instruments.
2703    ///
2704    /// Duplicate IDs are removed in first-occurrence order.
2705    ///
2706    /// # Panics
2707    ///
2708    /// Panics if:
2709    /// - The IDs are empty.
2710    /// - The IDs span venues without an explicit client.
2711    fn unsubscribe_option_greeks(
2712        &mut self,
2713        instrument_ids: impl IntoInstrumentIds,
2714        client_id: Option<ClientId>,
2715        params: Option<Params>,
2716    ) where
2717        Self: DataActorNative,
2718        Self: 'static + Debug + Sized,
2719    {
2720        DataActorCore::unsubscribe_option_greeks(
2721            self.core_mut(),
2722            instrument_ids,
2723            client_id,
2724            params,
2725        )
2726        .expect("invalid instrument subscription identifier list");
2727    }
2728
2729    /// Unsubscribe from streaming [`InstrumentStatus`] data for one or more instruments.
2730    ///
2731    /// Duplicate IDs are removed in first-occurrence order.
2732    ///
2733    /// # Panics
2734    ///
2735    /// Panics if:
2736    /// - The IDs are empty.
2737    /// - The IDs span venues without an explicit client.
2738    fn unsubscribe_instrument_status(
2739        &mut self,
2740        instrument_ids: impl IntoInstrumentIds,
2741        client_id: Option<ClientId>,
2742        params: Option<Params>,
2743    ) where
2744        Self: DataActorNative,
2745        Self: 'static + Debug + Sized,
2746    {
2747        DataActorCore::unsubscribe_instrument_status(
2748            self.core_mut(),
2749            instrument_ids,
2750            client_id,
2751            params,
2752        )
2753        .expect("invalid instrument subscription identifier list");
2754    }
2755
2756    /// Unsubscribe from streaming [`InstrumentClose`] data for one or more instruments.
2757    ///
2758    /// Duplicate IDs are removed in first-occurrence order.
2759    ///
2760    /// # Panics
2761    ///
2762    /// Panics if:
2763    /// - The IDs are empty.
2764    /// - The IDs span venues without an explicit client.
2765    fn unsubscribe_instrument_close(
2766        &mut self,
2767        instrument_ids: impl IntoInstrumentIds,
2768        client_id: Option<ClientId>,
2769        params: Option<Params>,
2770    ) where
2771        Self: DataActorNative,
2772        Self: 'static + Debug + Sized,
2773    {
2774        DataActorCore::unsubscribe_instrument_close(
2775            self.core_mut(),
2776            instrument_ids,
2777            client_id,
2778            params,
2779        )
2780        .expect("invalid instrument subscription identifier list");
2781    }
2782
2783    /// Unsubscribe from streaming [`OptionChainSlice`] snapshots for the option `series_id`.
2784    fn unsubscribe_option_chain(&mut self, series_id: OptionSeriesId, client_id: Option<ClientId>)
2785    where
2786        Self: DataActorNative,
2787        Self: 'static + Debug + Sized,
2788    {
2789        DataActorCore::unsubscribe_option_chain(self.core_mut(), series_id, client_id);
2790    }
2791
2792    #[cfg(feature = "defi")]
2793    /// Unsubscribe from streaming [`Block`] data for the `chain`.
2794    fn unsubscribe_blocks(
2795        &mut self,
2796        chain: Blockchain,
2797        client_id: Option<ClientId>,
2798        params: Option<Params>,
2799    ) where
2800        Self: DataActorNative,
2801        Self: 'static + Debug + Sized,
2802    {
2803        DataActorCore::unsubscribe_blocks(self.core_mut(), chain, client_id, params);
2804    }
2805
2806    #[cfg(feature = "defi")]
2807    /// Unsubscribe from streaming [`Pool`] definition updates for the AMM pool at the `instrument_id`.
2808    fn unsubscribe_pool(
2809        &mut self,
2810        instrument_id: InstrumentId,
2811        client_id: Option<ClientId>,
2812        params: Option<Params>,
2813    ) where
2814        Self: DataActorNative,
2815        Self: 'static + Debug + Sized,
2816    {
2817        DataActorCore::unsubscribe_pool(self.core_mut(), instrument_id, client_id, params);
2818    }
2819
2820    #[cfg(feature = "defi")]
2821    /// Unsubscribe from streaming [`PoolSwap`] data for the `instrument_id`.
2822    fn unsubscribe_pool_swaps(
2823        &mut self,
2824        instrument_id: InstrumentId,
2825        client_id: Option<ClientId>,
2826        params: Option<Params>,
2827    ) where
2828        Self: DataActorNative,
2829        Self: 'static + Debug + Sized,
2830    {
2831        DataActorCore::unsubscribe_pool_swaps(self.core_mut(), instrument_id, client_id, params);
2832    }
2833
2834    #[cfg(feature = "defi")]
2835    /// Unsubscribe from streaming [`PoolLiquidityUpdate`] data for the `instrument_id`.
2836    fn unsubscribe_pool_liquidity_updates(
2837        &mut self,
2838        instrument_id: InstrumentId,
2839        client_id: Option<ClientId>,
2840        params: Option<Params>,
2841    ) where
2842        Self: DataActorNative,
2843        Self: 'static + Debug + Sized,
2844    {
2845        DataActorCore::unsubscribe_pool_liquidity_updates(
2846            self.core_mut(),
2847            instrument_id,
2848            client_id,
2849            params,
2850        );
2851    }
2852
2853    #[cfg(feature = "defi")]
2854    /// Unsubscribe from streaming [`PoolFeeCollect`] data for the `instrument_id`.
2855    fn unsubscribe_pool_fee_collects(
2856        &mut self,
2857        instrument_id: InstrumentId,
2858        client_id: Option<ClientId>,
2859        params: Option<Params>,
2860    ) where
2861        Self: DataActorNative,
2862        Self: 'static + Debug + Sized,
2863    {
2864        DataActorCore::unsubscribe_pool_fee_collects(
2865            self.core_mut(),
2866            instrument_id,
2867            client_id,
2868            params,
2869        );
2870    }
2871
2872    #[cfg(feature = "defi")]
2873    /// Unsubscribe from streaming [`PoolFlash`] events for the given `instrument_id`.
2874    fn unsubscribe_pool_flash_events(
2875        &mut self,
2876        instrument_id: InstrumentId,
2877        client_id: Option<ClientId>,
2878        params: Option<Params>,
2879    ) where
2880        Self: DataActorNative,
2881        Self: 'static + Debug + Sized,
2882    {
2883        DataActorCore::unsubscribe_pool_flash_events(
2884            self.core_mut(),
2885            instrument_id,
2886            client_id,
2887            params,
2888        );
2889    }
2890
2891    /// Request historical custom data of the given `data_type`.
2892    ///
2893    /// # Errors
2894    ///
2895    /// Returns an error if input parameters are invalid.
2896    fn request_data(
2897        &mut self,
2898        data_type: DataType,
2899        client_id: ClientId,
2900        start: Option<Timestamp>,
2901        end: Option<Timestamp>,
2902        limit: Option<NonZeroUsize>,
2903        params: Option<Params>,
2904    ) -> anyhow::Result<UUID4>
2905    where
2906        Self: DataActorNative,
2907        Self: 'static + Debug + Sized,
2908    {
2909        let handler = self.response_handler();
2910
2911        DataActorCore::request_data(
2912            self.core_mut(),
2913            data_type,
2914            client_id,
2915            start,
2916            end,
2917            limit,
2918            params,
2919            handler,
2920        )
2921    }
2922
2923    /// Request historical [`InstrumentResponse`] data for the given `instrument_ids`.
2924    ///
2925    /// # Errors
2926    ///
2927    /// Returns an error if input parameters are invalid.
2928    fn request_instrument(
2929        &mut self,
2930        instrument_ids: impl IntoInstrumentIds,
2931        start: Option<Timestamp>,
2932        end: Option<Timestamp>,
2933        client_id: Option<ClientId>,
2934        params: Option<Params>,
2935    ) -> anyhow::Result<UUID4>
2936    where
2937        Self: DataActorNative,
2938        Self: 'static + Debug + Sized,
2939    {
2940        let handler = self.response_handler();
2941
2942        DataActorCore::request_instrument(
2943            self.core_mut(),
2944            instrument_ids,
2945            start,
2946            end,
2947            client_id,
2948            params,
2949            handler,
2950        )
2951    }
2952
2953    /// Request historical [`InstrumentsResponse`] definitions for the optional `venue`.
2954    ///
2955    /// # Errors
2956    ///
2957    /// Returns an error if input parameters are invalid.
2958    fn request_instruments(
2959        &mut self,
2960        venue: Option<Venue>,
2961        start: Option<Timestamp>,
2962        end: Option<Timestamp>,
2963        client_id: Option<ClientId>,
2964        params: Option<Params>,
2965    ) -> anyhow::Result<UUID4>
2966    where
2967        Self: DataActorNative,
2968        Self: 'static + Debug + Sized,
2969    {
2970        let handler = self.response_handler();
2971
2972        DataActorCore::request_instruments(
2973            self.core_mut(),
2974            venue,
2975            start,
2976            end,
2977            client_id,
2978            params,
2979            handler,
2980        )
2981    }
2982
2983    /// Request an [`OrderBook`] snapshot for the given `instrument_ids`.
2984    ///
2985    /// # Errors
2986    ///
2987    /// Returns an error if input parameters are invalid.
2988    fn request_book_snapshot(
2989        &mut self,
2990        instrument_ids: impl IntoInstrumentIds,
2991        depth: Option<NonZeroUsize>,
2992        client_id: Option<ClientId>,
2993        params: Option<Params>,
2994    ) -> anyhow::Result<UUID4>
2995    where
2996        Self: DataActorNative,
2997        Self: 'static + Debug + Sized,
2998    {
2999        let handler = self.response_handler();
3000
3001        DataActorCore::request_book_snapshot(
3002            self.core_mut(),
3003            instrument_ids,
3004            depth,
3005            client_id,
3006            params,
3007            handler,
3008        )
3009    }
3010
3011    /// Request historical [`OrderBookDelta`] data for the given `instrument_ids`.
3012    ///
3013    /// # Errors
3014    ///
3015    /// Returns an error if input parameters are invalid.
3016    fn request_book_deltas(
3017        &mut self,
3018        instrument_ids: impl IntoInstrumentIds,
3019        start: Option<Timestamp>,
3020        end: Option<Timestamp>,
3021        limit: Option<NonZeroUsize>,
3022        client_id: Option<ClientId>,
3023        params: Option<Params>,
3024    ) -> anyhow::Result<UUID4>
3025    where
3026        Self: DataActorNative,
3027        Self: 'static + Debug + Sized,
3028    {
3029        let handler = self.response_handler();
3030
3031        DataActorCore::request_book_deltas(
3032            self.core_mut(),
3033            instrument_ids,
3034            start,
3035            end,
3036            limit,
3037            client_id,
3038            params,
3039            handler,
3040        )
3041    }
3042
3043    /// Request historical [`OrderBookDepth`] data for the given `instrument_ids`.
3044    ///
3045    /// # Errors
3046    ///
3047    /// Returns an error if input parameters are invalid.
3048    #[expect(clippy::too_many_arguments)]
3049    fn request_book_depth(
3050        &mut self,
3051        instrument_ids: impl IntoInstrumentIds,
3052        start: Option<Timestamp>,
3053        end: Option<Timestamp>,
3054        limit: Option<NonZeroUsize>,
3055        depth: Option<NonZeroUsize>,
3056        client_id: Option<ClientId>,
3057        params: Option<Params>,
3058    ) -> anyhow::Result<UUID4>
3059    where
3060        Self: DataActorNative,
3061        Self: 'static + Debug + Sized,
3062    {
3063        let handler = self.response_handler();
3064
3065        DataActorCore::request_book_depth(
3066            self.core_mut(),
3067            instrument_ids,
3068            start,
3069            end,
3070            limit,
3071            depth,
3072            client_id,
3073            params,
3074            handler,
3075        )
3076    }
3077
3078    /// Request historical [`QuoteTick`] data for the given `instrument_ids`.
3079    ///
3080    /// # Errors
3081    ///
3082    /// Returns an error if input parameters are invalid.
3083    fn request_quotes(
3084        &mut self,
3085        instrument_ids: impl IntoInstrumentIds,
3086        start: Option<Timestamp>,
3087        end: Option<Timestamp>,
3088        limit: Option<NonZeroUsize>,
3089        client_id: Option<ClientId>,
3090        params: Option<Params>,
3091    ) -> anyhow::Result<UUID4>
3092    where
3093        Self: DataActorNative,
3094        Self: 'static + Debug + Sized,
3095    {
3096        let handler = self.response_handler();
3097
3098        DataActorCore::request_quotes(
3099            self.core_mut(),
3100            instrument_ids,
3101            start,
3102            end,
3103            limit,
3104            client_id,
3105            params,
3106            handler,
3107        )
3108    }
3109
3110    /// Request historical [`TradeTick`] data for the given `instrument_ids`.
3111    ///
3112    /// # Errors
3113    ///
3114    /// Returns an error if input parameters are invalid.
3115    fn request_trades(
3116        &mut self,
3117        instrument_ids: impl IntoInstrumentIds,
3118        start: Option<Timestamp>,
3119        end: Option<Timestamp>,
3120        limit: Option<NonZeroUsize>,
3121        client_id: Option<ClientId>,
3122        params: Option<Params>,
3123    ) -> anyhow::Result<UUID4>
3124    where
3125        Self: DataActorNative,
3126        Self: 'static + Debug + Sized,
3127    {
3128        let handler = self.response_handler();
3129
3130        DataActorCore::request_trades(
3131            self.core_mut(),
3132            instrument_ids,
3133            start,
3134            end,
3135            limit,
3136            client_id,
3137            params,
3138            handler,
3139        )
3140    }
3141
3142    /// Request historical [`Bar`] data for the given `bar_types`.
3143    ///
3144    /// # Errors
3145    ///
3146    /// Returns an error if input parameters are invalid.
3147    fn request_bars(
3148        &mut self,
3149        bar_types: impl IntoBarTypes,
3150        start: Option<Timestamp>,
3151        end: Option<Timestamp>,
3152        limit: Option<NonZeroUsize>,
3153        client_id: Option<ClientId>,
3154        params: Option<Params>,
3155    ) -> anyhow::Result<UUID4>
3156    where
3157        Self: DataActorNative,
3158        Self: 'static + Debug + Sized,
3159    {
3160        let handler = self.response_handler();
3161
3162        DataActorCore::request_bars(
3163            self.core_mut(),
3164            bar_types,
3165            start,
3166            end,
3167            limit,
3168            client_id,
3169            params,
3170            handler,
3171        )
3172    }
3173
3174    /// Request historical [`FundingRateUpdate`] data for the given `instrument_ids`.
3175    ///
3176    /// # Errors
3177    ///
3178    /// Returns an error if input parameters are invalid.
3179    fn request_funding_rates(
3180        &mut self,
3181        instrument_ids: impl IntoInstrumentIds,
3182        start: Option<Timestamp>,
3183        end: Option<Timestamp>,
3184        limit: Option<NonZeroUsize>,
3185        client_id: Option<ClientId>,
3186        params: Option<Params>,
3187    ) -> anyhow::Result<UUID4>
3188    where
3189        Self: DataActorNative,
3190        Self: 'static + Debug + Sized,
3191    {
3192        let handler = self.response_handler();
3193
3194        DataActorCore::request_funding_rates(
3195            self.core_mut(),
3196            instrument_ids,
3197            start,
3198            end,
3199            limit,
3200            client_id,
3201            params,
3202            handler,
3203        )
3204    }
3205
3206    /// Registers a completion group for this actor's historical requests and returns its ID.
3207    ///
3208    /// Each child keeps its own payload delivery. The group emits one payload-free terminal
3209    /// envelope through [`Self::on_data_response`] after every child completes. Failure reports
3210    /// the first failed child in the supplied order, without cancelling the other children.
3211    /// Completed IDs remain available for grouping until request cleanup on successful reset or
3212    /// disposal, fault, or [`Component::release_subscriptions`].
3213    /// Groups can contain other groups and can share children.
3214    ///
3215    /// A group of completed requests can notify before this method returns. Groups created in
3216    /// a response hook notify after that hook returns, in readiness order. Groups made ready by
3217    /// the same child notify in registration order. Hook errors are logged and do not block groups.
3218    /// Supply a previously stored `request_id` to correlate completion that occurs before return.
3219    /// Passing `None` generates a new group ID.
3220    /// Invalid input returns an error without registering a group.
3221    ///
3222    /// # Errors
3223    ///
3224    /// Returns an error if:
3225    /// - The child list is empty or contains duplicate or unknown IDs.
3226    /// - The group ID is already used by one of this actor's requests or groups.
3227    fn request_group(
3228        &mut self,
3229        request_ids: Vec<UUID4>,
3230        request_id: Option<UUID4>,
3231    ) -> anyhow::Result<UUID4>
3232    where
3233        Self: DataActorNative,
3234    {
3235        self.core().check_registered();
3236        let id = self
3237            .core()
3238            .register_request_group(request_ids, request_id, None)?;
3239        self.drain_request_groups();
3240        Ok(id)
3241    }
3242
3243    /// Requests reconnect of one socket endpoint owned by `client_id`.
3244    ///
3245    /// This is a fire-and-observe command. A successful return means the live runner queued the
3246    /// request. [`SocketStateChanged`] events for the same endpoint report whether the transport
3247    /// enters reconnect mode and later recovers.
3248    ///
3249    /// # Errors
3250    ///
3251    /// Returns an error if the actor is not registered, the endpoint label is invalid, the live
3252    /// runner is unavailable, or the runner command channel is closed.
3253    #[cfg(feature = "live")]
3254    fn reconnect_socket(&self, client_id: ClientId, endpoint: &str) -> anyhow::Result<()>
3255    where
3256        Self: DataActorNative,
3257    {
3258        DataActorCore::reconnect_socket(self.core(), client_id, endpoint)
3259    }
3260}
3261
3262// Blanket implementation: any DataActor automatically implements Actor
3263impl<T> Actor for T
3264where
3265    T: DataActor + DataActorNative + Debug + 'static,
3266{
3267    fn id(&self) -> Ustr {
3268        self.core().actor_id.inner()
3269    }
3270
3271    #[allow(unused_variables)]
3272    fn handle(&mut self, msg: &dyn Any) {
3273        // Default empty implementation - concrete actors can override if needed
3274    }
3275
3276    fn as_any(&self) -> &dyn Any {
3277        self
3278    }
3279}
3280
3281impl<T> Component for T
3282where
3283    T: DataActor + DataActorNative + Debug + 'static,
3284{
3285    fn component_id(&self) -> ComponentId {
3286        ComponentId::from(self.core().actor_id)
3287    }
3288
3289    fn release_subscriptions(&mut self) {
3290        self.core().clear_pending_requests();
3291        self.core_mut().unsubscribe_all();
3292    }
3293
3294    fn state(&self) -> ComponentState {
3295        self.core().state
3296    }
3297
3298    fn transition_state(&mut self, trigger: ComponentTrigger) -> anyhow::Result<()> {
3299        let core = self.core_mut();
3300        core.state = core.state.transition(&trigger)?;
3301
3302        #[cfg(feature = "python")]
3303        if core.state == ComponentState::Disposed {
3304            core.message_bus.invalidate();
3305        }
3306
3307        log::info!(
3308            component = core.actor_id.inner().as_str();
3309            "{}",
3310            core.state.variant_name()
3311        );
3312        Ok(())
3313    }
3314
3315    fn register(
3316        &mut self,
3317        trader_id: TraderId,
3318        clock: Rc<RefCell<dyn Clock>>,
3319        cache: Rc<RefCell<Cache>>,
3320    ) -> anyhow::Result<()> {
3321        DataActorCore::register(self.core_mut(), trader_id, clock.clone(), cache)?;
3322
3323        // Register default time event handler for this actor
3324        let actor_id = self.core().actor_id().inner();
3325        let callback = TimeEventCallback::from(move |event: TimeEvent| {
3326            if let Some(mut actor) = try_get_actor_unchecked::<Self>(&actor_id) {
3327                actor.handle_time_event(&event);
3328            } else {
3329                log::error!("Actor {actor_id} not found for time event handling");
3330            }
3331        });
3332
3333        clock.borrow_mut().register_default_handler(callback);
3334
3335        self.initialize()
3336    }
3337
3338    fn on_start(&mut self) -> anyhow::Result<()> {
3339        DataActor::on_start(self)
3340    }
3341
3342    fn on_stop(&mut self) -> anyhow::Result<()> {
3343        DataActor::on_stop(self)
3344    }
3345
3346    fn on_resume(&mut self) -> anyhow::Result<()> {
3347        DataActor::on_resume(self)
3348    }
3349
3350    fn on_degrade(&mut self) -> anyhow::Result<()> {
3351        DataActor::on_degrade(self)
3352    }
3353
3354    fn on_fault(&mut self) -> anyhow::Result<()> {
3355        DataActor::on_fault(self)
3356    }
3357
3358    fn on_reset(&mut self) -> anyhow::Result<()> {
3359        DataActor::on_reset(self)
3360    }
3361
3362    fn on_dispose(&mut self) -> anyhow::Result<()> {
3363        DataActor::on_dispose(self)
3364    }
3365}
3366
3367/// Core functionality for all actors.
3368#[derive(Clone)]
3369#[allow(dead_code, reason = "TODO: Under development (signal_classes)")]
3370pub struct DataActorCore {
3371    /// The actor identifier.
3372    pub actor_id: ActorId,
3373    /// The actors configuration.
3374    pub config: DataActorConfig,
3375    trader_id: Option<TraderId>,
3376    clock: Option<Rc<RefCell<dyn Clock>>>, // Wired up on registration
3377    cache: Option<Rc<RefCell<Cache>>>,     // Wired up on registration
3378    state: ComponentState,
3379    topic_handlers: AHashMap<MStr<Pattern>, Subscription<ShareableMessageHandler>>,
3380    instrument_handlers: AHashMap<MStr<Pattern>, Subscription<TypedHandler<InstrumentAny>>>,
3381    deltas_handlers: AHashMap<MStr<Pattern>, Subscription<TypedHandler<OrderBookDeltas>>>,
3382    depth_handlers: AHashMap<MStr<Pattern>, Subscription<TypedHandler<OrderBookDepth>>>,
3383    book_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<OrderBook>>>,
3384    quote_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<QuoteTick>>>,
3385    trade_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<TradeTick>>>,
3386    bar_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<Bar>>>,
3387    mark_price_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<MarkPriceUpdate>>>,
3388    index_price_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<IndexPriceUpdate>>>,
3389    funding_rate_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<FundingRateUpdate>>>,
3390    option_greeks_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<OptionGreeks>>>,
3391    option_chain_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<OptionChainSlice>>>,
3392    indicators: Indicators,
3393    warning_events: AHashSet<String>, // TODO: TBD
3394    requests: RefCell<RequestLifecycle>,
3395    signal_classes: AHashMap<String, String>,
3396    #[cfg(feature = "defi")]
3397    block_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<Block>>>,
3398    #[cfg(feature = "defi")]
3399    pool_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<Pool>>>,
3400    #[cfg(feature = "defi")]
3401    pool_swap_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<PoolSwap>>>,
3402    #[cfg(feature = "defi")]
3403    pool_liquidity_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<PoolLiquidityUpdate>>>,
3404    #[cfg(feature = "defi")]
3405    pool_collect_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<PoolFeeCollect>>>,
3406    #[cfg(feature = "defi")]
3407    pool_flash_handlers: AHashMap<MStr<Topic>, Subscription<TypedHandler<PoolFlash>>>,
3408    #[cfg(feature = "python")]
3409    message_bus: Rc<PyMessageBusScope>,
3410}
3411
3412#[derive(Clone)]
3413struct Subscription<T> {
3414    handler: T,
3415    command: Option<DataCommand>,
3416}
3417
3418impl Debug for DataActorCore {
3419    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3420        f.debug_struct(stringify!(DataActorCore))
3421            .field("actor_id", &self.actor_id)
3422            .field("config", &self.config)
3423            .field("state", &self.state)
3424            .field("trader_id", &self.trader_id)
3425            .finish()
3426    }
3427}
3428
3429impl DataActorCore {
3430    /// Adds a subscription handler for the `topic`.
3431    //// Logs a warning if the actor is already subscribed to the topic.
3432    pub(crate) fn add_subscription_any(
3433        &mut self,
3434        topic: impl Into<MStr<Pattern>>,
3435        handler: ShareableMessageHandler,
3436        priority: Option<u32>,
3437        command: Option<DataCommand>,
3438    ) -> bool {
3439        let pattern: MStr<Pattern> = topic.into();
3440        if let Some(subscription) = self.topic_handlers.get_mut(&pattern) {
3441            if subscription.command.is_none() && command.is_some() {
3442                subscription.command = command;
3443                return true;
3444            }
3445
3446            log::warn!(
3447                "Actor {} attempted duplicate subscription to topic '{pattern}'",
3448                self.actor_id,
3449            );
3450            return false;
3451        }
3452
3453        self.topic_handlers.insert(
3454            pattern,
3455            Subscription {
3456                handler: handler.clone(),
3457                command,
3458            },
3459        );
3460        msgbus::subscribe_any(pattern, handler, priority);
3461        true
3462    }
3463
3464    /// Removes a subscription handler for the `topic` if present.
3465    ///
3466    /// Logs a warning if the actor is not currently subscribed to the topic.
3467    pub(crate) fn remove_subscription_any(
3468        &mut self,
3469        topic: impl Into<MStr<Pattern>>,
3470    ) -> Option<DataCommand> {
3471        let pattern: MStr<Pattern> = topic.into();
3472        if let Some(subscription) = self.topic_handlers.remove(&pattern) {
3473            msgbus::unsubscribe_any(pattern, &subscription.handler);
3474            subscription.command
3475        } else {
3476            log::warn!(
3477                "Actor {} attempted to unsubscribe from topic '{pattern}' when not subscribed",
3478                self.actor_id,
3479            );
3480            None
3481        }
3482    }
3483
3484    pub(crate) fn add_quote_subscription(
3485        &mut self,
3486        topic: MStr<Topic>,
3487        handler: TypedHandler<QuoteTick>,
3488        command: DataCommand,
3489    ) -> bool {
3490        if self.quote_handlers.contains_key(&topic) {
3491            log::warn!(
3492                "Actor {} attempted duplicate quote subscription to '{topic}'",
3493                self.actor_id
3494            );
3495            return false;
3496        }
3497        self.quote_handlers.insert(
3498            topic,
3499            Subscription {
3500                handler: handler.clone(),
3501                command: Some(command),
3502            },
3503        );
3504        msgbus::subscribe_quotes(topic.into(), handler, None);
3505        true
3506    }
3507
3508    #[allow(dead_code)]
3509    pub(crate) fn remove_quote_subscription(&mut self, topic: MStr<Topic>) -> Option<DataCommand> {
3510        let subscription = self.quote_handlers.remove(&topic)?;
3511        msgbus::unsubscribe_quotes(topic.into(), &subscription.handler);
3512        subscription.command
3513    }
3514
3515    pub(crate) fn add_trade_subscription(
3516        &mut self,
3517        topic: MStr<Topic>,
3518        handler: TypedHandler<TradeTick>,
3519        command: DataCommand,
3520    ) -> bool {
3521        if self.trade_handlers.contains_key(&topic) {
3522            log::warn!(
3523                "Actor {} attempted duplicate trade subscription to '{topic}'",
3524                self.actor_id
3525            );
3526            return false;
3527        }
3528        self.trade_handlers.insert(
3529            topic,
3530            Subscription {
3531                handler: handler.clone(),
3532                command: Some(command),
3533            },
3534        );
3535        msgbus::subscribe_trades(topic.into(), handler, None);
3536        true
3537    }
3538
3539    #[allow(dead_code)]
3540    pub(crate) fn remove_trade_subscription(&mut self, topic: MStr<Topic>) -> Option<DataCommand> {
3541        let subscription = self.trade_handlers.remove(&topic)?;
3542        msgbus::unsubscribe_trades(topic.into(), &subscription.handler);
3543        subscription.command
3544    }
3545
3546    pub(crate) fn add_bar_subscription(
3547        &mut self,
3548        topic: MStr<Topic>,
3549        handler: TypedHandler<Bar>,
3550        command: DataCommand,
3551    ) -> bool {
3552        if self.bar_handlers.contains_key(&topic) {
3553            return false;
3554        }
3555
3556        self.bar_handlers.insert(
3557            topic,
3558            Subscription {
3559                handler: handler.clone(),
3560                command: Some(command),
3561            },
3562        );
3563        msgbus::subscribe_bars(topic.into(), handler, None);
3564        true
3565    }
3566
3567    fn remove_bar_subscription(&mut self, bar_type: BarType) -> Option<DataCommand> {
3568        let topic = get_bars_topic(bar_type.standard());
3569        let subscription = self.bar_handlers.remove(&topic)?;
3570        msgbus::unsubscribe_bars(topic.into(), &subscription.handler);
3571        subscription.command
3572    }
3573
3574    pub(crate) fn add_deltas_subscription(
3575        &mut self,
3576        pattern: MStr<Pattern>,
3577        handler: TypedHandler<OrderBookDeltas>,
3578        command: DataCommand,
3579    ) -> bool {
3580        if self.deltas_handlers.contains_key(&pattern) {
3581            log::warn!(
3582                "Actor {} attempted duplicate deltas subscription to '{pattern}'",
3583                self.actor_id
3584            );
3585            return false;
3586        }
3587        self.deltas_handlers.insert(
3588            pattern,
3589            Subscription {
3590                handler: handler.clone(),
3591                command: Some(command),
3592            },
3593        );
3594        msgbus::subscribe_book_deltas(pattern, handler, None);
3595        true
3596    }
3597
3598    #[allow(dead_code)]
3599    pub(crate) fn remove_deltas_subscription(
3600        &mut self,
3601        pattern: MStr<Pattern>,
3602    ) -> Option<DataCommand> {
3603        let subscription = self.deltas_handlers.remove(&pattern)?;
3604        msgbus::unsubscribe_book_deltas(pattern, &subscription.handler);
3605        subscription.command
3606    }
3607
3608    pub(crate) fn add_depth_subscription(
3609        &mut self,
3610        pattern: MStr<Pattern>,
3611        handler: TypedHandler<OrderBookDepth>,
3612        command: DataCommand,
3613    ) -> bool {
3614        if self.depth_handlers.contains_key(&pattern) {
3615            log::warn!(
3616                "Actor {} attempted duplicate depth subscription to '{pattern}'",
3617                self.actor_id
3618            );
3619            return false;
3620        }
3621        self.depth_handlers.insert(
3622            pattern,
3623            Subscription {
3624                handler: handler.clone(),
3625                command: Some(command),
3626            },
3627        );
3628        msgbus::subscribe_book_depth(pattern, handler, None);
3629        true
3630    }
3631
3632    pub(crate) fn remove_depth_subscription(
3633        &mut self,
3634        pattern: MStr<Pattern>,
3635    ) -> Option<DataCommand> {
3636        let subscription = self.depth_handlers.remove(&pattern)?;
3637        msgbus::unsubscribe_book_depth(pattern, &subscription.handler);
3638        subscription.command
3639    }
3640
3641    pub(crate) fn add_instrument_subscription(
3642        &mut self,
3643        pattern: MStr<Pattern>,
3644        handler: TypedHandler<InstrumentAny>,
3645        command: DataCommand,
3646    ) -> bool {
3647        if self.instrument_handlers.contains_key(&pattern) {
3648            log::warn!(
3649                "Actor {} attempted duplicate instrument subscription to '{pattern}'",
3650                self.actor_id
3651            );
3652            return false;
3653        }
3654        self.instrument_handlers.insert(
3655            pattern,
3656            Subscription {
3657                handler: handler.clone(),
3658                command: Some(command),
3659            },
3660        );
3661        msgbus::subscribe_instruments(pattern, handler, None);
3662        true
3663    }
3664
3665    #[allow(dead_code)]
3666    pub(crate) fn remove_instrument_subscription(
3667        &mut self,
3668        pattern: MStr<Pattern>,
3669    ) -> Option<DataCommand> {
3670        let subscription = self.instrument_handlers.remove(&pattern)?;
3671        msgbus::unsubscribe_instruments(pattern, &subscription.handler);
3672        subscription.command
3673    }
3674
3675    pub(crate) fn add_instrument_close_subscription(
3676        &mut self,
3677        topic: MStr<Topic>,
3678        handler: ShareableMessageHandler,
3679        command: DataCommand,
3680    ) -> bool {
3681        let pattern: MStr<Pattern> = topic.into();
3682        if self.topic_handlers.contains_key(&pattern) {
3683            log::warn!(
3684                "Actor {} attempted duplicate instrument close subscription to '{topic}'",
3685                self.actor_id
3686            );
3687            return false;
3688        }
3689        self.topic_handlers.insert(
3690            pattern,
3691            Subscription {
3692                handler: handler.clone(),
3693                command: Some(command),
3694            },
3695        );
3696        msgbus::subscribe_any(pattern, handler, None);
3697        true
3698    }
3699
3700    #[allow(dead_code)]
3701    pub(crate) fn remove_instrument_close_subscription(
3702        &mut self,
3703        topic: MStr<Topic>,
3704    ) -> Option<DataCommand> {
3705        let pattern: MStr<Pattern> = topic.into();
3706        let subscription = self.topic_handlers.remove(&pattern)?;
3707        msgbus::unsubscribe_any(pattern, &subscription.handler);
3708        subscription.command
3709    }
3710
3711    pub(crate) fn add_book_snapshot_subscription(
3712        &mut self,
3713        topic: MStr<Topic>,
3714        handler: TypedHandler<OrderBook>,
3715        command: DataCommand,
3716    ) -> bool {
3717        if self.book_handlers.contains_key(&topic) {
3718            log::warn!(
3719                "Actor {} attempted duplicate book snapshot subscription to '{topic}'",
3720                self.actor_id
3721            );
3722            return false;
3723        }
3724        self.book_handlers.insert(
3725            topic,
3726            Subscription {
3727                handler: handler.clone(),
3728                command: Some(command),
3729            },
3730        );
3731        msgbus::subscribe_book_snapshots(topic.into(), handler, None);
3732        true
3733    }
3734
3735    #[allow(dead_code)]
3736    pub(crate) fn remove_book_snapshot_subscription(
3737        &mut self,
3738        topic: MStr<Topic>,
3739    ) -> Option<DataCommand> {
3740        let subscription = self.book_handlers.remove(&topic)?;
3741        msgbus::unsubscribe_book_snapshots(topic.into(), &subscription.handler);
3742        subscription.command
3743    }
3744
3745    pub(crate) fn add_mark_price_subscription(
3746        &mut self,
3747        topic: MStr<Topic>,
3748        handler: TypedHandler<MarkPriceUpdate>,
3749        command: DataCommand,
3750    ) -> bool {
3751        if self.mark_price_handlers.contains_key(&topic) {
3752            log::warn!(
3753                "Actor {} attempted duplicate mark price subscription to '{topic}'",
3754                self.actor_id
3755            );
3756            return false;
3757        }
3758        self.mark_price_handlers.insert(
3759            topic,
3760            Subscription {
3761                handler: handler.clone(),
3762                command: Some(command),
3763            },
3764        );
3765        msgbus::subscribe_mark_prices(topic.into(), handler, None);
3766        true
3767    }
3768
3769    #[allow(dead_code)]
3770    pub(crate) fn remove_mark_price_subscription(
3771        &mut self,
3772        topic: MStr<Topic>,
3773    ) -> Option<DataCommand> {
3774        let subscription = self.mark_price_handlers.remove(&topic)?;
3775        msgbus::unsubscribe_mark_prices(topic.into(), &subscription.handler);
3776        subscription.command
3777    }
3778
3779    pub(crate) fn add_index_price_subscription(
3780        &mut self,
3781        topic: MStr<Topic>,
3782        handler: TypedHandler<IndexPriceUpdate>,
3783        command: DataCommand,
3784    ) -> bool {
3785        if self.index_price_handlers.contains_key(&topic) {
3786            log::warn!(
3787                "Actor {} attempted duplicate index price subscription to '{topic}'",
3788                self.actor_id
3789            );
3790            return false;
3791        }
3792        self.index_price_handlers.insert(
3793            topic,
3794            Subscription {
3795                handler: handler.clone(),
3796                command: Some(command),
3797            },
3798        );
3799        msgbus::subscribe_index_prices(topic.into(), handler, None);
3800        true
3801    }
3802
3803    #[allow(dead_code)]
3804    pub(crate) fn remove_index_price_subscription(
3805        &mut self,
3806        topic: MStr<Topic>,
3807    ) -> Option<DataCommand> {
3808        let subscription = self.index_price_handlers.remove(&topic)?;
3809        msgbus::unsubscribe_index_prices(topic.into(), &subscription.handler);
3810        subscription.command
3811    }
3812
3813    pub(crate) fn add_funding_rate_subscription(
3814        &mut self,
3815        topic: MStr<Topic>,
3816        handler: TypedHandler<FundingRateUpdate>,
3817        command: DataCommand,
3818    ) -> bool {
3819        if self.funding_rate_handlers.contains_key(&topic) {
3820            log::warn!(
3821                "Actor {} attempted duplicate funding rate subscription to '{topic}'",
3822                self.actor_id
3823            );
3824            return false;
3825        }
3826        self.funding_rate_handlers.insert(
3827            topic,
3828            Subscription {
3829                handler: handler.clone(),
3830                command: Some(command),
3831            },
3832        );
3833        msgbus::subscribe_funding_rates(topic.into(), handler, None);
3834        true
3835    }
3836
3837    #[allow(dead_code)]
3838    pub(crate) fn remove_funding_rate_subscription(
3839        &mut self,
3840        topic: MStr<Topic>,
3841    ) -> Option<DataCommand> {
3842        let subscription = self.funding_rate_handlers.remove(&topic)?;
3843        msgbus::unsubscribe_funding_rates(topic.into(), &subscription.handler);
3844        subscription.command
3845    }
3846
3847    pub(crate) fn add_option_greeks_subscription(
3848        &mut self,
3849        topic: MStr<Topic>,
3850        handler: TypedHandler<OptionGreeks>,
3851        command: DataCommand,
3852    ) -> bool {
3853        if self.option_greeks_handlers.contains_key(&topic) {
3854            log::warn!(
3855                "Actor {} attempted duplicate option greeks subscription to '{topic}'",
3856                self.actor_id
3857            );
3858            return false;
3859        }
3860        self.option_greeks_handlers.insert(
3861            topic,
3862            Subscription {
3863                handler: handler.clone(),
3864                command: Some(command),
3865            },
3866        );
3867        msgbus::subscribe_option_greeks(topic.into(), handler, None);
3868        true
3869    }
3870
3871    #[allow(dead_code)]
3872    pub(crate) fn remove_option_greeks_subscription(
3873        &mut self,
3874        topic: MStr<Topic>,
3875    ) -> Option<DataCommand> {
3876        let subscription = self.option_greeks_handlers.remove(&topic)?;
3877        msgbus::unsubscribe_option_greeks(topic.into(), &subscription.handler);
3878        subscription.command
3879    }
3880
3881    pub(crate) fn set_option_chain_subscription(
3882        &mut self,
3883        topic: MStr<Topic>,
3884        handler: TypedHandler<OptionChainSlice>,
3885        command: DataCommand,
3886    ) {
3887        if let Some(subscription) = self.option_chain_handlers.get_mut(&topic) {
3888            subscription.command = Some(command);
3889            return;
3890        }
3891
3892        self.option_chain_handlers.insert(
3893            topic,
3894            Subscription {
3895                handler: handler.clone(),
3896                command: Some(command),
3897            },
3898        );
3899        msgbus::subscribe_option_chain(topic.into(), handler, None);
3900    }
3901
3902    pub(crate) fn remove_option_chain_subscription(
3903        &mut self,
3904        topic: MStr<Topic>,
3905    ) -> Option<DataCommand> {
3906        let subscription = self.option_chain_handlers.remove(&topic)?;
3907        msgbus::unsubscribe_option_chain(topic.into(), &subscription.handler);
3908        subscription.command
3909    }
3910
3911    #[cfg(feature = "defi")]
3912    pub(crate) fn add_block_subscription(
3913        &mut self,
3914        topic: MStr<Topic>,
3915        handler: TypedHandler<Block>,
3916        command: DataCommand,
3917    ) -> bool {
3918        if self.block_handlers.contains_key(&topic) {
3919            log::warn!(
3920                "Actor {} attempted duplicate block subscription to '{topic}'",
3921                self.actor_id
3922            );
3923            return false;
3924        }
3925        self.block_handlers.insert(
3926            topic,
3927            Subscription {
3928                handler: handler.clone(),
3929                command: Some(command),
3930            },
3931        );
3932        msgbus::subscribe_defi_blocks(topic.into(), handler, None);
3933        true
3934    }
3935
3936    #[cfg(feature = "defi")]
3937    #[allow(dead_code)]
3938    pub(crate) fn remove_block_subscription(&mut self, topic: MStr<Topic>) -> Option<DataCommand> {
3939        let subscription = self.block_handlers.remove(&topic)?;
3940        msgbus::unsubscribe_defi_blocks(topic.into(), &subscription.handler);
3941        subscription.command
3942    }
3943
3944    #[cfg(feature = "defi")]
3945    pub(crate) fn add_pool_subscription(
3946        &mut self,
3947        topic: MStr<Topic>,
3948        handler: TypedHandler<Pool>,
3949        command: DataCommand,
3950    ) -> bool {
3951        if self.pool_handlers.contains_key(&topic) {
3952            log::warn!(
3953                "Actor {} attempted duplicate pool subscription to '{topic}'",
3954                self.actor_id
3955            );
3956            return false;
3957        }
3958        self.pool_handlers.insert(
3959            topic,
3960            Subscription {
3961                handler: handler.clone(),
3962                command: Some(command),
3963            },
3964        );
3965        msgbus::subscribe_defi_pools(topic.into(), handler, None);
3966        true
3967    }
3968
3969    #[cfg(feature = "defi")]
3970    #[allow(dead_code)]
3971    pub(crate) fn remove_pool_subscription(&mut self, topic: MStr<Topic>) -> Option<DataCommand> {
3972        let subscription = self.pool_handlers.remove(&topic)?;
3973        msgbus::unsubscribe_defi_pools(topic.into(), &subscription.handler);
3974        subscription.command
3975    }
3976
3977    #[cfg(feature = "defi")]
3978    pub(crate) fn add_pool_swap_subscription(
3979        &mut self,
3980        topic: MStr<Topic>,
3981        handler: TypedHandler<PoolSwap>,
3982        command: DataCommand,
3983    ) -> bool {
3984        if self.pool_swap_handlers.contains_key(&topic) {
3985            log::warn!(
3986                "Actor {} attempted duplicate pool swap subscription to '{topic}'",
3987                self.actor_id
3988            );
3989            return false;
3990        }
3991        self.pool_swap_handlers.insert(
3992            topic,
3993            Subscription {
3994                handler: handler.clone(),
3995                command: Some(command),
3996            },
3997        );
3998        msgbus::subscribe_defi_swaps(topic.into(), handler, None);
3999        true
4000    }
4001
4002    #[cfg(feature = "defi")]
4003    #[allow(dead_code)]
4004    pub(crate) fn remove_pool_swap_subscription(
4005        &mut self,
4006        topic: MStr<Topic>,
4007    ) -> Option<DataCommand> {
4008        let subscription = self.pool_swap_handlers.remove(&topic)?;
4009        msgbus::unsubscribe_defi_swaps(topic.into(), &subscription.handler);
4010        subscription.command
4011    }
4012
4013    #[cfg(feature = "defi")]
4014    pub(crate) fn add_pool_liquidity_subscription(
4015        &mut self,
4016        topic: MStr<Topic>,
4017        handler: TypedHandler<PoolLiquidityUpdate>,
4018        command: DataCommand,
4019    ) -> bool {
4020        if self.pool_liquidity_handlers.contains_key(&topic) {
4021            log::warn!(
4022                "Actor {} attempted duplicate pool liquidity subscription to '{topic}'",
4023                self.actor_id
4024            );
4025            return false;
4026        }
4027        self.pool_liquidity_handlers.insert(
4028            topic,
4029            Subscription {
4030                handler: handler.clone(),
4031                command: Some(command),
4032            },
4033        );
4034        msgbus::subscribe_defi_liquidity(topic.into(), handler, None);
4035        true
4036    }
4037
4038    #[cfg(feature = "defi")]
4039    #[allow(dead_code)]
4040    pub(crate) fn remove_pool_liquidity_subscription(
4041        &mut self,
4042        topic: MStr<Topic>,
4043    ) -> Option<DataCommand> {
4044        let subscription = self.pool_liquidity_handlers.remove(&topic)?;
4045        msgbus::unsubscribe_defi_liquidity(topic.into(), &subscription.handler);
4046        subscription.command
4047    }
4048
4049    #[cfg(feature = "defi")]
4050    pub(crate) fn add_pool_collect_subscription(
4051        &mut self,
4052        topic: MStr<Topic>,
4053        handler: TypedHandler<PoolFeeCollect>,
4054        command: DataCommand,
4055    ) -> bool {
4056        if self.pool_collect_handlers.contains_key(&topic) {
4057            log::warn!(
4058                "Actor {} attempted duplicate pool collect subscription to '{topic}'",
4059                self.actor_id
4060            );
4061            return false;
4062        }
4063        self.pool_collect_handlers.insert(
4064            topic,
4065            Subscription {
4066                handler: handler.clone(),
4067                command: Some(command),
4068            },
4069        );
4070        msgbus::subscribe_defi_collects(topic.into(), handler, None);
4071        true
4072    }
4073
4074    #[cfg(feature = "defi")]
4075    #[allow(dead_code)]
4076    pub(crate) fn remove_pool_collect_subscription(
4077        &mut self,
4078        topic: MStr<Topic>,
4079    ) -> Option<DataCommand> {
4080        let subscription = self.pool_collect_handlers.remove(&topic)?;
4081        msgbus::unsubscribe_defi_collects(topic.into(), &subscription.handler);
4082        subscription.command
4083    }
4084
4085    #[cfg(feature = "defi")]
4086    pub(crate) fn add_pool_flash_subscription(
4087        &mut self,
4088        topic: MStr<Topic>,
4089        handler: TypedHandler<PoolFlash>,
4090        command: DataCommand,
4091    ) -> bool {
4092        if self.pool_flash_handlers.contains_key(&topic) {
4093            log::warn!(
4094                "Actor {} attempted duplicate pool flash subscription to '{topic}'",
4095                self.actor_id
4096            );
4097            return false;
4098        }
4099        self.pool_flash_handlers.insert(
4100            topic,
4101            Subscription {
4102                handler: handler.clone(),
4103                command: Some(command),
4104            },
4105        );
4106        msgbus::subscribe_defi_flash(topic.into(), handler, None);
4107        true
4108    }
4109
4110    #[cfg(feature = "defi")]
4111    #[allow(dead_code)]
4112    pub(crate) fn remove_pool_flash_subscription(
4113        &mut self,
4114        topic: MStr<Topic>,
4115    ) -> Option<DataCommand> {
4116        let subscription = self.pool_flash_handlers.remove(&topic)?;
4117        msgbus::unsubscribe_defi_flash(topic.into(), &subscription.handler);
4118        subscription.command
4119    }
4120
4121    /// Removes every message bus handler and releases each retained venue subscription.
4122    ///
4123    /// Called on disposal so retirement leaves no handler which would resolve an actor that
4124    /// deregistration has already removed.
4125    pub(crate) fn unsubscribe_all(&mut self) {
4126        let mut commands = Vec::new();
4127
4128        Self::drain_subscriptions(
4129            std::mem::take(&mut self.topic_handlers),
4130            &mut commands,
4131            msgbus::unsubscribe_any,
4132        );
4133        Self::drain_subscriptions(
4134            std::mem::take(&mut self.instrument_handlers),
4135            &mut commands,
4136            msgbus::unsubscribe_instruments,
4137        );
4138        Self::drain_subscriptions(
4139            std::mem::take(&mut self.deltas_handlers),
4140            &mut commands,
4141            msgbus::unsubscribe_book_deltas,
4142        );
4143        Self::drain_subscriptions(
4144            std::mem::take(&mut self.depth_handlers),
4145            &mut commands,
4146            msgbus::unsubscribe_book_depth,
4147        );
4148        Self::drain_subscriptions(
4149            std::mem::take(&mut self.book_handlers),
4150            &mut commands,
4151            |topic, handler| msgbus::unsubscribe_book_snapshots(topic.into(), handler),
4152        );
4153        Self::drain_subscriptions(
4154            std::mem::take(&mut self.quote_handlers),
4155            &mut commands,
4156            |topic, handler| msgbus::unsubscribe_quotes(topic.into(), handler),
4157        );
4158        Self::drain_subscriptions(
4159            std::mem::take(&mut self.trade_handlers),
4160            &mut commands,
4161            |topic, handler| msgbus::unsubscribe_trades(topic.into(), handler),
4162        );
4163        Self::drain_subscriptions(
4164            std::mem::take(&mut self.bar_handlers),
4165            &mut commands,
4166            |topic, handler| msgbus::unsubscribe_bars(topic.into(), handler),
4167        );
4168        Self::drain_subscriptions(
4169            std::mem::take(&mut self.mark_price_handlers),
4170            &mut commands,
4171            |topic, handler| msgbus::unsubscribe_mark_prices(topic.into(), handler),
4172        );
4173        Self::drain_subscriptions(
4174            std::mem::take(&mut self.index_price_handlers),
4175            &mut commands,
4176            |topic, handler| msgbus::unsubscribe_index_prices(topic.into(), handler),
4177        );
4178        Self::drain_subscriptions(
4179            std::mem::take(&mut self.funding_rate_handlers),
4180            &mut commands,
4181            |topic, handler| msgbus::unsubscribe_funding_rates(topic.into(), handler),
4182        );
4183        Self::drain_subscriptions(
4184            std::mem::take(&mut self.option_greeks_handlers),
4185            &mut commands,
4186            |topic, handler| msgbus::unsubscribe_option_greeks(topic.into(), handler),
4187        );
4188        Self::drain_subscriptions(
4189            std::mem::take(&mut self.option_chain_handlers),
4190            &mut commands,
4191            |topic, handler| msgbus::unsubscribe_option_chain(topic.into(), handler),
4192        );
4193
4194        #[cfg(feature = "defi")]
4195        self.unsubscribe_all_defi(&mut commands);
4196
4197        let mut retained = Vec::new();
4198        commands.retain(|command| {
4199            if matches!(command, DataCommand::Subscribe(subscribe)
4200                if subscribe.instrument_ids().is_some() || subscribe.bar_types().is_some())
4201            {
4202                retained.push(command.clone());
4203                false
4204            } else {
4205                true
4206            }
4207        });
4208
4209        self.release_subscriptions(retained);
4210
4211        for command in commands {
4212            if let Some(command) = command.into_unsubscribe(UUID4::new(), self.timestamp_ns()) {
4213                self.send_data_cmd(command);
4214            }
4215        }
4216        #[cfg(feature = "python")]
4217        self.message_bus.clear();
4218    }
4219
4220    #[cfg(feature = "defi")]
4221    fn unsubscribe_all_defi(&mut self, commands: &mut Vec<DataCommand>) {
4222        Self::drain_subscriptions(
4223            std::mem::take(&mut self.block_handlers),
4224            commands,
4225            |topic, handler| msgbus::unsubscribe_defi_blocks(topic.into(), handler),
4226        );
4227        Self::drain_subscriptions(
4228            std::mem::take(&mut self.pool_handlers),
4229            commands,
4230            |topic, handler| msgbus::unsubscribe_defi_pools(topic.into(), handler),
4231        );
4232        Self::drain_subscriptions(
4233            std::mem::take(&mut self.pool_swap_handlers),
4234            commands,
4235            |topic, handler| msgbus::unsubscribe_defi_swaps(topic.into(), handler),
4236        );
4237        Self::drain_subscriptions(
4238            std::mem::take(&mut self.pool_liquidity_handlers),
4239            commands,
4240            |topic, handler| msgbus::unsubscribe_defi_liquidity(topic.into(), handler),
4241        );
4242        Self::drain_subscriptions(
4243            std::mem::take(&mut self.pool_collect_handlers),
4244            commands,
4245            |topic, handler| msgbus::unsubscribe_defi_collects(topic.into(), handler),
4246        );
4247        Self::drain_subscriptions(
4248            std::mem::take(&mut self.pool_flash_handlers),
4249            commands,
4250            |topic, handler| msgbus::unsubscribe_defi_flash(topic.into(), handler),
4251        );
4252    }
4253
4254    fn drain_subscriptions<K, T>(
4255        subscriptions: AHashMap<K, Subscription<T>>,
4256        commands: &mut Vec<DataCommand>,
4257        mut unsubscribe: impl FnMut(K, &T),
4258    ) where
4259        K: AsRef<str>,
4260    {
4261        let mut subscriptions = subscriptions.into_iter().collect::<Vec<_>>();
4262        subscriptions.sort_unstable_by(|(left, _), (right, _)| left.as_ref().cmp(right.as_ref()));
4263
4264        for (key, subscription) in subscriptions {
4265            unsubscribe(key, &subscription.handler);
4266            commands.extend(subscription.command);
4267        }
4268    }
4269
4270    /// Creates a new [`DataActorCore`] instance.
4271    pub fn new(config: DataActorConfig) -> Self {
4272        let actor_id = config.actor_id.unwrap_or_else(Self::default_actor_id);
4273
4274        Self {
4275            actor_id,
4276            config,
4277            trader_id: None, // None until registered
4278            clock: None,     // None until registered
4279            cache: None,     // None until registered
4280            state: ComponentState::default(),
4281            topic_handlers: AHashMap::new(),
4282            instrument_handlers: AHashMap::new(),
4283            deltas_handlers: AHashMap::new(),
4284            depth_handlers: AHashMap::new(),
4285            book_handlers: AHashMap::new(),
4286            quote_handlers: AHashMap::new(),
4287            trade_handlers: AHashMap::new(),
4288            bar_handlers: AHashMap::new(),
4289            mark_price_handlers: AHashMap::new(),
4290            index_price_handlers: AHashMap::new(),
4291            funding_rate_handlers: AHashMap::new(),
4292            option_greeks_handlers: AHashMap::new(),
4293            option_chain_handlers: AHashMap::new(),
4294            indicators: Indicators::default(),
4295            warning_events: AHashSet::new(),
4296            requests: RefCell::new(RequestLifecycle::default()),
4297            signal_classes: AHashMap::new(),
4298            #[cfg(feature = "defi")]
4299            block_handlers: AHashMap::new(),
4300            #[cfg(feature = "defi")]
4301            pool_handlers: AHashMap::new(),
4302            #[cfg(feature = "defi")]
4303            pool_swap_handlers: AHashMap::new(),
4304            #[cfg(feature = "defi")]
4305            pool_liquidity_handlers: AHashMap::new(),
4306            #[cfg(feature = "defi")]
4307            pool_collect_handlers: AHashMap::new(),
4308            #[cfg(feature = "defi")]
4309            pool_flash_handlers: AHashMap::new(),
4310            #[cfg(feature = "python")]
4311            message_bus: Rc::default(),
4312        }
4313    }
4314
4315    /// Returns the registered indicators.
4316    #[must_use]
4317    pub fn registered_indicators(&self) -> Vec<SharedActorIndicator> {
4318        self.indicators.registered_indicators()
4319    }
4320
4321    /// Returns whether all registered indicators are initialized.
4322    ///
4323    /// # Errors
4324    ///
4325    /// Returns an error if a registered indicator cannot report readiness.
4326    pub fn indicators_initialized(&self) -> anyhow::Result<bool> {
4327        self.indicators.initialized()
4328    }
4329
4330    /// Registers an indicator to receive quote ticks for an instrument.
4331    pub fn register_indicator_for_quote_ticks(
4332        &mut self,
4333        instrument_id: InstrumentId,
4334        indicator: SharedActorIndicator,
4335    ) {
4336        self.indicators
4337            .register_indicator_for_quote_ticks(instrument_id, indicator);
4338    }
4339
4340    /// Registers an indicator to receive trade ticks for an instrument.
4341    pub fn register_indicator_for_trade_ticks(
4342        &mut self,
4343        instrument_id: InstrumentId,
4344        indicator: SharedActorIndicator,
4345    ) {
4346        self.indicators
4347            .register_indicator_for_trade_ticks(instrument_id, indicator);
4348    }
4349
4350    /// Registers an indicator to receive bars for a bar type.
4351    pub fn register_indicator_for_bars(
4352        &mut self,
4353        bar_type: BarType,
4354        indicator: SharedActorIndicator,
4355    ) {
4356        self.indicators
4357            .register_indicator_for_bars(bar_type, indicator);
4358    }
4359
4360    pub(crate) fn handle_indicators_for_quote(&self, quote: &QuoteTick) -> anyhow::Result<()> {
4361        self.indicators.handle_quote(quote)
4362    }
4363
4364    pub(crate) fn handle_indicators_for_quotes(&self, quotes: &[QuoteTick]) -> anyhow::Result<()> {
4365        self.indicators.handle_quotes(quotes)
4366    }
4367
4368    pub(crate) fn handle_indicators_for_trade(&self, trade: &TradeTick) -> anyhow::Result<()> {
4369        self.indicators.handle_trade(trade)
4370    }
4371
4372    pub(crate) fn handle_indicators_for_trades(&self, trades: &[TradeTick]) -> anyhow::Result<()> {
4373        self.indicators.handle_trades(trades)
4374    }
4375
4376    pub(crate) fn handle_indicators_for_bar(&self, bar: &Bar) -> anyhow::Result<()> {
4377        self.indicators.handle_bar(bar)
4378    }
4379
4380    pub(crate) fn handle_indicators_for_bars(&self, bars: &[Bar]) -> anyhow::Result<()> {
4381        self.indicators.handle_bars(bars)
4382    }
4383
4384    /// Returns the memory address of this instance as a hexadecimal string.
4385    #[must_use]
4386    pub fn mem_address(&self) -> String {
4387        format!("{self:p}")
4388    }
4389
4390    /// Returns the actors state.
4391    pub fn state(&self) -> ComponentState {
4392        self.state
4393    }
4394
4395    /// Returns the trader ID this actor is registered to.
4396    pub fn trader_id(&self) -> Option<TraderId> {
4397        self.trader_id
4398    }
4399
4400    /// Returns the actors ID.
4401    pub fn actor_id(&self) -> ActorId {
4402        self.actor_id
4403    }
4404
4405    fn default_actor_id() -> ActorId {
4406        ActorId::from(stringify!(DataActor))
4407    }
4408
4409    /// Returns a UNIX nanoseconds timestamp from the actor's internal clock.
4410    pub fn timestamp_ns(&self) -> UnixNanos {
4411        self.clock_ref().timestamp_ns()
4412    }
4413
4414    pub(super) fn clock_api(&self) -> ClockApi<'_> {
4415        let clock = self.clock.as_ref().unwrap_or_else(|| {
4416            panic!(
4417                "DataActor {} must be registered before calling `clock()` - trader_id: {:?}",
4418                self.actor_id, self.trader_id
4419            )
4420        });
4421        ClockApi::new(clock.as_ref())
4422    }
4423
4424    fn clock_ref(&self) -> Ref<'_, dyn Clock> {
4425        self.clock
4426            .as_ref()
4427            .unwrap_or_else(|| {
4428                panic!(
4429                    "DataActor {} must be registered before calling `clock_ref()` - trader_id: {:?}",
4430                    self.actor_id, self.trader_id
4431                )
4432            })
4433            .borrow()
4434    }
4435
4436    fn cache_api(&self) -> CacheApi<'_> {
4437        let cache = self.cache.as_ref().unwrap_or_else(|| {
4438            panic!(
4439                "DataActor {} must be registered before calling `cache()` - trader_id: {:?}",
4440                self.actor_id, self.trader_id
4441            )
4442        });
4443        CacheApi::new(cache.as_ref())
4444    }
4445
4446    /// Register the data actor with a trader.
4447    ///
4448    /// # Errors
4449    ///
4450    /// Returns an error if the actor has already been registered with a trader
4451    /// or if the provided dependencies are invalid.
4452    pub fn register(
4453        &mut self,
4454        trader_id: TraderId,
4455        clock: Rc<RefCell<dyn Clock>>,
4456        cache: Rc<RefCell<Cache>>,
4457    ) -> anyhow::Result<()> {
4458        if let Some(existing_trader_id) = self.trader_id {
4459            anyhow::bail!(
4460                "DataActor {} already registered with trader {existing_trader_id}",
4461                self.actor_id
4462            );
4463        }
4464
4465        // Validate clock by attempting to access it
4466        {
4467            let _timestamp = clock.borrow().timestamp_ns();
4468        }
4469
4470        // Validate cache by attempting to access it
4471        {
4472            let _cache_borrow = cache.borrow();
4473        }
4474
4475        #[cfg(feature = "python")]
4476        self.message_bus.register();
4477
4478        self.trader_id = Some(trader_id);
4479        self.clock = Some(clock);
4480        self.cache = Some(cache);
4481
4482        // Verify complete registration
4483        if !self.is_properly_registered() {
4484            anyhow::bail!(
4485                "DataActor {} registration incomplete - validation failed",
4486                self.actor_id
4487            );
4488        }
4489
4490        log::debug!("Registered {} with trader {trader_id}", self.actor_id);
4491        Ok(())
4492    }
4493
4494    /// Register an event type for warning log levels.
4495    pub fn register_warning_event(&mut self, event_type: &str) {
4496        self.warning_events.insert(event_type.to_string());
4497        log::debug!("Registered event type '{event_type}' for warning logs");
4498    }
4499
4500    /// Deregister an event type from warning log levels.
4501    pub fn deregister_warning_event(&mut self, event_type: &str) {
4502        self.warning_events.remove(event_type);
4503        log::debug!("Deregistered event type '{event_type}' from warning logs");
4504    }
4505
4506    /// Returns this component's shared Python message-bus state.
4507    #[cfg(feature = "python")]
4508    pub fn message_bus(&self) -> Rc<PyMessageBusScope> {
4509        Rc::clone(&self.message_bus)
4510    }
4511
4512    pub fn is_registered(&self) -> bool {
4513        self.trader_id.is_some()
4514    }
4515
4516    pub(crate) fn check_registered(&self) {
4517        assert!(
4518            self.is_registered(),
4519            "Actor has not been registered with a Trader"
4520        );
4521    }
4522
4523    /// Validates registration state without panicking.
4524    fn is_properly_registered(&self) -> bool {
4525        self.trader_id.is_some() && self.clock.is_some() && self.cache.is_some()
4526    }
4527
4528    pub(crate) fn send_data_cmd(&self, command: DataCommand) {
4529        if self.config.log_commands {
4530            log::info!("{CMD}{SEND} {command:?}");
4531        }
4532
4533        let endpoint = MessagingSwitchboard::data_engine_queue_execute();
4534        msgbus::send_data_command(endpoint, command);
4535    }
4536
4537    pub(crate) fn send_unsubscribe_cmd(
4538        &self,
4539        retained: Option<DataCommand>,
4540        fallback: DataCommand,
4541    ) {
4542        let Some(retained) = retained else {
4543            return;
4544        };
4545        let command = retained
4546            .into_unsubscribe(UUID4::new(), self.timestamp_ns())
4547            .unwrap_or(fallback);
4548        self.send_data_cmd(command);
4549    }
4550
4551    #[allow(dead_code)]
4552    fn send_data_req(&self, request: &RequestCommand) {
4553        if self.config.log_commands {
4554            log::info!("{REQ}{SEND} {request:?}");
4555        }
4556
4557        // For now, simplified approach - data requests without dynamic handlers
4558        // TODO: Implement proper dynamic dispatch for response handlers
4559        let endpoint = MessagingSwitchboard::data_engine_queue_execute();
4560        msgbus::send_any(endpoint, request.as_any());
4561    }
4562
4563    /// Sends a shutdown command to the system with an optional reason.
4564    ///
4565    /// # Panics
4566    ///
4567    /// Panics if the actor is not registered or has no trader ID.
4568    pub fn shutdown_system(&self, reason: Option<String>) {
4569        self.check_registered();
4570
4571        // Checked registered before unwrapping trader ID
4572        let command = ShutdownSystem::new(
4573            self.trader_id().unwrap(),
4574            self.actor_id.inner(),
4575            reason,
4576            UUID4::new(),
4577            self.timestamp_ns(),
4578            None, // correlation_id
4579        );
4580
4581        let topic = MessagingSwitchboard::shutdown_system_topic();
4582        msgbus::publish_any(topic, command.as_any());
4583    }
4584
4585    /// Publishes `data` on the message bus under the topic derived from `data_type`.
4586    ///
4587    /// `data_type` is kept as an explicit parameter (rather than deriving it from
4588    /// `data.data_type`) to mirror the v1 Python `publish_data(data_type, data)` API and
4589    /// to allow callers to override the routing topic from the payload's intrinsic type.
4590    ///
4591    /// # Panics
4592    ///
4593    /// Panics if the actor is not registered with a trader.
4594    pub fn publish_data(&self, data_type: &DataType, data: &CustomData) {
4595        self.check_registered();
4596
4597        let topic = get_custom_topic(data_type);
4598        msgbus::publish_any(topic, data);
4599    }
4600
4601    /// Publishes a [`Signal`] constructed from `name` and `value`, wrapped in [`CustomData`]
4602    /// so it is consumed by signal subscribers and by any `CustomData`-aware pipeline
4603    /// (for example the feather persistence writer).
4604    ///
4605    /// The topic mirrors the v1 Python scheme `data.Signal<TitleName>` so subscribers
4606    /// using either a specific name or the global wildcard are both notified.
4607    /// If `ts_event` is zero then the current clock timestamp is used.
4608    ///
4609    /// # Panics
4610    ///
4611    /// Panics if the actor is not registered with a trader.
4612    pub fn publish_signal(&self, name: &str, value: String, ts_event: UnixNanos) {
4613        self.check_registered();
4614
4615        let now = self.timestamp_ns();
4616        let ts_event = if ts_event.as_u64() == 0 {
4617            now
4618        } else {
4619            ts_event
4620        };
4621        let signal = Signal::new(Ustr::from(name), value, ts_event, now);
4622
4623        let data_type = DataType::new(
4624            &format!(
4625                "Signal{}",
4626                nautilus_core::string::conversions::title_case(name)
4627            ),
4628            None,
4629            None,
4630        );
4631        let data = CustomData::new(Arc::new(signal), data_type);
4632        let topic = get_custom_topic(&data.data_type);
4633        msgbus::publish_any(topic, &data);
4634    }
4635
4636    /// Adds the `synthetic` instrument to the cache.
4637    ///
4638    /// # Errors
4639    ///
4640    /// Returns an error if a synthetic with the same ID already exists, or if the
4641    /// backing cache fails to persist it. Panics if the actor is not registered
4642    /// with a trader. // panics-doc-ok
4643    pub fn add_synthetic(&self, synthetic: SyntheticInstrument) -> anyhow::Result<()> {
4644        self.check_registered();
4645
4646        let cache = self.cache_rc();
4647        if cache.borrow().synthetic(&synthetic.id).is_some() {
4648            anyhow::bail!("`synthetic` {} already exists", synthetic.id);
4649        }
4650        cache.borrow_mut().add_synthetic(synthetic)
4651    }
4652
4653    /// Updates the `synthetic` instrument in the cache, replacing the existing entry.
4654    ///
4655    /// # Errors
4656    ///
4657    /// Returns an error if no synthetic with the same ID already exists, or if the
4658    /// backing cache fails to persist the replacement. Panics if the actor is not
4659    /// registered with a trader. // panics-doc-ok
4660    pub fn update_synthetic(&self, synthetic: SyntheticInstrument) -> anyhow::Result<()> {
4661        self.check_registered();
4662
4663        let cache = self.cache_rc();
4664        if cache.borrow().synthetic(&synthetic.id).is_none() {
4665            anyhow::bail!("`synthetic` {} does not exist", synthetic.id);
4666        }
4667        cache.borrow_mut().add_synthetic(synthetic)
4668    }
4669
4670    /// Subscribes the actor to data.
4671    ///
4672    /// # Panics
4673    ///
4674    /// Panics if the actor is not properly registered.
4675    pub fn subscribe_data(
4676        &mut self,
4677        handler: ShareableMessageHandler,
4678        data_type: DataType,
4679        client_id: Option<ClientId>,
4680        params: Option<Params>,
4681    ) {
4682        assert!(
4683            self.is_properly_registered(),
4684            "DataActor {} is not properly registered - trader_id: {:?}, clock: {}, cache: {}",
4685            self.actor_id,
4686            self.trader_id,
4687            self.clock.is_some(),
4688            self.cache.is_some()
4689        );
4690
4691        let mut topics = get_custom_subscription_topics(&data_type);
4692        let Some(topic) = topics.pop() else {
4693            return;
4694        };
4695
4696        for alias in topics {
4697            self.add_subscription_any(alias, handler.clone(), None, None);
4698        }
4699
4700        // If no client ID specified, just subscribe to the topic
4701        if client_id.is_none() {
4702            self.add_subscription_any(topic, handler, None, None);
4703            return;
4704        }
4705
4706        let command = DataCommand::Subscribe(SubscribeCommand::Data(SubscribeCustomData {
4707            data_type,
4708            client_id,
4709            venue: None,
4710            command_id: UUID4::new(),
4711            ts_init: self.timestamp_ns(),
4712            correlation_id: None,
4713            params,
4714        }));
4715
4716        if self.add_subscription_any(topic, handler, None, Some(command.clone())) {
4717            self.send_data_cmd(command);
4718        }
4719    }
4720
4721    /// Subscribes the actor to signal.
4722    ///
4723    /// An empty `name` subscribes to every signal via the `data.Signal*` wildcard pattern.
4724    ///
4725    /// # Panics
4726    ///
4727    /// Panics if the actor is not registered with a trader.
4728    pub fn subscribe_signal(
4729        &mut self,
4730        handler: ShareableMessageHandler,
4731        name: &str,
4732        priority: Option<u32>,
4733    ) {
4734        self.check_registered();
4735
4736        let pattern = get_signal_pattern(name);
4737        if self.topic_handlers.contains_key(&pattern) {
4738            log::warn!(
4739                "Actor {} attempted duplicate signal subscription to '{pattern}'",
4740                self.actor_id,
4741            );
4742            return;
4743        }
4744        self.topic_handlers.insert(
4745            pattern,
4746            Subscription {
4747                handler: handler.clone(),
4748                command: None,
4749            },
4750        );
4751        msgbus::subscribe_any(pattern, handler, priority);
4752    }
4753
4754    /// Registers a queue state change subscription from the trait.
4755    ///
4756    /// # Panics
4757    ///
4758    /// Panics if the actor is not registered with a trader.
4759    pub fn subscribe_queue_state(
4760        &mut self,
4761        handler: ShareableMessageHandler,
4762        channel: Option<SystemChannel>,
4763        priority: Option<u32>,
4764    ) {
4765        self.check_registered();
4766
4767        let topic = MessagingSwitchboard::queue_state_changed_pattern(channel);
4768        self.add_subscription_any(topic, handler, priority, None);
4769    }
4770
4771    /// Registers a socket state change subscription from the trait.
4772    ///
4773    /// # Panics
4774    ///
4775    /// Panics if the actor is not registered with a trader.
4776    pub fn subscribe_socket_state(
4777        &mut self,
4778        handler: ShareableMessageHandler,
4779        client_id: Option<ClientId>,
4780        endpoint: Option<&str>,
4781        priority: Option<u32>,
4782    ) {
4783        self.check_registered();
4784
4785        let topic = MessagingSwitchboard::socket_state_changed_pattern(client_id, endpoint);
4786        self.add_subscription_any(topic, handler, priority, None);
4787    }
4788
4789    /// Subscribes the actor to quotes.
4790    ///
4791    /// # Errors
4792    ///
4793    /// Returns an error if:
4794    /// - The list is empty.
4795    /// - The IDs span venues without an explicit client.
4796    /// - The client is not external and the list mixes client, synthetic, or spread quote routes.
4797    pub fn subscribe_quotes(
4798        &mut self,
4799        handler: TypedHandler<QuoteTick>,
4800        instrument_ids: impl IntoInstrumentIds,
4801        client_id: Option<ClientId>,
4802        params: Option<Params>,
4803    ) -> anyhow::Result<()> {
4804        self.check_registered();
4805        let instrument_ids = instrument_ids.into_instrument_ids()?;
4806        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
4807        quote_routing(
4808            &instrument_ids,
4809            client_id.as_ref(),
4810            params.as_ref(),
4811            &self.cache_ref(),
4812        )?;
4813        let first_venue = instrument_ids[0].venue;
4814        let venue = instrument_ids
4815            .iter()
4816            .all(|id| id.venue == first_venue)
4817            .then_some(first_venue);
4818
4819        let command = SubscribeQuotes {
4820            instrument_ids,
4821            client_id,
4822            venue,
4823            command_id: UUID4::new(),
4824            ts_init: self.timestamp_ns(),
4825            correlation_id: None,
4826            params,
4827        };
4828
4829        let mut acquired = Vec::with_capacity(command.instrument_ids.len());
4830        let handlers = std::iter::repeat_n(handler, command.instrument_ids.len());
4831        for (instrument_id, handler) in command.instrument_ids.iter().zip(handlers) {
4832            let member = command.with_instrument_ids(*instrument_id);
4833
4834            let topic = get_quotes_topic(*instrument_id);
4835            if self.add_quote_subscription(
4836                topic,
4837                handler,
4838                DataCommand::Subscribe(SubscribeCommand::Quotes(member)),
4839            ) {
4840                acquired.push(*instrument_id);
4841            }
4842        }
4843
4844        if !acquired.is_empty() {
4845            let dispatch = command.with_instrument_ids(acquired);
4846            self.send_data_cmd(DataCommand::Subscribe(SubscribeCommand::Quotes(dispatch)));
4847        }
4848
4849        Ok(())
4850    }
4851
4852    /// Subscribes the actor to instruments.
4853    pub fn subscribe_instruments(
4854        &mut self,
4855        pattern: MStr<Pattern>,
4856        handler: TypedHandler<InstrumentAny>,
4857        venue: Venue,
4858        client_id: Option<ClientId>,
4859        params: Option<Params>,
4860    ) {
4861        self.check_registered();
4862
4863        let command = DataCommand::Subscribe(SubscribeCommand::Instruments(SubscribeInstruments {
4864            client_id,
4865            venue,
4866            command_id: UUID4::new(),
4867            ts_init: self.timestamp_ns(),
4868            correlation_id: None,
4869            params,
4870        }));
4871
4872        if self.add_instrument_subscription(pattern, handler, command.clone()) {
4873            self.send_data_cmd(command);
4874        }
4875    }
4876
4877    /// Subscribes the actor to instrument.
4878    ///
4879    /// # Errors
4880    ///
4881    /// Returns an error if:
4882    /// - The IDs are empty.
4883    /// - The IDs span venues without an explicit client.
4884    /// - The client is not external and the list contains multiple IDs with a synthetic member.
4885    pub fn subscribe_instrument(
4886        &mut self,
4887        handler: TypedHandler<InstrumentAny>,
4888        instrument_ids: impl IntoInstrumentIds,
4889        client_id: Option<ClientId>,
4890        params: Option<Params>,
4891    ) -> anyhow::Result<()> {
4892        self.check_registered();
4893        let instrument_ids = instrument_ids.into_instrument_ids()?;
4894        self.validate_subscription(&instrument_ids, client_id.as_ref())?;
4895        let venue = instrument_ids
4896            .iter()
4897            .all(|id| id.venue == instrument_ids[0].venue)
4898            .then_some(instrument_ids[0].venue);
4899
4900        let command = SubscribeInstrument {
4901            instrument_ids,
4902            client_id,
4903            venue,
4904            command_id: UUID4::new(),
4905            ts_init: self.timestamp_ns(),
4906            correlation_id: None,
4907            params,
4908        };
4909
4910        let mut acquired = Vec::new();
4911        let handlers = std::iter::repeat_n(handler, command.instrument_ids.len());
4912        for (&instrument_id, handler) in command.instrument_ids.iter().zip(handlers) {
4913            let member = command.with_instrument_ids(instrument_id);
4914
4915            let topic = get_instrument_topic(instrument_id);
4916            if self.add_instrument_subscription(
4917                topic.into(),
4918                handler,
4919                DataCommand::Subscribe(SubscribeCommand::Instrument(member)),
4920            ) {
4921                acquired.push(instrument_id);
4922            }
4923        }
4924
4925        if !acquired.is_empty() {
4926            let dispatch = command.with_instrument_ids(acquired);
4927            self.send_data_cmd(DataCommand::Subscribe(SubscribeCommand::Instrument(
4928                dispatch,
4929            )));
4930        }
4931
4932        Ok(())
4933    }
4934
4935    /// Subscribes the actor to book deltas.
4936    ///
4937    /// # Errors
4938    ///
4939    /// Returns an error if:
4940    /// - The IDs are empty.
4941    /// - The IDs span venues without an explicit client.
4942    /// - The client is not external and the list contains multiple IDs with a synthetic member.
4943    #[expect(clippy::too_many_arguments)]
4944    pub fn subscribe_book_deltas(
4945        &mut self,
4946        handler: TypedHandler<OrderBookDeltas>,
4947        instrument_ids: impl IntoInstrumentIds,
4948        book_type: BookType,
4949        depth: Option<NonZeroUsize>,
4950        client_id: Option<ClientId>,
4951        managed: bool,
4952        params: Option<Params>,
4953    ) -> anyhow::Result<()> {
4954        self.check_registered();
4955        let instrument_ids = instrument_ids.into_instrument_ids()?;
4956        self.validate_subscription(&instrument_ids, client_id.as_ref())?;
4957        let venue = instrument_ids
4958            .iter()
4959            .all(|id| id.venue == instrument_ids[0].venue)
4960            .then_some(instrument_ids[0].venue);
4961
4962        let command = SubscribeBookDeltas {
4963            instrument_ids,
4964            book_type,
4965            client_id,
4966            venue,
4967            command_id: UUID4::new(),
4968            ts_init: self.timestamp_ns(),
4969            depth,
4970            managed,
4971            correlation_id: None,
4972            params,
4973        };
4974
4975        let mut acquired = Vec::new();
4976        let handlers = std::iter::repeat_n(handler, command.instrument_ids.len());
4977        for (&instrument_id, handler) in command.instrument_ids.iter().zip(handlers) {
4978            let member = command.with_instrument_ids(instrument_id);
4979            let topic = if is_parent_subscription(command.params.as_ref()) {
4980                get_book_deltas_pattern(instrument_id)
4981            } else {
4982                get_book_deltas_topic(instrument_id).into()
4983            };
4984
4985            if self.add_deltas_subscription(
4986                topic,
4987                handler,
4988                DataCommand::Subscribe(SubscribeCommand::BookDeltas(member)),
4989            ) {
4990                acquired.push(instrument_id);
4991            }
4992        }
4993
4994        if !acquired.is_empty() {
4995            let dispatch = command.with_instrument_ids(acquired);
4996            self.send_data_cmd(DataCommand::Subscribe(SubscribeCommand::BookDeltas(
4997                dispatch,
4998            )));
4999        }
5000
5001        Ok(())
5002    }
5003
5004    /// Subscribes the actor to book depth.
5005    ///
5006    /// # Errors
5007    ///
5008    /// Returns an error if:
5009    /// - The IDs are empty.
5010    /// - The IDs span venues without an explicit client.
5011    /// - The client is not external and the list contains multiple IDs with a synthetic member.
5012    #[expect(clippy::too_many_arguments)]
5013    pub fn subscribe_book_depth(
5014        &mut self,
5015        handler: TypedHandler<OrderBookDepth>,
5016        instrument_ids: impl IntoInstrumentIds,
5017        book_type: BookType,
5018        depth: Option<NonZeroUsize>,
5019        client_id: Option<ClientId>,
5020        managed: bool,
5021        params: Option<Params>,
5022    ) -> anyhow::Result<()> {
5023        self.check_registered();
5024        let instrument_ids = instrument_ids.into_instrument_ids()?;
5025        self.validate_subscription(&instrument_ids, client_id.as_ref())?;
5026        let venue = instrument_ids
5027            .iter()
5028            .all(|id| id.venue == instrument_ids[0].venue)
5029            .then_some(instrument_ids[0].venue);
5030
5031        let command = SubscribeBookDepth {
5032            instrument_ids,
5033            book_type,
5034            client_id,
5035            venue,
5036            command_id: UUID4::new(),
5037            ts_init: self.timestamp_ns(),
5038            depth,
5039            managed,
5040            correlation_id: None,
5041            params,
5042        };
5043
5044        let mut acquired = Vec::new();
5045        let handlers = std::iter::repeat_n(handler, command.instrument_ids.len());
5046        for (&instrument_id, handler) in command.instrument_ids.iter().zip(handlers) {
5047            let member = command.with_instrument_ids(instrument_id);
5048            let topic = if is_parent_subscription(command.params.as_ref()) {
5049                get_book_depth_pattern(instrument_id)
5050            } else {
5051                get_book_depth_topic(instrument_id).into()
5052            };
5053
5054            if self.add_depth_subscription(
5055                topic,
5056                handler,
5057                DataCommand::Subscribe(SubscribeCommand::BookDepth(member)),
5058            ) {
5059                acquired.push(instrument_id);
5060            }
5061        }
5062
5063        if !acquired.is_empty() {
5064            let dispatch = command.with_instrument_ids(acquired);
5065            self.send_data_cmd(DataCommand::Subscribe(SubscribeCommand::BookDepth(
5066                dispatch,
5067            )));
5068        }
5069
5070        Ok(())
5071    }
5072
5073    /// Subscribes the actor to book snapshots.
5074    ///
5075    /// # Errors
5076    ///
5077    /// Returns an error if:
5078    /// - The IDs are empty.
5079    /// - The IDs span venues without an explicit client.
5080    /// - The client is not external and the list contains multiple IDs with a synthetic member.
5081    #[expect(clippy::too_many_arguments)]
5082    pub fn subscribe_book_at_interval(
5083        &mut self,
5084        handler: TypedHandler<OrderBook>,
5085        instrument_ids: impl IntoInstrumentIds,
5086        book_type: BookType,
5087        depth: Option<NonZeroUsize>,
5088        interval_ms: NonZeroUsize,
5089        client_id: Option<ClientId>,
5090        params: Option<Params>,
5091    ) -> anyhow::Result<()> {
5092        self.check_registered();
5093        let instrument_ids = instrument_ids.into_instrument_ids()?;
5094        self.validate_subscription(&instrument_ids, client_id.as_ref())?;
5095        let venue = instrument_ids
5096            .iter()
5097            .all(|id| id.venue == instrument_ids[0].venue)
5098            .then_some(instrument_ids[0].venue);
5099
5100        let command = SubscribeBookSnapshots {
5101            instrument_ids,
5102            book_type,
5103            client_id,
5104            venue,
5105            command_id: UUID4::new(),
5106            ts_init: self.timestamp_ns(),
5107            depth,
5108            interval_ms,
5109            correlation_id: None,
5110            params,
5111        };
5112
5113        let mut acquired = Vec::new();
5114        let handlers = std::iter::repeat_n(handler, command.instrument_ids.len());
5115        for (&instrument_id, handler) in command.instrument_ids.iter().zip(handlers) {
5116            let member = command.with_instrument_ids(instrument_id);
5117
5118            let topic = get_book_snapshots_topic(instrument_id, interval_ms);
5119            if self.add_book_snapshot_subscription(
5120                topic,
5121                handler,
5122                DataCommand::Subscribe(SubscribeCommand::BookSnapshots(member)),
5123            ) {
5124                acquired.push(instrument_id);
5125            }
5126        }
5127
5128        if !acquired.is_empty() {
5129            let dispatch = command.with_instrument_ids(acquired);
5130            self.send_data_cmd(DataCommand::Subscribe(SubscribeCommand::BookSnapshots(
5131                dispatch,
5132            )));
5133        }
5134
5135        Ok(())
5136    }
5137
5138    /// Subscribes the actor to trades.
5139    ///
5140    /// # Errors
5141    ///
5142    /// Returns an error if:
5143    /// - The list is empty.
5144    /// - The IDs span venues without an explicit client.
5145    pub fn subscribe_trades(
5146        &mut self,
5147        handler: TypedHandler<TradeTick>,
5148        instrument_ids: impl IntoInstrumentIds,
5149        client_id: Option<ClientId>,
5150        params: Option<Params>,
5151    ) -> anyhow::Result<()> {
5152        self.check_registered();
5153        let instrument_ids = instrument_ids.into_instrument_ids()?;
5154        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
5155        let mut groups: IndexMap<Option<Venue>, Vec<InstrumentId>> = IndexMap::new();
5156        for id in instrument_ids {
5157            groups
5158                .entry(id.is_synthetic().then_some(id.venue))
5159                .or_default()
5160                .push(id);
5161        }
5162
5163        let contexts = std::iter::repeat_n((handler, params), groups.len());
5164        for ((venue, ids), (handler, params)) in groups.into_iter().zip(contexts) {
5165            let venue = venue.or_else(|| {
5166                ids.iter()
5167                    .all(|id| id.venue == ids[0].venue)
5168                    .then_some(ids[0].venue)
5169            });
5170
5171            let command = SubscribeTrades::new(
5172                ids,
5173                client_id,
5174                venue,
5175                UUID4::new(),
5176                self.timestamp_ns(),
5177                None,
5178                params,
5179            );
5180            let mut acquired = Vec::new();
5181
5182            let handlers = std::iter::repeat_n(handler, command.instrument_ids.len());
5183            for (id, handler) in command.instrument_ids.iter().zip(handlers) {
5184                let member = command.with_instrument_ids(*id);
5185
5186                let topic = get_trades_topic(*id);
5187                if self.add_trade_subscription(
5188                    topic,
5189                    handler,
5190                    DataCommand::Subscribe(SubscribeCommand::Trades(member)),
5191                ) {
5192                    acquired.push(*id);
5193                }
5194            }
5195
5196            if !acquired.is_empty() {
5197                let dispatch = command.with_instrument_ids(acquired);
5198                self.send_data_cmd(DataCommand::Subscribe(SubscribeCommand::Trades(dispatch)));
5199            }
5200        }
5201
5202        Ok(())
5203    }
5204
5205    /// Subscribes the actor to bars for one or more full bar types.
5206    ///
5207    /// # Errors
5208    ///
5209    /// Returns an error if the list is empty or spans venues without an explicit client.
5210    #[expect(
5211        clippy::needless_pass_by_value,
5212        reason = "preserves the owned bar handler API"
5213    )]
5214    pub fn subscribe_bars(
5215        &mut self,
5216        handler: TypedHandler<Bar>,
5217        bar_types: impl IntoBarTypes,
5218        client_id: Option<ClientId>,
5219        params: Option<Params>,
5220    ) -> anyhow::Result<()> {
5221        self.check_registered();
5222        let bar_types = bar_types.into_bar_types()?;
5223        ensure_routable_bar_types(&bar_types, client_id.is_some())?;
5224        let first_venue = bar_types[0].instrument_id().venue;
5225        let venue = bar_types
5226            .iter()
5227            .all(|bt| bt.instrument_id().venue == first_venue)
5228            .then_some(first_venue);
5229
5230        let command = SubscribeBars::new(
5231            bar_types,
5232            client_id,
5233            venue,
5234            UUID4::new(),
5235            self.timestamp_ns(),
5236            None,
5237            params,
5238        );
5239        let mut acquired = Vec::new();
5240
5241        for bar_type in &command.bar_types {
5242            let member = command.with_bar_types(*bar_type);
5243            let topic = get_bars_topic(bar_type.standard());
5244            if self.add_bar_subscription(
5245                topic,
5246                handler.clone(),
5247                DataCommand::Subscribe(SubscribeCommand::Bars(member)),
5248            ) {
5249                acquired.push(*bar_type);
5250            }
5251        }
5252
5253        if !acquired.is_empty() {
5254            let dispatch = command.with_bar_types(acquired);
5255            self.send_data_cmd(DataCommand::Subscribe(SubscribeCommand::Bars(dispatch)));
5256        }
5257
5258        Ok(())
5259    }
5260
5261    /// Subscribes the actor to mark prices.
5262    ///
5263    /// # Errors
5264    ///
5265    /// Returns an error if:
5266    /// - The IDs are empty.
5267    /// - The IDs span venues without an explicit client.
5268    pub fn subscribe_mark_prices(
5269        &mut self,
5270        handler: TypedHandler<MarkPriceUpdate>,
5271        instrument_ids: impl IntoInstrumentIds,
5272        client_id: Option<ClientId>,
5273        params: Option<Params>,
5274    ) -> anyhow::Result<()> {
5275        self.check_registered();
5276        let instrument_ids = instrument_ids.into_instrument_ids()?;
5277        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
5278        let venue = instrument_ids
5279            .iter()
5280            .all(|id| id.venue == instrument_ids[0].venue)
5281            .then_some(instrument_ids[0].venue);
5282
5283        let command = SubscribeMarkPrices {
5284            instrument_ids,
5285            client_id,
5286            venue,
5287            command_id: UUID4::new(),
5288            ts_init: self.timestamp_ns(),
5289            correlation_id: None,
5290            params,
5291        };
5292
5293        let mut acquired = Vec::new();
5294        let handlers = std::iter::repeat_n(handler, command.instrument_ids.len());
5295        for (&instrument_id, handler) in command.instrument_ids.iter().zip(handlers) {
5296            let member = command.with_instrument_ids(instrument_id);
5297
5298            let topic = get_mark_price_topic(instrument_id);
5299            if self.add_mark_price_subscription(
5300                topic,
5301                handler,
5302                DataCommand::Subscribe(SubscribeCommand::MarkPrices(member)),
5303            ) {
5304                acquired.push(instrument_id);
5305            }
5306        }
5307
5308        if !acquired.is_empty() {
5309            let dispatch = command.with_instrument_ids(acquired);
5310            self.send_data_cmd(DataCommand::Subscribe(SubscribeCommand::MarkPrices(
5311                dispatch,
5312            )));
5313        }
5314
5315        Ok(())
5316    }
5317
5318    /// Subscribes the actor to index prices.
5319    ///
5320    /// # Errors
5321    ///
5322    /// Returns an error if:
5323    /// - The IDs are empty.
5324    /// - The IDs span venues without an explicit client.
5325    pub fn subscribe_index_prices(
5326        &mut self,
5327        handler: TypedHandler<IndexPriceUpdate>,
5328        instrument_ids: impl IntoInstrumentIds,
5329        client_id: Option<ClientId>,
5330        params: Option<Params>,
5331    ) -> anyhow::Result<()> {
5332        self.check_registered();
5333        let instrument_ids = instrument_ids.into_instrument_ids()?;
5334        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
5335        let venue = instrument_ids
5336            .iter()
5337            .all(|id| id.venue == instrument_ids[0].venue)
5338            .then_some(instrument_ids[0].venue);
5339
5340        let command = SubscribeIndexPrices {
5341            instrument_ids,
5342            client_id,
5343            venue,
5344            command_id: UUID4::new(),
5345            ts_init: self.timestamp_ns(),
5346            correlation_id: None,
5347            params,
5348        };
5349
5350        let mut acquired = Vec::new();
5351        let handlers = std::iter::repeat_n(handler, command.instrument_ids.len());
5352        for (&instrument_id, handler) in command.instrument_ids.iter().zip(handlers) {
5353            let member = command.with_instrument_ids(instrument_id);
5354
5355            let topic = get_index_price_topic(instrument_id);
5356            if self.add_index_price_subscription(
5357                topic,
5358                handler,
5359                DataCommand::Subscribe(SubscribeCommand::IndexPrices(member)),
5360            ) {
5361                acquired.push(instrument_id);
5362            }
5363        }
5364
5365        if !acquired.is_empty() {
5366            let dispatch = command.with_instrument_ids(acquired);
5367            self.send_data_cmd(DataCommand::Subscribe(SubscribeCommand::IndexPrices(
5368                dispatch,
5369            )));
5370        }
5371
5372        Ok(())
5373    }
5374
5375    /// Subscribes the actor to funding rates.
5376    ///
5377    /// # Errors
5378    ///
5379    /// Returns an error if:
5380    /// - The IDs are empty.
5381    /// - The IDs span venues without an explicit client.
5382    pub fn subscribe_funding_rates(
5383        &mut self,
5384        handler: TypedHandler<FundingRateUpdate>,
5385        instrument_ids: impl IntoInstrumentIds,
5386        client_id: Option<ClientId>,
5387        params: Option<Params>,
5388    ) -> anyhow::Result<()> {
5389        self.check_registered();
5390        let instrument_ids = instrument_ids.into_instrument_ids()?;
5391        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
5392        let venue = instrument_ids
5393            .iter()
5394            .all(|id| id.venue == instrument_ids[0].venue)
5395            .then_some(instrument_ids[0].venue);
5396
5397        let command = SubscribeFundingRates {
5398            instrument_ids,
5399            client_id,
5400            venue,
5401            command_id: UUID4::new(),
5402            ts_init: self.timestamp_ns(),
5403            correlation_id: None,
5404            params,
5405        };
5406
5407        let mut acquired = Vec::new();
5408        let handlers = std::iter::repeat_n(handler, command.instrument_ids.len());
5409        for (&instrument_id, handler) in command.instrument_ids.iter().zip(handlers) {
5410            let member = command.with_instrument_ids(instrument_id);
5411
5412            let topic = get_funding_rate_topic(instrument_id);
5413            if self.add_funding_rate_subscription(
5414                topic,
5415                handler,
5416                DataCommand::Subscribe(SubscribeCommand::FundingRates(member)),
5417            ) {
5418                acquired.push(instrument_id);
5419            }
5420        }
5421
5422        if !acquired.is_empty() {
5423            let dispatch = command.with_instrument_ids(acquired);
5424            self.send_data_cmd(DataCommand::Subscribe(SubscribeCommand::FundingRates(
5425                dispatch,
5426            )));
5427        }
5428
5429        Ok(())
5430    }
5431
5432    /// Subscribes the actor to option greeks.
5433    ///
5434    /// # Errors
5435    ///
5436    /// Returns an error if:
5437    /// - The IDs are empty.
5438    /// - The IDs span venues without an explicit client.
5439    /// - The client is not external and the list contains multiple IDs with a synthetic member.
5440    pub fn subscribe_option_greeks(
5441        &mut self,
5442        handler: TypedHandler<OptionGreeks>,
5443        instrument_ids: impl IntoInstrumentIds,
5444        client_id: Option<ClientId>,
5445        params: Option<Params>,
5446    ) -> anyhow::Result<()> {
5447        self.check_registered();
5448        let instrument_ids = instrument_ids.into_instrument_ids()?;
5449        self.validate_subscription(&instrument_ids, client_id.as_ref())?;
5450        let venue = instrument_ids
5451            .iter()
5452            .all(|id| id.venue == instrument_ids[0].venue)
5453            .then_some(instrument_ids[0].venue);
5454
5455        let command = SubscribeOptionGreeks {
5456            instrument_ids,
5457            client_id,
5458            venue,
5459            command_id: UUID4::new(),
5460            ts_init: self.timestamp_ns(),
5461            correlation_id: None,
5462            params,
5463        };
5464
5465        let mut acquired = Vec::new();
5466        let handlers = std::iter::repeat_n(handler, command.instrument_ids.len());
5467        for (&instrument_id, handler) in command.instrument_ids.iter().zip(handlers) {
5468            let member = command.with_instrument_ids(instrument_id);
5469
5470            let topic = get_option_greeks_topic(instrument_id);
5471            if self.add_option_greeks_subscription(
5472                topic,
5473                handler,
5474                DataCommand::Subscribe(SubscribeCommand::OptionGreeks(member)),
5475            ) {
5476                acquired.push(instrument_id);
5477            }
5478        }
5479
5480        if !acquired.is_empty() {
5481            let dispatch = command.with_instrument_ids(acquired);
5482            self.send_data_cmd(DataCommand::Subscribe(SubscribeCommand::OptionGreeks(
5483                dispatch,
5484            )));
5485        }
5486
5487        Ok(())
5488    }
5489
5490    /// Subscribes the actor to instrument status.
5491    ///
5492    /// # Errors
5493    ///
5494    /// Returns an error if:
5495    /// - The IDs are empty.
5496    /// - The IDs span venues without an explicit client.
5497    /// - The client is not external and the list contains multiple IDs with a synthetic member.
5498    pub fn subscribe_instrument_status(
5499        &mut self,
5500        handler: ShareableMessageHandler,
5501        instrument_ids: impl IntoInstrumentIds,
5502        client_id: Option<ClientId>,
5503        params: Option<Params>,
5504    ) -> anyhow::Result<()> {
5505        self.check_registered();
5506        let instrument_ids = instrument_ids.into_instrument_ids()?;
5507        self.validate_subscription(&instrument_ids, client_id.as_ref())?;
5508        let venue = instrument_ids
5509            .iter()
5510            .all(|id| id.venue == instrument_ids[0].venue)
5511            .then_some(instrument_ids[0].venue);
5512
5513        let command = SubscribeInstrumentStatus {
5514            instrument_ids,
5515            client_id,
5516            venue,
5517            command_id: UUID4::new(),
5518            ts_init: self.timestamp_ns(),
5519            correlation_id: None,
5520            params,
5521        };
5522
5523        let mut acquired = Vec::new();
5524        let handlers = std::iter::repeat_n(handler, command.instrument_ids.len());
5525        for (&instrument_id, handler) in command.instrument_ids.iter().zip(handlers) {
5526            let member = command.with_instrument_ids(instrument_id);
5527
5528            let topic = get_instrument_status_topic(instrument_id);
5529            if self.add_subscription_any(
5530                topic,
5531                handler,
5532                None,
5533                Some(DataCommand::Subscribe(SubscribeCommand::InstrumentStatus(
5534                    member,
5535                ))),
5536            ) {
5537                acquired.push(instrument_id);
5538            }
5539        }
5540
5541        if !acquired.is_empty() {
5542            let dispatch = command.with_instrument_ids(acquired);
5543            self.send_data_cmd(DataCommand::Subscribe(SubscribeCommand::InstrumentStatus(
5544                dispatch,
5545            )));
5546        }
5547
5548        Ok(())
5549    }
5550
5551    /// Subscribes the actor to instrument close.
5552    ///
5553    /// # Errors
5554    ///
5555    /// Returns an error if:
5556    /// - The IDs are empty.
5557    /// - The IDs span venues without an explicit client.
5558    /// - The client is not external and the list contains multiple IDs with a synthetic member.
5559    pub fn subscribe_instrument_close(
5560        &mut self,
5561        handler: ShareableMessageHandler,
5562        instrument_ids: impl IntoInstrumentIds,
5563        client_id: Option<ClientId>,
5564        params: Option<Params>,
5565    ) -> anyhow::Result<()> {
5566        self.check_registered();
5567        let instrument_ids = instrument_ids.into_instrument_ids()?;
5568        self.validate_subscription(&instrument_ids, client_id.as_ref())?;
5569        let venue = instrument_ids
5570            .iter()
5571            .all(|id| id.venue == instrument_ids[0].venue)
5572            .then_some(instrument_ids[0].venue);
5573
5574        let command = SubscribeInstrumentClose {
5575            instrument_ids,
5576            client_id,
5577            venue,
5578            command_id: UUID4::new(),
5579            ts_init: self.timestamp_ns(),
5580            correlation_id: None,
5581            params,
5582        };
5583
5584        let mut acquired = Vec::new();
5585        let handlers = std::iter::repeat_n(handler, command.instrument_ids.len());
5586        for (&instrument_id, handler) in command.instrument_ids.iter().zip(handlers) {
5587            let member = command.with_instrument_ids(instrument_id);
5588
5589            let topic = get_instrument_close_topic(instrument_id);
5590            if self.add_instrument_close_subscription(
5591                topic,
5592                handler,
5593                DataCommand::Subscribe(SubscribeCommand::InstrumentClose(member)),
5594            ) {
5595                acquired.push(instrument_id);
5596            }
5597        }
5598
5599        if !acquired.is_empty() {
5600            let dispatch = command.with_instrument_ids(acquired);
5601            self.send_data_cmd(DataCommand::Subscribe(SubscribeCommand::InstrumentClose(
5602                dispatch,
5603            )));
5604        }
5605
5606        Ok(())
5607    }
5608
5609    fn validate_subscription(
5610        &self,
5611        instrument_ids: &[InstrumentId],
5612        client_id: Option<&ClientId>,
5613    ) -> anyhow::Result<()> {
5614        ensure_routable_instrument_ids(instrument_ids, client_id.is_some())?;
5615        ensure_subscription_routes(instrument_ids, client_id, &self.cache_ref())
5616    }
5617
5618    /// Subscribes the actor to option chain snapshots.
5619    #[expect(
5620        clippy::too_many_arguments,
5621        reason = "subscription command mirrors the option chain request fields"
5622    )]
5623    pub fn subscribe_option_chain(
5624        &mut self,
5625        topic: MStr<Topic>,
5626        handler: TypedHandler<OptionChainSlice>,
5627        series_id: OptionSeriesId,
5628        strike_range: StrikeRange,
5629        snapshot_interval_ms: Option<u64>,
5630        client_id: Option<ClientId>,
5631        params: Option<Params>,
5632    ) {
5633        self.check_registered();
5634
5635        let correlation_id = self
5636            .option_chain_handlers
5637            .get(&topic)
5638            .and_then(|subscription| match subscription.command.as_ref() {
5639                Some(DataCommand::Subscribe(SubscribeCommand::OptionChain(command))) => {
5640                    Some(command.correlation_id.unwrap_or(command.command_id))
5641                }
5642                _ => None,
5643            });
5644
5645        let mut subscribe = SubscribeOptionChain::new(
5646            series_id,
5647            strike_range,
5648            snapshot_interval_ms,
5649            UUID4::new(),
5650            self.timestamp_ns(),
5651            client_id,
5652            Some(series_id.venue),
5653            params,
5654        );
5655        subscribe.correlation_id = correlation_id;
5656        let command = DataCommand::Subscribe(SubscribeCommand::OptionChain(subscribe));
5657
5658        self.set_option_chain_subscription(topic, handler, command.clone());
5659        self.send_data_cmd(command);
5660    }
5661
5662    /// Unsubscribes the actor from data.
5663    pub fn unsubscribe_data(
5664        &mut self,
5665        data_type: DataType,
5666        client_id: Option<ClientId>,
5667        params: Option<Params>,
5668    ) {
5669        self.check_registered();
5670
5671        let mut retained = None;
5672
5673        for topic in get_custom_subscription_topics(&data_type) {
5674            if let Some(command) = self.remove_subscription_any(topic) {
5675                retained = Some(command);
5676            }
5677        }
5678
5679        if client_id.is_none() && retained.is_none() {
5680            return;
5681        }
5682
5683        let command = UnsubscribeCommand::Data(UnsubscribeCustomData {
5684            data_type,
5685            client_id,
5686            venue: None,
5687            command_id: UUID4::new(),
5688            ts_init: self.timestamp_ns(),
5689            correlation_id: None,
5690            params,
5691        });
5692
5693        self.send_unsubscribe_cmd(retained, DataCommand::Unsubscribe(command));
5694    }
5695
5696    /// Unsubscribes the actor from signals.
5697    ///
5698    /// # Panics
5699    ///
5700    /// Panics if the actor is not registered with a trader.
5701    pub fn unsubscribe_signal(&mut self, name: &str) {
5702        self.check_registered();
5703
5704        let pattern = get_signal_pattern(name);
5705        if let Some(subscription) = self.topic_handlers.remove(&pattern) {
5706            msgbus::unsubscribe_any(pattern, &subscription.handler);
5707        } else {
5708            log::warn!(
5709                "Actor {} attempted to unsubscribe from signal pattern '{pattern}' when not subscribed",
5710                self.actor_id,
5711            );
5712        }
5713    }
5714
5715    /// Unsubscribes from queue state changes.
5716    ///
5717    /// # Panics
5718    ///
5719    /// Panics if the actor is not registered with a trader.
5720    pub fn unsubscribe_queue_state(&mut self, channel: Option<SystemChannel>) {
5721        self.check_registered();
5722
5723        let topic = MessagingSwitchboard::queue_state_changed_pattern(channel);
5724        let _ = self.remove_subscription_any(topic);
5725    }
5726
5727    /// Unsubscribes from socket state changes.
5728    ///
5729    /// # Panics
5730    ///
5731    /// Panics if the actor is not registered with a trader.
5732    pub fn unsubscribe_socket_state(
5733        &mut self,
5734        client_id: Option<ClientId>,
5735        endpoint: Option<&str>,
5736    ) {
5737        self.check_registered();
5738
5739        let topic = MessagingSwitchboard::socket_state_changed_pattern(client_id, endpoint);
5740        let _ = self.remove_subscription_any(topic);
5741    }
5742
5743    /// Unsubscribes the actor from instruments.
5744    pub fn unsubscribe_instruments(
5745        &mut self,
5746        venue: Venue,
5747        client_id: Option<ClientId>,
5748        params: Option<Params>,
5749    ) {
5750        self.check_registered();
5751
5752        let pattern = get_instruments_pattern(venue);
5753        let retained = self.remove_instrument_subscription(pattern);
5754
5755        let command = UnsubscribeCommand::Instruments(UnsubscribeInstruments {
5756            client_id,
5757            venue,
5758            command_id: UUID4::new(),
5759            ts_init: self.timestamp_ns(),
5760            correlation_id: None,
5761            params,
5762        });
5763
5764        self.send_unsubscribe_cmd(retained, DataCommand::Unsubscribe(command));
5765    }
5766
5767    /// Unsubscribes the actor from instrument.
5768    ///
5769    /// # Errors
5770    ///
5771    /// Returns an error if:
5772    /// - The IDs are empty.
5773    /// - The IDs span venues without an explicit client.
5774    pub fn unsubscribe_instrument(
5775        &mut self,
5776        instrument_ids: impl IntoInstrumentIds,
5777        client_id: Option<ClientId>,
5778        _params: Option<Params>,
5779    ) -> anyhow::Result<()> {
5780        self.check_registered();
5781        let instrument_ids = instrument_ids.into_instrument_ids()?;
5782        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
5783        let mut retained = Vec::new();
5784
5785        for instrument_id in instrument_ids {
5786            let topic = get_instrument_topic(instrument_id);
5787            if let Some(command) = self.remove_instrument_subscription(topic.into()) {
5788                retained.push(command);
5789            }
5790        }
5791
5792        self.release_subscriptions(retained);
5793        Ok(())
5794    }
5795
5796    /// Unsubscribes the actor from book deltas.
5797    ///
5798    /// # Errors
5799    ///
5800    /// Returns an error if:
5801    /// - The IDs are empty.
5802    /// - The IDs span venues without an explicit client.
5803    #[expect(
5804        clippy::needless_pass_by_value,
5805        reason = "preserves the instrument unsubscribe API"
5806    )]
5807    pub fn unsubscribe_book_deltas(
5808        &mut self,
5809        instrument_ids: impl IntoInstrumentIds,
5810        client_id: Option<ClientId>,
5811        params: Option<Params>,
5812    ) -> anyhow::Result<()> {
5813        self.check_registered();
5814        let instrument_ids = instrument_ids.into_instrument_ids()?;
5815        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
5816        let mut retained = Vec::new();
5817
5818        for instrument_id in instrument_ids {
5819            let topic = if is_parent_subscription(params.as_ref()) {
5820                get_book_deltas_pattern(instrument_id)
5821            } else {
5822                get_book_deltas_topic(instrument_id).into()
5823            };
5824
5825            if let Some(command) = self.remove_deltas_subscription(topic) {
5826                retained.push(command);
5827            }
5828        }
5829
5830        self.release_subscriptions(retained);
5831        Ok(())
5832    }
5833
5834    /// Unsubscribes the actor from book depth snapshots.
5835    ///
5836    /// # Errors
5837    ///
5838    /// Returns an error if:
5839    /// - The IDs are empty.
5840    /// - The IDs span venues without an explicit client.
5841    #[expect(
5842        clippy::needless_pass_by_value,
5843        reason = "preserves the instrument unsubscribe API"
5844    )]
5845    pub fn unsubscribe_book_depth(
5846        &mut self,
5847        instrument_ids: impl IntoInstrumentIds,
5848        client_id: Option<ClientId>,
5849        params: Option<Params>,
5850    ) -> anyhow::Result<()> {
5851        self.check_registered();
5852        let instrument_ids = instrument_ids.into_instrument_ids()?;
5853        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
5854        let mut retained = Vec::new();
5855
5856        for instrument_id in instrument_ids {
5857            let topic = if is_parent_subscription(params.as_ref()) {
5858                get_book_depth_pattern(instrument_id)
5859            } else {
5860                get_book_depth_topic(instrument_id).into()
5861            };
5862
5863            if let Some(command) = self.remove_depth_subscription(topic) {
5864                retained.push(command);
5865            }
5866        }
5867
5868        self.release_subscriptions(retained);
5869        Ok(())
5870    }
5871
5872    /// Unsubscribes the actor from book snapshots at interval.
5873    ///
5874    /// # Errors
5875    ///
5876    /// Returns an error if:
5877    /// - The IDs are empty.
5878    /// - The IDs span venues without an explicit client.
5879    pub fn unsubscribe_book_at_interval(
5880        &mut self,
5881        instrument_ids: impl IntoInstrumentIds,
5882        interval_ms: NonZeroUsize,
5883        client_id: Option<ClientId>,
5884        _params: Option<Params>,
5885    ) -> anyhow::Result<()> {
5886        self.check_registered();
5887        let instrument_ids = instrument_ids.into_instrument_ids()?;
5888        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
5889        let mut retained = Vec::new();
5890
5891        for instrument_id in instrument_ids {
5892            let topic = get_book_snapshots_topic(instrument_id, interval_ms);
5893            if let Some(command) = self.remove_book_snapshot_subscription(topic) {
5894                retained.push(command);
5895            }
5896        }
5897
5898        self.release_subscriptions(retained);
5899        Ok(())
5900    }
5901
5902    /// Unsubscribes the actor from quotes.
5903    ///
5904    /// Groups releases by acquisition and assigns a unique ID to each emitted command.
5905    ///
5906    /// # Errors
5907    ///
5908    /// Returns an error if:
5909    /// - The list is empty.
5910    /// - The IDs span venues without an explicit client.
5911    pub fn unsubscribe_quotes(
5912        &mut self,
5913        instrument_ids: impl IntoInstrumentIds,
5914        client_id: Option<ClientId>,
5915        _params: Option<Params>,
5916    ) -> anyhow::Result<()> {
5917        self.check_registered();
5918        let instrument_ids = instrument_ids.into_instrument_ids()?;
5919        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
5920        let mut retained = Vec::new();
5921        for instrument_id in instrument_ids {
5922            retained.extend(self.remove_quote_subscription(get_quotes_topic(instrument_id)));
5923        }
5924
5925        self.release_subscriptions(retained);
5926        Ok(())
5927    }
5928
5929    /// Unsubscribes the actor from trades using each instrument's retained route.
5930    ///
5931    /// # Errors
5932    ///
5933    /// Returns an error if:
5934    /// - The list is empty.
5935    /// - The IDs span venues without an explicit client.
5936    pub fn unsubscribe_trades(
5937        &mut self,
5938        instrument_ids: impl IntoInstrumentIds,
5939        client_id: Option<ClientId>,
5940        _params: Option<Params>,
5941    ) -> anyhow::Result<()> {
5942        self.check_registered();
5943        let instrument_ids = instrument_ids.into_instrument_ids()?;
5944        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
5945        let mut retained = Vec::new();
5946        for instrument_id in instrument_ids {
5947            retained.extend(self.remove_trade_subscription(get_trades_topic(instrument_id)));
5948        }
5949
5950        self.release_subscriptions(retained);
5951        Ok(())
5952    }
5953
5954    fn release_subscriptions(&self, retained: Vec<DataCommand>) {
5955        let ts_init = self.timestamp_ns();
5956        let mut releases = IndexMap::new();
5957
5958        for retained in retained {
5959            let DataCommand::Subscribe(subscribe) = &retained else {
5960                unreachable!("instrument subscriptions retain a subscribe command");
5961            };
5962
5963            let acquisition_id = subscribe.command_id();
5964
5965            let Some(DataCommand::Unsubscribe(command)) =
5966                retained.into_unsubscribe(acquisition_id, ts_init)
5967            else {
5968                unreachable!("instrument subscriptions retain a subscribe command");
5969            };
5970
5971            releases
5972                .entry(acquisition_id)
5973                .or_insert_with(Vec::new)
5974                .push(command);
5975        }
5976
5977        for group in releases.into_values() {
5978            for command in UnsubscribeCommand::group(group) {
5979                let command_id = UUID4::new();
5980
5981                let command = match command {
5982                    UnsubscribeCommand::Instrument(command) => {
5983                        UnsubscribeCommand::Instrument(UnsubscribeInstrument {
5984                            command_id,
5985                            ..command
5986                        })
5987                    }
5988                    UnsubscribeCommand::BookDeltas(command) => {
5989                        UnsubscribeCommand::BookDeltas(UnsubscribeBookDeltas {
5990                            command_id,
5991                            ..command
5992                        })
5993                    }
5994                    UnsubscribeCommand::BookDepth(command) => {
5995                        UnsubscribeCommand::BookDepth(UnsubscribeBookDepth {
5996                            command_id,
5997                            ..command
5998                        })
5999                    }
6000                    UnsubscribeCommand::BookSnapshots(command) => {
6001                        UnsubscribeCommand::BookSnapshots(UnsubscribeBookSnapshots {
6002                            command_id,
6003                            ..command
6004                        })
6005                    }
6006                    UnsubscribeCommand::Quotes(command) => {
6007                        UnsubscribeCommand::Quotes(UnsubscribeQuotes {
6008                            command_id,
6009                            ..command
6010                        })
6011                    }
6012                    UnsubscribeCommand::Trades(command) => {
6013                        UnsubscribeCommand::Trades(UnsubscribeTrades {
6014                            command_id,
6015                            ..command
6016                        })
6017                    }
6018                    UnsubscribeCommand::Bars(command) => {
6019                        UnsubscribeCommand::Bars(UnsubscribeBars {
6020                            command_id,
6021                            ..command
6022                        })
6023                    }
6024                    UnsubscribeCommand::MarkPrices(command) => {
6025                        UnsubscribeCommand::MarkPrices(UnsubscribeMarkPrices {
6026                            command_id,
6027                            ..command
6028                        })
6029                    }
6030                    UnsubscribeCommand::IndexPrices(command) => {
6031                        UnsubscribeCommand::IndexPrices(UnsubscribeIndexPrices {
6032                            command_id,
6033                            ..command
6034                        })
6035                    }
6036                    UnsubscribeCommand::FundingRates(command) => {
6037                        UnsubscribeCommand::FundingRates(UnsubscribeFundingRates {
6038                            command_id,
6039                            ..command
6040                        })
6041                    }
6042                    UnsubscribeCommand::InstrumentStatus(command) => {
6043                        UnsubscribeCommand::InstrumentStatus(UnsubscribeInstrumentStatus {
6044                            command_id,
6045                            ..command
6046                        })
6047                    }
6048                    UnsubscribeCommand::InstrumentClose(command) => {
6049                        UnsubscribeCommand::InstrumentClose(UnsubscribeInstrumentClose {
6050                            command_id,
6051                            ..command
6052                        })
6053                    }
6054                    UnsubscribeCommand::OptionGreeks(command) => {
6055                        UnsubscribeCommand::OptionGreeks(UnsubscribeOptionGreeks {
6056                            command_id,
6057                            ..command
6058                        })
6059                    }
6060                    _ => unreachable!("instrument releases retain instrument commands"),
6061                };
6062
6063                self.send_data_cmd(DataCommand::Unsubscribe(command));
6064            }
6065        }
6066    }
6067
6068    /// Unsubscribes the actor from bars using each type's retained route.
6069    ///
6070    /// # Errors
6071    ///
6072    /// Returns an error if the list is empty or spans venues without an explicit client.
6073    pub fn unsubscribe_bars(
6074        &mut self,
6075        bar_types: impl IntoBarTypes,
6076        client_id: Option<ClientId>,
6077        _params: Option<Params>,
6078    ) -> anyhow::Result<()> {
6079        self.check_registered();
6080        let bar_types = bar_types.into_bar_types()?;
6081        ensure_routable_bar_types(&bar_types, client_id.is_some())?;
6082        let retained = bar_types
6083            .into_iter()
6084            .filter_map(|bar_type| self.remove_bar_subscription(bar_type))
6085            .collect();
6086        self.release_subscriptions(retained);
6087        Ok(())
6088    }
6089
6090    /// Unsubscribes the actor from mark prices.
6091    ///
6092    /// # Errors
6093    ///
6094    /// Returns an error if:
6095    /// - The IDs are empty.
6096    /// - The IDs span venues without an explicit client.
6097    pub fn unsubscribe_mark_prices(
6098        &mut self,
6099        instrument_ids: impl IntoInstrumentIds,
6100        client_id: Option<ClientId>,
6101        _params: Option<Params>,
6102    ) -> anyhow::Result<()> {
6103        self.check_registered();
6104        let instrument_ids = instrument_ids.into_instrument_ids()?;
6105        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
6106        let mut retained = Vec::new();
6107
6108        for instrument_id in instrument_ids {
6109            let topic = get_mark_price_topic(instrument_id);
6110            if let Some(command) = self.remove_mark_price_subscription(topic) {
6111                retained.push(command);
6112            }
6113        }
6114
6115        self.release_subscriptions(retained);
6116        Ok(())
6117    }
6118
6119    /// Unsubscribes the actor from index prices.
6120    ///
6121    /// # Errors
6122    ///
6123    /// Returns an error if:
6124    /// - The IDs are empty.
6125    /// - The IDs span venues without an explicit client.
6126    pub fn unsubscribe_index_prices(
6127        &mut self,
6128        instrument_ids: impl IntoInstrumentIds,
6129        client_id: Option<ClientId>,
6130        _params: Option<Params>,
6131    ) -> anyhow::Result<()> {
6132        self.check_registered();
6133        let instrument_ids = instrument_ids.into_instrument_ids()?;
6134        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
6135        let mut retained = Vec::new();
6136
6137        for instrument_id in instrument_ids {
6138            let topic = get_index_price_topic(instrument_id);
6139            if let Some(command) = self.remove_index_price_subscription(topic) {
6140                retained.push(command);
6141            }
6142        }
6143
6144        self.release_subscriptions(retained);
6145        Ok(())
6146    }
6147
6148    /// Unsubscribes the actor from funding rates.
6149    ///
6150    /// # Errors
6151    ///
6152    /// Returns an error if:
6153    /// - The IDs are empty.
6154    /// - The IDs span venues without an explicit client.
6155    pub fn unsubscribe_funding_rates(
6156        &mut self,
6157        instrument_ids: impl IntoInstrumentIds,
6158        client_id: Option<ClientId>,
6159        _params: Option<Params>,
6160    ) -> anyhow::Result<()> {
6161        self.check_registered();
6162        let instrument_ids = instrument_ids.into_instrument_ids()?;
6163        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
6164        let mut retained = Vec::new();
6165
6166        for instrument_id in instrument_ids {
6167            let topic = get_funding_rate_topic(instrument_id);
6168            if let Some(command) = self.remove_funding_rate_subscription(topic) {
6169                retained.push(command);
6170            }
6171        }
6172
6173        self.release_subscriptions(retained);
6174        Ok(())
6175    }
6176
6177    /// Unsubscribes the actor from option greeks.
6178    ///
6179    /// # Errors
6180    ///
6181    /// Returns an error if:
6182    /// - The IDs are empty.
6183    /// - The IDs span venues without an explicit client.
6184    pub fn unsubscribe_option_greeks(
6185        &mut self,
6186        instrument_ids: impl IntoInstrumentIds,
6187        client_id: Option<ClientId>,
6188        _params: Option<Params>,
6189    ) -> anyhow::Result<()> {
6190        self.check_registered();
6191        let instrument_ids = instrument_ids.into_instrument_ids()?;
6192        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
6193        let mut retained = Vec::new();
6194
6195        for instrument_id in instrument_ids {
6196            let topic = get_option_greeks_topic(instrument_id);
6197            if let Some(command) = self.remove_option_greeks_subscription(topic) {
6198                retained.push(command);
6199            }
6200        }
6201
6202        self.release_subscriptions(retained);
6203        Ok(())
6204    }
6205
6206    /// Unsubscribes the actor from instrument status.
6207    ///
6208    /// # Errors
6209    ///
6210    /// Returns an error if:
6211    /// - The IDs are empty.
6212    /// - The IDs span venues without an explicit client.
6213    pub fn unsubscribe_instrument_status(
6214        &mut self,
6215        instrument_ids: impl IntoInstrumentIds,
6216        client_id: Option<ClientId>,
6217        _params: Option<Params>,
6218    ) -> anyhow::Result<()> {
6219        self.check_registered();
6220        let instrument_ids = instrument_ids.into_instrument_ids()?;
6221        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
6222        let mut retained = Vec::new();
6223
6224        for instrument_id in instrument_ids {
6225            let topic = get_instrument_status_topic(instrument_id);
6226            if let Some(command) = self.remove_subscription_any(topic) {
6227                retained.push(command);
6228            }
6229        }
6230
6231        self.release_subscriptions(retained);
6232        Ok(())
6233    }
6234
6235    /// Unsubscribes the actor from instrument close.
6236    ///
6237    /// # Errors
6238    ///
6239    /// Returns an error if:
6240    /// - The IDs are empty.
6241    /// - The IDs span venues without an explicit client.
6242    pub fn unsubscribe_instrument_close(
6243        &mut self,
6244        instrument_ids: impl IntoInstrumentIds,
6245        client_id: Option<ClientId>,
6246        _params: Option<Params>,
6247    ) -> anyhow::Result<()> {
6248        self.check_registered();
6249        let instrument_ids = instrument_ids.into_instrument_ids()?;
6250        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
6251        let mut retained = Vec::new();
6252
6253        for instrument_id in instrument_ids {
6254            let topic = get_instrument_close_topic(instrument_id);
6255            if let Some(command) = self.remove_instrument_close_subscription(topic) {
6256                retained.push(command);
6257            }
6258        }
6259
6260        self.release_subscriptions(retained);
6261        Ok(())
6262    }
6263
6264    /// Unsubscribes the actor from option chain snapshots.
6265    pub fn unsubscribe_option_chain(
6266        &mut self,
6267        series_id: OptionSeriesId,
6268        client_id: Option<ClientId>,
6269    ) {
6270        self.check_registered();
6271
6272        let topic = get_option_chain_topic(series_id);
6273        let retained = self.remove_option_chain_subscription(topic);
6274
6275        let command = UnsubscribeCommand::OptionChain(UnsubscribeOptionChain::new(
6276            series_id,
6277            UUID4::new(),
6278            self.timestamp_ns(),
6279            client_id,
6280            Some(series_id.venue),
6281        ));
6282
6283        self.send_unsubscribe_cmd(retained, DataCommand::Unsubscribe(command));
6284    }
6285
6286    /// Requests data for the actor.
6287    ///
6288    /// # Errors
6289    ///
6290    /// Returns an error if input parameters are invalid.
6291    #[expect(clippy::too_many_arguments)]
6292    pub fn request_data(
6293        &self,
6294        data_type: DataType,
6295        client_id: ClientId,
6296        start: Option<Timestamp>,
6297        end: Option<Timestamp>,
6298        limit: Option<NonZeroUsize>,
6299        params: Option<Params>,
6300        handler: TypedHandler<DataResponseEnvelope>,
6301    ) -> anyhow::Result<UUID4> {
6302        self.check_registered();
6303
6304        let now = self.clock_ref().utc_now();
6305        check_timestamps(now, start, end)?;
6306
6307        let request_id = UUID4::new();
6308        let command = RequestCommand::Data(RequestCustomData {
6309            client_id,
6310            data_type,
6311            start,
6312            end,
6313            limit,
6314            request_id,
6315            ts_init: self.timestamp_ns(),
6316            params,
6317        });
6318
6319        self.register_request_handler(command.request_id(), handler)?;
6320
6321        self.send_data_cmd(DataCommand::Request(command));
6322
6323        Ok(request_id)
6324    }
6325
6326    /// Requests instrument for the actor.
6327    ///
6328    /// # Errors
6329    ///
6330    /// Returns an error if input parameters are invalid.
6331    pub fn request_instrument(
6332        &self,
6333        instrument_ids: impl IntoInstrumentIds,
6334        start: Option<Timestamp>,
6335        end: Option<Timestamp>,
6336        client_id: Option<ClientId>,
6337        params: Option<Params>,
6338        handler: TypedHandler<DataResponseEnvelope>,
6339    ) -> anyhow::Result<UUID4> {
6340        self.check_registered();
6341
6342        let instrument_ids = instrument_ids.into_instrument_ids()?;
6343        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
6344
6345        let now = self.clock_ref().utc_now();
6346        check_timestamps(now, start, end)?;
6347
6348        let request_id = UUID4::new();
6349        let command = RequestCommand::Instrument(RequestInstrument {
6350            instrument_ids,
6351            start,
6352            end,
6353            client_id,
6354            request_id,
6355            ts_init: now.into(),
6356            params,
6357        });
6358
6359        self.register_request_handler(command.request_id(), handler)?;
6360
6361        self.send_data_cmd(DataCommand::Request(command));
6362
6363        Ok(request_id)
6364    }
6365
6366    /// Requests instruments for the actor.
6367    ///
6368    /// # Errors
6369    ///
6370    /// Returns an error if input parameters are invalid.
6371    pub fn request_instruments(
6372        &self,
6373        venue: Option<Venue>,
6374        start: Option<Timestamp>,
6375        end: Option<Timestamp>,
6376        client_id: Option<ClientId>,
6377        params: Option<Params>,
6378        handler: TypedHandler<DataResponseEnvelope>,
6379    ) -> anyhow::Result<UUID4> {
6380        self.check_registered();
6381
6382        let now = self.clock_ref().utc_now();
6383        check_timestamps(now, start, end)?;
6384
6385        let request_id = UUID4::new();
6386        let command = RequestCommand::Instruments(RequestInstruments {
6387            venue,
6388            start,
6389            end,
6390            client_id,
6391            request_id,
6392            ts_init: now.into(),
6393            params,
6394        });
6395
6396        self.register_request_handler(command.request_id(), handler)?;
6397
6398        self.send_data_cmd(DataCommand::Request(command));
6399
6400        Ok(request_id)
6401    }
6402
6403    /// Requests book snapshot for the actor.
6404    ///
6405    /// # Errors
6406    ///
6407    /// Returns an error if input parameters are invalid.
6408    pub fn request_book_snapshot(
6409        &self,
6410        instrument_ids: impl IntoInstrumentIds,
6411        depth: Option<NonZeroUsize>,
6412        client_id: Option<ClientId>,
6413        params: Option<Params>,
6414        handler: TypedHandler<DataResponseEnvelope>,
6415    ) -> anyhow::Result<UUID4> {
6416        self.check_registered();
6417
6418        let instrument_ids = instrument_ids.into_instrument_ids()?;
6419        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
6420
6421        let request_id = UUID4::new();
6422        let command = RequestCommand::BookSnapshot(RequestBookSnapshot {
6423            instrument_ids,
6424            depth,
6425            client_id,
6426            request_id,
6427            ts_init: self.timestamp_ns(),
6428            params,
6429        });
6430
6431        self.register_request_handler(command.request_id(), handler)?;
6432
6433        self.send_data_cmd(DataCommand::Request(command));
6434
6435        Ok(request_id)
6436    }
6437
6438    /// Requests book deltas for the actor.
6439    ///
6440    /// # Errors
6441    ///
6442    /// Returns an error if input parameters are invalid.
6443    #[expect(clippy::too_many_arguments)]
6444    pub fn request_book_deltas(
6445        &self,
6446        instrument_ids: impl IntoInstrumentIds,
6447        start: Option<Timestamp>,
6448        end: Option<Timestamp>,
6449        limit: Option<NonZeroUsize>,
6450        client_id: Option<ClientId>,
6451        params: Option<Params>,
6452        handler: TypedHandler<DataResponseEnvelope>,
6453    ) -> anyhow::Result<UUID4> {
6454        self.check_registered();
6455
6456        let instrument_ids = instrument_ids.into_instrument_ids()?;
6457        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
6458
6459        let now = self.clock_ref().utc_now();
6460        check_timestamps(now, start, end)?;
6461
6462        let request_id = UUID4::new();
6463        let command = RequestCommand::BookDeltas(RequestBookDeltas {
6464            instrument_ids,
6465            start,
6466            end,
6467            limit,
6468            client_id,
6469            request_id,
6470            ts_init: now.into(),
6471            params,
6472        });
6473
6474        self.register_request_handler(command.request_id(), handler)?;
6475
6476        self.send_data_cmd(DataCommand::Request(command));
6477
6478        Ok(request_id)
6479    }
6480
6481    /// Sends a request for historical book depth.
6482    ///
6483    /// # Errors
6484    ///
6485    /// Returns an error if input parameters are invalid.
6486    #[expect(clippy::too_many_arguments)]
6487    pub fn request_book_depth(
6488        &self,
6489        instrument_ids: impl IntoInstrumentIds,
6490        start: Option<Timestamp>,
6491        end: Option<Timestamp>,
6492        limit: Option<NonZeroUsize>,
6493        depth: Option<NonZeroUsize>,
6494        client_id: Option<ClientId>,
6495        params: Option<Params>,
6496        handler: TypedHandler<DataResponseEnvelope>,
6497    ) -> anyhow::Result<UUID4> {
6498        self.check_registered();
6499
6500        let instrument_ids = instrument_ids.into_instrument_ids()?;
6501        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
6502
6503        let now = self.clock_ref().utc_now();
6504        check_timestamps(now, start, end)?;
6505
6506        let request_id = UUID4::new();
6507        let command = RequestCommand::BookDepth(RequestBookDepth {
6508            instrument_ids,
6509            start,
6510            end,
6511            limit,
6512            depth,
6513            client_id,
6514            request_id,
6515            ts_init: now.into(),
6516            params,
6517        });
6518
6519        self.register_request_handler(command.request_id(), handler)?;
6520
6521        self.send_data_cmd(DataCommand::Request(command));
6522
6523        Ok(request_id)
6524    }
6525
6526    /// Requests quotes for the actor.
6527    ///
6528    /// # Errors
6529    ///
6530    /// Returns an error if input parameters are invalid.
6531    #[expect(clippy::too_many_arguments)]
6532    pub fn request_quotes(
6533        &self,
6534        instrument_ids: impl IntoInstrumentIds,
6535        start: Option<Timestamp>,
6536        end: Option<Timestamp>,
6537        limit: Option<NonZeroUsize>,
6538        client_id: Option<ClientId>,
6539        params: Option<Params>,
6540        handler: TypedHandler<DataResponseEnvelope>,
6541    ) -> anyhow::Result<UUID4> {
6542        self.check_registered();
6543
6544        let instrument_ids = instrument_ids.into_instrument_ids()?;
6545        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
6546        anyhow::ensure!(
6547            instrument_ids.len() == 1 || limit.is_none(),
6548            "A record limit is unsupported for multi-instrument quotes requests",
6549        );
6550
6551        let now = self.clock_ref().utc_now();
6552        check_timestamps(now, start, end)?;
6553
6554        let request_id = UUID4::new();
6555        let command = RequestCommand::Quotes(RequestQuotes {
6556            instrument_ids,
6557            start,
6558            end,
6559            limit,
6560            client_id,
6561            request_id,
6562            ts_init: now.into(),
6563            params,
6564        });
6565
6566        self.register_request_handler(command.request_id(), handler)?;
6567
6568        self.send_data_cmd(DataCommand::Request(command));
6569
6570        Ok(request_id)
6571    }
6572
6573    /// Requests trades for the actor.
6574    ///
6575    /// # Errors
6576    ///
6577    /// Returns an error if input parameters are invalid.
6578    #[expect(clippy::too_many_arguments)]
6579    pub fn request_trades(
6580        &self,
6581        instrument_ids: impl IntoInstrumentIds,
6582        start: Option<Timestamp>,
6583        end: Option<Timestamp>,
6584        limit: Option<NonZeroUsize>,
6585        client_id: Option<ClientId>,
6586        params: Option<Params>,
6587        handler: TypedHandler<DataResponseEnvelope>,
6588    ) -> anyhow::Result<UUID4> {
6589        self.check_registered();
6590
6591        let instrument_ids = instrument_ids.into_instrument_ids()?;
6592        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
6593        anyhow::ensure!(
6594            instrument_ids.len() == 1 || limit.is_none(),
6595            "A record limit is unsupported for multi-instrument trades requests",
6596        );
6597
6598        let now = self.clock_ref().utc_now();
6599        check_timestamps(now, start, end)?;
6600
6601        let request_id = UUID4::new();
6602        let command = RequestCommand::Trades(RequestTrades {
6603            instrument_ids,
6604            start,
6605            end,
6606            limit,
6607            client_id,
6608            request_id,
6609            ts_init: now.into(),
6610            params,
6611        });
6612
6613        self.register_request_handler(command.request_id(), handler)?;
6614
6615        self.send_data_cmd(DataCommand::Request(command));
6616
6617        Ok(request_id)
6618    }
6619
6620    /// Requests bars for the actor.
6621    ///
6622    /// # Errors
6623    ///
6624    /// Returns an error if input parameters are invalid.
6625    #[expect(clippy::too_many_arguments)]
6626    pub fn request_bars(
6627        &self,
6628        bar_types: impl IntoBarTypes,
6629        start: Option<Timestamp>,
6630        end: Option<Timestamp>,
6631        limit: Option<NonZeroUsize>,
6632        client_id: Option<ClientId>,
6633        params: Option<Params>,
6634        handler: TypedHandler<DataResponseEnvelope>,
6635    ) -> anyhow::Result<UUID4> {
6636        self.check_registered();
6637
6638        let bar_types = bar_types.into_bar_types()?;
6639        ensure_routable_bar_types(&bar_types, client_id.is_some())?;
6640
6641        for bar_type in &bar_types {
6642            anyhow::ensure!(
6643                bar_type.is_standard(),
6644                "Composite bar types are not supported for `request_bars`, was {bar_type}; \
6645                 request aggregation via the `bar_types` params instead",
6646            );
6647        }
6648
6649        let now = self.clock_ref().utc_now();
6650        check_timestamps(now, start, end)?;
6651
6652        let request_id = UUID4::new();
6653        let command = RequestCommand::Bars(RequestBars {
6654            bar_types,
6655            start,
6656            end,
6657            limit,
6658            client_id,
6659            request_id,
6660            ts_init: now.into(),
6661            params,
6662        });
6663
6664        self.register_request_handler(command.request_id(), handler)?;
6665
6666        self.send_data_cmd(DataCommand::Request(command));
6667
6668        Ok(request_id)
6669    }
6670
6671    /// Requests funding rates for the actor.
6672    ///
6673    /// # Errors
6674    ///
6675    /// Returns an error if input parameters are invalid.
6676    #[expect(clippy::too_many_arguments)]
6677    pub fn request_funding_rates(
6678        &self,
6679        instrument_ids: impl IntoInstrumentIds,
6680        start: Option<Timestamp>,
6681        end: Option<Timestamp>,
6682        limit: Option<NonZeroUsize>,
6683        client_id: Option<ClientId>,
6684        params: Option<Params>,
6685        handler: TypedHandler<DataResponseEnvelope>,
6686    ) -> anyhow::Result<UUID4> {
6687        self.check_registered();
6688
6689        let instrument_ids = instrument_ids.into_instrument_ids()?;
6690        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())?;
6691
6692        let now = self.clock_ref().utc_now();
6693        check_timestamps(now, start, end)?;
6694
6695        let request_id = UUID4::new();
6696        let command = RequestCommand::FundingRates(RequestFundingRates {
6697            instrument_ids,
6698            start,
6699            end,
6700            limit,
6701            client_id,
6702            request_id,
6703            ts_init: now.into(),
6704            params,
6705        });
6706
6707        self.register_request_handler(command.request_id(), handler)?;
6708
6709        self.send_data_cmd(DataCommand::Request(command));
6710
6711        Ok(request_id)
6712    }
6713
6714    fn register_request_handler(
6715        &self,
6716        request_id: &UUID4,
6717        handler: TypedHandler<DataResponseEnvelope>,
6718    ) -> anyhow::Result<()> {
6719        get_message_bus()
6720            .borrow_mut()
6721            .register_response_handler(request_id, handler)?;
6722        self.requests.borrow_mut().register(*request_id);
6723        Ok(())
6724    }
6725
6726    /// Returns whether this actor is awaiting completion of a request.
6727    #[must_use]
6728    pub fn is_pending_request(&self, request_id: &UUID4) -> bool {
6729        self.requests.borrow().is_pending(request_id)
6730    }
6731
6732    /// Returns the IDs of this actor's pending historical requests and groups in unspecified order.
6733    #[must_use]
6734    pub fn pending_requests(&self) -> Vec<UUID4> {
6735        self.requests.borrow().pending()
6736    }
6737
6738    pub(crate) fn register_request_group(
6739        &self,
6740        request_ids: Vec<UUID4>,
6741        request_id: Option<UUID4>,
6742        handler: Option<TypedHandler<DataResponseEnvelope>>,
6743    ) -> anyhow::Result<UUID4> {
6744        self.requests
6745            .borrow_mut()
6746            .register_group(request_ids, request_id, handler)
6747    }
6748
6749    pub(crate) fn begin_response(&self, request_id: &UUID4) -> Option<ResponseScope> {
6750        self.requests.borrow().begin(request_id)
6751    }
6752
6753    #[cfg(feature = "python")]
6754    pub(crate) fn is_response_current(&self, scope: &ResponseScope) -> bool {
6755        self.requests.borrow().is_current(scope)
6756    }
6757
6758    pub(crate) fn complete_request(
6759        &self,
6760        response: &DataResponseEnvelope,
6761        scope: &ResponseScope,
6762    ) -> bool {
6763        self.requests.borrow_mut().complete(response, scope)
6764    }
6765
6766    pub(crate) fn next_group_response(
6767        &self,
6768    ) -> Option<(
6769        DataResponseEnvelope,
6770        Option<TypedHandler<DataResponseEnvelope>>,
6771    )> {
6772        self.requests.borrow_mut().next_group_response()
6773    }
6774
6775    fn clear_pending_requests(&self) {
6776        let (ids, mut handlers) = self.requests.borrow_mut().clear();
6777        {
6778            let bus = get_message_bus();
6779            let mut bus = bus.borrow_mut();
6780            handlers.extend(ids.iter().filter_map(|id| bus.take_response_handler(id)));
6781        }
6782
6783        drop(handlers);
6784    }
6785
6786    /// Sends a fire-and-observe reconnect command.
6787    ///
6788    /// # Errors
6789    ///
6790    /// Returns an error if the actor is not registered, the endpoint label is invalid, the live
6791    /// runner is unavailable, or the command channel is closed.
6792    #[cfg(feature = "live")]
6793    pub fn reconnect_socket(&self, client_id: ClientId, endpoint: &str) -> anyhow::Result<()> {
6794        let endpoint = socket_endpoint(endpoint)?;
6795
6796        if !self.is_properly_registered() {
6797            anyhow::bail!(
6798                "Actor {} has not been registered with a Trader",
6799                self.actor_id
6800            );
6801        }
6802
6803        let sender = try_get_system_command_sender()
6804            .ok_or_else(|| anyhow::anyhow!("Live runner system command channel is unavailable"))?;
6805        let trader_id = self
6806            .trader_id
6807            .ok_or_else(|| anyhow::anyhow!("Actor {} has no trader ID", self.actor_id))?;
6808        let command = ReconnectSocket::new(trader_id, client_id, endpoint, self.timestamp_ns());
6809        sender
6810            .send(SystemCommand::ReconnectSocket(command))
6811            .map_err(|_| anyhow::anyhow!("Live runner system command channel is closed"))?;
6812        Ok(())
6813    }
6814
6815    #[cfg(test)]
6816    pub fn quote_handler_count(&self) -> usize {
6817        self.quote_handlers.len()
6818    }
6819
6820    #[cfg(test)]
6821    pub fn trade_handler_count(&self) -> usize {
6822        self.trade_handlers.len()
6823    }
6824
6825    #[cfg(test)]
6826    pub fn bar_handler_count(&self) -> usize {
6827        self.bar_handlers.len()
6828    }
6829
6830    #[cfg(test)]
6831    pub fn deltas_handler_count(&self) -> usize {
6832        self.deltas_handlers.len()
6833    }
6834
6835    #[cfg(test)]
6836    pub fn depth_handler_count(&self) -> usize {
6837        self.depth_handlers.len()
6838    }
6839
6840    #[cfg(test)]
6841    pub fn has_quote_handler(&self, topic: &str) -> bool {
6842        self.quote_handlers
6843            .contains_key(&MStr::<Topic>::from(topic))
6844    }
6845
6846    #[cfg(test)]
6847    pub fn has_trade_handler(&self, topic: &str) -> bool {
6848        self.trade_handlers
6849            .contains_key(&MStr::<Topic>::from(topic))
6850    }
6851
6852    #[cfg(test)]
6853    pub fn has_bar_handler(&self, topic: &str) -> bool {
6854        self.bar_handlers.contains_key(&MStr::<Topic>::from(topic))
6855    }
6856
6857    #[cfg(test)]
6858    pub fn has_deltas_handler(&self, pattern: &str) -> bool {
6859        self.deltas_handlers
6860            .contains_key(&MStr::<Pattern>::from(pattern))
6861    }
6862
6863    #[cfg(test)]
6864    pub fn has_depth_handler(&self, pattern: &str) -> bool {
6865        self.depth_handlers
6866            .contains_key(&MStr::<Pattern>::from(pattern))
6867    }
6868}
6869
6870impl DataActorNative for DataActorCore {
6871    fn core(&self) -> &DataActorCore {
6872        self
6873    }
6874
6875    fn core_mut(&mut self) -> &mut DataActorCore {
6876        self
6877    }
6878}
6879
6880fn check_timestamps(
6881    now: Timestamp,
6882    start: Option<Timestamp>,
6883    end: Option<Timestamp>,
6884) -> anyhow::Result<()> {
6885    if let Some(start) = start {
6886        check_predicate_true(start <= now, "start was > now")?;
6887    }
6888
6889    if let Some(end) = end {
6890        check_predicate_true(end <= now, "end was > now")?;
6891    }
6892
6893    if let (Some(start), Some(end)) = (start, end) {
6894        check_predicate_true(start <= end, "start was > end")?;
6895    }
6896
6897    Ok(())
6898}
6899
6900fn log_error(e: &anyhow::Error) {
6901    log::error!("{e}");
6902}
6903
6904fn log_not_running<T>(msg: &T)
6905where
6906    T: Debug,
6907{
6908    log::trace!("Received message when not running - skipping {msg:?}");
6909}
6910
6911fn log_received<T>(msg: &T)
6912where
6913    T: Debug,
6914{
6915    log::debug!("{RECV} {msg:?}");
6916}
6917
6918fn log_received_bulk(kind: &str, correlation_id: &UUID4, records: usize) {
6919    log::debug!("{RECV} {kind} correlation_id={correlation_id} records={records}");
6920}