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