Skip to main content

nautilus_common/python/
actor.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
5//  You may not use this file except in compliance with the License.
6//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
7//  Unless required by applicable law or agreed to in writing, software
8//  distributed under the License is distributed on an "AS IS" BASIS,
9//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10//  See the License for the specific language governing permissions and
11//  limitations under the License.
12// -------------------------------------------------------------------------------------------------
13
14//! Python bindings for `DataActor` with complete command and event handler forwarding.
15
16use std::{
17    any::Any,
18    cell::{RefCell, UnsafeCell},
19    collections::HashMap,
20    fmt::Debug,
21    num::NonZeroUsize,
22    ops::{Deref, DerefMut},
23    rc::Rc,
24};
25
26use indexmap::IndexMap;
27use jiff::Timestamp;
28use nautilus_core::{
29    from_pydict,
30    nanos::UnixNanos,
31    python::{to_pyruntime_err, to_pytype_err, to_pyvalue_err, upgrade_py_weakref},
32};
33#[cfg(feature = "defi")]
34use nautilus_model::defi::{
35    Block, Blockchain, Pool, PoolFeeCollect, PoolFlash, PoolLiquidityUpdate, PoolSwap,
36};
37use nautilus_model::{
38    data::{
39        Bar, BarType, CustomData, DataType, FundingRateUpdate, IndexPriceUpdate, InstrumentStatus,
40        MarkPriceUpdate, OrderBookDelta, OrderBookDeltas, OrderBookDepth, QuoteTick, TradeTick,
41        close::InstrumentClose,
42        option_chain::{OptionChainSlice, OptionGreeks},
43    },
44    enums::BookType,
45    identifiers::{
46        ActorId, ClientId, ExecAlgorithmId, InstrumentId, OptionSeriesId, TraderId, Venue,
47    },
48    instruments::{InstrumentAny, SyntheticInstrument},
49    orderbook::OrderBook,
50    orders::{OrderAny, OrderList},
51    python::{
52        data::option_chain::PyStrikeRange, instruments::instrument_any_to_pyobject,
53        orders::order_any_to_pyobject,
54    },
55};
56use pyo3::{
57    IntoPyObjectExt,
58    prelude::*,
59    types::{PyBytes, PyDict, PyList, PySet, PyWeakrefReference},
60};
61use ustr::Ustr;
62
63use crate::{
64    actor::{
65        Actor, DataActor, DataActorNative,
66        data_actor::{DataActorConfig, DataActorCore, ImportableActorConfig},
67        registry::{try_get_actor_unchecked, with_actor_registry},
68    },
69    cache::Cache,
70    clock::Clock,
71    component::{Component, ComponentAccessError, with_component_registry},
72    enums::ComponentState,
73    logging::{CMD, RECV},
74    messages::{
75        data::{
76            DataResponseEnvelope, IntoBarTypes, IntoInstrumentIds, ensure_routable_bar_types,
77            ensure_routable_instrument_ids, ensure_subscription_routes, quote_routing,
78        },
79        execution::TradingCommand,
80        system::{QueueStateChanged, SocketStateChanged},
81    },
82    msgbus::{self, ShareableMessageHandler, TypedHandler},
83    python::{
84        cache::PyCache,
85        clock::PyClock,
86        indicators::{registered_python_indicators, wrap_python_indicator},
87        logging::{PyLogger, format_exception},
88        response::PyDataResponse,
89        wrappers::{get_python_message_bus, retain_python_wrapper},
90    },
91    runner::SystemChannel,
92    signal::Signal,
93    timer::{TimeEvent, TimeEventCallback},
94};
95
96#[pyo3::pymethods]
97#[pyo3_stub_gen::derive::gen_stub_pymethods]
98impl DataActorConfig {
99    /// Common configuration for `DataActor` based components.
100    #[new]
101    #[pyo3(signature = (actor_id=None, log_events=true, log_commands=true, **_kwargs))]
102    fn py_new(
103        actor_id: Option<ActorId>,
104        log_events: bool,
105        log_commands: bool,
106        _kwargs: Option<&Bound<'_, PyDict>>,
107    ) -> Self {
108        Self {
109            actor_id,
110            log_events,
111            log_commands,
112        }
113    }
114
115    #[getter]
116    fn actor_id(&self) -> Option<ActorId> {
117        self.actor_id
118    }
119
120    #[setter]
121    fn set_actor_id(&mut self, actor_id: Option<ActorId>) {
122        self.actor_id = actor_id;
123    }
124
125    #[getter]
126    fn log_events(&self) -> bool {
127        self.log_events
128    }
129
130    #[setter]
131    fn set_log_events(&mut self, log_events: bool) {
132        self.log_events = log_events;
133    }
134
135    #[getter]
136    fn log_commands(&self) -> bool {
137        self.log_commands
138    }
139
140    #[setter]
141    fn set_log_commands(&mut self, log_commands: bool) {
142        self.log_commands = log_commands;
143    }
144}
145
146#[pyo3::pymethods]
147#[pyo3_stub_gen::derive::gen_stub_pymethods]
148impl ImportableActorConfig {
149    /// Configuration for creating actors from importable paths.
150    #[new]
151    #[expect(clippy::needless_pass_by_value)]
152    fn py_new(actor_path: String, config_path: String, config: Py<PyDict>) -> PyResult<Self> {
153        let json_config = Python::attach(|py| -> PyResult<HashMap<String, serde_json::Value>> {
154            let kwargs = PyDict::new(py);
155            kwargs.set_item("default", py.eval(pyo3::ffi::c_str!("str"), None, None)?)?;
156            let json_str: String = PyModule::import(py, "json")?
157                .call_method("dumps", (config.bind(py),), Some(&kwargs))?
158                .extract()?;
159
160            let json_value: serde_json::Value =
161                serde_json::from_str(&json_str).map_err(to_pyvalue_err)?;
162
163            if let serde_json::Value::Object(map) = json_value {
164                Ok(map.into_iter().collect())
165            } else {
166                Err(to_pyvalue_err("Config must be a dictionary"))
167            }
168        })?;
169
170        Ok(Self {
171            actor_path,
172            config_path,
173            config: json_config,
174        })
175    }
176
177    #[getter]
178    fn actor_path(&self) -> &String {
179        &self.actor_path
180    }
181
182    #[getter]
183    fn config_path(&self) -> &String {
184        &self.config_path
185    }
186
187    #[getter]
188    fn config(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
189        // Convert HashMap<String, serde_json::Value> back to Python dict
190        let py_dict = PyDict::new(py);
191
192        for (key, value) in &self.config {
193            // Convert serde_json::Value back to Python object via JSON
194            let json_str = serde_json::to_string(value).map_err(to_pyvalue_err)?;
195            let py_value = PyModule::import(py, "json")?.call_method("loads", (json_str,), None)?;
196            py_dict.set_item(key, py_value)?;
197        }
198        Ok(py_dict.unbind())
199    }
200}
201
202/// Inner state of `PyDataActor`, shared between Python wrapper and Rust registries.
203///
204/// This type holds the actual actor state and implements all the actor traits.
205/// It is wrapped in `Rc<UnsafeCell<>>` to allow shared ownership between Python
206/// and the global registries without copying.
207pub struct PyDataActorInner {
208    core: DataActorCore,
209    py_self: Option<Py<PyWeakrefReference>>,
210    config: Option<Py<PyAny>>,
211    clock: PyClock,
212    logger: PyLogger,
213}
214
215impl Debug for PyDataActorInner {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        f.debug_struct(stringify!(PyDataActorInner))
218            .field("core", &self.core)
219            .field(
220                "py_self",
221                &self.py_self.as_ref().map(|_| "<Py<PyWeakrefReference>>"),
222            )
223            .field("config", &self.config.as_ref().map(|_| "<Py<PyAny>>"))
224            .field("clock", &self.clock)
225            .field("logger", &self.logger)
226            .finish()
227    }
228}
229
230impl Deref for PyDataActorInner {
231    type Target = DataActorCore;
232
233    fn deref(&self) -> &Self::Target {
234        &self.core
235    }
236}
237
238impl DerefMut for PyDataActorInner {
239    fn deref_mut(&mut self) -> &mut Self::Target {
240        &mut self.core
241    }
242}
243
244impl DataActorNative for PyDataActorInner {
245    fn core(&self) -> &DataActorCore {
246        &self.core
247    }
248
249    fn core_mut(&mut self) -> &mut DataActorCore {
250        &mut self.core
251    }
252}
253
254#[expect(clippy::needless_pass_by_ref_mut)]
255impl PyDataActorInner {
256    fn execute_exec_algorithm_command(&mut self, command: &TradingCommand) -> anyhow::Result<()> {
257        if self.core.config.log_commands {
258            let id = self.core.actor_id;
259            log::info!("{id} {RECV}{CMD} {command}");
260        }
261
262        if self.core.state() != ComponentState::Running {
263            return Ok(());
264        }
265
266        match command {
267            TradingCommand::SubmitOrder(cmd) => {
268                let order = DataActor::cache(self).try_order(&cmd.client_order_id)?;
269                self.dispatch_on_order(order).map_err(|e| {
270                    anyhow::anyhow!("Python on_order failed:\n{}", format_exception(&e))
271                })
272            }
273            TradingCommand::SubmitOrderList(cmd) => {
274                let orders = self.orders_for_list(&cmd.order_list)?;
275                self.dispatch_on_order_list(cmd.order_list.clone(), orders)
276                    .map_err(|e| {
277                        anyhow::anyhow!("Python on_order_list failed:\n{}", format_exception(&e))
278                    })
279            }
280            _ => {
281                log::warn!("Unhandled command type: {command}");
282                Ok(())
283            }
284        }
285    }
286
287    fn orders_for_list(&self, order_list: &OrderList) -> anyhow::Result<Vec<OrderAny>> {
288        let cache = DataActor::cache(self);
289        let mut orders = Vec::with_capacity(order_list.client_order_ids.len());
290
291        for client_order_id in &order_list.client_order_ids {
292            orders.push(cache.try_order(client_order_id)?);
293        }
294
295        Ok(orders)
296    }
297
298    fn dispatch_on_order(&mut self, order: OrderAny) -> PyResult<()> {
299        if let Some(py_self) = self.python_instance()? {
300            Python::attach(|py| -> PyResult<()> {
301                let py_order = order_any_to_pyobject(py, order)?;
302                py_self.call_method1(py, "on_order", (py_order,))?;
303                Ok(())
304            })?;
305        }
306        Ok(())
307    }
308
309    fn dispatch_on_order_list(
310        &mut self,
311        order_list: OrderList,
312        orders: Vec<OrderAny>,
313    ) -> PyResult<()> {
314        if let Some(py_self) = self.python_instance()? {
315            Python::attach(|py| -> PyResult<()> {
316                if py_self.bind(py).hasattr("on_order_list")? {
317                    let py_order_list = order_list.into_py_any(py)?;
318                    let py_orders = orders
319                        .into_iter()
320                        .map(|order| order_any_to_pyobject(py, order))
321                        .collect::<PyResult<Vec<_>>>()?;
322                    let py_orders = PyList::new(py, py_orders)?;
323
324                    py_self.call_method1(py, "on_order_list", (py_order_list, py_orders))?;
325                } else {
326                    for order in orders {
327                        let py_order = order_any_to_pyobject(py, order)?;
328                        py_self.call_method1(py, "on_order", (py_order,))?;
329                    }
330                }
331                Ok(())
332            })?;
333        }
334        Ok(())
335    }
336
337    fn dispatch_on_start(&self) -> PyResult<()> {
338        if let Some(py_self) = self.python_instance()? {
339            Python::attach(|py| py_self.call_method0(py, "on_start"))?;
340        }
341        Ok(())
342    }
343
344    fn dispatch_on_stop(&mut self) -> PyResult<()> {
345        if let Some(py_self) = self.python_instance()? {
346            Python::attach(|py| py_self.call_method0(py, "on_stop"))?;
347        }
348        Ok(())
349    }
350
351    fn dispatch_on_resume(&mut self) -> PyResult<()> {
352        if let Some(py_self) = self.python_instance()? {
353            Python::attach(|py| py_self.call_method0(py, "on_resume"))?;
354        }
355        Ok(())
356    }
357
358    fn dispatch_on_reset(&mut self) -> PyResult<()> {
359        if let Some(py_self) = self.python_instance()? {
360            Python::attach(|py| py_self.call_method0(py, "on_reset"))?;
361        }
362        Ok(())
363    }
364
365    fn dispatch_on_dispose(&mut self) -> PyResult<()> {
366        if let Some(py_self) = self.python_instance()? {
367            Python::attach(|py| py_self.call_method0(py, "on_dispose"))?;
368        }
369        Ok(())
370    }
371
372    fn dispatch_on_degrade(&mut self) -> PyResult<()> {
373        if let Some(py_self) = self.python_instance()? {
374            Python::attach(|py| py_self.call_method0(py, "on_degrade"))?;
375        }
376        Ok(())
377    }
378
379    fn dispatch_on_fault(&mut self) -> PyResult<()> {
380        if let Some(py_self) = self.python_instance()? {
381            Python::attach(|py| py_self.call_method0(py, "on_fault"))?;
382        }
383        Ok(())
384    }
385
386    fn dispatch_on_save(&self) -> PyResult<IndexMap<String, Vec<u8>>> {
387        if let Some(py_self) = self.python_instance()? {
388            Python::attach(|py| {
389                let py_state = py_self.call_method0(py, "on_save")?;
390                let py_state: &Bound<'_, PyDict> = py_state.cast_bound::<PyDict>(py)?;
391                pydict_to_state(py_state)
392            })
393        } else {
394            Ok(IndexMap::new())
395        }
396    }
397
398    fn dispatch_on_load(&mut self, state: &IndexMap<String, Vec<u8>>) -> PyResult<()> {
399        if let Some(py_self) = self.python_instance()? {
400            Python::attach(|py| -> PyResult<()> {
401                let py_state = state_to_pydict(py, state)?;
402                py_self.call_method1(py, "on_load", (py_state,))?;
403                Ok(())
404            })?;
405        }
406        Ok(())
407    }
408
409    fn dispatch_on_time_event(&mut self, event: TimeEvent) -> PyResult<()> {
410        if let Some(py_self) = self.python_instance()? {
411            Python::attach(|py| {
412                py_self.call_method1(py, "on_time_event", (event.into_py_any(py)?,))
413            })?;
414        }
415        Ok(())
416    }
417
418    fn dispatch_on_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
419        if let Some(py_self) = self.python_instance()? {
420            Python::attach(|py| py_self.call_method1(py, "on_data", (data,)))?;
421        }
422        Ok(())
423    }
424
425    fn dispatch_on_signal(&mut self, signal: &Signal) -> PyResult<()> {
426        if let Some(py_self) = self.python_instance()? {
427            Python::attach(|py| {
428                py_self.call_method1(py, "on_signal", (signal.clone().into_py_any(py)?,))
429            })?;
430        }
431        Ok(())
432    }
433
434    fn dispatch_on_queue_state(&mut self, event: &QueueStateChanged) -> PyResult<()> {
435        if let Some(py_self) = self.python_instance()? {
436            Python::attach(|py| {
437                py_self.call_method1(py, "on_queue_state", (event.clone().into_py_any(py)?,))
438            })?;
439        }
440        Ok(())
441    }
442
443    fn dispatch_on_socket_state(&mut self, event: &SocketStateChanged) -> PyResult<()> {
444        if let Some(py_self) = self.python_instance()? {
445            Python::attach(|py| {
446                py_self.call_method1(py, "on_socket_state", (event.clone().into_py_any(py)?,))
447            })?;
448        }
449        Ok(())
450    }
451
452    fn dispatch_on_instrument(&mut self, instrument: Py<PyAny>) -> PyResult<()> {
453        if let Some(py_self) = self.python_instance()? {
454            Python::attach(|py| py_self.call_method1(py, "on_instrument", (instrument,)))?;
455        }
456        Ok(())
457    }
458
459    fn dispatch_on_quote(&mut self, quote: QuoteTick) -> PyResult<()> {
460        if let Some(py_self) = self.python_instance()? {
461            Python::attach(|py| py_self.call_method1(py, "on_quote", (quote.into_py_any(py)?,)))?;
462        }
463        Ok(())
464    }
465
466    fn dispatch_on_trade(&mut self, trade: TradeTick) -> PyResult<()> {
467        if let Some(py_self) = self.python_instance()? {
468            Python::attach(|py| py_self.call_method1(py, "on_trade", (trade.into_py_any(py)?,)))?;
469        }
470        Ok(())
471    }
472
473    fn dispatch_on_bar(&mut self, bar: Bar) -> PyResult<()> {
474        if let Some(py_self) = self.python_instance()? {
475            Python::attach(|py| py_self.call_method1(py, "on_bar", (bar.into_py_any(py)?,)))?;
476        }
477        Ok(())
478    }
479
480    fn dispatch_on_book_deltas(&mut self, deltas: OrderBookDeltas) -> PyResult<()> {
481        if let Some(py_self) = self.python_instance()? {
482            Python::attach(|py| {
483                py_self.call_method1(py, "on_book_deltas", (deltas.into_py_any(py)?,))
484            })?;
485        }
486        Ok(())
487    }
488
489    fn dispatch_on_book_depth(&mut self, depth: &OrderBookDepth) -> PyResult<()> {
490        if let Some(py_self) = self.python_instance()? {
491            Python::attach(|py| {
492                py_self.call_method1(py, "on_book_depth", (depth.clone().into_py_any(py)?,))
493            })?;
494        }
495        Ok(())
496    }
497
498    fn dispatch_on_book(&mut self, book: &OrderBook) -> PyResult<()> {
499        if let Some(py_self) = self.python_instance()? {
500            Python::attach(|py| {
501                py_self.call_method1(py, "on_book", (book.clone().into_py_any(py)?,))
502            })?;
503        }
504        Ok(())
505    }
506
507    fn dispatch_on_mark_price(&mut self, mark_price: MarkPriceUpdate) -> PyResult<()> {
508        if let Some(py_self) = self.python_instance()? {
509            Python::attach(|py| {
510                py_self.call_method1(py, "on_mark_price", (mark_price.into_py_any(py)?,))
511            })?;
512        }
513        Ok(())
514    }
515
516    fn dispatch_on_index_price(&mut self, index_price: IndexPriceUpdate) -> PyResult<()> {
517        if let Some(py_self) = self.python_instance()? {
518            Python::attach(|py| {
519                py_self.call_method1(py, "on_index_price", (index_price.into_py_any(py)?,))
520            })?;
521        }
522        Ok(())
523    }
524
525    fn dispatch_on_funding_rate(&mut self, funding_rate: FundingRateUpdate) -> PyResult<()> {
526        if let Some(py_self) = self.python_instance()? {
527            Python::attach(|py| {
528                py_self.call_method1(py, "on_funding_rate", (funding_rate.into_py_any(py)?,))
529            })?;
530        }
531        Ok(())
532    }
533
534    fn dispatch_on_instrument_status(&mut self, data: InstrumentStatus) -> PyResult<()> {
535        if let Some(py_self) = self.python_instance()? {
536            Python::attach(|py| {
537                py_self.call_method1(py, "on_instrument_status", (data.into_py_any(py)?,))
538            })?;
539        }
540        Ok(())
541    }
542
543    fn dispatch_on_instrument_close(&mut self, update: InstrumentClose) -> PyResult<()> {
544        if let Some(py_self) = self.python_instance()? {
545            Python::attach(|py| {
546                py_self.call_method1(py, "on_instrument_close", (update.into_py_any(py)?,))
547            })?;
548        }
549        Ok(())
550    }
551
552    fn dispatch_on_option_greeks(&mut self, greeks: OptionGreeks) -> PyResult<()> {
553        if let Some(py_self) = self.python_instance()? {
554            Python::attach(|py| {
555                py_self.call_method1(py, "on_option_greeks", (greeks.into_py_any(py)?,))
556            })?;
557        }
558        Ok(())
559    }
560
561    fn dispatch_on_option_chain(&mut self, slice: OptionChainSlice) -> PyResult<()> {
562        if let Some(py_self) = self.python_instance()? {
563            Python::attach(|py| {
564                py_self.call_method1(py, "on_option_chain", (slice.into_py_any(py)?,))
565            })?;
566        }
567        Ok(())
568    }
569
570    fn dispatch_on_historical_data(&mut self, data: Py<PyAny>) -> PyResult<()> {
571        if let Some(py_self) = self.python_instance()? {
572            Python::attach(|py| py_self.call_method1(py, "on_historical_data", (data,)))?;
573        }
574        Ok(())
575    }
576
577    fn dispatch_on_historical_book_deltas(&mut self, deltas: Vec<OrderBookDelta>) -> PyResult<()> {
578        if let Some(py_self) = self.python_instance()? {
579            Python::attach(|py| {
580                let py_deltas = deltas
581                    .into_iter()
582                    .map(|delta| delta.into_py_any(py))
583                    .collect::<PyResult<Vec<_>>>()?;
584                py_self.call_method1(py, "on_historical_book_deltas", (py_deltas,))
585            })?;
586        }
587        Ok(())
588    }
589
590    fn dispatch_on_historical_book_depth(&mut self, depths: Vec<OrderBookDepth>) -> PyResult<()> {
591        if let Some(py_self) = self.python_instance()? {
592            Python::attach(|py| {
593                let py_depths = depths
594                    .into_iter()
595                    .map(|depth| depth.into_py_any(py))
596                    .collect::<PyResult<Vec<_>>>()?;
597                py_self.call_method1(py, "on_historical_book_depth", (py_depths,))
598            })?;
599        }
600        Ok(())
601    }
602
603    fn dispatch_on_historical_quotes(&mut self, quotes: Vec<QuoteTick>) -> PyResult<()> {
604        if let Some(py_self) = self.python_instance()? {
605            Python::attach(|py| {
606                let py_quotes = quotes
607                    .into_iter()
608                    .map(|q| q.into_py_any(py))
609                    .collect::<PyResult<Vec<_>>>()?;
610                py_self.call_method1(py, "on_historical_quotes", (py_quotes,))
611            })?;
612        }
613        Ok(())
614    }
615
616    fn dispatch_on_historical_trades(&mut self, trades: Vec<TradeTick>) -> PyResult<()> {
617        if let Some(py_self) = self.python_instance()? {
618            Python::attach(|py| {
619                let py_trades = trades
620                    .into_iter()
621                    .map(|t| t.into_py_any(py))
622                    .collect::<PyResult<Vec<_>>>()?;
623                py_self.call_method1(py, "on_historical_trades", (py_trades,))
624            })?;
625        }
626        Ok(())
627    }
628
629    fn dispatch_on_historical_funding_rates(
630        &mut self,
631        funding_rates: Vec<FundingRateUpdate>,
632    ) -> PyResult<()> {
633        if let Some(py_self) = self.python_instance()? {
634            Python::attach(|py| {
635                let py_rates = funding_rates
636                    .into_iter()
637                    .map(|r| r.into_py_any(py))
638                    .collect::<PyResult<Vec<_>>>()?;
639                py_self.call_method1(py, "on_historical_funding_rates", (py_rates,))
640            })?;
641        }
642        Ok(())
643    }
644
645    fn dispatch_on_historical_bars(&mut self, bars: Vec<Bar>) -> PyResult<()> {
646        if let Some(py_self) = self.python_instance()? {
647            Python::attach(|py| {
648                let py_bars = bars
649                    .into_iter()
650                    .map(|b| b.into_py_any(py))
651                    .collect::<PyResult<Vec<_>>>()?;
652                py_self.call_method1(py, "on_historical_bars", (py_bars,))
653            })?;
654        }
655        Ok(())
656    }
657
658    fn dispatch_on_historical_mark_prices(
659        &mut self,
660        mark_prices: Vec<MarkPriceUpdate>,
661    ) -> PyResult<()> {
662        if let Some(py_self) = self.python_instance()? {
663            Python::attach(|py| {
664                let py_prices = mark_prices
665                    .into_iter()
666                    .map(|p| p.into_py_any(py))
667                    .collect::<PyResult<Vec<_>>>()?;
668                py_self.call_method1(py, "on_historical_mark_prices", (py_prices,))
669            })?;
670        }
671        Ok(())
672    }
673
674    fn dispatch_on_historical_index_prices(
675        &mut self,
676        index_prices: Vec<IndexPriceUpdate>,
677    ) -> PyResult<()> {
678        if let Some(py_self) = self.python_instance()? {
679            Python::attach(|py| {
680                let py_prices = index_prices
681                    .into_iter()
682                    .map(|p| p.into_py_any(py))
683                    .collect::<PyResult<Vec<_>>>()?;
684                py_self.call_method1(py, "on_historical_index_prices", (py_prices,))
685            })?;
686        }
687        Ok(())
688    }
689
690    #[cfg(feature = "defi")]
691    fn dispatch_on_block(&mut self, block: Block) -> PyResult<()> {
692        if let Some(py_self) = self.python_instance()? {
693            Python::attach(|py| py_self.call_method1(py, "on_block", (block.into_py_any(py)?,)))?;
694        }
695        Ok(())
696    }
697
698    #[cfg(feature = "defi")]
699    fn dispatch_on_pool(&mut self, pool: Pool) -> PyResult<()> {
700        if let Some(py_self) = self.python_instance()? {
701            Python::attach(|py| py_self.call_method1(py, "on_pool", (pool.into_py_any(py)?,)))?;
702        }
703        Ok(())
704    }
705
706    #[cfg(feature = "defi")]
707    fn dispatch_on_pool_swap(&mut self, swap: PoolSwap) -> PyResult<()> {
708        if let Some(py_self) = self.python_instance()? {
709            Python::attach(|py| {
710                py_self.call_method1(py, "on_pool_swap", (swap.into_py_any(py)?,))
711            })?;
712        }
713        Ok(())
714    }
715
716    #[cfg(feature = "defi")]
717    fn dispatch_on_pool_liquidity_update(&mut self, update: PoolLiquidityUpdate) -> PyResult<()> {
718        if let Some(py_self) = self.python_instance()? {
719            Python::attach(|py| {
720                py_self.call_method1(py, "on_pool_liquidity_update", (update.into_py_any(py)?,))
721            })?;
722        }
723        Ok(())
724    }
725
726    #[cfg(feature = "defi")]
727    fn dispatch_on_pool_fee_collect(&mut self, collect: PoolFeeCollect) -> PyResult<()> {
728        if let Some(py_self) = self.python_instance()? {
729            Python::attach(|py| {
730                py_self.call_method1(py, "on_pool_fee_collect", (collect.into_py_any(py)?,))
731            })?;
732        }
733        Ok(())
734    }
735
736    #[cfg(feature = "defi")]
737    fn dispatch_on_pool_flash(&mut self, flash: PoolFlash) -> PyResult<()> {
738        if let Some(py_self) = self.python_instance()? {
739            Python::attach(|py| {
740                py_self.call_method1(py, "on_pool_flash", (flash.into_py_any(py)?,))
741            })?;
742        }
743        Ok(())
744    }
745
746    // The trader owns the wrapper for as long as the actor stays registered, so a collected
747    // wrapper propagates as an error rather than a skipped callback.
748    fn python_instance(&self) -> PyResult<Option<Py<PyAny>>> {
749        upgrade_py_weakref(self.py_self.as_ref(), &self.core.actor_id)
750    }
751}
752
753fn dict_to_params(
754    py: Python<'_>,
755    params: Option<Py<PyDict>>,
756) -> PyResult<Option<nautilus_core::Params>> {
757    match params {
758        Some(dict) => from_pydict(py, &dict),
759        None => Ok(None),
760    }
761}
762
763fn state_to_pydict(py: Python<'_>, state: &IndexMap<String, Vec<u8>>) -> PyResult<Py<PyDict>> {
764    let py_state = PyDict::new(py);
765    for (key, value) in state {
766        py_state.set_item(key, PyBytes::new(py, value))?;
767    }
768    Ok(py_state.unbind())
769}
770
771fn pydict_to_state(state: &Bound<'_, PyDict>) -> PyResult<IndexMap<String, Vec<u8>>> {
772    let mut rust_state = IndexMap::with_capacity(state.len());
773    for (key, value) in state.iter() {
774        rust_state.insert(key.extract()?, value.extract()?);
775    }
776    Ok(rust_state)
777}
778
779/// Python-facing wrapper for `DataActor`.
780///
781/// This wrapper holds shared ownership of `PyDataActorInner` via `Rc<UnsafeCell<>>`.
782/// Both Python (through this wrapper) and the global registries share the same
783/// underlying actor instance, ensuring mutations are visible from both sides.
784#[allow(non_camel_case_types)]
785#[pyo3::pyclass(
786    module = "nautilus_trader.common",
787    name = "DataActor",
788    unsendable,
789    subclass,
790    weakref
791)]
792#[pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")]
793pub struct PyDataActor {
794    inner: Rc<UnsafeCell<PyDataActorInner>>,
795}
796
797impl Debug for PyDataActor {
798    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
799        f.debug_struct(stringify!(PyDataActor))
800            .field("inner", &self.inner())
801            .finish()
802    }
803}
804
805impl PyDataActor {
806    /// Returns a reference to the inner actor state.
807    ///
808    /// # Safety
809    ///
810    /// This is safe for single-threaded use. The `UnsafeCell` allows interior
811    /// mutability which is required for the registries to mutate the actor.
812    #[inline]
813    #[allow(unsafe_code)]
814    pub(crate) fn inner(&self) -> &PyDataActorInner {
815        unsafe { &*self.inner.get() }
816    }
817
818    /// Returns a mutable reference to the inner actor state.
819    ///
820    /// # Safety
821    ///
822    /// This is safe for single-threaded use. Callers must ensure no aliasing
823    /// mutable references exist.
824    #[inline]
825    #[allow(unsafe_code, clippy::mut_from_ref)]
826    pub(crate) fn inner_mut(&self) -> &mut PyDataActorInner {
827        unsafe { &mut *self.inner.get() }
828    }
829}
830
831impl Deref for PyDataActor {
832    type Target = DataActorCore;
833
834    fn deref(&self) -> &Self::Target {
835        &self.inner().core
836    }
837}
838
839impl DerefMut for PyDataActor {
840    fn deref_mut(&mut self) -> &mut Self::Target {
841        &mut self.inner_mut().core
842    }
843}
844
845impl PyDataActor {
846    // Rust constructor for tests and direct Rust usage
847    pub fn new(config: Option<DataActorConfig>) -> Self {
848        let config = config.unwrap_or_default();
849        let core = DataActorCore::new(config);
850        let clock = PyClock::new_test(); // Temporary clock, will be updated on registration
851        let logger = PyLogger::new(core.actor_id().as_str());
852
853        let inner = PyDataActorInner {
854            core,
855            py_self: None,
856            config: None,
857            clock,
858            logger,
859        };
860
861        Self {
862            inner: Rc::new(UnsafeCell::new(inner)),
863        }
864    }
865
866    /// Creates a new [`PyDataActor`] from a Python config object.
867    ///
868    /// Extracts a [`DataActorConfig`] when the object provides one, falling back to defaults
869    /// otherwise, and retains the original object so `.config` returns what the caller passed.
870    /// Subclass constructors share this so the base is built the same way everywhere.
871    #[must_use]
872    pub fn from_py_config(config: Option<Py<PyAny>>) -> Self {
873        let actor_config = config
874            .as_ref()
875            .and_then(|obj| Python::attach(|py| obj.extract::<DataActorConfig>(py).ok()));
876        let mut actor = Self::new(actor_config);
877        actor.set_config(config);
878        actor
879    }
880
881    /// Sets the Python instance reference for method dispatch.
882    ///
883    /// This enables the `PyDataActor` to forward method calls (like `on_start`, `on_stop`)
884    /// to the original Python instance that contains this `PyDataActor`. This is essential
885    /// for Python inheritance to work correctly, allowing Python subclasses to override
886    /// `DataActor` methods and have them called by the Rust system.
887    ///
888    /// Only a weak reference is stored, so the caller keeps ownership of `py_obj`. The trader
889    /// owns registered wrappers; an unregistered actor stays collectable.
890    ///
891    /// # Errors
892    ///
893    /// Returns an error if `py_obj` cannot be weakly referenced.
894    pub fn set_python_instance(&mut self, py_obj: &Bound<'_, PyAny>) -> PyResult<()> {
895        self.inner_mut().py_self = Some(PyWeakrefReference::new(py_obj)?.unbind());
896        Ok(())
897    }
898
899    /// Stores the original Python config object passed at construction.
900    ///
901    /// Retained so the constructed instance exposes `.config` and instance-based registration can
902    /// source the actor ID and logging flags from the same single config object.
903    pub fn set_config(&mut self, config: Option<Py<PyAny>>) {
904        self.inner_mut().config = config;
905    }
906
907    /// Updates the `actor_id` in both the core config and the `actor_id` field.
908    ///
909    /// This method is only exposed for the Python actor to assist with configuration and should
910    /// **never** be called post registration. Calling this after registration will cause
911    /// inconsistent state where the actor is registered under one ID but its internal `actor_id`
912    /// field contains another, breaking message routing and lifecycle management.
913    pub fn set_actor_id(&mut self, actor_id: ActorId) {
914        let inner = self.inner_mut();
915        inner.core.config.actor_id = Some(actor_id);
916        inner.core.actor_id = actor_id;
917        inner.logger = PyLogger::new(actor_id.as_str());
918    }
919
920    /// Updates the `log_events` setting in the core config.
921    pub fn set_log_events(&mut self, log_events: bool) {
922        self.inner_mut().core.config.log_events = log_events;
923    }
924
925    /// Updates the `log_commands` setting in the core config.
926    pub fn set_log_commands(&mut self, log_commands: bool) {
927        self.inner_mut().core.config.log_commands = log_commands;
928    }
929
930    /// Returns the memory address of this instance as a hexadecimal string.
931    pub fn mem_address(&self) -> String {
932        self.inner().core.mem_address()
933    }
934
935    /// Returns a value indicating whether the actor has been registered with a trader.
936    pub fn is_registered(&self) -> bool {
937        self.inner().core.is_registered()
938    }
939
940    /// Register the actor with a trader.
941    ///
942    /// # Errors
943    ///
944    /// Returns an error if the actor is already registered or if the registration process fails.
945    pub fn register(
946        &mut self,
947        trader_id: TraderId,
948        clock: Rc<RefCell<dyn Clock>>,
949        cache: Rc<RefCell<Cache>>,
950    ) -> anyhow::Result<()> {
951        let inner = self.inner_mut();
952        inner.core.register(trader_id, clock, cache)?;
953
954        inner.clock = PyClock::from_rc(inner.core.clock_rc());
955
956        // Register default time event handler for this actor
957        let actor_id = inner.actor_id().inner();
958        let callback = TimeEventCallback::from(move |event: TimeEvent| {
959            if let Some(mut actor) = try_get_actor_unchecked::<PyDataActorInner>(&actor_id) {
960                if let Err(e) = actor.on_time_event(&event) {
961                    log::error!("Python time event handler failed for actor {actor_id}: {e}");
962                }
963            } else {
964                log::error!("Actor {actor_id} not found for time event handling");
965            }
966        });
967
968        inner.clock.inner_mut().register_default_handler(callback);
969
970        inner.initialize()
971    }
972
973    /// Registers this actor in the global component, actor, and wrapper registries.
974    ///
975    /// Clones the internal `Rc` and inserts into both registries. This ensures
976    /// Python and the registries share the exact same actor instance. The Python wrapper is
977    /// retained as part of the same act, so a registered actor always has an owner for the
978    /// wrapper its inner only weakly references.
979    ///
980    /// # Errors
981    ///
982    /// Returns an error if no Python wrapper is attached, or if the attached wrapper has already
983    /// been collected. Nothing is registered in that case.
984    pub fn register_in_global_registries(&self) -> PyResult<()> {
985        let inner = self.inner();
986        let component_id = inner.component_id();
987        let actor_id = Actor::id(inner);
988
989        let Some(wrapper) = inner.python_instance()? else {
990            return Err(to_pyruntime_err(format!(
991                "Cannot register actor {actor_id} without a Python wrapper, call `set_python_instance` first"
992            )));
993        };
994
995        let inner_ref: Rc<UnsafeCell<PyDataActorInner>> = self.inner.clone();
996
997        let component_trait_ref: Rc<UnsafeCell<dyn Component>> = inner_ref.clone();
998        with_component_registry(|registry| {
999            registry.insert(component_id.inner(), component_trait_ref);
1000        });
1001
1002        let actor_trait_ref: Rc<UnsafeCell<dyn Actor>> = inner_ref;
1003        with_actor_registry(|registry| registry.insert(actor_id, actor_trait_ref));
1004
1005        retain_python_wrapper(component_id, wrapper, inner.core.message_bus());
1006
1007        Ok(())
1008    }
1009}
1010
1011pub fn register_python_exec_algorithm_endpoint(exec_algorithm_id: ExecAlgorithmId) {
1012    let actor_id = exec_algorithm_id.inner();
1013    let endpoint: Ustr = format!("{exec_algorithm_id}.execute").into();
1014    let handler = ShareableMessageHandler::from_typed(move |command: &TradingCommand| {
1015        if let Some(mut algo) = try_get_actor_unchecked::<PyDataActorInner>(&actor_id) {
1016            if let Err(e) = algo.execute_exec_algorithm_command(command) {
1017                log::error!("Error executing command on Python algorithm {actor_id}: {e}");
1018            }
1019        } else {
1020            log::error!("Python execution algorithm {actor_id} not found in registry");
1021        }
1022    });
1023    msgbus::register_any(endpoint.into(), handler);
1024}
1025
1026impl DataActor for PyDataActorInner {
1027    fn response_handler(&self) -> TypedHandler<DataResponseEnvelope> {
1028        crate::python::response::response_handler(
1029            self.core().actor_id().inner(),
1030            Self::python_instance,
1031            None,
1032        )
1033    }
1034
1035    fn on_start(&mut self) -> anyhow::Result<()> {
1036        self.dispatch_on_start()
1037            .map_err(|e| anyhow::anyhow!("Python on_start failed:\n{}", format_exception(&e)))
1038    }
1039
1040    fn on_stop(&mut self) -> anyhow::Result<()> {
1041        self.dispatch_on_stop()
1042            .map_err(|e| anyhow::anyhow!("Python on_stop failed:\n{}", format_exception(&e)))
1043    }
1044
1045    fn on_resume(&mut self) -> anyhow::Result<()> {
1046        self.dispatch_on_resume()
1047            .map_err(|e| anyhow::anyhow!("Python on_resume failed:\n{}", format_exception(&e)))
1048    }
1049
1050    fn on_reset(&mut self) -> anyhow::Result<()> {
1051        self.dispatch_on_reset()
1052            .map_err(|e| anyhow::anyhow!("Python on_reset failed:\n{}", format_exception(&e)))
1053    }
1054
1055    fn on_dispose(&mut self) -> anyhow::Result<()> {
1056        self.dispatch_on_dispose()
1057            .map_err(|e| anyhow::anyhow!("Python on_dispose failed:\n{}", format_exception(&e)))
1058    }
1059
1060    fn on_degrade(&mut self) -> anyhow::Result<()> {
1061        self.dispatch_on_degrade()
1062            .map_err(|e| anyhow::anyhow!("Python on_degrade failed:\n{}", format_exception(&e)))
1063    }
1064
1065    fn on_fault(&mut self) -> anyhow::Result<()> {
1066        self.dispatch_on_fault()
1067            .map_err(|e| anyhow::anyhow!("Python on_fault failed:\n{}", format_exception(&e)))
1068    }
1069
1070    fn on_save(&self) -> anyhow::Result<IndexMap<String, Vec<u8>>> {
1071        self.dispatch_on_save()
1072            .map_err(|e| anyhow::anyhow!("Python on_save failed:\n{}", format_exception(&e)))
1073    }
1074
1075    fn on_load(&mut self, state: IndexMap<String, Vec<u8>>) -> anyhow::Result<()> {
1076        self.dispatch_on_load(&state)
1077            .map_err(|e| anyhow::anyhow!("Python on_load failed:\n{}", format_exception(&e)))
1078    }
1079
1080    fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
1081        self.dispatch_on_time_event(event.clone())
1082            .map_err(|e| anyhow::anyhow!("Python on_time_event failed:\n{}", format_exception(&e)))
1083    }
1084
1085    #[allow(unused_variables)]
1086    fn on_data(&mut self, data: &CustomData) -> anyhow::Result<()> {
1087        Python::attach(|py| {
1088            let py_data: Py<PyAny> = Py::new(py, data.clone())?.into_any();
1089            self.dispatch_on_data(py_data)
1090                .map_err(|e| anyhow::anyhow!("Python on_data failed:\n{}", format_exception(&e)))
1091        })
1092    }
1093
1094    fn on_signal(&mut self, signal: &Signal) -> anyhow::Result<()> {
1095        self.dispatch_on_signal(signal)
1096            .map_err(|e| anyhow::anyhow!("Python on_signal failed:\n{}", format_exception(&e)))
1097    }
1098
1099    fn on_queue_state(&mut self, event: &QueueStateChanged) -> anyhow::Result<()> {
1100        self.dispatch_on_queue_state(event)
1101            .map_err(|e| anyhow::anyhow!("Python on_queue_state failed:\n{}", format_exception(&e)))
1102    }
1103
1104    fn on_socket_state(&mut self, event: &SocketStateChanged) -> anyhow::Result<()> {
1105        self.dispatch_on_socket_state(event).map_err(|e| {
1106            anyhow::anyhow!("Python on_socket_state failed:\n{}", format_exception(&e))
1107        })
1108    }
1109
1110    fn on_instrument(&mut self, instrument: &InstrumentAny) -> anyhow::Result<()> {
1111        Python::attach(|py| {
1112            let py_instrument = instrument_any_to_pyobject(py, instrument.clone())
1113                .map_err(|e| anyhow::anyhow!("Failed to convert InstrumentAny to Python: {e}"))?;
1114            self.dispatch_on_instrument(py_instrument).map_err(|e| {
1115                anyhow::anyhow!("Python on_instrument failed:\n{}", format_exception(&e))
1116            })
1117        })
1118    }
1119
1120    fn on_quote(&mut self, quote: &QuoteTick) -> anyhow::Result<()> {
1121        self.dispatch_on_quote(*quote)
1122            .map_err(|e| anyhow::anyhow!("Python on_quote failed:\n{}", format_exception(&e)))
1123    }
1124
1125    fn on_trade(&mut self, tick: &TradeTick) -> anyhow::Result<()> {
1126        self.dispatch_on_trade(*tick)
1127            .map_err(|e| anyhow::anyhow!("Python on_trade failed:\n{}", format_exception(&e)))
1128    }
1129
1130    fn on_bar(&mut self, bar: &Bar) -> anyhow::Result<()> {
1131        self.dispatch_on_bar(*bar)
1132            .map_err(|e| anyhow::anyhow!("Python on_bar failed:\n{}", format_exception(&e)))
1133    }
1134
1135    fn on_book_deltas(&mut self, deltas: &OrderBookDeltas) -> anyhow::Result<()> {
1136        self.dispatch_on_book_deltas(deltas.clone())
1137            .map_err(|e| anyhow::anyhow!("Python on_book_deltas failed:\n{}", format_exception(&e)))
1138    }
1139
1140    fn on_book_depth(&mut self, depth: &OrderBookDepth) -> anyhow::Result<()> {
1141        self.dispatch_on_book_depth(depth)
1142            .map_err(|e| anyhow::anyhow!("Python on_book_depth failed:\n{}", format_exception(&e)))
1143    }
1144
1145    fn on_book(&mut self, order_book: &OrderBook) -> anyhow::Result<()> {
1146        self.dispatch_on_book(order_book)
1147            .map_err(|e| anyhow::anyhow!("Python on_book failed:\n{}", format_exception(&e)))
1148    }
1149
1150    fn on_mark_price(&mut self, mark_price: &MarkPriceUpdate) -> anyhow::Result<()> {
1151        self.dispatch_on_mark_price(*mark_price)
1152            .map_err(|e| anyhow::anyhow!("Python on_mark_price failed:\n{}", format_exception(&e)))
1153    }
1154
1155    fn on_index_price(&mut self, index_price: &IndexPriceUpdate) -> anyhow::Result<()> {
1156        self.dispatch_on_index_price(*index_price)
1157            .map_err(|e| anyhow::anyhow!("Python on_index_price failed:\n{}", format_exception(&e)))
1158    }
1159
1160    fn on_funding_rate(&mut self, funding_rate: &FundingRateUpdate) -> anyhow::Result<()> {
1161        self.dispatch_on_funding_rate(*funding_rate).map_err(|e| {
1162            anyhow::anyhow!("Python on_funding_rate failed:\n{}", format_exception(&e))
1163        })
1164    }
1165
1166    fn on_instrument_status(&mut self, data: &InstrumentStatus) -> anyhow::Result<()> {
1167        self.dispatch_on_instrument_status(*data).map_err(|e| {
1168            anyhow::anyhow!(
1169                "Python on_instrument_status failed:\n{}",
1170                format_exception(&e)
1171            )
1172        })
1173    }
1174
1175    fn on_instrument_close(&mut self, update: &InstrumentClose) -> anyhow::Result<()> {
1176        self.dispatch_on_instrument_close(*update).map_err(|e| {
1177            anyhow::anyhow!(
1178                "Python on_instrument_close failed:\n{}",
1179                format_exception(&e)
1180            )
1181        })
1182    }
1183
1184    fn on_option_greeks(&mut self, greeks: &OptionGreeks) -> anyhow::Result<()> {
1185        self.dispatch_on_option_greeks(*greeks).map_err(|e| {
1186            anyhow::anyhow!("Python on_option_greeks failed:\n{}", format_exception(&e))
1187        })
1188    }
1189
1190    fn on_option_chain(&mut self, slice: &OptionChainSlice) -> anyhow::Result<()> {
1191        self.dispatch_on_option_chain(slice.clone()).map_err(|e| {
1192            anyhow::anyhow!("Python on_option_chain failed:\n{}", format_exception(&e))
1193        })
1194    }
1195
1196    #[cfg(feature = "defi")]
1197    fn on_block(&mut self, block: &Block) -> anyhow::Result<()> {
1198        self.dispatch_on_block(block.clone())
1199            .map_err(|e| anyhow::anyhow!("Python on_block failed:\n{}", format_exception(&e)))
1200    }
1201
1202    #[cfg(feature = "defi")]
1203    fn on_pool(&mut self, pool: &Pool) -> anyhow::Result<()> {
1204        self.dispatch_on_pool(pool.clone())
1205            .map_err(|e| anyhow::anyhow!("Python on_pool failed:\n{}", format_exception(&e)))
1206    }
1207
1208    #[cfg(feature = "defi")]
1209    fn on_pool_swap(&mut self, swap: &PoolSwap) -> anyhow::Result<()> {
1210        self.dispatch_on_pool_swap(swap.clone())
1211            .map_err(|e| anyhow::anyhow!("Python on_pool_swap failed:\n{}", format_exception(&e)))
1212    }
1213
1214    #[cfg(feature = "defi")]
1215    fn on_pool_liquidity_update(&mut self, update: &PoolLiquidityUpdate) -> anyhow::Result<()> {
1216        self.dispatch_on_pool_liquidity_update(update.clone())
1217            .map_err(|e| {
1218                anyhow::anyhow!(
1219                    "Python on_pool_liquidity_update failed:\n{}",
1220                    format_exception(&e)
1221                )
1222            })
1223    }
1224
1225    #[cfg(feature = "defi")]
1226    fn on_pool_fee_collect(&mut self, collect: &PoolFeeCollect) -> anyhow::Result<()> {
1227        self.dispatch_on_pool_fee_collect(collect.clone())
1228            .map_err(|e| {
1229                anyhow::anyhow!(
1230                    "Python on_pool_fee_collect failed:\n{}",
1231                    format_exception(&e)
1232                )
1233            })
1234    }
1235
1236    #[cfg(feature = "defi")]
1237    fn on_pool_flash(&mut self, flash: &PoolFlash) -> anyhow::Result<()> {
1238        self.dispatch_on_pool_flash(flash.clone())
1239            .map_err(|e| anyhow::anyhow!("Python on_pool_flash failed:\n{}", format_exception(&e)))
1240    }
1241
1242    fn on_historical_data(&mut self, data: &dyn Any) -> anyhow::Result<()> {
1243        Python::attach(|py| {
1244            let py_data: Py<PyAny> = if let Some(custom_data) = data.downcast_ref::<CustomData>() {
1245                Py::new(py, custom_data.clone())?.into_any()
1246            } else if let Some(custom_data) = data.downcast_ref::<Vec<CustomData>>() {
1247                custom_data.clone().into_py_any(py)?
1248            } else {
1249                anyhow::bail!("Failed to convert historical data to Python: unsupported type");
1250            };
1251
1252            self.dispatch_on_historical_data(py_data).map_err(|e| {
1253                anyhow::anyhow!(
1254                    "Python on_historical_data failed:\n{}",
1255                    format_exception(&e)
1256                )
1257            })
1258        })
1259    }
1260
1261    fn on_historical_book_deltas(&mut self, deltas: &[OrderBookDelta]) -> anyhow::Result<()> {
1262        self.dispatch_on_historical_book_deltas(deltas.to_vec())
1263            .map_err(|e| {
1264                anyhow::anyhow!(
1265                    "Python on_historical_book_deltas failed:\n{}",
1266                    format_exception(&e)
1267                )
1268            })
1269    }
1270
1271    fn on_historical_book_depth(&mut self, depths: &[OrderBookDepth]) -> anyhow::Result<()> {
1272        self.dispatch_on_historical_book_depth(depths.to_vec())
1273            .map_err(|e| {
1274                anyhow::anyhow!(
1275                    "Python on_historical_book_depth failed:\n{}",
1276                    format_exception(&e)
1277                )
1278            })
1279    }
1280
1281    fn on_historical_quotes(&mut self, quotes: &[QuoteTick]) -> anyhow::Result<()> {
1282        self.dispatch_on_historical_quotes(quotes.to_vec())
1283            .map_err(|e| {
1284                anyhow::anyhow!(
1285                    "Python on_historical_quotes failed:\n{}",
1286                    format_exception(&e)
1287                )
1288            })
1289    }
1290
1291    fn on_historical_trades(&mut self, trades: &[TradeTick]) -> anyhow::Result<()> {
1292        self.dispatch_on_historical_trades(trades.to_vec())
1293            .map_err(|e| {
1294                anyhow::anyhow!(
1295                    "Python on_historical_trades failed:\n{}",
1296                    format_exception(&e)
1297                )
1298            })
1299    }
1300
1301    fn on_historical_funding_rates(
1302        &mut self,
1303        funding_rates: &[FundingRateUpdate],
1304    ) -> anyhow::Result<()> {
1305        self.dispatch_on_historical_funding_rates(funding_rates.to_vec())
1306            .map_err(|e| {
1307                anyhow::anyhow!(
1308                    "Python on_historical_funding_rates failed:\n{}",
1309                    format_exception(&e)
1310                )
1311            })
1312    }
1313
1314    fn on_historical_bars(&mut self, bars: &[Bar]) -> anyhow::Result<()> {
1315        self.dispatch_on_historical_bars(bars.to_vec())
1316            .map_err(|e| {
1317                anyhow::anyhow!(
1318                    "Python on_historical_bars failed:\n{}",
1319                    format_exception(&e)
1320                )
1321            })
1322    }
1323
1324    fn on_historical_mark_prices(&mut self, mark_prices: &[MarkPriceUpdate]) -> anyhow::Result<()> {
1325        self.dispatch_on_historical_mark_prices(mark_prices.to_vec())
1326            .map_err(|e| {
1327                anyhow::anyhow!(
1328                    "Python on_historical_mark_prices failed:\n{}",
1329                    format_exception(&e)
1330                )
1331            })
1332    }
1333
1334    fn on_historical_index_prices(
1335        &mut self,
1336        index_prices: &[IndexPriceUpdate],
1337    ) -> anyhow::Result<()> {
1338        self.dispatch_on_historical_index_prices(index_prices.to_vec())
1339            .map_err(|e| {
1340                anyhow::anyhow!(
1341                    "Python on_historical_index_prices failed:\n{}",
1342                    format_exception(&e)
1343                )
1344            })
1345    }
1346}
1347
1348#[pymethods]
1349#[pyo3_stub_gen::derive::gen_stub_pymethods]
1350impl PyDataActor {
1351    /// Creates a new [`PyDataActor`] instance.
1352    ///
1353    /// Accepts `None` or any Python object. If the object is a [`DataActorConfig`]
1354    /// (or can be extracted as one via `from_py_object`), its values are used;
1355    /// otherwise the actor falls back to [`DataActorConfig::default()`].
1356    ///
1357    /// This permissive signature is required so that Python subclasses can pass a
1358    /// **custom** config dataclass to their `__init__`. The original object is retained
1359    /// here in `__new__`, which always receives the constructor arguments, so `.config`
1360    /// and registration see the config even when a subclass omits forwarding it to
1361    /// `super().__init__()`.
1362    #[new]
1363    #[pyo3(signature = (config=None))]
1364    fn py_new(config: Option<Py<PyAny>>) -> Self {
1365        Self::from_py_config(config)
1366    }
1367
1368    #[pyo3(signature = (config=None))]
1369    fn __init__(slf: &Bound<'_, Self>, config: Option<Py<PyAny>>) -> PyResult<()> {
1370        {
1371            let mut borrowed = slf.borrow_mut();
1372            borrowed.set_python_instance(slf.as_any())?;
1373            // `__new__` retained the config; only a forwarded config overrides it
1374            if config.is_some() {
1375                borrowed.set_config(config);
1376            }
1377        }
1378
1379        if !has_configured_actor_id(slf) {
1380            let py_type = slf.get_type();
1381            let type_name = py_type.name()?;
1382            let actor_id = ActorId::new_checked(type_name.to_str()?).map_err(to_pyvalue_err)?;
1383            slf.borrow_mut().set_actor_id(actor_id);
1384        }
1385
1386        Ok(())
1387    }
1388
1389    #[getter]
1390    #[pyo3(name = "clock")]
1391    fn py_clock(&self) -> PyResult<PyClock> {
1392        let inner = self.inner();
1393        if inner.core.is_registered() {
1394            Ok(inner.clock.clone())
1395        } else {
1396            Err(to_pyruntime_err(
1397                "Actor must be registered with a trader before accessing clock",
1398            ))
1399        }
1400    }
1401
1402    #[getter]
1403    #[pyo3(name = "cache")]
1404    fn py_cache(&self) -> PyResult<PyCache> {
1405        let inner = self.inner();
1406        if inner.core.is_registered() {
1407            Ok(PyCache::from_rc(inner.core.cache_rc()))
1408        } else {
1409            Err(to_pyruntime_err(
1410                "Actor must be registered with a trader before accessing cache",
1411            ))
1412        }
1413    }
1414
1415    #[getter]
1416    #[pyo3(name = "log")]
1417    fn py_log(&self) -> PyLogger {
1418        self.inner().logger.clone()
1419    }
1420
1421    #[getter]
1422    #[pyo3(name = "actor_id")]
1423    fn py_actor_id(&self) -> ActorId {
1424        self.inner().core.actor_id
1425    }
1426
1427    #[getter]
1428    #[pyo3(name = "config")]
1429    fn py_config(&self, py: Python<'_>) -> Option<Py<PyAny>> {
1430        self.inner()
1431            .config
1432            .as_ref()
1433            .map(|config| config.clone_ref(py))
1434    }
1435
1436    #[getter]
1437    #[pyo3(name = "trader_id")]
1438    fn py_trader_id(&self) -> Option<TraderId> {
1439        self.inner().core.trader_id()
1440    }
1441
1442    #[pyo3(name = "state")]
1443    fn py_state(&self) -> ComponentState {
1444        Component::state(self.inner())
1445    }
1446
1447    #[pyo3(name = "is_ready")]
1448    fn py_is_ready(&self) -> bool {
1449        Component::is_ready(self.inner())
1450    }
1451
1452    #[pyo3(name = "is_running")]
1453    fn py_is_running(&self) -> bool {
1454        Component::is_running(self.inner())
1455    }
1456
1457    #[pyo3(name = "is_stopped")]
1458    fn py_is_stopped(&self) -> bool {
1459        Component::is_stopped(self.inner())
1460    }
1461
1462    #[pyo3(name = "is_degraded")]
1463    fn py_is_degraded(&self) -> bool {
1464        Component::is_degraded(self.inner())
1465    }
1466
1467    #[pyo3(name = "is_faulted")]
1468    fn py_is_faulted(&self) -> bool {
1469        Component::is_faulted(self.inner())
1470    }
1471
1472    #[pyo3(name = "is_disposed")]
1473    fn py_is_disposed(&self) -> bool {
1474        Component::is_disposed(self.inner())
1475    }
1476
1477    #[pyo3(name = "start")]
1478    fn py_start(&mut self) -> PyResult<()> {
1479        Component::start(self.inner_mut()).map_err(to_pyruntime_err)
1480    }
1481
1482    #[pyo3(name = "stop")]
1483    fn py_stop(&mut self) -> PyResult<()> {
1484        Component::stop(self.inner_mut()).map_err(to_pyruntime_err)
1485    }
1486
1487    #[pyo3(name = "save")]
1488    fn py_save(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
1489        let state = DataActor::on_save(self.inner()).map_err(to_pyruntime_err)?;
1490        state_to_pydict(py, &state)
1491    }
1492
1493    #[pyo3(name = "load")]
1494    fn py_load(&mut self, state: &Bound<'_, PyDict>) -> PyResult<()> {
1495        let state = pydict_to_state(state)?;
1496        DataActor::on_load(self.inner_mut(), state).map_err(to_pyruntime_err)
1497    }
1498
1499    #[pyo3(name = "resume")]
1500    fn py_resume(&mut self) -> PyResult<()> {
1501        Component::resume(self.inner_mut()).map_err(to_pyruntime_err)
1502    }
1503
1504    #[pyo3(name = "reset")]
1505    fn py_reset(&mut self) -> PyResult<()> {
1506        Component::reset(self.inner_mut()).map_err(to_pyruntime_err)
1507    }
1508
1509    #[pyo3(name = "dispose")]
1510    fn py_dispose(&mut self) -> PyResult<()> {
1511        Component::dispose(self.inner_mut()).map_err(to_pyruntime_err)
1512    }
1513
1514    #[pyo3(name = "degrade")]
1515    fn py_degrade(&mut self) -> PyResult<()> {
1516        Component::degrade(self.inner_mut()).map_err(to_pyruntime_err)
1517    }
1518
1519    #[pyo3(name = "fault")]
1520    fn py_fault(&mut self) -> PyResult<()> {
1521        Component::fault(self.inner_mut()).map_err(to_pyruntime_err)
1522    }
1523
1524    #[pyo3(name = "shutdown_system")]
1525    #[pyo3(signature = (reason=None))]
1526    fn py_shutdown_system(&self, reason: Option<String>) -> PyResult<()> {
1527        if !self.inner().core.is_registered() {
1528            return Err(to_pyruntime_err(
1529                "Actor must be registered with a trader before shutting down the system",
1530            ));
1531        }
1532
1533        self.inner().shutdown_system(reason);
1534        Ok(())
1535    }
1536
1537    #[pyo3(name = "publish_data")]
1538    fn py_publish_data(&self, data_type: &DataType, data: &CustomData) -> PyResult<()> {
1539        self.ensure_registered_for_data()?;
1540        self.inner().publish_data(data_type, data);
1541        Ok(())
1542    }
1543
1544    #[pyo3(name = "publish_signal")]
1545    #[pyo3(signature = (name, value, ts_event=0))]
1546    #[expect(
1547        clippy::needless_pass_by_value,
1548        reason = "PyO3 accepts an owned PyAny handle for Python signal values"
1549    )]
1550    fn py_publish_signal(
1551        &self,
1552        py: Python<'_>,
1553        name: &str,
1554        value: Py<PyAny>,
1555        ts_event: u64,
1556    ) -> PyResult<()> {
1557        self.ensure_registered_for_data()?;
1558        let value_str: String = value.bind(py).str()?.extract()?;
1559        self.inner()
1560            .publish_signal(name, value_str, UnixNanos::from(ts_event));
1561        Ok(())
1562    }
1563
1564    #[pyo3(name = "add_synthetic")]
1565    fn py_add_synthetic(&self, synthetic: SyntheticInstrument) -> PyResult<()> {
1566        self.ensure_registered_for_data()?;
1567        self.inner()
1568            .add_synthetic(synthetic)
1569            .map_err(to_pyvalue_err)
1570    }
1571
1572    #[pyo3(name = "update_synthetic")]
1573    fn py_update_synthetic(&self, synthetic: SyntheticInstrument) -> PyResult<()> {
1574        self.ensure_registered_for_data()?;
1575        self.inner()
1576            .update_synthetic(synthetic)
1577            .map_err(to_pyvalue_err)
1578    }
1579
1580    #[getter]
1581    #[pyo3(name = "registered_indicators")]
1582    fn py_registered_indicators(&self, py: Python<'_>) -> PyResult<Py<PyList>> {
1583        registered_python_indicators(py, self.inner().core.registered_indicators())
1584    }
1585
1586    #[pyo3(name = "indicators_initialized")]
1587    fn py_indicators_initialized(&self, _py: Python<'_>) -> PyResult<bool> {
1588        self.inner()
1589            .core
1590            .indicators_initialized()
1591            .map_err(to_pyruntime_err)
1592    }
1593
1594    #[pyo3(name = "register_indicator_for_quote_ticks")]
1595    fn py_register_indicator_for_quote_ticks(
1596        &mut self,
1597        py: Python<'_>,
1598        instrument_id: InstrumentId,
1599        indicator: Py<PyAny>,
1600    ) {
1601        let indicator = wrap_python_indicator(py, indicator);
1602        self.inner_mut()
1603            .core
1604            .register_indicator_for_quote_ticks(instrument_id, indicator);
1605    }
1606
1607    #[pyo3(name = "register_indicator_for_trade_ticks")]
1608    fn py_register_indicator_for_trade_ticks(
1609        &mut self,
1610        py: Python<'_>,
1611        instrument_id: InstrumentId,
1612        indicator: Py<PyAny>,
1613    ) {
1614        let indicator = wrap_python_indicator(py, indicator);
1615        self.inner_mut()
1616            .core
1617            .register_indicator_for_trade_ticks(instrument_id, indicator);
1618    }
1619
1620    #[pyo3(name = "register_indicator_for_bars")]
1621    fn py_register_indicator_for_bars(
1622        &mut self,
1623        py: Python<'_>,
1624        bar_type: BarType,
1625        indicator: Py<PyAny>,
1626    ) {
1627        let indicator = wrap_python_indicator(py, indicator);
1628        self.inner_mut()
1629            .core
1630            .register_indicator_for_bars(bar_type, indicator);
1631    }
1632
1633    #[pyo3(name = "on_start")]
1634    fn py_on_start(_slf: &Bound<'_, Self>) {}
1635
1636    #[pyo3(name = "on_stop")]
1637    fn py_on_stop(_slf: &Bound<'_, Self>) {}
1638
1639    #[pyo3(name = "on_resume")]
1640    fn py_on_resume(_slf: &Bound<'_, Self>) {}
1641
1642    #[pyo3(name = "on_reset")]
1643    fn py_on_reset(_slf: &Bound<'_, Self>) {}
1644
1645    #[pyo3(name = "on_dispose")]
1646    fn py_on_dispose(_slf: &Bound<'_, Self>) {}
1647
1648    #[pyo3(name = "on_degrade")]
1649    fn py_on_degrade(_slf: &Bound<'_, Self>) {}
1650
1651    #[pyo3(name = "on_fault")]
1652    fn py_on_fault(_slf: &Bound<'_, Self>) {}
1653
1654    #[pyo3(name = "on_save")]
1655    fn py_on_save(_slf: &Bound<'_, Self>, py: Python<'_>) -> Py<PyDict> {
1656        PyDict::new(py).unbind()
1657    }
1658
1659    #[allow(unused_variables)]
1660    #[pyo3(name = "on_load")]
1661    fn py_on_load(_slf: &Bound<'_, Self>, state: &Bound<'_, PyDict>) {}
1662
1663    #[allow(unused_variables, clippy::needless_pass_by_value)]
1664    #[pyo3(name = "on_time_event")]
1665    fn py_on_time_event(_slf: &Bound<'_, Self>, event: TimeEvent) {}
1666
1667    #[allow(unused_variables, clippy::needless_pass_by_value)]
1668    #[pyo3(name = "on_data")]
1669    fn py_on_data(_slf: &Bound<'_, Self>, data: Py<PyAny>) {}
1670
1671    #[allow(unused_variables)]
1672    #[pyo3(name = "on_signal")]
1673    fn py_on_signal(_slf: &Bound<'_, Self>, signal: &Signal) {}
1674
1675    #[allow(unused_variables, clippy::needless_pass_by_value)]
1676    #[pyo3(name = "on_queue_state")]
1677    fn py_on_queue_state(_slf: &Bound<'_, Self>, event: QueueStateChanged) {}
1678
1679    #[allow(unused_variables, clippy::needless_pass_by_value)]
1680    #[pyo3(name = "on_socket_state")]
1681    fn py_on_socket_state(_slf: &Bound<'_, Self>, event: SocketStateChanged) {}
1682
1683    #[allow(unused_variables, clippy::needless_pass_by_value)]
1684    #[pyo3(name = "on_instrument")]
1685    fn py_on_instrument(_slf: &Bound<'_, Self>, instrument: Py<PyAny>) {}
1686
1687    #[allow(unused_variables)]
1688    #[pyo3(name = "on_quote")]
1689    fn py_on_quote(_slf: &Bound<'_, Self>, quote: QuoteTick) {}
1690
1691    #[allow(unused_variables)]
1692    #[pyo3(name = "on_trade")]
1693    fn py_on_trade(_slf: &Bound<'_, Self>, trade: TradeTick) {}
1694
1695    #[allow(unused_variables)]
1696    #[pyo3(name = "on_bar")]
1697    fn py_on_bar(_slf: &Bound<'_, Self>, bar: Bar) {}
1698
1699    #[allow(unused_variables, clippy::needless_pass_by_value)]
1700    #[pyo3(name = "on_book_deltas")]
1701    fn py_on_book_deltas(_slf: &Bound<'_, Self>, deltas: OrderBookDeltas) {}
1702
1703    #[allow(unused_variables)]
1704    #[pyo3(name = "on_book_depth")]
1705    fn py_on_book_depth(_slf: &Bound<'_, Self>, depth: &OrderBookDepth) {}
1706
1707    #[allow(unused_variables)]
1708    #[pyo3(name = "on_book")]
1709    fn py_on_book(_slf: &Bound<'_, Self>, book: &OrderBook) {}
1710
1711    #[allow(unused_variables)]
1712    #[pyo3(name = "on_mark_price")]
1713    fn py_on_mark_price(_slf: &Bound<'_, Self>, mark_price: MarkPriceUpdate) {}
1714
1715    #[allow(unused_variables)]
1716    #[pyo3(name = "on_index_price")]
1717    fn py_on_index_price(_slf: &Bound<'_, Self>, index_price: IndexPriceUpdate) {}
1718
1719    #[allow(unused_variables)]
1720    #[pyo3(name = "on_funding_rate")]
1721    fn py_on_funding_rate(_slf: &Bound<'_, Self>, funding_rate: FundingRateUpdate) {}
1722
1723    #[allow(unused_variables)]
1724    #[pyo3(name = "on_instrument_status")]
1725    fn py_on_instrument_status(_slf: &Bound<'_, Self>, status: InstrumentStatus) {}
1726
1727    #[allow(unused_variables)]
1728    #[pyo3(name = "on_instrument_close")]
1729    fn py_on_instrument_close(_slf: &Bound<'_, Self>, close: InstrumentClose) {}
1730
1731    #[allow(unused_variables)]
1732    #[pyo3(name = "on_option_greeks")]
1733    fn py_on_option_greeks(_slf: &Bound<'_, Self>, greeks: OptionGreeks) {}
1734
1735    #[allow(unused_variables, clippy::needless_pass_by_value)]
1736    #[pyo3(name = "on_option_chain")]
1737    fn py_on_option_chain(_slf: &Bound<'_, Self>, slice: OptionChainSlice) {}
1738
1739    #[pyo3(name = "subscribe_data")]
1740    #[pyo3(signature = (data_type, client_id=None, params=None))]
1741    fn py_subscribe_data(
1742        &mut self,
1743        py: Python<'_>,
1744        data_type: DataType,
1745        client_id: Option<ClientId>,
1746        params: Option<Py<PyDict>>,
1747    ) -> PyResult<()> {
1748        self.ensure_registered()?;
1749        let params = dict_to_params(py, params)?;
1750        DataActor::subscribe_data(self.inner_mut(), data_type, client_id, params);
1751        Ok(())
1752    }
1753
1754    #[pyo3(name = "subscribe_signal")]
1755    #[pyo3(signature = (name="", priority=None))]
1756    fn py_subscribe_signal(
1757        slf: &Bound<'_, Self>,
1758        name: &str,
1759        priority: Option<u32>,
1760    ) -> PyResult<()> {
1761        let actor = borrow_actor_mut(slf, "subscribe_signal")?;
1762        actor.ensure_registered()?;
1763        DataActor::subscribe_signal(actor.inner_mut(), name, priority);
1764        Ok(())
1765    }
1766
1767    #[pyo3(name = "subscribe_queue_state")]
1768    #[pyo3(signature = (channel=None, priority=None))]
1769    fn py_subscribe_queue_state(
1770        &mut self,
1771        channel: Option<SystemChannel>,
1772        priority: Option<u32>,
1773    ) -> PyResult<()> {
1774        self.ensure_registered()?;
1775        DataActor::subscribe_queue_state(self.inner_mut(), channel, priority);
1776        Ok(())
1777    }
1778
1779    #[pyo3(name = "subscribe_socket_state")]
1780    #[pyo3(signature = (client_id=None, endpoint=None, priority=None))]
1781    fn py_subscribe_socket_state(
1782        &mut self,
1783        client_id: Option<ClientId>,
1784        endpoint: Option<&str>,
1785        priority: Option<u32>,
1786    ) -> PyResult<()> {
1787        self.ensure_registered()?;
1788        DataActor::subscribe_socket_state(self.inner_mut(), client_id, endpoint, priority);
1789        Ok(())
1790    }
1791
1792    #[pyo3(name = "subscribe_instruments")]
1793    #[pyo3(signature = (venue, client_id=None, params=None))]
1794    fn py_subscribe_instruments(
1795        &mut self,
1796        py: Python<'_>,
1797        venue: Venue,
1798        client_id: Option<ClientId>,
1799        params: Option<Py<PyDict>>,
1800    ) -> PyResult<()> {
1801        self.ensure_registered()?;
1802        let params = dict_to_params(py, params)?;
1803        DataActor::subscribe_instruments(self.inner_mut(), venue, client_id, params);
1804        Ok(())
1805    }
1806
1807    #[pyo3(name = "subscribe_instrument")]
1808    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1809    fn py_subscribe_instrument(
1810        &mut self,
1811        py: Python<'_>,
1812        #[gen_stub(override_type(
1813            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
1814        ))]
1815        instrument_id: &Bound<'_, PyAny>,
1816        client_id: Option<ClientId>,
1817        params: Option<Py<PyDict>>,
1818    ) -> PyResult<()> {
1819        self.ensure_registered()?;
1820        let instrument_id = extract_instrument_ids(instrument_id)?;
1821        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
1822            .map_err(to_pyvalue_err)?;
1823        ensure_subscription_routes(
1824            &instrument_id,
1825            client_id.as_ref(),
1826            &DataActorNative::cache_ref(self.inner()),
1827        )
1828        .map_err(to_pyvalue_err)?;
1829        let params = dict_to_params(py, params)?;
1830        DataActor::subscribe_instrument(self.inner_mut(), instrument_id, client_id, params);
1831        Ok(())
1832    }
1833
1834    #[pyo3(name = "subscribe_book_deltas")]
1835    #[pyo3(signature = (instrument_id, book_type, depth=None, client_id=None, managed=false, params=None))]
1836    #[expect(clippy::too_many_arguments)]
1837    fn py_subscribe_book_deltas(
1838        &mut self,
1839        py: Python<'_>,
1840        #[gen_stub(override_type(
1841            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
1842        ))]
1843        instrument_id: &Bound<'_, PyAny>,
1844        book_type: BookType,
1845        depth: Option<usize>,
1846        client_id: Option<ClientId>,
1847        managed: bool,
1848        params: Option<Py<PyDict>>,
1849    ) -> PyResult<()> {
1850        self.ensure_registered()?;
1851        let instrument_id = extract_instrument_ids(instrument_id)?;
1852        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
1853            .map_err(to_pyvalue_err)?;
1854        ensure_subscription_routes(
1855            &instrument_id,
1856            client_id.as_ref(),
1857            &DataActorNative::cache_ref(self.inner()),
1858        )
1859        .map_err(to_pyvalue_err)?;
1860        let params = dict_to_params(py, params)?;
1861        let depth = depth.and_then(NonZeroUsize::new);
1862        DataActor::subscribe_book_deltas(
1863            self.inner_mut(),
1864            instrument_id,
1865            book_type,
1866            depth,
1867            client_id,
1868            managed,
1869            params,
1870        );
1871        Ok(())
1872    }
1873
1874    #[expect(clippy::too_many_arguments)]
1875    #[pyo3(name = "subscribe_book_depth")]
1876    #[pyo3(signature = (instrument_id, book_type, depth=None, client_id=None, managed=false, params=None))]
1877    fn py_subscribe_book_depth(
1878        &mut self,
1879        py: Python<'_>,
1880        #[gen_stub(override_type(
1881            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
1882        ))]
1883        instrument_id: &Bound<'_, PyAny>,
1884        book_type: BookType,
1885        depth: Option<usize>,
1886        client_id: Option<ClientId>,
1887        managed: bool,
1888        params: Option<Py<PyDict>>,
1889    ) -> PyResult<()> {
1890        self.ensure_registered()?;
1891        let instrument_id = extract_instrument_ids(instrument_id)?;
1892        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
1893            .map_err(to_pyvalue_err)?;
1894        ensure_subscription_routes(
1895            &instrument_id,
1896            client_id.as_ref(),
1897            &DataActorNative::cache_ref(self.inner()),
1898        )
1899        .map_err(to_pyvalue_err)?;
1900
1901        let depth = depth
1902            .map(|value| {
1903                NonZeroUsize::new(value).ok_or_else(|| to_pyvalue_err("depth must be positive"))
1904            })
1905            .transpose()?;
1906
1907        let params = dict_to_params(py, params)?;
1908        DataActor::subscribe_book_depth(
1909            self.inner_mut(),
1910            instrument_id,
1911            book_type,
1912            depth,
1913            client_id,
1914            managed,
1915            params,
1916        );
1917        Ok(())
1918    }
1919
1920    #[pyo3(name = "subscribe_book_at_interval")]
1921    #[pyo3(signature = (instrument_id, book_type, interval_ms, depth=None, client_id=None, params=None))]
1922    #[expect(clippy::too_many_arguments)]
1923    fn py_subscribe_book_at_interval(
1924        &mut self,
1925        py: Python<'_>,
1926        #[gen_stub(override_type(
1927            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
1928        ))]
1929        instrument_id: &Bound<'_, PyAny>,
1930        book_type: BookType,
1931        interval_ms: usize,
1932        depth: Option<usize>,
1933        client_id: Option<ClientId>,
1934        params: Option<Py<PyDict>>,
1935    ) -> PyResult<()> {
1936        let interval_ms = NonZeroUsize::new(interval_ms)
1937            .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
1938
1939        self.ensure_registered()?;
1940        let instrument_id = extract_instrument_ids(instrument_id)?;
1941        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
1942            .map_err(to_pyvalue_err)?;
1943        ensure_subscription_routes(
1944            &instrument_id,
1945            client_id.as_ref(),
1946            &DataActorNative::cache_ref(self.inner()),
1947        )
1948        .map_err(to_pyvalue_err)?;
1949        let params = dict_to_params(py, params)?;
1950        let depth = depth.and_then(NonZeroUsize::new);
1951        DataActor::subscribe_book_at_interval(
1952            self.inner_mut(),
1953            instrument_id,
1954            book_type,
1955            depth,
1956            interval_ms,
1957            client_id,
1958            params,
1959        );
1960        Ok(())
1961    }
1962
1963    #[pyo3(name = "subscribe_quotes")]
1964    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1965    fn py_subscribe_quotes(
1966        &mut self,
1967        py: Python<'_>,
1968        #[gen_stub(override_type(
1969            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
1970        ))]
1971        instrument_id: &Bound<'_, PyAny>,
1972        client_id: Option<ClientId>,
1973        params: Option<Py<PyDict>>,
1974    ) -> PyResult<()> {
1975        self.ensure_registered()?;
1976        let instrument_ids = extract_instrument_ids(instrument_id)?;
1977        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())
1978            .map_err(to_pyvalue_err)?;
1979        let params = dict_to_params(py, params)?;
1980        quote_routing(
1981            &instrument_ids,
1982            client_id.as_ref(),
1983            params.as_ref(),
1984            &DataActorNative::cache_ref(self.inner()),
1985        )
1986        .map_err(to_pyvalue_err)?;
1987        DataActor::subscribe_quotes(self.inner_mut(), instrument_ids, client_id, params);
1988        Ok(())
1989    }
1990
1991    #[pyo3(name = "subscribe_trades")]
1992    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
1993    fn py_subscribe_trades(
1994        &mut self,
1995        py: Python<'_>,
1996        #[gen_stub(override_type(
1997            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
1998        ))]
1999        instrument_id: &Bound<'_, PyAny>,
2000        client_id: Option<ClientId>,
2001        params: Option<Py<PyDict>>,
2002    ) -> PyResult<()> {
2003        self.ensure_registered()?;
2004        let instrument_ids = extract_instrument_ids(instrument_id)?;
2005        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())
2006            .map_err(to_pyvalue_err)?;
2007        let params = dict_to_params(py, params)?;
2008        DataActor::subscribe_trades(self.inner_mut(), instrument_ids, client_id, params);
2009        Ok(())
2010    }
2011
2012    #[pyo3(name = "subscribe_bars")]
2013    #[pyo3(signature = (bar_type, client_id=None, params=None))]
2014    fn py_subscribe_bars(
2015        &mut self,
2016        py: Python<'_>,
2017        #[gen_stub(override_type(type_repr = "model.BarType | typing.Sequence[model.BarType]"))]
2018        bar_type: &Bound<'_, PyAny>,
2019        client_id: Option<ClientId>,
2020        params: Option<Py<PyDict>>,
2021    ) -> PyResult<()> {
2022        self.ensure_registered()?;
2023        let bar_types = extract_bar_types(bar_type)?;
2024        ensure_routable_bar_types(&bar_types, client_id.is_some()).map_err(to_pyvalue_err)?;
2025        let params = dict_to_params(py, params)?;
2026        DataActor::subscribe_bars(self.inner_mut(), bar_types, client_id, params);
2027        Ok(())
2028    }
2029
2030    #[pyo3(name = "subscribe_mark_prices")]
2031    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2032    fn py_subscribe_mark_prices(
2033        &mut self,
2034        py: Python<'_>,
2035        #[gen_stub(override_type(
2036            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2037        ))]
2038        instrument_id: &Bound<'_, PyAny>,
2039        client_id: Option<ClientId>,
2040        params: Option<Py<PyDict>>,
2041    ) -> PyResult<()> {
2042        self.ensure_registered()?;
2043        let instrument_id = extract_instrument_ids(instrument_id)?;
2044        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2045            .map_err(to_pyvalue_err)?;
2046        let params = dict_to_params(py, params)?;
2047        DataActor::subscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params);
2048        Ok(())
2049    }
2050
2051    #[pyo3(name = "subscribe_index_prices")]
2052    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2053    fn py_subscribe_index_prices(
2054        &mut self,
2055        py: Python<'_>,
2056        #[gen_stub(override_type(
2057            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2058        ))]
2059        instrument_id: &Bound<'_, PyAny>,
2060        client_id: Option<ClientId>,
2061        params: Option<Py<PyDict>>,
2062    ) -> PyResult<()> {
2063        self.ensure_registered()?;
2064        let instrument_id = extract_instrument_ids(instrument_id)?;
2065        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2066            .map_err(to_pyvalue_err)?;
2067        let params = dict_to_params(py, params)?;
2068        DataActor::subscribe_index_prices(self.inner_mut(), instrument_id, client_id, params);
2069        Ok(())
2070    }
2071
2072    #[pyo3(name = "subscribe_funding_rates")]
2073    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2074    fn py_subscribe_funding_rates(
2075        &mut self,
2076        py: Python<'_>,
2077        #[gen_stub(override_type(
2078            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2079        ))]
2080        instrument_id: &Bound<'_, PyAny>,
2081        client_id: Option<ClientId>,
2082        params: Option<Py<PyDict>>,
2083    ) -> PyResult<()> {
2084        self.ensure_registered()?;
2085        let instrument_id = extract_instrument_ids(instrument_id)?;
2086        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2087            .map_err(to_pyvalue_err)?;
2088        let params = dict_to_params(py, params)?;
2089        DataActor::subscribe_funding_rates(self.inner_mut(), instrument_id, client_id, params);
2090        Ok(())
2091    }
2092
2093    #[pyo3(name = "subscribe_option_greeks")]
2094    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2095    fn py_subscribe_option_greeks(
2096        &mut self,
2097        py: Python<'_>,
2098        #[gen_stub(override_type(
2099            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2100        ))]
2101        instrument_id: &Bound<'_, PyAny>,
2102        client_id: Option<ClientId>,
2103        params: Option<Py<PyDict>>,
2104    ) -> PyResult<()> {
2105        self.ensure_registered()?;
2106        let instrument_id = extract_instrument_ids(instrument_id)?;
2107        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2108            .map_err(to_pyvalue_err)?;
2109        ensure_subscription_routes(
2110            &instrument_id,
2111            client_id.as_ref(),
2112            &DataActorNative::cache_ref(self.inner()),
2113        )
2114        .map_err(to_pyvalue_err)?;
2115        let params = dict_to_params(py, params)?;
2116        DataActor::subscribe_option_greeks(self.inner_mut(), instrument_id, client_id, params);
2117        Ok(())
2118    }
2119
2120    #[pyo3(name = "subscribe_instrument_status")]
2121    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2122    fn py_subscribe_instrument_status(
2123        &mut self,
2124        py: Python<'_>,
2125        #[gen_stub(override_type(
2126            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2127        ))]
2128        instrument_id: &Bound<'_, PyAny>,
2129        client_id: Option<ClientId>,
2130        params: Option<Py<PyDict>>,
2131    ) -> PyResult<()> {
2132        self.ensure_registered()?;
2133        let instrument_id = extract_instrument_ids(instrument_id)?;
2134        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2135            .map_err(to_pyvalue_err)?;
2136        ensure_subscription_routes(
2137            &instrument_id,
2138            client_id.as_ref(),
2139            &DataActorNative::cache_ref(self.inner()),
2140        )
2141        .map_err(to_pyvalue_err)?;
2142        let params = dict_to_params(py, params)?;
2143        DataActor::subscribe_instrument_status(self.inner_mut(), instrument_id, client_id, params);
2144        Ok(())
2145    }
2146
2147    #[pyo3(name = "subscribe_instrument_close")]
2148    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2149    fn py_subscribe_instrument_close(
2150        &mut self,
2151        py: Python<'_>,
2152        #[gen_stub(override_type(
2153            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2154        ))]
2155        instrument_id: &Bound<'_, PyAny>,
2156        client_id: Option<ClientId>,
2157        params: Option<Py<PyDict>>,
2158    ) -> PyResult<()> {
2159        self.ensure_registered()?;
2160        let instrument_id = extract_instrument_ids(instrument_id)?;
2161        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2162            .map_err(to_pyvalue_err)?;
2163        ensure_subscription_routes(
2164            &instrument_id,
2165            client_id.as_ref(),
2166            &DataActorNative::cache_ref(self.inner()),
2167        )
2168        .map_err(to_pyvalue_err)?;
2169        let params = dict_to_params(py, params)?;
2170        DataActor::subscribe_instrument_close(self.inner_mut(), instrument_id, client_id, params);
2171        Ok(())
2172    }
2173
2174    #[pyo3(name = "subscribe_option_chain")]
2175    #[pyo3(signature = (series_id, strike_range, snapshot_interval_ms=None, client_id=None, params=None))]
2176    fn py_subscribe_option_chain(
2177        &mut self,
2178        py: Python<'_>,
2179        series_id: OptionSeriesId,
2180        strike_range: PyStrikeRange,
2181        snapshot_interval_ms: Option<u64>,
2182        client_id: Option<ClientId>,
2183        params: Option<Py<PyDict>>,
2184    ) -> PyResult<()> {
2185        self.ensure_registered()?;
2186        let params = dict_to_params(py, params)?;
2187        DataActor::subscribe_option_chain(
2188            self.inner_mut(),
2189            series_id,
2190            strike_range.inner,
2191            snapshot_interval_ms,
2192            client_id,
2193            params,
2194        );
2195        Ok(())
2196    }
2197
2198    #[pyo3(name = "unsubscribe_data")]
2199    #[pyo3(signature = (data_type, client_id=None, params=None))]
2200    fn py_unsubscribe_data(
2201        &mut self,
2202        py: Python<'_>,
2203        data_type: DataType,
2204        client_id: Option<ClientId>,
2205        params: Option<Py<PyDict>>,
2206    ) -> PyResult<()> {
2207        self.ensure_registered()?;
2208        let params = dict_to_params(py, params)?;
2209        DataActor::unsubscribe_data(self.inner_mut(), data_type, client_id, params);
2210        Ok(())
2211    }
2212
2213    #[pyo3(name = "unsubscribe_signal")]
2214    #[pyo3(signature = (name=""))]
2215    fn py_unsubscribe_signal(slf: &Bound<'_, Self>, name: &str) -> PyResult<()> {
2216        let actor = borrow_actor_mut(slf, "unsubscribe_signal")?;
2217        actor.ensure_registered()?;
2218        DataActor::unsubscribe_signal(actor.inner_mut(), name);
2219        Ok(())
2220    }
2221
2222    #[pyo3(name = "unsubscribe_queue_state")]
2223    #[pyo3(signature = (channel=None))]
2224    fn py_unsubscribe_queue_state(&mut self, channel: Option<SystemChannel>) -> PyResult<()> {
2225        self.ensure_registered()?;
2226        DataActor::unsubscribe_queue_state(self.inner_mut(), channel);
2227        Ok(())
2228    }
2229
2230    #[pyo3(name = "unsubscribe_socket_state")]
2231    #[pyo3(signature = (client_id=None, endpoint=None))]
2232    fn py_unsubscribe_socket_state(
2233        &mut self,
2234        client_id: Option<ClientId>,
2235        endpoint: Option<&str>,
2236    ) -> PyResult<()> {
2237        self.ensure_registered()?;
2238        DataActor::unsubscribe_socket_state(self.inner_mut(), client_id, endpoint);
2239        Ok(())
2240    }
2241
2242    #[pyo3(name = "unsubscribe_instruments")]
2243    #[pyo3(signature = (venue, client_id=None, params=None))]
2244    fn py_unsubscribe_instruments(
2245        &mut self,
2246        py: Python<'_>,
2247        venue: Venue,
2248        client_id: Option<ClientId>,
2249        params: Option<Py<PyDict>>,
2250    ) -> PyResult<()> {
2251        self.ensure_registered()?;
2252        let params = dict_to_params(py, params)?;
2253        DataActor::unsubscribe_instruments(self.inner_mut(), venue, client_id, params);
2254        Ok(())
2255    }
2256
2257    #[pyo3(name = "unsubscribe_instrument")]
2258    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2259    fn py_unsubscribe_instrument(
2260        &mut self,
2261        py: Python<'_>,
2262        #[gen_stub(override_type(
2263            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2264        ))]
2265        instrument_id: &Bound<'_, PyAny>,
2266        client_id: Option<ClientId>,
2267        params: Option<Py<PyDict>>,
2268    ) -> PyResult<()> {
2269        self.ensure_registered()?;
2270        let instrument_id = extract_instrument_ids(instrument_id)?;
2271        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2272            .map_err(to_pyvalue_err)?;
2273        let params = dict_to_params(py, params)?;
2274        DataActor::unsubscribe_instrument(self.inner_mut(), instrument_id, client_id, params);
2275        Ok(())
2276    }
2277
2278    #[pyo3(name = "unsubscribe_book_deltas")]
2279    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2280    fn py_unsubscribe_book_deltas(
2281        &mut self,
2282        py: Python<'_>,
2283        #[gen_stub(override_type(
2284            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2285        ))]
2286        instrument_id: &Bound<'_, PyAny>,
2287        client_id: Option<ClientId>,
2288        params: Option<Py<PyDict>>,
2289    ) -> PyResult<()> {
2290        self.ensure_registered()?;
2291        let instrument_id = extract_instrument_ids(instrument_id)?;
2292        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2293            .map_err(to_pyvalue_err)?;
2294        let params = dict_to_params(py, params)?;
2295        DataActor::unsubscribe_book_deltas(self.inner_mut(), instrument_id, client_id, params);
2296        Ok(())
2297    }
2298
2299    #[pyo3(name = "unsubscribe_book_depth")]
2300    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2301    fn py_unsubscribe_book_depth(
2302        &mut self,
2303        py: Python<'_>,
2304        #[gen_stub(override_type(
2305            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2306        ))]
2307        instrument_id: &Bound<'_, PyAny>,
2308        client_id: Option<ClientId>,
2309        params: Option<Py<PyDict>>,
2310    ) -> PyResult<()> {
2311        self.ensure_registered()?;
2312        let instrument_id = extract_instrument_ids(instrument_id)?;
2313        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2314            .map_err(to_pyvalue_err)?;
2315        let params = dict_to_params(py, params)?;
2316        DataActor::unsubscribe_book_depth(self.inner_mut(), instrument_id, client_id, params);
2317        Ok(())
2318    }
2319
2320    #[pyo3(name = "unsubscribe_book_at_interval")]
2321    #[pyo3(signature = (instrument_id, interval_ms, client_id=None, params=None))]
2322    fn py_unsubscribe_book_at_interval(
2323        &mut self,
2324        py: Python<'_>,
2325        #[gen_stub(override_type(
2326            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2327        ))]
2328        instrument_id: &Bound<'_, PyAny>,
2329        interval_ms: usize,
2330        client_id: Option<ClientId>,
2331        params: Option<Py<PyDict>>,
2332    ) -> PyResult<()> {
2333        let interval_ms = NonZeroUsize::new(interval_ms)
2334            .ok_or_else(|| to_pyvalue_err("interval_ms must be > 0"))?;
2335
2336        self.ensure_registered()?;
2337        let instrument_id = extract_instrument_ids(instrument_id)?;
2338        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2339            .map_err(to_pyvalue_err)?;
2340        let params = dict_to_params(py, params)?;
2341        DataActor::unsubscribe_book_at_interval(
2342            self.inner_mut(),
2343            instrument_id,
2344            interval_ms,
2345            client_id,
2346            params,
2347        );
2348        Ok(())
2349    }
2350
2351    #[pyo3(name = "unsubscribe_quotes")]
2352    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2353    fn py_unsubscribe_quotes(
2354        &mut self,
2355        py: Python<'_>,
2356        #[gen_stub(override_type(
2357            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2358        ))]
2359        instrument_id: &Bound<'_, PyAny>,
2360        client_id: Option<ClientId>,
2361        params: Option<Py<PyDict>>,
2362    ) -> PyResult<()> {
2363        self.ensure_registered()?;
2364        let instrument_ids = extract_instrument_ids(instrument_id)?;
2365        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())
2366            .map_err(to_pyvalue_err)?;
2367        let params = dict_to_params(py, params)?;
2368        DataActor::unsubscribe_quotes(self.inner_mut(), instrument_ids, client_id, params);
2369        Ok(())
2370    }
2371
2372    #[pyo3(name = "unsubscribe_trades")]
2373    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2374    fn py_unsubscribe_trades(
2375        &mut self,
2376        py: Python<'_>,
2377        #[gen_stub(override_type(
2378            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2379        ))]
2380        instrument_id: &Bound<'_, PyAny>,
2381        client_id: Option<ClientId>,
2382        params: Option<Py<PyDict>>,
2383    ) -> PyResult<()> {
2384        self.ensure_registered()?;
2385        let instrument_ids = extract_instrument_ids(instrument_id)?;
2386        ensure_routable_instrument_ids(&instrument_ids, client_id.is_some())
2387            .map_err(to_pyvalue_err)?;
2388        let params = dict_to_params(py, params)?;
2389        DataActor::unsubscribe_trades(self.inner_mut(), instrument_ids, client_id, params);
2390        Ok(())
2391    }
2392
2393    #[pyo3(name = "unsubscribe_bars")]
2394    #[pyo3(signature = (bar_type, client_id=None, params=None))]
2395    fn py_unsubscribe_bars(
2396        &mut self,
2397        py: Python<'_>,
2398        #[gen_stub(override_type(type_repr = "model.BarType | typing.Sequence[model.BarType]"))]
2399        bar_type: &Bound<'_, PyAny>,
2400        client_id: Option<ClientId>,
2401        params: Option<Py<PyDict>>,
2402    ) -> PyResult<()> {
2403        self.ensure_registered()?;
2404        let bar_types = extract_bar_types(bar_type)?;
2405        ensure_routable_bar_types(&bar_types, client_id.is_some()).map_err(to_pyvalue_err)?;
2406        let params = dict_to_params(py, params)?;
2407        DataActor::unsubscribe_bars(self.inner_mut(), bar_types, client_id, params);
2408        Ok(())
2409    }
2410
2411    #[pyo3(name = "unsubscribe_mark_prices")]
2412    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2413    fn py_unsubscribe_mark_prices(
2414        &mut self,
2415        py: Python<'_>,
2416        #[gen_stub(override_type(
2417            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2418        ))]
2419        instrument_id: &Bound<'_, PyAny>,
2420        client_id: Option<ClientId>,
2421        params: Option<Py<PyDict>>,
2422    ) -> PyResult<()> {
2423        self.ensure_registered()?;
2424        let instrument_id = extract_instrument_ids(instrument_id)?;
2425        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2426            .map_err(to_pyvalue_err)?;
2427        let params = dict_to_params(py, params)?;
2428        DataActor::unsubscribe_mark_prices(self.inner_mut(), instrument_id, client_id, params);
2429        Ok(())
2430    }
2431
2432    #[pyo3(name = "unsubscribe_index_prices")]
2433    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2434    fn py_unsubscribe_index_prices(
2435        &mut self,
2436        py: Python<'_>,
2437        #[gen_stub(override_type(
2438            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2439        ))]
2440        instrument_id: &Bound<'_, PyAny>,
2441        client_id: Option<ClientId>,
2442        params: Option<Py<PyDict>>,
2443    ) -> PyResult<()> {
2444        self.ensure_registered()?;
2445        let instrument_id = extract_instrument_ids(instrument_id)?;
2446        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2447            .map_err(to_pyvalue_err)?;
2448        let params = dict_to_params(py, params)?;
2449        DataActor::unsubscribe_index_prices(self.inner_mut(), instrument_id, client_id, params);
2450        Ok(())
2451    }
2452
2453    #[pyo3(name = "unsubscribe_funding_rates")]
2454    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2455    fn py_unsubscribe_funding_rates(
2456        &mut self,
2457        py: Python<'_>,
2458        #[gen_stub(override_type(
2459            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2460        ))]
2461        instrument_id: &Bound<'_, PyAny>,
2462        client_id: Option<ClientId>,
2463        params: Option<Py<PyDict>>,
2464    ) -> PyResult<()> {
2465        self.ensure_registered()?;
2466        let instrument_id = extract_instrument_ids(instrument_id)?;
2467        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2468            .map_err(to_pyvalue_err)?;
2469        let params = dict_to_params(py, params)?;
2470        DataActor::unsubscribe_funding_rates(self.inner_mut(), instrument_id, client_id, params);
2471        Ok(())
2472    }
2473
2474    #[pyo3(name = "unsubscribe_option_greeks")]
2475    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2476    fn py_unsubscribe_option_greeks(
2477        &mut self,
2478        py: Python<'_>,
2479        #[gen_stub(override_type(
2480            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2481        ))]
2482        instrument_id: &Bound<'_, PyAny>,
2483        client_id: Option<ClientId>,
2484        params: Option<Py<PyDict>>,
2485    ) -> PyResult<()> {
2486        self.ensure_registered()?;
2487        let instrument_id = extract_instrument_ids(instrument_id)?;
2488        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2489            .map_err(to_pyvalue_err)?;
2490        let params = dict_to_params(py, params)?;
2491        DataActor::unsubscribe_option_greeks(self.inner_mut(), instrument_id, client_id, params);
2492        Ok(())
2493    }
2494
2495    #[pyo3(name = "unsubscribe_instrument_status")]
2496    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2497    fn py_unsubscribe_instrument_status(
2498        &mut self,
2499        py: Python<'_>,
2500        #[gen_stub(override_type(
2501            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2502        ))]
2503        instrument_id: &Bound<'_, PyAny>,
2504        client_id: Option<ClientId>,
2505        params: Option<Py<PyDict>>,
2506    ) -> PyResult<()> {
2507        self.ensure_registered()?;
2508        let instrument_id = extract_instrument_ids(instrument_id)?;
2509        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2510            .map_err(to_pyvalue_err)?;
2511        let params = dict_to_params(py, params)?;
2512        DataActor::unsubscribe_instrument_status(
2513            self.inner_mut(),
2514            instrument_id,
2515            client_id,
2516            params,
2517        );
2518        Ok(())
2519    }
2520
2521    #[pyo3(name = "unsubscribe_instrument_close")]
2522    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
2523    fn py_unsubscribe_instrument_close(
2524        &mut self,
2525        py: Python<'_>,
2526        #[gen_stub(override_type(
2527            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2528        ))]
2529        instrument_id: &Bound<'_, PyAny>,
2530        client_id: Option<ClientId>,
2531        params: Option<Py<PyDict>>,
2532    ) -> PyResult<()> {
2533        self.ensure_registered()?;
2534        let instrument_id = extract_instrument_ids(instrument_id)?;
2535        ensure_routable_instrument_ids(&instrument_id, client_id.is_some())
2536            .map_err(to_pyvalue_err)?;
2537        let params = dict_to_params(py, params)?;
2538        DataActor::unsubscribe_instrument_close(self.inner_mut(), instrument_id, client_id, params);
2539        Ok(())
2540    }
2541
2542    #[pyo3(name = "unsubscribe_option_chain")]
2543    #[pyo3(signature = (series_id, client_id=None))]
2544    fn py_unsubscribe_option_chain(
2545        &mut self,
2546        series_id: OptionSeriesId,
2547        client_id: Option<ClientId>,
2548    ) -> PyResult<()> {
2549        self.ensure_registered()?;
2550        DataActor::unsubscribe_option_chain(self.inner_mut(), series_id, client_id);
2551        Ok(())
2552    }
2553
2554    #[pyo3(name = "request_data")]
2555    #[pyo3(signature = (data_type, client_id, start=None, end=None, limit=None, params=None, callback=None))]
2556    #[expect(clippy::too_many_arguments)]
2557    fn py_request_data(
2558        slf: &Bound<'_, Self>,
2559        py: Python<'_>,
2560        data_type: DataType,
2561        client_id: ClientId,
2562        start: Option<Timestamp>,
2563        end: Option<Timestamp>,
2564        limit: Option<usize>,
2565        params: Option<Py<PyDict>>,
2566        #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
2567        callback: Option<Py<PyAny>>,
2568    ) -> PyResult<String> {
2569        let actor = Self {
2570            inner: Rc::clone(&slf.try_borrow()?.inner),
2571        };
2572
2573        actor.ensure_registered_for_data()?;
2574        let params = dict_to_params(py, params)?;
2575        let limit = limit.and_then(NonZeroUsize::new);
2576        let handler = crate::python::response::request_handler(
2577            py,
2578            actor.inner().core().actor_id().inner(),
2579            PyDataActorInner::python_instance,
2580            callback,
2581        )?;
2582        let request_id = DataActorCore::request_data(
2583            actor.inner().core(),
2584            data_type,
2585            client_id,
2586            start,
2587            end,
2588            limit,
2589            params,
2590            handler,
2591        )
2592        .map_err(to_pyvalue_err)?;
2593        Ok(request_id.to_string())
2594    }
2595
2596    #[pyo3(name = "request_instrument")]
2597    #[pyo3(signature = (instrument_id, start=None, end=None, client_id=None, params=None, callback=None))]
2598    #[expect(clippy::too_many_arguments)]
2599    fn py_request_instrument(
2600        slf: &Bound<'_, Self>,
2601        py: Python<'_>,
2602        #[gen_stub(override_type(
2603            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2604        ))]
2605        instrument_id: &Bound<'_, PyAny>,
2606        start: Option<Timestamp>,
2607        end: Option<Timestamp>,
2608        client_id: Option<ClientId>,
2609        params: Option<Py<PyDict>>,
2610        #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
2611        callback: Option<Py<PyAny>>,
2612    ) -> PyResult<String> {
2613        let actor = Self {
2614            inner: Rc::clone(&slf.try_borrow()?.inner),
2615        };
2616
2617        let instrument_id = extract_instrument_ids(instrument_id)?;
2618        actor.ensure_registered_for_data()?;
2619        let params = dict_to_params(py, params)?;
2620        let handler = crate::python::response::request_handler(
2621            py,
2622            actor.inner().core().actor_id().inner(),
2623            PyDataActorInner::python_instance,
2624            callback,
2625        )?;
2626        let request_id = DataActorCore::request_instrument(
2627            actor.inner().core(),
2628            instrument_id,
2629            start,
2630            end,
2631            client_id,
2632            params,
2633            handler,
2634        )
2635        .map_err(to_pyvalue_err)?;
2636        Ok(request_id.to_string())
2637    }
2638
2639    #[pyo3(name = "request_instruments")]
2640    #[pyo3(signature = (venue=None, start=None, end=None, client_id=None, params=None, callback=None))]
2641    #[expect(clippy::too_many_arguments)]
2642    fn py_request_instruments(
2643        slf: &Bound<'_, Self>,
2644        py: Python<'_>,
2645        venue: Option<Venue>,
2646        start: Option<Timestamp>,
2647        end: Option<Timestamp>,
2648        client_id: Option<ClientId>,
2649        params: Option<Py<PyDict>>,
2650        #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
2651        callback: Option<Py<PyAny>>,
2652    ) -> PyResult<String> {
2653        let actor = Self {
2654            inner: Rc::clone(&slf.try_borrow()?.inner),
2655        };
2656
2657        actor.ensure_registered_for_data()?;
2658        let params = dict_to_params(py, params)?;
2659        let handler = crate::python::response::request_handler(
2660            py,
2661            actor.inner().core().actor_id().inner(),
2662            PyDataActorInner::python_instance,
2663            callback,
2664        )?;
2665        let request_id = DataActorCore::request_instruments(
2666            actor.inner().core(),
2667            venue,
2668            start,
2669            end,
2670            client_id,
2671            params,
2672            handler,
2673        )
2674        .map_err(to_pyvalue_err)?;
2675        Ok(request_id.to_string())
2676    }
2677
2678    #[pyo3(name = "request_book_snapshot")]
2679    #[pyo3(signature = (instrument_id, depth=None, client_id=None, params=None, callback=None))]
2680    fn py_request_book_snapshot(
2681        slf: &Bound<'_, Self>,
2682        py: Python<'_>,
2683        #[gen_stub(override_type(
2684            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2685        ))]
2686        instrument_id: &Bound<'_, PyAny>,
2687        depth: Option<usize>,
2688        client_id: Option<ClientId>,
2689        params: Option<Py<PyDict>>,
2690        #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
2691        callback: Option<Py<PyAny>>,
2692    ) -> PyResult<String> {
2693        let actor = Self {
2694            inner: Rc::clone(&slf.try_borrow()?.inner),
2695        };
2696
2697        let instrument_id = extract_instrument_ids(instrument_id)?;
2698        actor.ensure_registered_for_data()?;
2699        let params = dict_to_params(py, params)?;
2700        let depth = depth.and_then(NonZeroUsize::new);
2701
2702        let handler = crate::python::response::request_handler(
2703            py,
2704            actor.inner().core().actor_id().inner(),
2705            PyDataActorInner::python_instance,
2706            callback,
2707        )?;
2708        let request_id = DataActorCore::request_book_snapshot(
2709            actor.inner().core(),
2710            instrument_id,
2711            depth,
2712            client_id,
2713            params,
2714            handler,
2715        )
2716        .map_err(to_pyvalue_err)?;
2717        Ok(request_id.to_string())
2718    }
2719
2720    #[pyo3(name = "request_book_deltas")]
2721    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None, callback=None))]
2722    #[expect(clippy::too_many_arguments)]
2723    fn py_request_book_deltas(
2724        slf: &Bound<'_, Self>,
2725        py: Python<'_>,
2726        #[gen_stub(override_type(
2727            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2728        ))]
2729        instrument_id: &Bound<'_, PyAny>,
2730        start: Option<Timestamp>,
2731        end: Option<Timestamp>,
2732        limit: Option<usize>,
2733        client_id: Option<ClientId>,
2734        params: Option<Py<PyDict>>,
2735        #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
2736        callback: Option<Py<PyAny>>,
2737    ) -> PyResult<String> {
2738        let actor = Self {
2739            inner: Rc::clone(&slf.try_borrow()?.inner),
2740        };
2741
2742        let instrument_id = extract_instrument_ids(instrument_id)?;
2743        actor.ensure_registered_for_data()?;
2744        let params = dict_to_params(py, params)?;
2745        let limit = limit.and_then(NonZeroUsize::new);
2746        let handler = crate::python::response::request_handler(
2747            py,
2748            actor.inner().core().actor_id().inner(),
2749            PyDataActorInner::python_instance,
2750            callback,
2751        )?;
2752        let request_id = DataActorCore::request_book_deltas(
2753            actor.inner().core(),
2754            instrument_id,
2755            start,
2756            end,
2757            limit,
2758            client_id,
2759            params,
2760            handler,
2761        )
2762        .map_err(to_pyvalue_err)?;
2763        Ok(request_id.to_string())
2764    }
2765
2766    #[pyo3(name = "request_book_depth")]
2767    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, depth=None, client_id=None, params=None, callback=None))]
2768    #[expect(clippy::too_many_arguments)]
2769    fn py_request_book_depth(
2770        slf: &Bound<'_, Self>,
2771        py: Python<'_>,
2772        #[gen_stub(override_type(
2773            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2774        ))]
2775        instrument_id: &Bound<'_, PyAny>,
2776        start: Option<Timestamp>,
2777        end: Option<Timestamp>,
2778        limit: Option<usize>,
2779        depth: Option<usize>,
2780        client_id: Option<ClientId>,
2781        params: Option<Py<PyDict>>,
2782        #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
2783        callback: Option<Py<PyAny>>,
2784    ) -> PyResult<String> {
2785        let actor = Self {
2786            inner: Rc::clone(&slf.try_borrow()?.inner),
2787        };
2788
2789        let instrument_id = extract_instrument_ids(instrument_id)?;
2790        actor.ensure_registered_for_data()?;
2791        let params = dict_to_params(py, params)?;
2792        let limit = limit.and_then(NonZeroUsize::new);
2793        let depth = depth.and_then(NonZeroUsize::new);
2794        let handler = crate::python::response::request_handler(
2795            py,
2796            actor.inner().core().actor_id().inner(),
2797            PyDataActorInner::python_instance,
2798            callback,
2799        )?;
2800        let request_id = DataActorCore::request_book_depth(
2801            actor.inner().core(),
2802            instrument_id,
2803            start,
2804            end,
2805            limit,
2806            depth,
2807            client_id,
2808            params,
2809            handler,
2810        )
2811        .map_err(to_pyvalue_err)?;
2812        Ok(request_id.to_string())
2813    }
2814
2815    #[pyo3(name = "request_quotes")]
2816    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None, callback=None))]
2817    #[expect(clippy::too_many_arguments)]
2818    fn py_request_quotes(
2819        slf: &Bound<'_, Self>,
2820        py: Python<'_>,
2821        #[gen_stub(override_type(
2822            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2823        ))]
2824        instrument_id: &Bound<'_, PyAny>,
2825        start: Option<Timestamp>,
2826        end: Option<Timestamp>,
2827        limit: Option<usize>,
2828        client_id: Option<ClientId>,
2829        params: Option<Py<PyDict>>,
2830        #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
2831        callback: Option<Py<PyAny>>,
2832    ) -> PyResult<String> {
2833        let actor = Self {
2834            inner: Rc::clone(&slf.try_borrow()?.inner),
2835        };
2836
2837        let instrument_id = extract_instrument_ids(instrument_id)?;
2838        actor.ensure_registered_for_data()?;
2839        let params = dict_to_params(py, params)?;
2840        let limit = limit.and_then(NonZeroUsize::new);
2841        let handler = crate::python::response::request_handler(
2842            py,
2843            actor.inner().core().actor_id().inner(),
2844            PyDataActorInner::python_instance,
2845            callback,
2846        )?;
2847        let request_id = DataActorCore::request_quotes(
2848            actor.inner().core(),
2849            instrument_id,
2850            start,
2851            end,
2852            limit,
2853            client_id,
2854            params,
2855            handler,
2856        )
2857        .map_err(to_pyvalue_err)?;
2858        Ok(request_id.to_string())
2859    }
2860
2861    #[pyo3(name = "request_trades")]
2862    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None, callback=None))]
2863    #[expect(clippy::too_many_arguments)]
2864    fn py_request_trades(
2865        slf: &Bound<'_, Self>,
2866        py: Python<'_>,
2867        #[gen_stub(override_type(
2868            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2869        ))]
2870        instrument_id: &Bound<'_, PyAny>,
2871        start: Option<Timestamp>,
2872        end: Option<Timestamp>,
2873        limit: Option<usize>,
2874        client_id: Option<ClientId>,
2875        params: Option<Py<PyDict>>,
2876        #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
2877        callback: Option<Py<PyAny>>,
2878    ) -> PyResult<String> {
2879        let actor = Self {
2880            inner: Rc::clone(&slf.try_borrow()?.inner),
2881        };
2882
2883        let instrument_id = extract_instrument_ids(instrument_id)?;
2884        actor.ensure_registered_for_data()?;
2885        let params = dict_to_params(py, params)?;
2886        let limit = limit.and_then(NonZeroUsize::new);
2887        let handler = crate::python::response::request_handler(
2888            py,
2889            actor.inner().core().actor_id().inner(),
2890            PyDataActorInner::python_instance,
2891            callback,
2892        )?;
2893        let request_id = DataActorCore::request_trades(
2894            actor.inner().core(),
2895            instrument_id,
2896            start,
2897            end,
2898            limit,
2899            client_id,
2900            params,
2901            handler,
2902        )
2903        .map_err(to_pyvalue_err)?;
2904        Ok(request_id.to_string())
2905    }
2906
2907    #[pyo3(name = "request_funding_rates")]
2908    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None, client_id=None, params=None, callback=None))]
2909    #[expect(clippy::too_many_arguments)]
2910    fn py_request_funding_rates(
2911        slf: &Bound<'_, Self>,
2912        py: Python<'_>,
2913        #[gen_stub(override_type(
2914            type_repr = "model.InstrumentId | typing.Sequence[model.InstrumentId]"
2915        ))]
2916        instrument_id: &Bound<'_, PyAny>,
2917        start: Option<Timestamp>,
2918        end: Option<Timestamp>,
2919        limit: Option<usize>,
2920        client_id: Option<ClientId>,
2921        params: Option<Py<PyDict>>,
2922        #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
2923        callback: Option<Py<PyAny>>,
2924    ) -> PyResult<String> {
2925        let actor = Self {
2926            inner: Rc::clone(&slf.try_borrow()?.inner),
2927        };
2928
2929        let instrument_id = extract_instrument_ids(instrument_id)?;
2930        actor.ensure_registered_for_data()?;
2931        let params = dict_to_params(py, params)?;
2932        let limit = limit.and_then(NonZeroUsize::new);
2933        let handler = crate::python::response::request_handler(
2934            py,
2935            actor.inner().core().actor_id().inner(),
2936            PyDataActorInner::python_instance,
2937            callback,
2938        )?;
2939        let request_id = DataActorCore::request_funding_rates(
2940            actor.inner().core(),
2941            instrument_id,
2942            start,
2943            end,
2944            limit,
2945            client_id,
2946            params,
2947            handler,
2948        )
2949        .map_err(to_pyvalue_err)?;
2950        Ok(request_id.to_string())
2951    }
2952
2953    #[pyo3(name = "request_bars")]
2954    #[pyo3(signature = (bar_type, start=None, end=None, limit=None, client_id=None, params=None, callback=None))]
2955    #[expect(clippy::too_many_arguments)]
2956    fn py_request_bars(
2957        slf: &Bound<'_, Self>,
2958        py: Python<'_>,
2959        #[gen_stub(override_type(type_repr = "model.BarType | typing.Sequence[model.BarType]"))]
2960        bar_type: &Bound<'_, PyAny>,
2961        start: Option<Timestamp>,
2962        end: Option<Timestamp>,
2963        limit: Option<usize>,
2964        client_id: Option<ClientId>,
2965        params: Option<Py<PyDict>>,
2966        #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
2967        callback: Option<Py<PyAny>>,
2968    ) -> PyResult<String> {
2969        let actor = Self {
2970            inner: Rc::clone(&slf.try_borrow()?.inner),
2971        };
2972
2973        let bar_type = extract_bar_types(bar_type)?;
2974        actor.ensure_registered_for_data()?;
2975        let params = dict_to_params(py, params)?;
2976        let limit = limit.and_then(NonZeroUsize::new);
2977        let handler = crate::python::response::request_handler(
2978            py,
2979            actor.inner().core().actor_id().inner(),
2980            PyDataActorInner::python_instance,
2981            callback,
2982        )?;
2983        let request_id = DataActorCore::request_bars(
2984            actor.inner().core(),
2985            bar_type,
2986            start,
2987            end,
2988            limit,
2989            client_id,
2990            params,
2991            handler,
2992        )
2993        .map_err(to_pyvalue_err)?;
2994        Ok(request_id.to_string())
2995    }
2996
2997    #[pyo3(name = "request_join", signature = (request_ids, callback=None, request_id=None))]
2998    fn py_request_join(
2999        slf: &Bound<'_, Self>,
3000        py: Python<'_>,
3001        #[gen_stub(override_type(type_repr = "typing.Sequence[str | core.UUID4]", imports = ("typing", "nautilus_trader.core")))]
3002        request_ids: Vec<Bound<'_, PyAny>>,
3003        #[gen_stub(override_type(type_repr = "typing.Callable[[str], None] | None"))]
3004        callback: Option<Py<PyAny>>,
3005        #[gen_stub(override_type(type_repr = "str | core.UUID4 | None", imports = ("nautilus_trader.core",)))]
3006        request_id: Option<&Bound<'_, PyAny>>,
3007    ) -> PyResult<String> {
3008        let actor_id = {
3009            let actor = slf.try_borrow()?;
3010            actor.ensure_registered_for_data()?;
3011            actor.inner().core().actor_id().inner()
3012        };
3013
3014        let request_ids = request_ids
3015            .into_iter()
3016            .map(|id| crate::python::response::extract_request_id(&id))
3017            .collect::<PyResult<Vec<_>>>()?;
3018        let request_id = request_id
3019            .map(crate::python::response::extract_request_id)
3020            .transpose()?;
3021        crate::python::response::request_join::<PyDataActorInner>(
3022            py,
3023            actor_id,
3024            PyDataActorInner::python_instance,
3025            request_ids,
3026            request_id,
3027            callback,
3028        )
3029    }
3030
3031    #[pyo3(name = "is_pending_request")]
3032    fn py_is_pending_request(
3033        &self,
3034        #[gen_stub(override_type(type_repr = "str | core.UUID4", imports = ("nautilus_trader.core",)))]
3035        request_id: &Bound<'_, PyAny>,
3036    ) -> PyResult<bool> {
3037        let id = crate::python::response::extract_request_id(request_id)?;
3038        Ok(self.inner().core().is_pending_request(&id))
3039    }
3040
3041    #[pyo3(name = "has_pending_requests")]
3042    fn py_has_pending_requests(&self) -> bool {
3043        !self.inner().core().pending_requests().is_empty()
3044    }
3045
3046    #[pyo3(name = "pending_requests")]
3047    #[gen_stub(override_return_type(type_repr = "set[str]"))]
3048    fn py_pending_requests(&self, py: Python<'_>) -> PyResult<Py<PySet>> {
3049        let ids = self.inner().core().pending_requests();
3050        PySet::new(py, ids.iter().map(ToString::to_string)).map(Bound::unbind)
3051    }
3052
3053    /// Requests reconnect of one socket endpoint owned by `client_id`.
3054    #[pyo3(name = "reconnect_socket")]
3055    fn py_reconnect_socket(&self, client_id: ClientId, endpoint: &str) -> PyResult<()> {
3056        DataActor::reconnect_socket(self.inner(), client_id, endpoint).map_err(to_pyruntime_err)
3057    }
3058
3059    #[allow(unused_variables, clippy::needless_pass_by_value)]
3060    #[pyo3(name = "on_data_response")]
3061    fn py_on_data_response(_slf: &Bound<'_, Self>, response: Py<PyDataResponse>) {}
3062
3063    #[allow(unused_variables, clippy::needless_pass_by_value)]
3064    #[pyo3(name = "on_historical_data")]
3065    fn py_on_historical_data(_slf: &Bound<'_, Self>, data: Py<PyAny>) {
3066        // Default implementation - can be overridden in Python subclasses
3067    }
3068
3069    #[allow(unused_variables, clippy::needless_pass_by_value)]
3070    #[pyo3(name = "on_historical_book_deltas")]
3071    fn py_on_historical_book_deltas(_slf: &Bound<'_, Self>, deltas: Vec<OrderBookDelta>) {}
3072
3073    #[allow(unused_variables, clippy::needless_pass_by_value)]
3074    #[pyo3(name = "on_historical_book_depth")]
3075    fn py_on_historical_book_depth(_slf: &Bound<'_, Self>, depths: Vec<OrderBookDepth>) {}
3076
3077    #[allow(unused_variables, clippy::needless_pass_by_value)]
3078    #[pyo3(name = "on_historical_quotes")]
3079    fn py_on_historical_quotes(_slf: &Bound<'_, Self>, quotes: Vec<QuoteTick>) {
3080        // Default implementation - can be overridden in Python subclasses
3081    }
3082
3083    #[allow(unused_variables, clippy::needless_pass_by_value)]
3084    #[pyo3(name = "on_historical_trades")]
3085    fn py_on_historical_trades(_slf: &Bound<'_, Self>, trades: Vec<TradeTick>) {
3086        // Default implementation - can be overridden in Python subclasses
3087    }
3088
3089    #[allow(unused_variables, clippy::needless_pass_by_value)]
3090    #[pyo3(name = "on_historical_funding_rates")]
3091    fn py_on_historical_funding_rates(
3092        _slf: &Bound<'_, Self>,
3093        funding_rates: Vec<FundingRateUpdate>,
3094    ) {
3095        // Default implementation - can be overridden in Python subclasses
3096    }
3097
3098    #[allow(unused_variables, clippy::needless_pass_by_value)]
3099    #[pyo3(name = "on_historical_bars")]
3100    fn py_on_historical_bars(_slf: &Bound<'_, Self>, bars: Vec<Bar>) {
3101        // Default implementation - can be overridden in Python subclasses
3102    }
3103
3104    #[allow(unused_variables, clippy::needless_pass_by_value)]
3105    #[pyo3(name = "on_historical_mark_prices")]
3106    fn py_on_historical_mark_prices(_slf: &Bound<'_, Self>, mark_prices: Vec<MarkPriceUpdate>) {
3107        // Default implementation - can be overridden in Python subclasses
3108    }
3109
3110    #[allow(unused_variables, clippy::needless_pass_by_value)]
3111    #[pyo3(name = "on_historical_index_prices")]
3112    fn py_on_historical_index_prices(_slf: &Bound<'_, Self>, index_prices: Vec<IndexPriceUpdate>) {
3113        // Default implementation - can be overridden in Python subclasses
3114    }
3115}
3116
3117#[pyo3_stub_gen::derive::gen_stub_pymethods]
3118#[pyo3::pymethods]
3119impl PyDataActor {
3120    #[pyo3(name = "publish_message", signature = (topic, message))]
3121    fn py_publish_message(
3122        slf: &Bound<'_, Self>,
3123        topic: &str,
3124        #[gen_stub(override_type(type_repr = "object"))] message: Py<PyAny>,
3125    ) -> PyResult<()> {
3126        let messages = get_python_message_bus(slf.as_any())?;
3127        messages.publish_message(topic, message)
3128    }
3129
3130    #[pyo3(name = "subscribe_topic")]
3131    #[pyo3(signature = (topic, handler, priority=0))]
3132    fn py_subscribe_topic(
3133        slf: &Bound<'_, Self>,
3134        topic: &str,
3135        #[gen_stub(override_type(type_repr = "collections.abc.Callable[[object], None]", imports = ("collections.abc",)))]
3136        handler: Py<PyAny>,
3137        priority: u32,
3138    ) -> PyResult<()> {
3139        let messages = get_python_message_bus(slf.as_any())?;
3140        messages.subscribe_topic(slf.py(), topic, handler, priority)
3141    }
3142
3143    #[pyo3(name = "unsubscribe_topic", signature = (topic, handler))]
3144    fn py_unsubscribe_topic(
3145        slf: &Bound<'_, Self>,
3146        topic: &str,
3147        #[gen_stub(override_type(type_repr = "collections.abc.Callable[[object], None]", imports = ("collections.abc",)))]
3148        handler: &Bound<'_, PyAny>,
3149    ) -> PyResult<()> {
3150        let messages = get_python_message_bus(slf.as_any())?;
3151        messages.unsubscribe_topic(topic, handler)
3152    }
3153}
3154
3155#[cfg(feature = "defi")]
3156#[pymethods]
3157#[pyo3_stub_gen::derive::gen_stub_pymethods]
3158impl PyDataActor {
3159    #[pyo3(name = "on_block")]
3160    #[allow(unused_variables, clippy::needless_pass_by_value)]
3161    fn py_on_block(_slf: &Bound<'_, Self>, block: Block) {}
3162
3163    #[pyo3(name = "on_pool")]
3164    #[allow(unused_variables, clippy::needless_pass_by_value)]
3165    fn py_on_pool(_slf: &Bound<'_, Self>, pool: Pool) {}
3166
3167    #[pyo3(name = "on_pool_swap")]
3168    #[allow(unused_variables, clippy::needless_pass_by_value)]
3169    fn py_on_pool_swap(_slf: &Bound<'_, Self>, swap: PoolSwap) {}
3170
3171    #[pyo3(name = "on_pool_liquidity_update")]
3172    #[allow(unused_variables, clippy::needless_pass_by_value)]
3173    fn py_on_pool_liquidity_update(_slf: &Bound<'_, Self>, update: PoolLiquidityUpdate) {}
3174
3175    #[pyo3(name = "on_pool_fee_collect")]
3176    #[allow(unused_variables, clippy::needless_pass_by_value)]
3177    fn py_on_pool_fee_collect(_slf: &Bound<'_, Self>, update: PoolFeeCollect) {}
3178
3179    #[pyo3(name = "on_pool_flash")]
3180    #[allow(unused_variables, clippy::needless_pass_by_value)]
3181    fn py_on_pool_flash(_slf: &Bound<'_, Self>, flash: PoolFlash) {}
3182
3183    #[pyo3(name = "subscribe_blocks")]
3184    #[pyo3(signature = (chain, client_id=None, params=None))]
3185    fn py_subscribe_blocks(
3186        &mut self,
3187        py: Python<'_>,
3188        chain: Blockchain,
3189        client_id: Option<ClientId>,
3190        params: Option<Py<PyDict>>,
3191    ) -> PyResult<()> {
3192        self.ensure_registered()?;
3193        let params = dict_to_params(py, params)?;
3194        DataActor::subscribe_blocks(self.inner_mut(), chain, client_id, params);
3195        Ok(())
3196    }
3197
3198    #[pyo3(name = "subscribe_pool")]
3199    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3200    fn py_subscribe_pool(
3201        &mut self,
3202        py: Python<'_>,
3203        instrument_id: InstrumentId,
3204        client_id: Option<ClientId>,
3205        params: Option<Py<PyDict>>,
3206    ) -> PyResult<()> {
3207        self.ensure_registered()?;
3208        let params = dict_to_params(py, params)?;
3209        DataActor::subscribe_pool(self.inner_mut(), instrument_id, client_id, params);
3210        Ok(())
3211    }
3212
3213    #[pyo3(name = "subscribe_pool_swaps")]
3214    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3215    fn py_subscribe_pool_swaps(
3216        &mut self,
3217        py: Python<'_>,
3218        instrument_id: InstrumentId,
3219        client_id: Option<ClientId>,
3220        params: Option<Py<PyDict>>,
3221    ) -> PyResult<()> {
3222        self.ensure_registered()?;
3223        let params = dict_to_params(py, params)?;
3224        DataActor::subscribe_pool_swaps(self.inner_mut(), instrument_id, client_id, params);
3225        Ok(())
3226    }
3227
3228    #[pyo3(name = "subscribe_pool_liquidity_updates")]
3229    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3230    fn py_subscribe_pool_liquidity_updates(
3231        &mut self,
3232        py: Python<'_>,
3233        instrument_id: InstrumentId,
3234        client_id: Option<ClientId>,
3235        params: Option<Py<PyDict>>,
3236    ) -> PyResult<()> {
3237        self.ensure_registered()?;
3238        let params = dict_to_params(py, params)?;
3239        DataActor::subscribe_pool_liquidity_updates(
3240            self.inner_mut(),
3241            instrument_id,
3242            client_id,
3243            params,
3244        );
3245        Ok(())
3246    }
3247
3248    #[pyo3(name = "subscribe_pool_fee_collects")]
3249    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3250    fn py_subscribe_pool_fee_collects(
3251        &mut self,
3252        py: Python<'_>,
3253        instrument_id: InstrumentId,
3254        client_id: Option<ClientId>,
3255        params: Option<Py<PyDict>>,
3256    ) -> PyResult<()> {
3257        self.ensure_registered()?;
3258        let params = dict_to_params(py, params)?;
3259        DataActor::subscribe_pool_fee_collects(self.inner_mut(), instrument_id, client_id, params);
3260        Ok(())
3261    }
3262
3263    #[pyo3(name = "subscribe_pool_flash_events")]
3264    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3265    fn py_subscribe_pool_flash_events(
3266        &mut self,
3267        py: Python<'_>,
3268        instrument_id: InstrumentId,
3269        client_id: Option<ClientId>,
3270        params: Option<Py<PyDict>>,
3271    ) -> PyResult<()> {
3272        self.ensure_registered()?;
3273        let params = dict_to_params(py, params)?;
3274        DataActor::subscribe_pool_flash_events(self.inner_mut(), instrument_id, client_id, params);
3275        Ok(())
3276    }
3277
3278    #[pyo3(name = "unsubscribe_blocks")]
3279    #[pyo3(signature = (chain, client_id=None, params=None))]
3280    fn py_unsubscribe_blocks(
3281        &mut self,
3282        py: Python<'_>,
3283        chain: Blockchain,
3284        client_id: Option<ClientId>,
3285        params: Option<Py<PyDict>>,
3286    ) -> PyResult<()> {
3287        self.ensure_registered()?;
3288        let params = dict_to_params(py, params)?;
3289        DataActor::unsubscribe_blocks(self.inner_mut(), chain, client_id, params);
3290        Ok(())
3291    }
3292
3293    #[pyo3(name = "unsubscribe_pool")]
3294    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3295    fn py_unsubscribe_pool(
3296        &mut self,
3297        py: Python<'_>,
3298        instrument_id: InstrumentId,
3299        client_id: Option<ClientId>,
3300        params: Option<Py<PyDict>>,
3301    ) -> PyResult<()> {
3302        self.ensure_registered()?;
3303        let params = dict_to_params(py, params)?;
3304        DataActor::unsubscribe_pool(self.inner_mut(), instrument_id, client_id, params);
3305        Ok(())
3306    }
3307
3308    #[pyo3(name = "unsubscribe_pool_swaps")]
3309    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3310    fn py_unsubscribe_pool_swaps(
3311        &mut self,
3312        py: Python<'_>,
3313        instrument_id: InstrumentId,
3314        client_id: Option<ClientId>,
3315        params: Option<Py<PyDict>>,
3316    ) -> PyResult<()> {
3317        self.ensure_registered()?;
3318        let params = dict_to_params(py, params)?;
3319        DataActor::unsubscribe_pool_swaps(self.inner_mut(), instrument_id, client_id, params);
3320        Ok(())
3321    }
3322
3323    #[pyo3(name = "unsubscribe_pool_liquidity_updates")]
3324    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3325    fn py_unsubscribe_pool_liquidity_updates(
3326        &mut self,
3327        py: Python<'_>,
3328        instrument_id: InstrumentId,
3329        client_id: Option<ClientId>,
3330        params: Option<Py<PyDict>>,
3331    ) -> PyResult<()> {
3332        self.ensure_registered()?;
3333        let params = dict_to_params(py, params)?;
3334        DataActor::unsubscribe_pool_liquidity_updates(
3335            self.inner_mut(),
3336            instrument_id,
3337            client_id,
3338            params,
3339        );
3340        Ok(())
3341    }
3342
3343    #[pyo3(name = "unsubscribe_pool_fee_collects")]
3344    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3345    fn py_unsubscribe_pool_fee_collects(
3346        &mut self,
3347        py: Python<'_>,
3348        instrument_id: InstrumentId,
3349        client_id: Option<ClientId>,
3350        params: Option<Py<PyDict>>,
3351    ) -> PyResult<()> {
3352        self.ensure_registered()?;
3353        let params = dict_to_params(py, params)?;
3354        DataActor::unsubscribe_pool_fee_collects(
3355            self.inner_mut(),
3356            instrument_id,
3357            client_id,
3358            params,
3359        );
3360        Ok(())
3361    }
3362
3363    #[pyo3(name = "unsubscribe_pool_flash_events")]
3364    #[pyo3(signature = (instrument_id, client_id=None, params=None))]
3365    fn py_unsubscribe_pool_flash_events(
3366        &mut self,
3367        py: Python<'_>,
3368        instrument_id: InstrumentId,
3369        client_id: Option<ClientId>,
3370        params: Option<Py<PyDict>>,
3371    ) -> PyResult<()> {
3372        self.ensure_registered()?;
3373        let params = dict_to_params(py, params)?;
3374        DataActor::unsubscribe_pool_flash_events(
3375            self.inner_mut(),
3376            instrument_id,
3377            client_id,
3378            params,
3379        );
3380        Ok(())
3381    }
3382}
3383
3384impl PyDataActor {
3385    fn ensure_registered_for_data(&self) -> PyResult<()> {
3386        if self.inner().core.is_registered() {
3387            Ok(())
3388        } else {
3389            Err(to_pyruntime_err(
3390                "DataActor must be registered before publishing, managing synthetics, or requesting data",
3391            ))
3392        }
3393    }
3394
3395    fn ensure_registered(&self) -> PyResult<()> {
3396        if self.inner().core.is_registered() {
3397            Ok(())
3398        } else {
3399            Err(to_pyruntime_err(
3400                "DataActor must be registered before managing subscriptions",
3401            ))
3402        }
3403    }
3404}
3405
3406/// Prepares a Python actor for registration and returns its final actor ID.
3407///
3408/// Applies optional config overrides and stores a weak reference to the Python instance used for
3409/// method dispatch. Missing or unreadable attributes are ignored, and non-boolean `log_events` or
3410/// `log_commands` values leave the existing settings unchanged. When neither the config nor
3411/// `DataActor.__init__` supplies an ID, the runtime class name replaces the shared default so
3412/// subclasses that skip the base initializer still receive distinct IDs.
3413///
3414/// # Errors
3415///
3416/// Returns an error if the actor cannot be extracted, its configured `actor_id` is neither an
3417/// [`ActorId`] nor a valid ID string, its class-derived ID is invalid, or its Python instance
3418/// cannot be weakly referenced.
3419pub fn prepare_python_actor(
3420    actor_obj: &Bound<'_, PyAny>,
3421    config: Option<&Bound<'_, PyAny>>,
3422) -> anyhow::Result<ActorId> {
3423    let mut actor = actor_obj
3424        .extract::<PyRefMut<PyDataActor>>()
3425        .map_err(Into::<PyErr>::into)
3426        .map_err(|e| anyhow::anyhow!("Failed to extract PyDataActor: {e}"))?;
3427
3428    if let Some(config) = config {
3429        if let Some(actor_id) = config
3430            .getattr("actor_id")
3431            .ok()
3432            .filter(|actor_id| !actor_id.is_none())
3433        {
3434            let actor_id = if let Ok(actor_id) = actor_id.extract::<ActorId>() {
3435                actor_id
3436            } else if let Ok(actor_id) = actor_id.extract::<String>() {
3437                ActorId::new_checked(&actor_id)?
3438            } else {
3439                anyhow::bail!("Invalid `actor_id` type");
3440            };
3441            actor.set_actor_id(actor_id);
3442        }
3443
3444        if let Some(log_events) = extract_bool_config_attr(config, "log_events") {
3445            actor.set_log_events(log_events);
3446        }
3447
3448        if let Some(log_commands) = extract_bool_config_attr(config, "log_commands") {
3449            actor.set_log_commands(log_commands);
3450        }
3451    }
3452
3453    actor.set_python_instance(actor_obj)?;
3454    apply_class_derived_actor_id(&mut actor, actor_obj)?;
3455    Ok(actor.actor_id())
3456}
3457
3458fn apply_class_derived_actor_id(
3459    actor: &mut PyRefMut<'_, PyDataActor>,
3460    actor_obj: &Bound<'_, PyAny>,
3461) -> PyResult<()> {
3462    if actor.inner().core.config.actor_id.is_some() {
3463        return Ok(());
3464    }
3465
3466    let py_type = actor_obj.get_type();
3467    let type_name = py_type.name()?;
3468    let actor_id = ActorId::new_checked(type_name.to_str()?).map_err(to_pyvalue_err)?;
3469    actor.set_actor_id(actor_id);
3470
3471    Ok(())
3472}
3473
3474fn extract_bool_config_attr(config: &Bound<'_, PyAny>, attr: &str) -> Option<bool> {
3475    config
3476        .getattr(attr)
3477        .ok()
3478        .and_then(|value| value.extract::<bool>().ok())
3479}
3480
3481fn borrow_actor_mut<'py>(
3482    actor: &Bound<'py, PyDataActor>,
3483    operation: &'static str,
3484) -> PyResult<PyRefMut<'py, PyDataActor>> {
3485    actor.try_borrow_mut().map_err(|_| {
3486        to_pyruntime_err(ComponentAccessError::WriteConflict {
3487            resource: "Python actor",
3488            operation,
3489        })
3490    })
3491}
3492
3493/// Returns whether the config retained by the actor supplies an actor ID.
3494///
3495/// The config is read through Python rather than the extracted [`DataActorConfig`] so that a
3496/// custom subclass config which cannot be extracted still counts as configuring an ID. The actor
3497/// borrow is released before the attribute lookup, which can run user code.
3498fn has_configured_actor_id(slf: &Bound<'_, PyDataActor>) -> bool {
3499    let py = slf.py();
3500    let config = slf
3501        .borrow()
3502        .inner()
3503        .config
3504        .as_ref()
3505        .map(|config| config.clone_ref(py));
3506
3507    config.is_some_and(|config| {
3508        config
3509            .bind(py)
3510            .getattr("actor_id")
3511            .is_ok_and(|actor_id| !actor_id.is_none())
3512    })
3513}
3514
3515/// Extracts one instrument ID or a sequence into a normalized, non-empty list.
3516///
3517/// Duplicate IDs are removed in first-occurrence order.
3518///
3519/// # Errors
3520///
3521/// Returns an error if:
3522/// - The value is not an instrument ID or a sequence of instrument IDs.
3523/// - The sequence is empty.
3524pub fn extract_instrument_ids(value: &Bound<'_, PyAny>) -> PyResult<Vec<InstrumentId>> {
3525    let instrument_ids = if let Ok(instrument_id) = value.extract::<InstrumentId>() {
3526        vec![instrument_id]
3527    } else {
3528        value.extract::<Vec<InstrumentId>>().map_err(|_| {
3529            to_pytype_err("expected InstrumentId or a sequence of InstrumentId values")
3530        })?
3531    };
3532
3533    instrument_ids.into_instrument_ids().map_err(to_pyvalue_err)
3534}
3535
3536/// Extracts one bar type or a sequence into a normalized, non-empty list.
3537///
3538/// Duplicate bar types are removed in first-occurrence order.
3539///
3540/// # Errors
3541///
3542/// Returns an error if the sequence is empty or the value is not a bar type or sequence of bar types.
3543pub fn extract_bar_types(value: &Bound<'_, PyAny>) -> PyResult<Vec<BarType>> {
3544    let bar_types = if let Ok(bar_type) = value.extract::<BarType>() {
3545        vec![bar_type]
3546    } else {
3547        value
3548            .extract::<Vec<BarType>>()
3549            .map_err(|_| to_pytype_err("expected BarType or a sequence of BarType values"))?
3550    };
3551
3552    bar_types.into_bar_types().map_err(to_pyvalue_err)
3553}
3554
3555#[cfg(test)]
3556mod tests {
3557    use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr, sync::Arc};
3558
3559    #[cfg(feature = "defi")]
3560    use alloy_primitives::{I256, U160, U256};
3561    use indexmap::IndexMap;
3562    use nautilus_core::{UUID4, UnixNanos, python::IntoPyObjectNautilusExt};
3563    #[cfg(feature = "defi")]
3564    use nautilus_model::defi::{
3565        AmmType, Block, Blockchain, Chain, Dex, DexType, Pool, PoolFeeCollect, PoolFlash,
3566        PoolIdentifier, PoolLiquidityUpdate, PoolLiquidityUpdateType, PoolSwap, Token,
3567    };
3568    use nautilus_model::{
3569        data::{
3570            Bar, BarType, CustomData, DataType, FundingRateUpdate, IndexPriceUpdate,
3571            InstrumentStatus, MarkPriceUpdate, OrderBookDelta, OrderBookDeltas, OrderBookDepth,
3572            QuoteTick, TradeTick,
3573            close::InstrumentClose,
3574            greeks::OptionGreekValues,
3575            option_chain::{OptionChainSlice, OptionGreeks},
3576            stubs::{stub_custom_data, stub_deltas, stub_depth10},
3577        },
3578        enums::{
3579            AggressorSide, BookType, GreeksConvention, InstrumentCloseType, MarketStatusAction,
3580        },
3581        identifiers::{
3582            ActorId, ClientId, ComponentId, InstrumentId, OptionSeriesId, TradeId, TraderId, Venue,
3583        },
3584        instruments::{CurrencyPair, InstrumentAny, stubs::audusd_sim},
3585        orderbook::OrderBook,
3586        types::{Price, Quantity},
3587    };
3588    use pyo3::{
3589        Bound, IntoPyObjectExt, Py, PyAny, PyRef, PyResult, Python,
3590        ffi::c_str,
3591        types::{
3592            PyAnyMethods, PyBytes, PyDict, PyList, PyModule, PyTuple, PyWeakrefMethods,
3593            PyWeakrefReference,
3594        },
3595    };
3596    use rstest::{fixture, rstest};
3597    use ustr::Ustr;
3598
3599    use super::{PyDataActor, prepare_python_actor};
3600    use crate::{
3601        actor::{DataActor, data_actor::DataActorConfig, registry::actor_exists},
3602        cache::Cache,
3603        clock::VirtualClock,
3604        component::{Component, get_component},
3605        enums::ComponentState,
3606        live::runner::replace_system_command_sender,
3607        messages::{
3608            SystemCommand,
3609            data::{
3610                BarsResponse, CustomDataResponse, DataCommand, QuotesResponse, RequestCommand,
3611                TradesResponse,
3612            },
3613            system::{
3614                QueueCondition, QueueState, QueueStateChanged, SocketState, SocketStateChanged,
3615            },
3616        },
3617        msgbus::{
3618            self, MessageBus, MessagingSwitchboard, get_message_bus,
3619            stubs::get_typed_into_message_saving_handler,
3620            switchboard::{get_quotes_topic, get_trades_topic},
3621        },
3622        python::wrappers::get_python_wrapper,
3623        runner::{SyncDataCommandSender, SystemChannel, set_data_cmd_sender},
3624        signal::Signal,
3625        timer::TimeEvent,
3626    };
3627
3628    #[fixture]
3629    fn clock() -> Rc<RefCell<VirtualClock>> {
3630        Rc::new(RefCell::new(VirtualClock::new()))
3631    }
3632
3633    #[fixture]
3634    fn cache() -> Rc<RefCell<Cache>> {
3635        Rc::new(RefCell::new(Cache::new(None, None)))
3636    }
3637
3638    #[fixture]
3639    fn trader_id() -> TraderId {
3640        TraderId::from("TRADER-001")
3641    }
3642
3643    #[fixture]
3644    fn client_id() -> ClientId {
3645        ClientId::new("TestClient")
3646    }
3647
3648    #[fixture]
3649    fn venue() -> Venue {
3650        Venue::from("SIM")
3651    }
3652
3653    #[fixture]
3654    fn data_type() -> DataType {
3655        DataType::new("TestData", None, None)
3656    }
3657
3658    #[fixture]
3659    fn bar_type(audusd_sim: CurrencyPair) -> BarType {
3660        BarType::from_str(&format!("{}-1-MINUTE-LAST-INTERNAL", audusd_sim.id)).unwrap()
3661    }
3662
3663    fn create_unregistered_actor() -> PyDataActor {
3664        PyDataActor::new(None)
3665    }
3666
3667    fn create_registered_actor(
3668        clock: Rc<RefCell<VirtualClock>>,
3669        cache: Rc<RefCell<Cache>>,
3670        trader_id: TraderId,
3671    ) -> PyDataActor {
3672        // Set up sync data command sender for tests
3673        let sender = SyncDataCommandSender;
3674        set_data_cmd_sender(Arc::new(sender));
3675
3676        let mut actor = PyDataActor::new(None);
3677        actor.register(trader_id, clock, cache).unwrap();
3678        actor
3679    }
3680
3681    #[rstest]
3682    fn test_python_identifier_lists_require_registration_before_validation(
3683        #[values(
3684            "subscribe_quotes",
3685            "unsubscribe_quotes",
3686            "subscribe_trades",
3687            "unsubscribe_trades"
3688        )]
3689        method: &str,
3690        #[values("empty", "invalid", "mixed_venues")] input: &str,
3691    ) {
3692        Python::initialize();
3693        Python::attach(|py| {
3694            let actor = create_unregistered_actor();
3695            let actor = Py::new(py, actor).unwrap();
3696            let argument = match input {
3697                "empty" => PyList::empty(py).into_any(),
3698                "invalid" => PyList::new(py, [42]).unwrap().into_any(),
3699                "mixed_venues" => PyList::new(
3700                    py,
3701                    [
3702                        InstrumentId::from("BTC/USD.SIM"),
3703                        InstrumentId::from("ETH/USD.OTHER"),
3704                    ],
3705                )
3706                .unwrap()
3707                .into_any(),
3708                _ => unreachable!(),
3709            };
3710
3711            let error = actor
3712                .bind(py)
3713                .call_method1(method, (&argument,))
3714                .unwrap_err();
3715
3716            assert!(error.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
3717            assert_eq!(
3718                error.value(py).to_string(),
3719                "DataActor must be registered before managing subscriptions"
3720            );
3721        });
3722    }
3723
3724    #[rstest]
3725    fn test_python_identifier_list_subscriptions(
3726        #[values(
3727            "instrument",
3728            "book_deltas",
3729            "book_depth",
3730            "book_at_interval",
3731            "quotes",
3732            "trades",
3733            "mark_prices",
3734            "index_prices",
3735            "funding_rates",
3736            "instrument_status",
3737            "instrument_close",
3738            "option_greeks"
3739        )]
3740        family: &str,
3741        #[values("single", "list", "tuple")] input: &str,
3742        #[values(false, true)] keyword: bool,
3743        clock: Rc<RefCell<VirtualClock>>,
3744        cache: Rc<RefCell<Cache>>,
3745        trader_id: TraderId,
3746    ) {
3747        let expected_variant = match family {
3748            "instrument" => "Instrument",
3749            "book_deltas" => "BookDeltas",
3750            "book_depth" => "BookDepth",
3751            "book_at_interval" => "BookSnapshots",
3752            "quotes" => "Quotes",
3753            "trades" => "Trades",
3754            "mark_prices" => "MarkPrices",
3755            "index_prices" => "IndexPrices",
3756            "funding_rates" => "FundingRates",
3757            "instrument_status" => "InstrumentStatus",
3758            "instrument_close" => "InstrumentClose",
3759            "option_greeks" => "OptionGreeks",
3760            _ => unreachable!(),
3761        };
3762
3763        let first = InstrumentId::from("BTC/USD.SIM");
3764        let second = InstrumentId::from("ETH/USD.SIM");
3765
3766        Python::initialize();
3767        Python::attach(|py| {
3768            *get_message_bus().borrow_mut() = MessageBus::default();
3769            let actor = create_registered_actor(clock, cache, trader_id);
3770            let actor = Py::new(py, actor).unwrap();
3771            let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
3772            msgbus::register_data_command_endpoint(
3773                MessagingSwitchboard::data_engine_queue_execute(),
3774                handler,
3775            );
3776
3777            let (argument, expected_ids) = match input {
3778                "single" => (first.into_py_any(py).unwrap(), vec![first]),
3779                "list" => (
3780                    PyList::new(py, [second, first, second])
3781                        .unwrap()
3782                        .into_any()
3783                        .unbind(),
3784                    vec![second, first],
3785                ),
3786                "tuple" => (
3787                    PyTuple::new(py, [second, first, second])
3788                        .unwrap()
3789                        .into_any()
3790                        .unbind(),
3791                    vec![second, first],
3792                ),
3793                _ => unreachable!(),
3794            };
3795
3796            let args = PyTuple::new(py, (!keyword).then_some(argument.bind(py))).unwrap();
3797            for action in ["subscribe", "unsubscribe"] {
3798                let kwargs = instrument_subscription_keywords(py, family, action == "subscribe");
3799                if keyword {
3800                    kwargs.set_item("instrument_id", argument.bind(py)).unwrap();
3801                }
3802
3803                let method = format!("{action}_{family}");
3804                actor
3805                    .bind(py)
3806                    .call_method(method.as_str(), &args, Some(&kwargs))
3807                    .unwrap();
3808            }
3809
3810            let commands = saver.get_messages();
3811
3812            assert_eq!(commands.len(), 2);
3813
3814            for (index, command) in commands.iter().enumerate() {
3815                let (subscribe, actual_ids, serialized) = match command {
3816                    DataCommand::Subscribe(command) => (
3817                        true,
3818                        command.instrument_ids().unwrap(),
3819                        serde_json::to_value(command).unwrap(),
3820                    ),
3821                    DataCommand::Unsubscribe(command) => (
3822                        false,
3823                        command.instrument_ids().unwrap(),
3824                        serde_json::to_value(command).unwrap(),
3825                    ),
3826                    _ => panic!("unexpected command"),
3827                };
3828
3829                assert_eq!(
3830                    serialized.as_object().unwrap().keys().collect::<Vec<_>>(),
3831                    vec![expected_variant]
3832                );
3833
3834                assert_eq!(subscribe, index == 0);
3835
3836                assert_eq!(actual_ids, expected_ids);
3837            }
3838        });
3839    }
3840
3841    #[rstest::rstest]
3842    fn test_python_subscription_lists_reject_invalid_routes_before_effects(
3843        #[values(
3844            "subscribe_quotes",
3845            "subscribe_instrument",
3846            "subscribe_book_deltas",
3847            "subscribe_book_depth",
3848            "subscribe_book_at_interval",
3849            "subscribe_option_greeks",
3850            "subscribe_instrument_status",
3851            "subscribe_instrument_close"
3852        )]
3853        method: &str,
3854        #[values(false, true)] external: bool,
3855        clock: Rc<RefCell<VirtualClock>>,
3856        cache: Rc<RefCell<Cache>>,
3857        trader_id: TraderId,
3858    ) {
3859        Python::initialize();
3860        Python::attach(|py| {
3861            let client_id = ClientId::from("ROUTED");
3862            if external {
3863                cache.borrow_mut().add_external_data_client(client_id);
3864            }
3865
3866            let actor = Py::new(py, create_registered_actor(clock, cache, trader_id)).unwrap();
3867            let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
3868            msgbus::register_data_command_endpoint(
3869                MessagingSwitchboard::data_engine_queue_execute(),
3870                handler,
3871            );
3872            let ids = PyList::new(
3873                py,
3874                [
3875                    InstrumentId::from("AUD/USD.SIM"),
3876                    InstrumentId::from("SYNTH.SYNTH"),
3877                ],
3878            )
3879            .unwrap();
3880            let kwargs = PyDict::new(py);
3881            kwargs.set_item("client_id", client_id).unwrap();
3882            if method.starts_with("subscribe_book") {
3883                kwargs.set_item("book_type", BookType::L2_MBP).unwrap();
3884            }
3885
3886            if method == "subscribe_book_at_interval" {
3887                kwargs.set_item("interval_ms", 13).unwrap();
3888            }
3889
3890            let result = actor.bind(py).call_method(method, (&ids,), Some(&kwargs));
3891            if external {
3892                result.unwrap();
3893            } else {
3894                let error = result.unwrap_err();
3895                assert!(error.is_instance_of::<pyo3::exceptions::PyValueError>(py));
3896
3897                let expected = if method == "subscribe_quotes" {
3898                    "quote identifier lists cannot mix client, synthetic, and spread routes"
3899                } else {
3900                    "Cannot subscribe synthetic instruments for this data type"
3901                };
3902
3903                assert_eq!(error.value(py).to_string(), expected);
3904                assert!(saver.get_messages().is_empty());
3905            }
3906
3907            if method.starts_with("subscribe_book") {
3908                kwargs.del_item("book_type").unwrap();
3909            }
3910
3911            let unsubscribe = method.replacen("subscribe", "unsubscribe", 1);
3912            actor
3913                .bind(py)
3914                .call_method(unsubscribe.as_str(), (&ids,), Some(&kwargs))
3915                .unwrap();
3916            assert_eq!(saver.get_messages().len(), if external { 2 } else { 0 });
3917        });
3918    }
3919
3920    #[rstest]
3921    fn test_python_identifier_lists_reject_mixed_venues_before_effects(
3922        #[values(
3923            "subscribe_quotes",
3924            "unsubscribe_quotes",
3925            "subscribe_trades",
3926            "unsubscribe_trades"
3927        )]
3928        method: &str,
3929        clock: Rc<RefCell<VirtualClock>>,
3930        cache: Rc<RefCell<Cache>>,
3931        trader_id: TraderId,
3932    ) {
3933        Python::initialize();
3934        Python::attach(|py| {
3935            *get_message_bus().borrow_mut() = MessageBus::default();
3936            let actor = create_registered_actor(clock, cache, trader_id);
3937            let actor = Py::new(py, actor).unwrap();
3938            let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
3939            msgbus::register_data_command_endpoint(
3940                MessagingSwitchboard::data_engine_queue_execute(),
3941                handler,
3942            );
3943            let first = InstrumentId::from("BTC/USD.SIM");
3944            let second = InstrumentId::from("ETH/USD.OTHER");
3945            let argument = PyList::new(py, [first, second]).unwrap();
3946            let quotes = method.ends_with("quotes");
3947            let subscribe = method.starts_with("subscribe_");
3948            if !subscribe {
3949                let kwargs = PyDict::new(py);
3950                kwargs
3951                    .set_item("client_id", ClientId::from("ORIGINAL"))
3952                    .unwrap();
3953                actor
3954                    .bind(py)
3955                    .call_method(
3956                        if quotes {
3957                            "subscribe_quotes"
3958                        } else {
3959                            "subscribe_trades"
3960                        },
3961                        (&argument,),
3962                        Some(&kwargs),
3963                    )
3964                    .unwrap();
3965            }
3966
3967            let error = actor
3968                .bind(py)
3969                .call_method1(method, (&argument,))
3970                .unwrap_err();
3971            assert!(error.is_instance_of::<pyo3::exceptions::PyValueError>(py));
3972            assert_eq!(
3973                error.value(py).to_string(),
3974                "multiple instrument venues require an explicit client ID"
3975            );
3976            assert_eq!(saver.get_messages().len(), usize::from(!subscribe));
3977
3978            for id in [first, second] {
3979                assert_eq!(
3980                    msgbus::exact_subscriber_count_quotes(get_quotes_topic(id)),
3981                    usize::from(!subscribe && quotes),
3982                );
3983                assert_eq!(
3984                    msgbus::exact_subscriber_count_trades(get_trades_topic(id)),
3985                    usize::from(!subscribe && !quotes),
3986                );
3987            }
3988        });
3989    }
3990
3991    #[rstest]
3992    fn test_python_identifier_list_rejects_invalid_inputs_before_dispatch(
3993        #[values(
3994            "subscribe_quotes",
3995            "unsubscribe_quotes",
3996            "subscribe_trades",
3997            "unsubscribe_trades"
3998        )]
3999        method: &str,
4000        #[values(false, true)] empty: bool,
4001        clock: Rc<RefCell<VirtualClock>>,
4002        cache: Rc<RefCell<Cache>>,
4003        trader_id: TraderId,
4004    ) {
4005        Python::initialize();
4006        Python::attach(|py| {
4007            *get_message_bus().borrow_mut() = MessageBus::default();
4008            let actor = create_registered_actor(clock, cache, trader_id);
4009            let actor = Py::new(py, actor).unwrap();
4010            let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
4011            msgbus::register_data_command_endpoint(
4012                MessagingSwitchboard::data_engine_queue_execute(),
4013                handler,
4014            );
4015
4016            let argument = if empty {
4017                PyList::empty(py).into_any().unbind()
4018            } else {
4019                let instrument_id = InstrumentId::from("BTC/USD.SIM").into_py_any(py).unwrap();
4020                PyList::new(py, [instrument_id, 42.into_py_any(py).unwrap()])
4021                    .unwrap()
4022                    .into_any()
4023                    .unbind()
4024            };
4025
4026            let error = actor
4027                .bind(py)
4028                .call_method1(method, (argument.bind(py),))
4029                .unwrap_err();
4030
4031            if empty {
4032                assert!(error.is_instance_of::<pyo3::exceptions::PyValueError>(py));
4033                assert_eq!(
4034                    error.value(py).to_string(),
4035                    "instrument IDs cannot be empty"
4036                );
4037            } else {
4038                assert!(error.is_instance_of::<pyo3::exceptions::PyTypeError>(py));
4039                assert_eq!(
4040                    error.value(py).to_string(),
4041                    "expected InstrumentId or a sequence of InstrumentId values"
4042                );
4043            }
4044
4045            assert_eq!(saver.get_messages().len(), 0);
4046        });
4047    }
4048
4049    #[rstest]
4050    fn test_new_actor_creation() {
4051        let actor = PyDataActor::new(None);
4052        assert!(actor.trader_id().is_none());
4053    }
4054
4055    #[rstest]
4056    fn test_actor_retains_python_config_object() {
4057        pyo3::Python::initialize();
4058        Python::attach(|py| {
4059            let config = py
4060                .eval(
4061                    c_str!("type('_Cfg', (), {'actor_id': 'A-RETAIN-001'})()"),
4062                    None,
4063                    None,
4064                )
4065                .unwrap();
4066
4067            let actor = py
4068                .get_type::<PyDataActor>()
4069                .as_any()
4070                .call1((config.clone(),))
4071                .unwrap();
4072
4073            let retained = actor.getattr("config").unwrap();
4074
4075            assert!(retained.is(&config));
4076        });
4077    }
4078
4079    #[rstest]
4080    fn test_prepare_python_actor_applies_shared_registration_boundary() {
4081        pyo3::Python::initialize();
4082
4083        Python::attach(|py| {
4084            let namespace = PyDict::new(py);
4085            namespace
4086                .set_item("DataActor", py.get_type::<PyDataActor>())
4087                .unwrap();
4088            py.run(
4089                c_str!(
4090                    r#"
4091class PreparedActor(DataActor):
4092    def __init__(self, _config=None):
4093        pass
4094"#
4095                ),
4096                Some(&namespace),
4097                None,
4098            )
4099            .unwrap();
4100            let actor_class = namespace.get_item("PreparedActor").unwrap();
4101
4102            let configured = py
4103                .eval(
4104                    c_str!(
4105                        "type('_Cfg', (), {'actor_id': 'CONFIGURED-001', 'log_events': False, 'log_commands': False})()"
4106                    ),
4107                    None,
4108                    None,
4109                )
4110                .unwrap();
4111            let configured_actor = actor_class.call1((configured.clone(),)).unwrap();
4112            let configured_id = prepare_python_actor(&configured_actor, Some(&configured)).unwrap();
4113            let configured_ref = configured_actor.extract::<PyRef<PyDataActor>>().unwrap();
4114            let configured_wrapper = configured_ref.inner().python_instance().unwrap().unwrap();
4115
4116            let fallback_actor = actor_class.call0().unwrap();
4117            let fallback_id = prepare_python_actor(&fallback_actor, None).unwrap();
4118            let fallback_ref = fallback_actor.extract::<PyRef<PyDataActor>>().unwrap();
4119
4120            let invalid_logging = py
4121                .eval(
4122                    c_str!(
4123                        "type('_Cfg', (), {'actor_id': None, 'log_events': object(), 'log_commands': object()})()"
4124                    ),
4125                    None,
4126                    None,
4127                )
4128                .unwrap();
4129            let invalid_logging_actor = actor_class.call1((invalid_logging.clone(),)).unwrap();
4130            let invalid_logging_id =
4131                prepare_python_actor(&invalid_logging_actor, Some(&invalid_logging)).unwrap();
4132            let invalid_logging_ref = invalid_logging_actor
4133                .extract::<PyRef<PyDataActor>>()
4134                .unwrap();
4135
4136            let invalid = py
4137                .eval(
4138                    c_str!("type('_Cfg', (), {'actor_id': object()})()"),
4139                    None,
4140                    None,
4141                )
4142                .unwrap();
4143            let invalid_actor = actor_class.call1((invalid.clone(),)).unwrap();
4144            let error = prepare_python_actor(&invalid_actor, Some(&invalid)).unwrap_err();
4145
4146            assert_eq!(configured_id, ActorId::from("CONFIGURED-001"));
4147            assert_eq!(configured_ref.config.actor_id, Some(configured_id));
4148            assert!(!configured_ref.config.log_events);
4149            assert!(!configured_ref.config.log_commands);
4150            assert!(configured_wrapper.bind(py).is(&configured_actor));
4151            assert_eq!(fallback_id, ActorId::from("PreparedActor"));
4152            assert_eq!(fallback_ref.config.actor_id, Some(fallback_id));
4153            assert_eq!(invalid_logging_id, ActorId::from("PreparedActor"));
4154            assert!(invalid_logging_ref.config.log_events);
4155            assert!(invalid_logging_ref.config.log_commands);
4156            assert_eq!(error.to_string(), "Invalid `actor_id` type");
4157        });
4158    }
4159
4160    #[rstest]
4161    fn test_clock_access_before_registration_raises_error() {
4162        let actor = PyDataActor::new(None);
4163
4164        // Accessing clock before registration should raise PyRuntimeError
4165        let result = actor.py_clock();
4166        assert!(result.is_err());
4167
4168        let error = result.unwrap_err();
4169        pyo3::Python::initialize();
4170
4171        pyo3::Python::attach(|py| {
4172            assert!(error.is_instance_of::<pyo3::exceptions::PyRuntimeError>(py));
4173        });
4174
4175        let error_msg = error.to_string();
4176        assert!(
4177            error_msg.contains("Actor must be registered with a trader before accessing clock")
4178        );
4179    }
4180
4181    #[rstest]
4182    fn test_unregistered_actor_methods_work() {
4183        let actor = create_unregistered_actor();
4184
4185        assert!(!actor.py_is_ready());
4186        assert!(!actor.py_is_running());
4187        assert!(!actor.py_is_stopped());
4188        assert!(!actor.py_is_disposed());
4189        assert!(!actor.py_is_degraded());
4190        assert!(!actor.py_is_faulted());
4191
4192        // Verify unregistered state
4193        assert_eq!(actor.trader_id(), None);
4194    }
4195
4196    #[rstest]
4197    fn test_registration_success(
4198        clock: Rc<RefCell<VirtualClock>>,
4199        cache: Rc<RefCell<Cache>>,
4200        trader_id: TraderId,
4201    ) {
4202        let mut actor = create_unregistered_actor();
4203        actor.register(trader_id, clock, cache).unwrap();
4204        assert!(actor.trader_id().is_some());
4205        assert_eq!(actor.trader_id().unwrap(), trader_id);
4206    }
4207
4208    #[rstest]
4209    fn test_registered_actor_basic_properties(
4210        clock: Rc<RefCell<VirtualClock>>,
4211        cache: Rc<RefCell<Cache>>,
4212        trader_id: TraderId,
4213    ) {
4214        let actor = create_registered_actor(clock, cache, trader_id);
4215
4216        assert_eq!(actor.state(), ComponentState::Ready);
4217        assert_eq!(actor.trader_id(), Some(TraderId::from("TRADER-001")));
4218        assert!(actor.py_is_ready());
4219        assert!(!actor.py_is_running());
4220        assert!(!actor.py_is_stopped());
4221        assert!(!actor.py_is_disposed());
4222        assert!(!actor.py_is_degraded());
4223        assert!(!actor.py_is_faulted());
4224    }
4225
4226    #[rstest]
4227    fn test_basic_subscription_methods_compile(
4228        clock: Rc<RefCell<VirtualClock>>,
4229        cache: Rc<RefCell<Cache>>,
4230        trader_id: TraderId,
4231        data_type: DataType,
4232        client_id: ClientId,
4233        audusd_sim: CurrencyPair,
4234    ) {
4235        let mut actor = create_registered_actor(clock, cache, trader_id);
4236
4237        pyo3::Python::initialize();
4238
4239        pyo3::Python::attach(|py| {
4240            assert!(
4241                actor
4242                    .py_subscribe_data(py, data_type.clone(), Some(client_id), None)
4243                    .is_ok()
4244            );
4245            assert!(
4246                actor
4247                    .py_subscribe_quotes(
4248                        py,
4249                        audusd_sim.id.into_py_any(py).unwrap().bind(py),
4250                        Some(client_id),
4251                        None
4252                    )
4253                    .is_ok()
4254            );
4255            assert!(
4256                actor
4257                    .py_unsubscribe_data(py, data_type, Some(client_id), None)
4258                    .is_ok()
4259            );
4260            assert!(
4261                actor
4262                    .py_unsubscribe_quotes(
4263                        py,
4264                        audusd_sim.id.into_py_any(py).unwrap().bind(py),
4265                        Some(client_id),
4266                        None
4267                    )
4268                    .is_ok()
4269            );
4270        });
4271    }
4272
4273    #[rstest]
4274    fn test_shutdown_system_passes_through(
4275        clock: Rc<RefCell<VirtualClock>>,
4276        cache: Rc<RefCell<Cache>>,
4277        trader_id: TraderId,
4278    ) {
4279        let actor = create_registered_actor(clock, cache, trader_id);
4280
4281        assert!(
4282            actor
4283                .py_shutdown_system(Some("Test shutdown".to_string()))
4284                .is_ok()
4285        );
4286        assert!(actor.py_shutdown_system(None).is_ok());
4287    }
4288
4289    #[rstest]
4290    fn test_publish_data_delivers_to_any_subscriber(
4291        clock: Rc<RefCell<VirtualClock>>,
4292        cache: Rc<RefCell<Cache>>,
4293        trader_id: TraderId,
4294    ) {
4295        use crate::msgbus::{
4296            self, MessageBus, get_message_bus, switchboard::get_custom_topic,
4297            typed_handler::ShareableMessageHandler,
4298        };
4299
4300        // Ensure clean msgbus for this test
4301        *get_message_bus().borrow_mut() = MessageBus::default();
4302
4303        let actor = create_registered_actor(clock, cache, trader_id);
4304        let data = stub_custom_data(1, 42, None, None);
4305        let topic = get_custom_topic(&data.data_type);
4306
4307        let received: Rc<RefCell<Vec<CustomData>>> = Rc::new(RefCell::new(Vec::new()));
4308        let received_clone = received.clone();
4309        let handler = ShareableMessageHandler::from_typed(move |d: &CustomData| {
4310            received_clone.borrow_mut().push(d.clone());
4311        });
4312        msgbus::subscribe_any(topic.into(), handler, None);
4313
4314        actor.py_publish_data(&data.data_type, &data).unwrap();
4315
4316        let received = received.borrow();
4317        assert_eq!(received.len(), 1);
4318        assert_eq!(received[0].data_type, data.data_type);
4319    }
4320
4321    #[rstest]
4322    fn test_publish_signal_delivers_to_customdata_subscriber(
4323        clock: Rc<RefCell<VirtualClock>>,
4324        cache: Rc<RefCell<Cache>>,
4325        trader_id: TraderId,
4326    ) {
4327        use crate::{
4328            msgbus::{
4329                self, MessageBus, Pattern, get_message_bus, typed_handler::ShareableMessageHandler,
4330            },
4331            signal::Signal,
4332        };
4333
4334        *get_message_bus().borrow_mut() = MessageBus::default();
4335
4336        let actor = create_registered_actor(clock, cache, trader_id);
4337
4338        // Signals travel as `CustomData` on the bus so persistence and other
4339        // `CustomData`-aware subscribers pick them up. Downcast inside the handler.
4340        let received: Rc<RefCell<Vec<Signal>>> = Rc::new(RefCell::new(Vec::new()));
4341        let received_clone = received.clone();
4342        let handler = ShareableMessageHandler::from_typed(move |data: &CustomData| {
4343            if let Some(sig) = data.data.as_any().downcast_ref::<Signal>() {
4344                received_clone.borrow_mut().push(sig.clone());
4345            }
4346        });
4347        let pattern: crate::msgbus::MStr<Pattern> = "data.Signal*".to_string().into();
4348        msgbus::subscribe_any(pattern, handler, None);
4349
4350        pyo3::Python::initialize();
4351
4352        Python::attach(|py| {
4353            let val1: Py<PyAny> = 1.0_f64.into_py_any_unwrap(py);
4354            let val2: Py<PyAny> = "HIGH".into_py_any_unwrap(py);
4355            actor.py_publish_signal(py, "example", val1, 0).unwrap();
4356            actor
4357                .py_publish_signal(py, "risk", val2, 1_700_000_000_000_000_000)
4358                .unwrap();
4359        });
4360
4361        let received = received.borrow();
4362        assert_eq!(received.len(), 2);
4363        assert_eq!(received[0].name, "example");
4364        assert_eq!(received[0].value, "1.0");
4365        assert_eq!(received[1].name, "risk");
4366        assert_eq!(received[1].value, "HIGH");
4367        assert_eq!(
4368            received[1].ts_event,
4369            UnixNanos::from(1_700_000_000_000_000_000_u64)
4370        );
4371    }
4372
4373    #[rstest]
4374    fn test_publish_signal_accepts_numeric_py_values(
4375        clock: Rc<RefCell<VirtualClock>>,
4376        cache: Rc<RefCell<Cache>>,
4377        trader_id: TraderId,
4378    ) {
4379        use crate::{
4380            msgbus::{
4381                self, MessageBus, Pattern, get_message_bus, typed_handler::ShareableMessageHandler,
4382            },
4383            signal::Signal,
4384        };
4385
4386        *get_message_bus().borrow_mut() = MessageBus::default();
4387
4388        let actor = create_registered_actor(clock, cache, trader_id);
4389
4390        let received: Rc<RefCell<Vec<Signal>>> = Rc::new(RefCell::new(Vec::new()));
4391        let received_clone = received.clone();
4392        let handler = ShareableMessageHandler::from_typed(move |data: &CustomData| {
4393            if let Some(sig) = data.data.as_any().downcast_ref::<Signal>() {
4394                received_clone.borrow_mut().push(sig.clone());
4395            }
4396        });
4397        let pattern: crate::msgbus::MStr<Pattern> = "data.Signal*".to_string().into();
4398        msgbus::subscribe_any(pattern, handler, None);
4399
4400        pyo3::Python::initialize();
4401
4402        Python::attach(|py| {
4403            let int_value: Py<PyAny> = 42_i64.into_py_any_unwrap(py);
4404            let float_value: Py<PyAny> = 3.5_f64.into_py_any_unwrap(py);
4405            let bool_value: Py<PyAny> = true.into_py_any_unwrap(py);
4406            actor.py_publish_signal(py, "count", int_value, 0).unwrap();
4407            actor
4408                .py_publish_signal(py, "ratio", float_value, 0)
4409                .unwrap();
4410            actor
4411                .py_publish_signal(py, "active", bool_value, 0)
4412                .unwrap();
4413        });
4414
4415        let received = received.borrow();
4416        assert_eq!(received.len(), 3);
4417        assert_eq!(received[0].value, "42");
4418        assert_eq!(received[1].value, "3.5");
4419        assert_eq!(received[2].value, "True");
4420    }
4421
4422    #[rstest]
4423    fn test_subscribe_and_unsubscribe_signal_compile(
4424        clock: Rc<RefCell<VirtualClock>>,
4425        cache: Rc<RefCell<Cache>>,
4426        trader_id: TraderId,
4427    ) {
4428        use crate::msgbus::{MessageBus, get_message_bus};
4429
4430        *get_message_bus().borrow_mut() = MessageBus::default();
4431
4432        let actor = create_registered_actor(clock, cache, trader_id);
4433        Python::initialize();
4434        Python::attach(|py| {
4435            let actor = Bound::new(py, actor).unwrap();
4436            PyDataActor::py_subscribe_signal(&actor, "example", None).unwrap();
4437            PyDataActor::py_unsubscribe_signal(&actor, "example").unwrap();
4438            PyDataActor::py_subscribe_signal(&actor, "", None).unwrap();
4439            PyDataActor::py_unsubscribe_signal(&actor, "").unwrap();
4440        });
4441    }
4442
4443    #[rstest]
4444    fn test_py_subscribe_signal_forwards_priority(
4445        clock: Rc<RefCell<VirtualClock>>,
4446        cache: Rc<RefCell<Cache>>,
4447        trader_id: TraderId,
4448    ) {
4449        use crate::msgbus::{MessageBus, get_message_bus, switchboard::get_signal_topic};
4450
4451        *get_message_bus().borrow_mut() = MessageBus::default();
4452
4453        let actor = create_registered_actor(clock, cache, trader_id);
4454        Python::initialize();
4455        Python::attach(|py| {
4456            let actor = Bound::new(py, actor).unwrap();
4457            PyDataActor::py_subscribe_signal(&actor, "trigger", Some(50)).unwrap();
4458
4459            // The PyO3 binding must forward the priority to the bus unchanged.
4460            let topic = get_signal_topic("trigger");
4461            let subs = get_message_bus().borrow_mut().matching_subscriptions(topic);
4462            assert_eq!(subs.len(), 1);
4463            assert_eq!(subs[0].priority, 50);
4464        });
4465    }
4466
4467    #[rstest]
4468    fn test_register_in_global_registries_retains_python_wrapper(
4469        clock: Rc<RefCell<VirtualClock>>,
4470        cache: Rc<RefCell<Cache>>,
4471        trader_id: TraderId,
4472    ) {
4473        pyo3::Python::initialize();
4474
4475        Python::attach(|py| {
4476            let py_actor = create_tracking_python_actor(py).unwrap();
4477
4478            let mut rust_actor = PyDataActor::new(Some(DataActorConfig {
4479                actor_id: Some(ActorId::from("RETAINED-ACTOR")),
4480                ..Default::default()
4481            }));
4482            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
4483            rust_actor.register(trader_id, clock, cache).unwrap();
4484
4485            rust_actor.register_in_global_registries().unwrap();
4486
4487            let retained = get_python_wrapper(ComponentId::from("RETAINED-ACTOR"))
4488                .expect("registering must retain the actor's Python wrapper");
4489
4490            assert!(retained.bind(py).is(py_actor.bind(py)));
4491            assert!(get_component(&Ustr::from("RETAINED-ACTOR")).is_some());
4492            assert!(actor_exists(&Ustr::from("RETAINED-ACTOR")));
4493        });
4494    }
4495
4496    #[rstest]
4497    fn test_register_in_global_registries_rejects_missing_python_wrapper(
4498        clock: Rc<RefCell<VirtualClock>>,
4499        cache: Rc<RefCell<Cache>>,
4500        trader_id: TraderId,
4501    ) {
4502        pyo3::Python::initialize();
4503
4504        Python::attach(|_py| {
4505            let mut rust_actor = PyDataActor::new(Some(DataActorConfig {
4506                actor_id: Some(ActorId::from("UNWRAPPED-ACTOR")),
4507                ..Default::default()
4508            }));
4509            rust_actor.register(trader_id, clock, cache).unwrap();
4510
4511            let error = rust_actor
4512                .register_in_global_registries()
4513                .expect_err("registering without a Python wrapper must fail");
4514
4515            assert!(error.to_string().contains("without a Python wrapper"));
4516            assert!(get_component(&Ustr::from("UNWRAPPED-ACTOR")).is_none());
4517            assert!(!actor_exists(&Ustr::from("UNWRAPPED-ACTOR")));
4518            assert!(get_python_wrapper(ComponentId::from("UNWRAPPED-ACTOR")).is_none());
4519        });
4520    }
4521
4522    #[rstest]
4523    fn test_publish_data_dispatches_to_python_on_data(
4524        clock: Rc<RefCell<VirtualClock>>,
4525        cache: Rc<RefCell<Cache>>,
4526        trader_id: TraderId,
4527    ) {
4528        use crate::msgbus::{MessageBus, get_message_bus};
4529
4530        *get_message_bus().borrow_mut() = MessageBus::default();
4531
4532        pyo3::Python::initialize();
4533
4534        Python::attach(|py| {
4535            let py_actor = create_tracking_python_actor(py).unwrap();
4536
4537            let mut rust_actor = PyDataActor::new(None);
4538            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
4539            rust_actor.register(trader_id, clock, cache).unwrap();
4540            rust_actor.register_in_global_registries().unwrap();
4541            rust_actor.py_start().unwrap();
4542
4543            let data = stub_custom_data(1, 42, None, None);
4544            rust_actor
4545                .py_subscribe_data(py, data.data_type.clone(), None, None)
4546                .unwrap();
4547
4548            rust_actor.py_publish_data(&data.data_type, &data).unwrap();
4549            rust_actor.py_publish_data(&data.data_type, &data).unwrap();
4550
4551            assert!(python_method_was_called(&py_actor, py, "on_data"));
4552            assert_eq!(python_method_call_count(&py_actor, py, "on_data"), 2);
4553        });
4554    }
4555
4556    #[rstest]
4557    fn test_publish_signal_dispatches_to_python_on_signal(
4558        clock: Rc<RefCell<VirtualClock>>,
4559        cache: Rc<RefCell<Cache>>,
4560        trader_id: TraderId,
4561    ) {
4562        use crate::msgbus::{MessageBus, get_message_bus};
4563
4564        *get_message_bus().borrow_mut() = MessageBus::default();
4565
4566        pyo3::Python::initialize();
4567
4568        Python::attach(|py| {
4569            let py_actor = create_tracking_python_actor(py).unwrap();
4570
4571            let mut rust_actor = PyDataActor::new(None);
4572            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
4573            rust_actor.register(trader_id, clock, cache).unwrap();
4574            rust_actor.register_in_global_registries().unwrap();
4575            rust_actor.py_start().unwrap();
4576
4577            let bound_actor = Bound::new(py, rust_actor).unwrap();
4578            PyDataActor::py_subscribe_signal(&bound_actor, "example", None).unwrap();
4579            let rust_actor = bound_actor.borrow_mut();
4580            let val1: Py<PyAny> = "1.5".into_py_any_unwrap(py);
4581            let val2: Py<PyAny> = 2.0_f64.into_py_any_unwrap(py);
4582            rust_actor
4583                .py_publish_signal(py, "example", val1, 0)
4584                .unwrap();
4585            rust_actor
4586                .py_publish_signal(py, "example", val2, 1_700_000_000_000_000_000)
4587                .unwrap();
4588
4589            assert!(python_method_was_called(&py_actor, py, "on_signal"));
4590            assert_eq!(python_method_call_count(&py_actor, py, "on_signal"), 2);
4591        });
4592    }
4593
4594    #[rstest]
4595    fn test_unsubscribe_signal_stops_python_dispatch(
4596        clock: Rc<RefCell<VirtualClock>>,
4597        cache: Rc<RefCell<Cache>>,
4598        trader_id: TraderId,
4599    ) {
4600        use crate::msgbus::{MessageBus, get_message_bus};
4601
4602        *get_message_bus().borrow_mut() = MessageBus::default();
4603
4604        pyo3::Python::initialize();
4605
4606        Python::attach(|py| {
4607            let py_actor = create_tracking_python_actor(py).unwrap();
4608
4609            let mut rust_actor = PyDataActor::new(None);
4610            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
4611            rust_actor.register(trader_id, clock, cache).unwrap();
4612            rust_actor.register_in_global_registries().unwrap();
4613            rust_actor.py_start().unwrap();
4614
4615            let bound_actor = Bound::new(py, rust_actor).unwrap();
4616            PyDataActor::py_subscribe_signal(&bound_actor, "example", None).unwrap();
4617            let rust_actor = bound_actor.borrow_mut();
4618            let val1: Py<PyAny> = "1".into_py_any_unwrap(py);
4619            let val2: Py<PyAny> = "2".into_py_any_unwrap(py);
4620            rust_actor
4621                .py_publish_signal(py, "example", val1, 0)
4622                .unwrap();
4623
4624            drop(rust_actor);
4625            PyDataActor::py_unsubscribe_signal(&bound_actor, "example").unwrap();
4626            let rust_actor = bound_actor.borrow_mut();
4627            rust_actor
4628                .py_publish_signal(py, "example", val2, 0)
4629                .unwrap();
4630
4631            assert_eq!(python_method_call_count(&py_actor, py, "on_signal"), 1);
4632        });
4633    }
4634
4635    #[rstest]
4636    #[case(None)]
4637    #[case(Some(SystemChannel::ExecCommands))]
4638    fn test_queue_state_changed_subscription_dispatches_and_unsubscribes(
4639        clock: Rc<RefCell<VirtualClock>>,
4640        cache: Rc<RefCell<Cache>>,
4641        trader_id: TraderId,
4642        #[case] channel: Option<SystemChannel>,
4643    ) {
4644        *get_message_bus().borrow_mut() = MessageBus::default();
4645
4646        pyo3::Python::initialize();
4647
4648        Python::attach(|py| {
4649            let py_actor = create_tracking_python_actor(py).unwrap();
4650
4651            let mut rust_actor = PyDataActor::new(None);
4652            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
4653            rust_actor.register(trader_id, clock, cache).unwrap();
4654            rust_actor.register_in_global_registries().unwrap();
4655            rust_actor.py_start().unwrap();
4656            rust_actor
4657                .py_subscribe_queue_state(channel, Some(50))
4658                .unwrap();
4659
4660            let topic =
4661                MessagingSwitchboard::queue_state_changed_topic(SystemChannel::ExecCommands);
4662            let subscriptions = get_message_bus().borrow_mut().matching_subscriptions(topic);
4663            assert_eq!(subscriptions.len(), 1);
4664            assert_eq!(subscriptions[0].priority, 50);
4665            let unrelated =
4666                MessagingSwitchboard::queue_state_changed_topic(SystemChannel::DataEvents);
4667            assert_eq!(
4668                get_message_bus()
4669                    .borrow_mut()
4670                    .matching_subscriptions(unrelated)
4671                    .len(),
4672                usize::from(channel.is_none())
4673            );
4674
4675            let triggered = sample_queue_state_changed(QueueState::Triggered);
4676            msgbus::publish_any(topic, &triggered);
4677
4678            let (received,) = py_actor
4679                .call_method1(py, "last_call_args", ("on_queue_state",))
4680                .unwrap()
4681                .extract::<(QueueStateChanged,)>(py)
4682                .unwrap();
4683            assert_eq!(received, triggered);
4684
4685            rust_actor.py_unsubscribe_queue_state(channel).unwrap();
4686            let cleared = sample_queue_state_changed(QueueState::Cleared);
4687            msgbus::publish_any(topic, &cleared);
4688
4689            assert_eq!(python_method_call_count(&py_actor, py, "on_queue_state"), 1);
4690        });
4691    }
4692
4693    #[rstest]
4694    #[case(None, None)]
4695    #[case(Some(ClientId::from("BINANCE")), None)]
4696    #[case(None, Some("binance-futures-market-streams"))]
4697    #[case(
4698        Some(ClientId::from("BINANCE")),
4699        Some("binance-futures-market-streams")
4700    )]
4701    fn test_socket_state_changed_subscription_dispatches_and_unsubscribes(
4702        clock: Rc<RefCell<VirtualClock>>,
4703        cache: Rc<RefCell<Cache>>,
4704        trader_id: TraderId,
4705        #[case] client_id: Option<ClientId>,
4706        #[case] endpoint: Option<&str>,
4707    ) {
4708        *get_message_bus().borrow_mut() = MessageBus::default();
4709
4710        pyo3::Python::initialize();
4711
4712        Python::attach(|py| {
4713            let py_actor = create_tracking_python_actor(py).unwrap();
4714
4715            let mut rust_actor = PyDataActor::new(None);
4716            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
4717            rust_actor.register(trader_id, clock, cache).unwrap();
4718            rust_actor.register_in_global_registries().unwrap();
4719            rust_actor.py_start().unwrap();
4720            rust_actor
4721                .py_subscribe_socket_state(client_id, endpoint, Some(50))
4722                .unwrap();
4723
4724            let topic = MessagingSwitchboard::socket_state_changed_topic(
4725                ClientId::from("BINANCE"),
4726                "binance-futures-market-streams",
4727            );
4728            let subscriptions = get_message_bus().borrow_mut().matching_subscriptions(topic);
4729            assert_eq!(subscriptions.len(), 1);
4730            assert_eq!(subscriptions[0].priority, 50);
4731            let unrelated =
4732                MessagingSwitchboard::socket_state_changed_topic(ClientId::from("BYBIT"), "orders");
4733            assert_eq!(
4734                get_message_bus()
4735                    .borrow_mut()
4736                    .matching_subscriptions(unrelated)
4737                    .len(),
4738                usize::from(client_id.is_none() && endpoint.is_none())
4739            );
4740
4741            let connected = sample_socket_state_changed(SocketState::Connected);
4742            msgbus::publish_any(topic, &connected);
4743
4744            let (received,) = py_actor
4745                .call_method1(py, "last_call_args", ("on_socket_state",))
4746                .unwrap()
4747                .extract::<(SocketStateChanged,)>(py)
4748                .unwrap();
4749            assert_eq!(received, connected);
4750
4751            rust_actor
4752                .py_unsubscribe_socket_state(client_id, endpoint)
4753                .unwrap();
4754            let disconnected = sample_socket_state_changed(SocketState::Disconnected);
4755            msgbus::publish_any(topic, &disconnected);
4756
4757            assert_eq!(
4758                python_method_call_count(&py_actor, py, "on_socket_state"),
4759                1
4760            );
4761        });
4762    }
4763
4764    #[rstest]
4765    fn test_subscribe_signal_wildcard_dispatches_all_names_to_python(
4766        clock: Rc<RefCell<VirtualClock>>,
4767        cache: Rc<RefCell<Cache>>,
4768        trader_id: TraderId,
4769    ) {
4770        use crate::msgbus::{MessageBus, get_message_bus};
4771
4772        *get_message_bus().borrow_mut() = MessageBus::default();
4773
4774        pyo3::Python::initialize();
4775
4776        Python::attach(|py| {
4777            let py_actor = create_tracking_python_actor(py).unwrap();
4778
4779            let mut rust_actor = PyDataActor::new(None);
4780            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
4781            rust_actor.register(trader_id, clock, cache).unwrap();
4782            rust_actor.register_in_global_registries().unwrap();
4783            rust_actor.py_start().unwrap();
4784
4785            let bound_actor = Bound::new(py, rust_actor).unwrap();
4786            PyDataActor::py_subscribe_signal(&bound_actor, "", None).unwrap();
4787            let rust_actor = bound_actor.borrow_mut();
4788            let val1: Py<PyAny> = "1".into_py_any_unwrap(py);
4789            let val2: Py<PyAny> = "2".into_py_any_unwrap(py);
4790            let val3: Py<PyAny> = "3".into_py_any_unwrap(py);
4791            rust_actor.py_publish_signal(py, "alpha", val1, 0).unwrap();
4792            rust_actor.py_publish_signal(py, "beta", val2, 0).unwrap();
4793            rust_actor.py_publish_signal(py, "gamma", val3, 0).unwrap();
4794
4795            assert_eq!(python_method_call_count(&py_actor, py, "on_signal"), 3);
4796        });
4797    }
4798
4799    #[rstest]
4800    fn test_signal_customdata_unwraps_to_python_signal(
4801        clock: Rc<RefCell<VirtualClock>>,
4802        cache: Rc<RefCell<Cache>>,
4803        trader_id: TraderId,
4804    ) {
4805        // Exercises the `Signal::to_pyobject` path: a `CustomData` wrapping a
4806        // `Signal` reaches Python `on_data`, and the PyO3 `.data` getter must
4807        // successfully unwrap the inner `Arc<dyn CustomDataTrait>` into a
4808        // Python `Signal`. Without `Signal::to_pyobject`, the getter raises
4809        // `TypeError` and this assertion fails.
4810        use crate::msgbus::{MessageBus, get_message_bus};
4811
4812        *get_message_bus().borrow_mut() = MessageBus::default();
4813
4814        pyo3::Python::initialize();
4815
4816        Python::attach(|py| {
4817            let capture_code = c_str!(
4818                r#"
4819class CapturingActor:
4820    def __init__(self):
4821        self.captured = []
4822
4823    def on_start(self): pass
4824    def on_stop(self): pass
4825    def on_resume(self): pass
4826    def on_reset(self): pass
4827    def on_dispose(self): pass
4828    def on_degrade(self): pass
4829    def on_fault(self): pass
4830    def on_signal(self, signal): pass
4831
4832    def on_data(self, custom):
4833        # Exercise the CustomData.data getter: raises TypeError if the
4834        # inner payload cannot be converted back to a Python object.
4835        inner = custom.data
4836        self.captured.append((type(inner).__name__, inner.name, inner.value))
4837"#
4838            );
4839            py.run(capture_code, None, None).unwrap();
4840            let cls = py.eval(c_str!("CapturingActor"), None, None).unwrap();
4841            let py_actor: Py<PyAny> = cls.call0().unwrap().unbind();
4842
4843            let mut rust_actor = PyDataActor::new(None);
4844            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
4845            rust_actor.register(trader_id, clock, cache).unwrap();
4846            rust_actor.register_in_global_registries().unwrap();
4847            rust_actor.py_start().unwrap();
4848
4849            // Subscribe as custom-data for the signal's advertised DataType
4850            // (`data.SignalExample`) so `on_data` fires with the wrapping CustomData.
4851            let data_type = DataType::new("SignalExample", None, None);
4852            rust_actor
4853                .py_subscribe_data(py, data_type, None, None)
4854                .unwrap();
4855
4856            let val: Py<PyAny> = "1.5".into_py_any_unwrap(py);
4857            rust_actor.py_publish_signal(py, "example", val, 0).unwrap();
4858
4859            let captured = py_actor
4860                .bind(py)
4861                .getattr("captured")
4862                .unwrap()
4863                .extract::<Vec<(String, String, String)>>()
4864                .unwrap();
4865            assert_eq!(captured.len(), 1);
4866            assert_eq!(captured[0].0, "Signal");
4867            assert_eq!(captured[0].1, "example");
4868            assert_eq!(captured[0].2, "1.5");
4869        });
4870    }
4871
4872    #[rstest]
4873    fn test_add_and_update_synthetic_via_pyo3(
4874        clock: Rc<RefCell<VirtualClock>>,
4875        cache: Rc<RefCell<Cache>>,
4876        trader_id: TraderId,
4877    ) {
4878        use nautilus_model::{
4879            identifiers::{InstrumentId, Symbol},
4880            instruments::SyntheticInstrument,
4881        };
4882
4883        let actor = create_registered_actor(clock, cache.clone(), trader_id);
4884
4885        let comp1 = InstrumentId::from_str("BTC-USD.VENUE").unwrap();
4886        let comp2 = InstrumentId::from_str("ETH-USD.VENUE").unwrap();
4887        let formula = format!("({comp1} + {comp2}) / 2.0");
4888        let synthetic = SyntheticInstrument::builder()
4889            .symbol(Symbol::from("SYN"))
4890            .price_precision(2)
4891            .components(vec![comp1, comp2])
4892            .formula(&formula)
4893            .ts_event(UnixNanos::default())
4894            .ts_init(UnixNanos::default())
4895            .build()
4896            .unwrap();
4897        let synthetic_id = synthetic.id;
4898
4899        actor.py_add_synthetic(synthetic.clone()).unwrap();
4900        assert!(cache.borrow().synthetic(&synthetic_id).is_some());
4901
4902        // Adding again raises
4903        assert!(actor.py_add_synthetic(synthetic).is_err());
4904
4905        let new_formula = format!("{comp1} + {comp2}");
4906        let updated = SyntheticInstrument::builder()
4907            .symbol(Symbol::from("SYN"))
4908            .price_precision(2)
4909            .components(vec![comp1, comp2])
4910            .formula(&new_formula)
4911            .ts_event(UnixNanos::default())
4912            .ts_init(UnixNanos::default())
4913            .build()
4914            .unwrap();
4915        actor.py_update_synthetic(updated).unwrap();
4916        assert_eq!(
4917            cache.borrow().synthetic(&synthetic_id).unwrap().formula,
4918            new_formula
4919        );
4920
4921        // Updating a non-existent raises
4922        let missing = SyntheticInstrument::builder()
4923            .symbol(Symbol::from("GONE"))
4924            .price_precision(2)
4925            .components(vec![comp1, comp2])
4926            .formula(&formula)
4927            .ts_event(UnixNanos::default())
4928            .ts_init(UnixNanos::default())
4929            .build()
4930            .unwrap();
4931        assert!(actor.py_update_synthetic(missing).is_err());
4932    }
4933
4934    #[rstest]
4935    fn test_book_at_interval_invalid_interval_ms(
4936        clock: Rc<RefCell<VirtualClock>>,
4937        cache: Rc<RefCell<Cache>>,
4938        trader_id: TraderId,
4939        audusd_sim: CurrencyPair,
4940    ) {
4941        pyo3::Python::initialize();
4942
4943        let mut actor = create_registered_actor(clock, cache, trader_id);
4944
4945        pyo3::Python::attach(|py| {
4946            let result = actor.py_subscribe_book_at_interval(
4947                py,
4948                &pyo3::Py::new(py, audusd_sim.id)
4949                    .unwrap()
4950                    .into_bound(py)
4951                    .into_any(),
4952                BookType::L2_MBP,
4953                0,
4954                None,
4955                None,
4956                None,
4957            );
4958            assert!(result.is_err());
4959            assert_eq!(
4960                result.unwrap_err().to_string(),
4961                "ValueError: interval_ms must be > 0"
4962            );
4963
4964            let result = actor.py_unsubscribe_book_at_interval(
4965                py,
4966                &pyo3::Py::new(py, audusd_sim.id)
4967                    .unwrap()
4968                    .into_bound(py)
4969                    .into_any(),
4970                0,
4971                None,
4972                None,
4973            );
4974            assert!(result.is_err());
4975            assert_eq!(
4976                result.unwrap_err().to_string(),
4977                "ValueError: interval_ms must be > 0"
4978            );
4979        });
4980    }
4981
4982    #[rstest]
4983    #[case(None)]
4984    #[case(Some(25))]
4985    fn test_book_depth_subscription_methods_manage_handler(
4986        #[case] depth: Option<usize>,
4987        clock: Rc<RefCell<VirtualClock>>,
4988        cache: Rc<RefCell<Cache>>,
4989        trader_id: TraderId,
4990        audusd_sim: CurrencyPair,
4991    ) {
4992        pyo3::Python::initialize();
4993
4994        let mut actor = create_registered_actor(clock, cache, trader_id);
4995
4996        Python::attach(|py| {
4997            actor
4998                .py_subscribe_book_depth(
4999                    py,
5000                    &pyo3::Py::new(py, audusd_sim.id)
5001                        .unwrap()
5002                        .into_bound(py)
5003                        .into_any(),
5004                    BookType::L2_MBP,
5005                    depth,
5006                    None,
5007                    false,
5008                    None,
5009                )
5010                .unwrap();
5011            assert_eq!(actor.inner().depth_handler_count(), 1);
5012
5013            actor
5014                .py_unsubscribe_book_depth(
5015                    py,
5016                    &pyo3::Py::new(py, audusd_sim.id)
5017                        .unwrap()
5018                        .into_bound(py)
5019                        .into_any(),
5020                    None,
5021                    None,
5022                )
5023                .unwrap();
5024            assert_eq!(actor.inner().depth_handler_count(), 0);
5025        });
5026    }
5027
5028    #[rstest]
5029    fn test_request_methods_signatures_exist() {
5030        let actor = create_unregistered_actor();
5031        assert!(actor.trader_id().is_none());
5032    }
5033
5034    #[rstest]
5035    #[case("request_instrument", "Instrument")]
5036    #[case("request_book_snapshot", "BookSnapshot")]
5037    #[case("request_book_deltas", "BookDeltas")]
5038    #[case("request_book_depth", "BookDepth")]
5039    #[case("request_quotes", "Quotes")]
5040    #[case("request_trades", "Trades")]
5041    #[case("request_funding_rates", "FundingRates")]
5042    #[case("request_bars", "Bars")]
5043    fn test_python_request_identifier_shapes(
5044        clock: Rc<RefCell<VirtualClock>>,
5045        cache: Rc<RefCell<Cache>>,
5046        trader_id: TraderId,
5047        #[case] method: &str,
5048        #[case] variant: &str,
5049        #[values("scalar", "list", "tuple")] shape: &str,
5050    ) {
5051        Python::initialize();
5052        Python::attach(|py| {
5053            *get_message_bus().borrow_mut() = MessageBus::default();
5054            let actor = Py::new(py, create_registered_actor(clock, cache, trader_id)).unwrap();
5055            let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
5056            msgbus::register_data_command_endpoint(
5057                MessagingSwitchboard::data_engine_queue_execute(),
5058                handler,
5059            );
5060
5061            let (first, second, key) = if variant == "Bars" {
5062                let first = BarType::from_str("AUD/USD.SIM-1-MINUTE-LAST-EXTERNAL").unwrap();
5063                let second = BarType::from_str("NZD/USD.SIM-5-MINUTE-LAST-EXTERNAL").unwrap();
5064                (
5065                    first.into_py_any(py).unwrap(),
5066                    second.into_py_any(py).unwrap(),
5067                    "bar_types",
5068                )
5069            } else {
5070                (
5071                    InstrumentId::from("AUD/USD.SIM").into_py_any(py).unwrap(),
5072                    InstrumentId::from("NZD/USD.SIM").into_py_any(py).unwrap(),
5073                    "instrument_ids",
5074                )
5075            };
5076
5077            let argument = match shape {
5078                "scalar" => first.bind(py).clone(),
5079                "list" => PyList::new(py, [&first, &second, &first])
5080                    .unwrap()
5081                    .into_any(),
5082                "tuple" => PyTuple::new(py, [&first, &second, &first])
5083                    .unwrap()
5084                    .into_any(),
5085                _ => unreachable!(),
5086            };
5087
5088            let result = actor.bind(py).call_method1(method, (&argument,));
5089            let commands = saver.get_messages();
5090
5091            let root = result
5092                .unwrap()
5093                .extract::<String>()
5094                .unwrap()
5095                .parse::<UUID4>()
5096                .unwrap();
5097            assert_eq!(commands.len(), 1);
5098
5099            let DataCommand::Request(command) = &commands[0] else {
5100                panic!("expected request");
5101            };
5102
5103            assert_eq!(command.request_id(), &root);
5104            let wire = serde_json::to_value(command).unwrap();
5105
5106            let count = if shape == "scalar" { 1 } else { 2 };
5107
5108            let expected = if variant == "Bars" {
5109                serde_json::to_value(
5110                    [
5111                        "AUD/USD.SIM-1-MINUTE-LAST-EXTERNAL",
5112                        "NZD/USD.SIM-5-MINUTE-LAST-EXTERNAL",
5113                    ][..count]
5114                        .to_vec(),
5115                )
5116                .unwrap()
5117            } else {
5118                serde_json::to_value(["AUD/USD.SIM", "NZD/USD.SIM"][..count].to_vec()).unwrap()
5119            };
5120
5121            assert_eq!(wire[variant][key], expected);
5122            assert!(actor.borrow(py).inner().core.is_pending_request(&root));
5123        });
5124    }
5125
5126    #[rstest]
5127    fn test_python_request_rejects_invalid_identifiers_before_dispatch(
5128        clock: Rc<RefCell<VirtualClock>>,
5129        cache: Rc<RefCell<Cache>>,
5130        trader_id: TraderId,
5131        #[values(
5132            "request_instrument",
5133            "request_book_snapshot",
5134            "request_book_deltas",
5135            "request_book_depth",
5136            "request_quotes",
5137            "request_trades",
5138            "request_funding_rates",
5139            "request_bars"
5140        )]
5141        method: &str,
5142        #[values(false, true)] empty: bool,
5143    ) {
5144        Python::initialize();
5145        Python::attach(|py| {
5146            *get_message_bus().borrow_mut() = MessageBus::default();
5147            let actor = Py::new(py, create_registered_actor(clock, cache, trader_id)).unwrap();
5148            let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
5149            msgbus::register_data_command_endpoint(
5150                MessagingSwitchboard::data_engine_queue_execute(),
5151                handler,
5152            );
5153
5154            let argument = if empty {
5155                PyList::empty(py)
5156            } else {
5157                PyList::new(py, [42]).unwrap()
5158            };
5159
5160            let error = actor
5161                .bind(py)
5162                .call_method1(method, (argument,))
5163                .unwrap_err();
5164
5165            let expected = match (method == "request_bars", empty) {
5166                (true, true) => "ValueError: bar types cannot be empty",
5167                (false, true) => "ValueError: instrument IDs cannot be empty",
5168                (true, false) => "TypeError: expected BarType or a sequence of BarType values",
5169                (false, false) => {
5170                    "TypeError: expected InstrumentId or a sequence of InstrumentId values"
5171                }
5172            };
5173
5174            assert_eq!(error.to_string(), expected);
5175            assert!(saver.get_messages().is_empty());
5176        });
5177    }
5178
5179    #[rstest]
5180    fn test_data_actor_trait_implementation(
5181        clock: Rc<RefCell<VirtualClock>>,
5182        cache: Rc<RefCell<Cache>>,
5183        trader_id: TraderId,
5184    ) {
5185        let actor = create_registered_actor(clock, cache, trader_id);
5186        let state = actor.state();
5187        assert_eq!(state, ComponentState::Ready);
5188    }
5189
5190    #[rstest]
5191    fn test_python_reconnect_socket_enqueues_typed_command(
5192        clock: Rc<RefCell<VirtualClock>>,
5193        cache: Rc<RefCell<Cache>>,
5194        trader_id: TraderId,
5195    ) {
5196        let (system_tx, mut system_rx) = tokio::sync::mpsc::unbounded_channel();
5197        replace_system_command_sender(system_tx);
5198        let actor = create_registered_actor(clock, cache, trader_id);
5199
5200        actor
5201            .py_reconnect_socket(ClientId::from("POLYMARKET"), "polymarket-market-streams")
5202            .expect("valid reconnect command");
5203        let command = system_rx
5204            .try_recv()
5205            .expect("reconnect command should be queued");
5206        let SystemCommand::ReconnectSocket(command) = command;
5207
5208        assert_eq!(command.trader_id, trader_id);
5209        assert_eq!(command.client_id, ClientId::from("POLYMARKET"));
5210        assert_eq!(command.endpoint, "polymarket-market-streams");
5211        assert_eq!(command.ts_init, UnixNanos::default());
5212    }
5213
5214    #[rstest]
5215    fn test_python_reconnect_socket_returns_errors_without_panicking() {
5216        let actor = create_unregistered_actor();
5217
5218        assert!(
5219            actor
5220                .py_reconnect_socket(ClientId::from("POLYMARKET"), "polymarket-market-streams")
5221                .is_err()
5222        );
5223        assert!(
5224            actor
5225                .py_reconnect_socket(ClientId::from("POLYMARKET"), "wss://secret.example")
5226                .is_err()
5227        );
5228    }
5229
5230    fn sample_instrument() -> CurrencyPair {
5231        audusd_sim()
5232    }
5233
5234    fn sample_data() -> CustomData {
5235        stub_custom_data(1, 42, None, None)
5236    }
5237
5238    fn sample_time_event() -> TimeEvent {
5239        TimeEvent::new(
5240            Ustr::from("test_timer"),
5241            UUID4::new(),
5242            UnixNanos::default(),
5243            UnixNanos::default(),
5244        )
5245    }
5246
5247    fn sample_signal() -> Signal {
5248        Signal::new(
5249            Ustr::from("test_signal"),
5250            "1.0".to_string(),
5251            UnixNanos::default(),
5252            UnixNanos::default(),
5253        )
5254    }
5255
5256    fn sample_queue_state_changed(state: QueueState) -> QueueStateChanged {
5257        QueueStateChanged::new(
5258            TraderId::from("TRADER-001"),
5259            SystemChannel::ExecCommands,
5260            QueueCondition::Backlogged,
5261            state,
5262            17,
5263            23,
5264            UUID4::from("00000000-0000-4000-8000-000000000001"),
5265            UnixNanos::from(1_700_000_000_000_000_001),
5266            UnixNanos::from(1_700_000_000_000_000_002),
5267        )
5268    }
5269
5270    fn sample_socket_state_changed(state: SocketState) -> SocketStateChanged {
5271        SocketStateChanged::new(
5272            TraderId::from("TRADER-001"),
5273            ClientId::from("BINANCE"),
5274            Some(Venue::from("BINANCE")),
5275            Ustr::from("binance-futures-market-streams"),
5276            state,
5277            UUID4::from("00000000-0000-4000-8000-000000000001"),
5278            UnixNanos::from(1_700_000_000_000_000_001),
5279            UnixNanos::from(1_700_000_000_000_000_002),
5280        )
5281    }
5282
5283    fn sample_quote() -> QuoteTick {
5284        let instrument = sample_instrument();
5285        QuoteTick::new(
5286            instrument.id,
5287            Price::from("1.00000"),
5288            Price::from("1.00001"),
5289            Quantity::from(100_000),
5290            Quantity::from(100_000),
5291            UnixNanos::default(),
5292            UnixNanos::default(),
5293        )
5294    }
5295
5296    fn sample_trade() -> TradeTick {
5297        let instrument = sample_instrument();
5298        TradeTick::new(
5299            instrument.id,
5300            Price::from("1.00000"),
5301            Quantity::from(100_000),
5302            AggressorSide::Buy,
5303            TradeId::new("123456"),
5304            UnixNanos::default(),
5305            UnixNanos::default(),
5306        )
5307    }
5308
5309    fn sample_bar() -> Bar {
5310        let instrument = sample_instrument();
5311        let bar_type =
5312            BarType::from_str(&format!("{}-1-MINUTE-LAST-INTERNAL", instrument.id)).unwrap();
5313        Bar::new(
5314            bar_type,
5315            Price::from("1.00000"),
5316            Price::from("1.00010"),
5317            Price::from("0.99990"),
5318            Price::from("1.00005"),
5319            Quantity::from(100_000),
5320            UnixNanos::default(),
5321            UnixNanos::default(),
5322        )
5323    }
5324
5325    fn sample_book() -> OrderBook {
5326        OrderBook::new(sample_instrument().id, BookType::L2_MBP)
5327    }
5328
5329    fn sample_book_deltas() -> OrderBookDeltas {
5330        let instrument = sample_instrument();
5331        let delta =
5332            OrderBookDelta::clear(instrument.id, 0, UnixNanos::default(), UnixNanos::default());
5333        OrderBookDeltas::new(instrument.id, vec![delta])
5334    }
5335
5336    fn sample_book_depth() -> OrderBookDepth {
5337        stub_depth10()
5338    }
5339
5340    fn sample_mark_price() -> MarkPriceUpdate {
5341        MarkPriceUpdate::new(
5342            sample_instrument().id,
5343            Price::from("1.00000"),
5344            UnixNanos::default(),
5345            UnixNanos::default(),
5346        )
5347    }
5348
5349    fn sample_index_price() -> IndexPriceUpdate {
5350        IndexPriceUpdate::new(
5351            sample_instrument().id,
5352            Price::from("1.00000"),
5353            UnixNanos::default(),
5354            UnixNanos::default(),
5355        )
5356    }
5357
5358    fn sample_funding_rate() -> FundingRateUpdate {
5359        FundingRateUpdate::new(
5360            sample_instrument().id,
5361            "0.0001".parse().unwrap(),
5362            None,
5363            None,
5364            UnixNanos::default(),
5365            UnixNanos::default(),
5366        )
5367    }
5368
5369    fn sample_instrument_status() -> InstrumentStatus {
5370        InstrumentStatus::new(
5371            sample_instrument().id,
5372            MarketStatusAction::Trading,
5373            UnixNanos::default(),
5374            UnixNanos::default(),
5375            None,
5376            None,
5377            None,
5378            None,
5379            None,
5380        )
5381    }
5382
5383    fn sample_instrument_close() -> InstrumentClose {
5384        InstrumentClose::new(
5385            sample_instrument().id,
5386            Price::from("1.00000"),
5387            InstrumentCloseType::EndOfSession,
5388            UnixNanos::default(),
5389            UnixNanos::default(),
5390        )
5391    }
5392
5393    fn sample_option_greeks() -> OptionGreeks {
5394        OptionGreeks {
5395            instrument_id: sample_instrument().id,
5396            convention: GreeksConvention::BlackScholes,
5397            greeks: OptionGreekValues {
5398                delta: 0.55,
5399                gamma: 0.03,
5400                vega: 0.12,
5401                theta: -0.05,
5402                rho: 0.01,
5403            },
5404            mark_iv: Some(0.25),
5405            bid_iv: None,
5406            ask_iv: None,
5407            underlying_price: None,
5408            open_interest: None,
5409            ts_event: UnixNanos::default(),
5410            ts_init: UnixNanos::default(),
5411        }
5412    }
5413
5414    fn sample_option_chain() -> OptionChainSlice {
5415        OptionChainSlice {
5416            series_id: OptionSeriesId::new_derived(
5417                Venue::from("SIM"),
5418                Ustr::from("AUD"),
5419                Ustr::from("USD"),
5420                UnixNanos::from(1_711_036_800_000_000_000),
5421            ),
5422            atm_strike: None,
5423            calls: Default::default(),
5424            puts: Default::default(),
5425            ts_event: UnixNanos::default(),
5426            ts_init: UnixNanos::default(),
5427        }
5428    }
5429
5430    #[cfg(feature = "defi")]
5431    fn sample_block() -> Block {
5432        Block::new(
5433            "0x1234567890abcdef".to_string(),
5434            "0xabcdef1234567890".to_string(),
5435            12345,
5436            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0".into(),
5437            21000,
5438            20000,
5439            UnixNanos::default(),
5440            Some(Blockchain::Ethereum),
5441        )
5442    }
5443
5444    #[cfg(feature = "defi")]
5445    fn sample_pool_components() -> (Arc<Chain>, Arc<Dex>, Pool) {
5446        let chain = Arc::new(Chain::new(Blockchain::Ethereum, 1));
5447        let dex = Arc::new(Dex::new(
5448            Chain::new(Blockchain::Ethereum, 1),
5449            DexType::UniswapV3,
5450            "0x1F98431c8aD98523631AE4a59f267346ea31F984",
5451            0,
5452            AmmType::CLAMM,
5453            "PoolCreated",
5454            "Swap",
5455            "Mint",
5456            "Burn",
5457            "Collect",
5458        ));
5459        let token0 = Token::new(
5460            chain.clone(),
5461            "0xa0b86a33e6441c8c06dd7b111a8c4e82e2b2a5e1"
5462                .parse()
5463                .unwrap(),
5464            "USDC".into(),
5465            "USD Coin".into(),
5466            6,
5467        );
5468        let token1 = Token::new(
5469            chain.clone(),
5470            "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
5471                .parse()
5472                .unwrap(),
5473            "WETH".into(),
5474            "Wrapped Ether".into(),
5475            18,
5476        );
5477        let pool_address = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8"
5478            .parse()
5479            .unwrap();
5480        let pool_identifier: PoolIdentifier = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8"
5481            .parse()
5482            .unwrap();
5483
5484        let pool = Pool::new(
5485            chain.clone(),
5486            dex.clone(),
5487            pool_address,
5488            pool_identifier,
5489            12345,
5490            token0,
5491            token1,
5492            Some(500),
5493            Some(10),
5494            UnixNanos::default(),
5495        );
5496
5497        (chain, dex, pool)
5498    }
5499
5500    #[cfg(feature = "defi")]
5501    fn sample_pool_swap() -> PoolSwap {
5502        let (chain, dex, pool) = sample_pool_components();
5503        PoolSwap::new(
5504            chain,
5505            dex,
5506            pool.instrument_id,
5507            pool.pool_identifier,
5508            12345,
5509            "0xabc123".to_string(),
5510            0,
5511            0,
5512            UnixNanos::default(),
5513            UnixNanos::default(),
5514            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
5515                .parse()
5516                .unwrap(),
5517            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
5518                .parse()
5519                .unwrap(),
5520            I256::from_str("1000000000000000000").unwrap(),
5521            I256::from_str("400000000000000").unwrap(),
5522            U160::from(59000000000000u128),
5523            1000000,
5524            100,
5525        )
5526    }
5527
5528    #[cfg(feature = "defi")]
5529    fn sample_pool_liquidity_update() -> PoolLiquidityUpdate {
5530        let (chain, dex, pool) = sample_pool_components();
5531        PoolLiquidityUpdate::new(
5532            chain,
5533            dex,
5534            pool.instrument_id,
5535            pool.pool_identifier,
5536            PoolLiquidityUpdateType::Mint,
5537            12345,
5538            "0xabc123".to_string(),
5539            0,
5540            0,
5541            Some(
5542                "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
5543                    .parse()
5544                    .unwrap(),
5545            ),
5546            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
5547                .parse()
5548                .unwrap(),
5549            1000,
5550            U256::from(1_000u64),
5551            U256::from(2_000u64),
5552            -10,
5553            10,
5554            UnixNanos::default(),
5555            UnixNanos::default(),
5556        )
5557    }
5558
5559    #[cfg(feature = "defi")]
5560    fn sample_pool_fee_collect() -> PoolFeeCollect {
5561        let (chain, dex, pool) = sample_pool_components();
5562        PoolFeeCollect::new(
5563            chain,
5564            dex,
5565            pool.instrument_id,
5566            pool.pool_identifier,
5567            12345,
5568            "0xabc123".to_string(),
5569            0,
5570            0,
5571            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
5572                .parse()
5573                .unwrap(),
5574            100,
5575            200,
5576            -10,
5577            10,
5578            UnixNanos::default(),
5579            UnixNanos::default(),
5580        )
5581    }
5582
5583    #[cfg(feature = "defi")]
5584    fn sample_pool_flash() -> PoolFlash {
5585        let (chain, dex, pool) = sample_pool_components();
5586        PoolFlash::new(
5587            chain,
5588            dex,
5589            pool.instrument_id,
5590            pool.pool_identifier,
5591            12345,
5592            "0xabc123".to_string(),
5593            0,
5594            0,
5595            UnixNanos::default(),
5596            UnixNanos::default(),
5597            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
5598                .parse()
5599                .unwrap(),
5600            "0x742E4422b21FB8B4dF463F28689AC98bD56c39e0"
5601                .parse()
5602                .unwrap(),
5603            U256::from(100u64),
5604            U256::from(200u64),
5605            U256::from(101u64),
5606            U256::from(201u64),
5607        )
5608    }
5609
5610    #[rstest]
5611    #[case("request_data")]
5612    #[case("request_instrument")]
5613    #[case("request_instruments")]
5614    #[case("request_book_snapshot")]
5615    #[case("request_book_deltas")]
5616    #[case("request_book_depth")]
5617    #[case("request_quotes")]
5618    #[case("request_trades")]
5619    #[case("request_funding_rates")]
5620    #[case("request_bars")]
5621    fn test_python_request_completion_callback_and_pending(
5622        #[case] method: &str,
5623        clock: Rc<RefCell<VirtualClock>>,
5624        cache: Rc<RefCell<Cache>>,
5625        trader_id: TraderId,
5626    ) {
5627        Python::initialize();
5628        Python::attach(|py| {
5629            *get_message_bus().borrow_mut() = MessageBus::default();
5630            let actor = Py::new(py, create_registered_actor(clock, cache, trader_id)).unwrap();
5631            let module = PyModule::from_code(
5632                py,
5633                c_str!(
5634                    r#"
5635class CompletionActor:
5636    def __init__(self, native):
5637        self.native = native
5638        self.events = []
5639    def on_data_response(self, response):
5640        self.events.append(("response", response.correlation_id, response.completed))
5641        assert self.native.is_pending_request(response.correlation_id) == (not response.completed)
5642    def complete(self, request_id):
5643        assert not self.native.is_pending_request(request_id)
5644        assert self.native.pending_requests() == set()
5645        assert not self.native.has_pending_requests()
5646        self.events.append(("callback", request_id, True))
5647        self.next_request = self.native.request_instrument(instrument_id)
5648    def submit(self, method):
5649        if method == "request_data":
5650            args = (data_type, client_id)
5651        elif method == "request_instruments":
5652            args = ()
5653        elif method == "request_bars":
5654            args = (bar_type,)
5655        else:
5656            args = (instrument_id,)
5657        return getattr(self.native, method)(*args, callback=self.complete)
5658"#
5659                ),
5660                c_str!("completion_actor.py"),
5661                c_str!("completion_actor"),
5662            )
5663            .unwrap();
5664            module
5665                .setattr("instrument_id", sample_quote().instrument_id)
5666                .unwrap();
5667            module.setattr("bar_type", sample_bar().bar_type).unwrap();
5668            module
5669                .setattr("data_type", DataType::new("TestData", None, None))
5670                .unwrap();
5671            module
5672                .setattr("client_id", ClientId::new("HISTORY"))
5673                .unwrap();
5674            let tracker = module
5675                .getattr("CompletionActor")
5676                .unwrap()
5677                .call1((actor.clone_ref(py),))
5678                .unwrap();
5679            actor.borrow_mut(py).set_python_instance(&tracker).unwrap();
5680            actor.borrow(py).register_in_global_registries().unwrap();
5681            let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
5682            msgbus::register_data_command_endpoint(
5683                MessagingSwitchboard::data_engine_queue_execute(),
5684                handler,
5685            );
5686            let callback = tracker.getattr("complete").unwrap();
5687            tracker.setattr("complete", 42).unwrap();
5688            let error = tracker.call_method1("submit", (method,)).unwrap_err();
5689            assert!(error.is_instance_of::<pyo3::exceptions::PyTypeError>(py));
5690            assert!(saver.get_messages().is_empty());
5691            assert!(actor.borrow(py).inner().core.pending_requests().is_empty());
5692            tracker.setattr("complete", callback).unwrap();
5693            let root = tracker
5694                .call_method1("submit", (method,))
5695                .unwrap()
5696                .extract::<String>()
5697                .unwrap();
5698            let id: UUID4 = root.parse().unwrap();
5699            assert_eq!(
5700                actor
5701                    .bind(py)
5702                    .call_method0("pending_requests")
5703                    .unwrap()
5704                    .extract::<std::collections::HashSet<String>>()
5705                    .unwrap(),
5706                [root.clone()].into()
5707            );
5708            let terminal = crate::messages::data::DataResponseEnvelope::terminal(
5709                id,
5710                Some("empty result failure".to_owned()),
5711            );
5712            msgbus::send_response_envelope(&terminal);
5713            msgbus::send_response_envelope(&terminal);
5714            assert_eq!(
5715                tracker
5716                    .getattr("events")
5717                    .unwrap()
5718                    .extract::<Vec<(String, String, bool)>>()
5719                    .unwrap(),
5720                vec![
5721                    ("response".to_owned(), root.clone(), true),
5722                    ("callback".to_owned(), root, true)
5723                ]
5724            );
5725            let next = tracker
5726                .getattr("next_request")
5727                .unwrap()
5728                .extract::<String>()
5729                .unwrap();
5730            assert_eq!(
5731                actor
5732                    .bind(py)
5733                    .call_method0("pending_requests")
5734                    .unwrap()
5735                    .extract::<std::collections::HashSet<String>>()
5736                    .unwrap(),
5737                [next].into()
5738            );
5739            assert_eq!(saver.get_messages().len(), 2);
5740        });
5741    }
5742
5743    #[rstest]
5744    #[case::success(false)]
5745    #[case::failure(true)]
5746    fn test_python_request_join_callback_order_and_late_join(
5747        #[case] failed: bool,
5748        #[values("none", "hook", "callback")] raises: &str,
5749        clock: Rc<RefCell<VirtualClock>>,
5750        cache: Rc<RefCell<Cache>>,
5751        trader_id: TraderId,
5752    ) {
5753        Python::initialize();
5754        Python::attach(|py| {
5755            *get_message_bus().borrow_mut() = MessageBus::default();
5756            let actor = Py::new(py, create_registered_actor(clock, cache, trader_id)).unwrap();
5757            let module = PyModule::from_code(
5758                py,
5759                c_str!(
5760                    r#"
5761class JoinActor:
5762    def __init__(self, native):
5763        self.native = native
5764        self.events = []
5765    def on_historical_quotes(self, quotes):
5766        self.events.append(("payload", len(quotes)))
5767    def on_data_response(self, response):
5768        self.events.append(("response", response.correlation_id))
5769        self.failure = response.failure_reason
5770        assert self.native.is_pending_request(response.correlation_id) == (not response.completed)
5771        if raises == "hook":
5772            raise RuntimeError("response hook failed")
5773    def complete(self, request_id):
5774        assert not self.native.is_pending_request(request_id)
5775        self.events.append(("callback", request_id))
5776        if raises == "callback":
5777            raise RuntimeError("completion callback failed")
5778    def late_complete(self, request_id):
5779        assert self.native.pending_requests() == set()
5780        self.events.append(("late", request_id))
5781        self.native.reset()
5782    def on_reset(self):
5783        pass
5784"#
5785                ),
5786                c_str!("join_actor.py"),
5787                c_str!("join_actor"),
5788            )
5789            .unwrap();
5790            module.setattr("raises", raises).unwrap();
5791            let tracker = module
5792                .getattr("JoinActor")
5793                .unwrap()
5794                .call1((actor.clone_ref(py),))
5795                .unwrap();
5796            actor.borrow_mut(py).set_python_instance(&tracker).unwrap();
5797            actor.borrow(py).register_in_global_registries().unwrap();
5798            let (handler, _) = get_typed_into_message_saving_handler::<DataCommand>(None);
5799            msgbus::register_data_command_endpoint(
5800                MessagingSwitchboard::data_engine_queue_execute(),
5801                handler,
5802            );
5803            let kwargs = PyDict::new(py);
5804            kwargs
5805                .set_item("callback", tracker.getattr("complete").unwrap())
5806                .unwrap();
5807            let quote = sample_quote();
5808            let root = actor
5809                .bind(py)
5810                .call_method("request_quotes", (quote.instrument_id,), Some(&kwargs))
5811                .unwrap()
5812                .extract::<String>()
5813                .unwrap();
5814
5815            for (children, message) in [
5816                (
5817                    Vec::<String>::new(),
5818                    "Request group must not be empty".to_owned(),
5819                ),
5820                (
5821                    vec![root.clone(), root.clone()],
5822                    format!("Duplicate request ID in group: {root}"),
5823                ),
5824                (
5825                    vec![UUID4::new().to_string()],
5826                    "Unknown request ID:".to_owned(),
5827                ),
5828            ] {
5829                let error = actor
5830                    .bind(py)
5831                    .call_method("request_join", (children,), Some(&kwargs))
5832                    .unwrap_err();
5833                assert!(error.is_instance_of::<pyo3::exceptions::PyValueError>(py));
5834                assert!(error.to_string().contains(&message));
5835                assert_eq!(actor.borrow(py).inner().core.pending_requests().len(), 1);
5836            }
5837
5838            let group = actor
5839                .bind(py)
5840                .call_method("request_join", (vec![root.clone()],), Some(&kwargs))
5841                .unwrap()
5842                .extract::<String>()
5843                .unwrap();
5844            let id: UUID4 = root.parse().unwrap();
5845
5846            let payload = QuotesResponse::new(
5847                id,
5848                ClientId::new("HISTORY"),
5849                quote.instrument_id,
5850                vec![quote],
5851                None,
5852                None,
5853                UnixNanos::from(47),
5854                None,
5855            );
5856
5857            let intermediate = crate::messages::data::DataResponseEnvelope::new(
5858                crate::messages::data::DataResponse::Quotes(payload),
5859                false,
5860                None,
5861            );
5862            msgbus::send_response_envelope(&intermediate);
5863            let failure = failed.then(|| "partial failure".to_owned());
5864            let terminal = crate::messages::data::DataResponseEnvelope::terminal(id, failure);
5865            msgbus::send_response_envelope(&terminal);
5866            msgbus::send_response_envelope(&terminal);
5867            let events = tracker
5868                .getattr("events")
5869                .unwrap()
5870                .repr()
5871                .unwrap()
5872                .to_string();
5873            assert_eq!(
5874                events,
5875                format!(
5876                    "[('payload', 1), ('response', '{root}'), ('response', '{root}'), ('callback', '{root}'), ('response', '{group}'), ('callback', '{group}')]"
5877                )
5878            );
5879            assert_eq!(
5880                tracker
5881                    .getattr("failure")
5882                    .unwrap()
5883                    .extract::<Option<String>>()
5884                    .unwrap(),
5885                failed.then(|| format!("Request {root}: partial failure"))
5886            );
5887            kwargs
5888                .set_item("callback", tracker.getattr("late_complete").unwrap())
5889                .unwrap();
5890            let late = UUID4::new().to_string();
5891            kwargs.set_item("request_id", &late).unwrap();
5892            let returned = actor
5893                .bind(py)
5894                .call_method("request_join", (vec![root, group],), Some(&kwargs))
5895                .unwrap()
5896                .extract::<String>()
5897                .unwrap();
5898            assert_eq!(returned, late);
5899            assert_eq!(
5900                tracker
5901                    .getattr("events")
5902                    .unwrap()
5903                    .get_item(-1)
5904                    .unwrap()
5905                    .extract::<(String, String)>()
5906                    .unwrap(),
5907                ("late".to_owned(), late)
5908            );
5909        });
5910    }
5911
5912    #[rstest]
5913    #[case::reset("reset")]
5914    #[case::dispose("dispose")]
5915    #[case::fault("fault")]
5916    fn test_python_request_callbacks_release_on_cleanup(
5917        #[case] action: &str,
5918        clock: Rc<RefCell<VirtualClock>>,
5919        cache: Rc<RefCell<Cache>>,
5920        trader_id: TraderId,
5921    ) {
5922        Python::initialize();
5923        Python::attach(|py| {
5924            let bus = py
5925                .get_type::<crate::python::msgbus::PyMessageBus>()
5926                .call1((trader_id,))
5927                .unwrap();
5928            let actor = Py::new(py, create_registered_actor(clock, cache, trader_id)).unwrap();
5929            actor
5930                .borrow_mut(py)
5931                .set_python_instance(actor.bind(py).as_any())
5932                .unwrap();
5933            actor.borrow(py).register_in_global_registries().unwrap();
5934            let (handler, _) = get_typed_into_message_saving_handler::<DataCommand>(None);
5935            msgbus::register_data_command_endpoint(
5936                MessagingSwitchboard::data_engine_queue_execute(),
5937                handler,
5938            );
5939            let module = PyModule::from_code(
5940                py,
5941                c_str!(
5942                    r#"
5943released = []
5944called = []
5945class Callback:
5946    def __init__(self, name):
5947        self.name = name
5948    def __call__(self, request_id):
5949        called.append(request_id)
5950    def __del__(self):
5951        bus.endpoints()
5952        released.append(self.name)
5953"#
5954                ),
5955                c_str!("cleanup_callbacks.py"),
5956                c_str!("cleanup_callbacks"),
5957            )
5958            .unwrap();
5959            module.setattr("bus", bus).unwrap();
5960            let callback = module
5961                .getattr("Callback")
5962                .unwrap()
5963                .call1(("request",))
5964                .unwrap();
5965            let kwargs = PyDict::new(py);
5966            kwargs.set_item("callback", &callback).unwrap();
5967            let root = actor
5968                .bind(py)
5969                .call_method(
5970                    "request_quotes",
5971                    (sample_quote().instrument_id,),
5972                    Some(&kwargs),
5973                )
5974                .unwrap()
5975                .extract::<String>()
5976                .unwrap();
5977            drop(callback);
5978            let callback = module
5979                .getattr("Callback")
5980                .unwrap()
5981                .call1(("group",))
5982                .unwrap();
5983            kwargs.set_item("callback", &callback).unwrap();
5984            let group = actor
5985                .bind(py)
5986                .call_method("request_join", (vec![root.clone()],), Some(&kwargs))
5987                .unwrap()
5988                .extract::<String>()
5989                .unwrap();
5990            drop(callback);
5991            drop(kwargs);
5992
5993            if action == "fault" {
5994                actor.bind(py).call_method0("start").unwrap();
5995            }
5996
5997            actor.bind(py).call_method0(action).unwrap();
5998            let root: UUID4 = root.parse().unwrap();
5999            msgbus::send_response_envelope(&crate::messages::data::DataResponseEnvelope::terminal(
6000                root, None,
6001            ));
6002            let mut released = module
6003                .getattr("released")
6004                .unwrap()
6005                .extract::<Vec<String>>()
6006                .unwrap();
6007            released.sort();
6008            assert_eq!(released, vec!["group", "request"]);
6009            assert_eq!(
6010                module
6011                    .getattr("called")
6012                    .unwrap()
6013                    .extract::<Vec<String>>()
6014                    .unwrap(),
6015                Vec::<String>::new()
6016            );
6017            assert!(
6018                !actor
6019                    .bind(py)
6020                    .call_method1("is_pending_request", (group,))
6021                    .unwrap()
6022                    .extract::<bool>()
6023                    .unwrap()
6024            );
6025            assert!(actor.borrow(py).inner().core.pending_requests().is_empty());
6026        });
6027    }
6028
6029    #[rstest]
6030    #[case::payload("payload")]
6031    #[case::root_hook("root")]
6032    #[case::group_hook("group")]
6033    fn test_python_request_callback_reset_during_hook(
6034        #[case] reset_at: &str,
6035        clock: Rc<RefCell<VirtualClock>>,
6036        cache: Rc<RefCell<Cache>>,
6037        trader_id: TraderId,
6038    ) {
6039        Python::initialize();
6040        Python::attach(|py| {
6041            *get_message_bus().borrow_mut() = MessageBus::default();
6042            let actor = Py::new(py, create_registered_actor(clock, cache, trader_id)).unwrap();
6043            let module = PyModule::from_code(
6044                py,
6045                c_str!(
6046                    r#"
6047class ResetActor:
6048    def __init__(self, native, reset_at):
6049        self.native = native
6050        self.reset_at = reset_at
6051        self.events = []
6052    def on_reset(self):
6053        pass
6054    def on_historical_quotes(self, quotes):
6055        self.events.append("payload")
6056        if self.reset_at == "payload":
6057            self.native.reset()
6058    def on_data_response(self, response):
6059        kind = "root" if response.correlation_id == self.root else "group"
6060        self.events.append(kind)
6061        if self.reset_at == kind:
6062            self.native.reset()
6063    def complete(self, request_id):
6064        assert not self.native.is_pending_request(request_id)
6065        self.events.append("callback:" + request_id)
6066"#
6067                ),
6068                c_str!("reset_callback_actor.py"),
6069                c_str!("reset_callback_actor"),
6070            )
6071            .unwrap();
6072            let tracker = module
6073                .getattr("ResetActor")
6074                .unwrap()
6075                .call1((actor.clone_ref(py), reset_at))
6076                .unwrap();
6077            actor.borrow_mut(py).set_python_instance(&tracker).unwrap();
6078            actor.borrow(py).register_in_global_registries().unwrap();
6079            let (handler, _) = get_typed_into_message_saving_handler::<DataCommand>(None);
6080            msgbus::register_data_command_endpoint(
6081                MessagingSwitchboard::data_engine_queue_execute(),
6082                handler,
6083            );
6084            let kwargs = PyDict::new(py);
6085            kwargs
6086                .set_item("callback", tracker.getattr("complete").unwrap())
6087                .unwrap();
6088            let quote = sample_quote();
6089            let root = actor
6090                .bind(py)
6091                .call_method("request_quotes", (quote.instrument_id,), Some(&kwargs))
6092                .unwrap()
6093                .extract::<String>()
6094                .unwrap();
6095            tracker.setattr("root", &root).unwrap();
6096            actor
6097                .bind(py)
6098                .call_method("request_join", (vec![root.clone()],), Some(&kwargs))
6099                .unwrap();
6100            let id: UUID4 = root.parse().unwrap();
6101
6102            let response = QuotesResponse::new(
6103                id,
6104                ClientId::new("HISTORY"),
6105                quote.instrument_id,
6106                vec![quote],
6107                None,
6108                None,
6109                UnixNanos::from(47),
6110                None,
6111            );
6112            msgbus::send_response(&id, &crate::messages::data::DataResponse::Quotes(response));
6113
6114            let expected = match reset_at {
6115                "payload" => vec!["payload".to_owned()],
6116                "root" => vec!["payload".to_owned(), "root".to_owned()],
6117                "group" => vec![
6118                    "payload".to_owned(),
6119                    "root".to_owned(),
6120                    format!("callback:{root}"),
6121                    "group".to_owned(),
6122                ],
6123                _ => unreachable!(),
6124            };
6125
6126            assert_eq!(
6127                tracker
6128                    .getattr("events")
6129                    .unwrap()
6130                    .extract::<Vec<String>>()
6131                    .unwrap(),
6132                expected
6133            );
6134            assert!(actor.borrow(py).inner().core.pending_requests().is_empty());
6135        });
6136    }
6137
6138    #[rstest]
6139    fn test_python_response_hook_can_submit_next_request(
6140        clock: Rc<RefCell<VirtualClock>>,
6141        cache: Rc<RefCell<Cache>>,
6142        trader_id: TraderId,
6143    ) {
6144        Python::initialize();
6145        Python::attach(|py| {
6146            *get_message_bus().borrow_mut() = MessageBus::default();
6147            let actor = Py::new(py, create_registered_actor(clock, cache, trader_id)).unwrap();
6148            let module = PyModule::from_code(
6149                py,
6150                c_str!(
6151                    r#"
6152class ResponseActor:
6153    def __init__(self, native):
6154        self.native = native
6155        self.events = []
6156        self.next_request = None
6157    def on_historical_quotes(self, quotes):
6158        self.events.append("quotes")
6159        self.native.request_instrument(quotes[0].instrument_id)
6160    def on_data_response(self, response):
6161        self.events.append("complete")
6162        self.next_request = self.native.request_trades(response.data[0].instrument_id)
6163"#
6164                ),
6165                c_str!("response_actor.py"),
6166                c_str!("response_actor"),
6167            )
6168            .unwrap();
6169            let tracker = module
6170                .getattr("ResponseActor")
6171                .unwrap()
6172                .call1((actor.clone_ref(py),))
6173                .unwrap();
6174            actor.borrow_mut(py).set_python_instance(&tracker).unwrap();
6175            actor.borrow(py).register_in_global_registries().unwrap();
6176            let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
6177            msgbus::register_data_command_endpoint(
6178                MessagingSwitchboard::data_engine_queue_execute(),
6179                handler,
6180            );
6181            let quote = sample_quote();
6182            let root = actor
6183                .bind(py)
6184                .call_method1("request_quotes", (quote.instrument_id,))
6185                .unwrap()
6186                .extract::<String>()
6187                .unwrap();
6188            let root: UUID4 = root.parse().unwrap();
6189
6190            let response = QuotesResponse::new(
6191                root,
6192                ClientId::new("HISTORY"),
6193                quote.instrument_id,
6194                vec![quote],
6195                None,
6196                None,
6197                UnixNanos::from(47),
6198                None,
6199            );
6200            msgbus::send_response(
6201                &root,
6202                &crate::messages::data::DataResponse::Quotes(response),
6203            );
6204            let commands = saver.get_messages();
6205            assert_eq!(commands.len(), 3);
6206            assert!(
6207                matches!(&commands[1], DataCommand::Request(crate::messages::data::RequestCommand::Instrument(r)) if r.instrument_ids == vec![quote.instrument_id])
6208            );
6209
6210            let DataCommand::Request(crate::messages::data::RequestCommand::Trades(next)) =
6211                &commands[2]
6212            else {
6213                panic!("expected chained trade request");
6214            };
6215
6216            assert_eq!(
6217                tracker
6218                    .getattr("events")
6219                    .unwrap()
6220                    .extract::<Vec<String>>()
6221                    .unwrap(),
6222                vec!["quotes", "complete"]
6223            );
6224            assert_eq!(
6225                tracker
6226                    .getattr("next_request")
6227                    .unwrap()
6228                    .extract::<String>()
6229                    .unwrap(),
6230                next.request_id.to_string()
6231            );
6232            assert!(!actor.borrow(py).inner().core.is_pending_request(&root));
6233            assert!(
6234                actor
6235                    .borrow(py)
6236                    .inner()
6237                    .core
6238                    .is_pending_request(&next.request_id)
6239            );
6240        });
6241    }
6242
6243    #[rstest]
6244    #[case::complete("none")]
6245    #[case::payload_reset("payload")]
6246    #[case::child_reset("child")]
6247    #[case::group_reset("group")]
6248    #[case::child_error("error")]
6249    fn test_python_request_group_delivery_releases_actor_access(
6250        #[case] reset_at: &str,
6251        clock: Rc<RefCell<VirtualClock>>,
6252        cache: Rc<RefCell<Cache>>,
6253        trader_id: TraderId,
6254    ) {
6255        Python::initialize();
6256        Python::attach(|py| {
6257            *get_message_bus().borrow_mut() = MessageBus::default();
6258            let actor = Py::new(py, create_registered_actor(clock, cache, trader_id)).unwrap();
6259            let module = PyModule::from_code(
6260                py,
6261                c_str!(
6262                    r#"
6263class ResponseActor:
6264    def __init__(self, native, reset_at):
6265        self.native = native
6266        self.reset_at = reset_at
6267        self.events = []
6268        self.next_request = None
6269    def on_reset(self):
6270        pass
6271    def on_historical_quotes(self, quotes):
6272        self.events.append("quotes")
6273        self.instrument_id = quotes[0].instrument_id
6274
6275        if self.reset_at == "payload":
6276            self.native.reset()
6277    def on_data_response(self, response):
6278        assert response.completed
6279        assert response.failure_reason is None
6280        self.events.append(response.correlation_id)
6281        if response.correlation_id == self.root:
6282
6283            if self.reset_at == "child":
6284                self.native.reset()
6285            elif self.reset_at == "error":
6286                raise RuntimeError("response hook failed")
6287        else:
6288            assert response.data is None
6289
6290            if self.reset_at == "group":
6291                self.native.reset()
6292            else:
6293                self.next_request = self.native.request_trades(self.instrument_id)
6294"#
6295                ),
6296                c_str!("group_actor.py"),
6297                c_str!("group_actor"),
6298            )
6299            .unwrap();
6300            let tracker = module
6301                .getattr("ResponseActor")
6302                .unwrap()
6303                .call1((actor.clone_ref(py), reset_at))
6304                .unwrap();
6305            actor.borrow_mut(py).set_python_instance(&tracker).unwrap();
6306            actor.borrow(py).register_in_global_registries().unwrap();
6307            let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
6308            msgbus::register_data_command_endpoint(
6309                MessagingSwitchboard::data_engine_queue_execute(),
6310                handler,
6311            );
6312            let quote = sample_quote();
6313            let root = actor
6314                .bind(py)
6315                .call_method1("request_quotes", (quote.instrument_id,))
6316                .unwrap()
6317                .extract::<String>()
6318                .unwrap();
6319            tracker.setattr("root", &root).unwrap();
6320            let root: UUID4 = root.parse().unwrap();
6321            let first = actor
6322                .borrow(py)
6323                .inner()
6324                .core
6325                .register_request_group(vec![root], None, None)
6326                .unwrap();
6327            let second = actor
6328                .borrow(py)
6329                .inner()
6330                .core
6331                .register_request_group(vec![root], None, None)
6332                .unwrap();
6333
6334            let payload = QuotesResponse::new(
6335                root,
6336                ClientId::new("SOURCE"),
6337                quote.instrument_id,
6338                vec![quote],
6339                None,
6340                None,
6341                UnixNanos::from(47),
6342                None,
6343            );
6344            msgbus::send_response(&root, &crate::messages::data::DataResponse::Quotes(payload));
6345            let mut expected = vec!["quotes".to_owned()];
6346            if reset_at != "payload" {
6347                expected.push(root.to_string());
6348            }
6349
6350            if !matches!(reset_at, "payload" | "child") {
6351                expected.push(first.to_string());
6352            }
6353
6354            if matches!(reset_at, "none" | "error") {
6355                expected.push(second.to_string());
6356            }
6357
6358            assert_eq!(
6359                tracker
6360                    .getattr("events")
6361                    .unwrap()
6362                    .extract::<Vec<String>>()
6363                    .unwrap(),
6364                expected
6365            );
6366            assert!(!actor.borrow(py).inner().core.is_pending_request(&root));
6367            assert!(!actor.borrow(py).inner().core.is_pending_request(&first));
6368            assert!(!actor.borrow(py).inner().core.is_pending_request(&second));
6369            let commands = saver.get_messages();
6370
6371            if matches!(reset_at, "none" | "error") {
6372                let [
6373                    DataCommand::Request(RequestCommand::Quotes(_)),
6374                    DataCommand::Request(RequestCommand::Trades(first)),
6375                    DataCommand::Request(RequestCommand::Trades(second)),
6376                ] = commands.as_slice()
6377                else {
6378                    panic!("expected quotes and two chained trades requests")
6379                };
6380
6381                assert_eq!(first.instrument_ids, vec![quote.instrument_id]);
6382                assert_eq!(second.instrument_ids, vec![quote.instrument_id]);
6383                assert_eq!(
6384                    tracker
6385                        .getattr("next_request")
6386                        .unwrap()
6387                        .extract::<String>()
6388                        .unwrap(),
6389                    second.request_id.to_string()
6390                );
6391            } else {
6392                assert_eq!(commands.len(), 1);
6393                assert!(tracker.getattr("next_request").unwrap().is_none());
6394            }
6395        });
6396    }
6397
6398    const TRACKING_ACTOR_CODE: &std::ffi::CStr = c_str!(
6399        r#"
6400class TrackingActor:
6401    """A mock Python actor that tracks all method calls."""
6402
6403    TRACKED_METHODS = {
6404        "on_start",
6405        "on_stop",
6406        "on_resume",
6407        "on_reset",
6408        "on_dispose",
6409        "on_degrade",
6410        "on_fault",
6411        "on_save",
6412        "on_load",
6413        "on_time_event",
6414        "on_data",
6415        "on_signal",
6416        "on_queue_state",
6417        "on_socket_state",
6418        "on_instrument",
6419        "on_quote",
6420        "on_trade",
6421        "on_bar",
6422        "on_book",
6423        "on_book_deltas",
6424        "on_book_depth",
6425        "on_mark_price",
6426        "on_index_price",
6427        "on_funding_rate",
6428        "on_instrument_status",
6429        "on_instrument_close",
6430        "on_option_greeks",
6431        "on_option_chain",
6432        "on_historical_data",
6433        "on_historical_book_deltas",
6434        "on_historical_book_depth",
6435        "on_historical_quotes",
6436        "on_historical_trades",
6437        "on_historical_funding_rates",
6438        "on_historical_bars",
6439        "on_historical_mark_prices",
6440        "on_historical_index_prices",
6441        "on_block",
6442        "on_pool",
6443        "on_pool_swap",
6444        "on_pool_liquidity_update",
6445        "on_pool_fee_collect",
6446        "on_pool_flash",
6447    }
6448
6449    def __init__(self):
6450        self.calls = []
6451        self.raises = False
6452
6453    def _record(self, method_name, *args):
6454        self.calls.append((method_name, args))
6455        if self.raises:
6456            raise RuntimeError("actor callback failure")
6457
6458    def was_called(self, method_name):
6459        return any(call[0] == method_name for call in self.calls)
6460
6461    def call_count(self, method_name):
6462        return sum(1 for call in self.calls if call[0] == method_name)
6463
6464    def last_call_args(self, method_name):
6465        for called_method, args in reversed(self.calls):
6466            if called_method == method_name:
6467                return args
6468        raise AssertionError(f"{method_name} was not called")
6469
6470    def last_loaded_state(self):
6471
6472        for method_name, args in reversed(self.calls):
6473            if method_name == "on_load":
6474                return args[0]
6475        return None
6476
6477    def on_save(self):
6478        self._record("on_save")
6479        return {"actor": b"saved"}
6480
6481    def on_load(self, state):
6482        self._record("on_load", dict(state))
6483
6484    def __getattr__(self, name):
6485        if name in self.TRACKED_METHODS:
6486            return lambda *args: self._record(name, *args)
6487        raise AttributeError(name)
6488"#
6489    );
6490
6491    fn create_tracking_python_actor(py: Python<'_>) -> PyResult<Py<PyAny>> {
6492        py.run(TRACKING_ACTOR_CODE, None, None)?;
6493        let tracking_actor_class = py.eval(c_str!("TrackingActor"), None, None)?;
6494        let instance = tracking_actor_class.call0()?;
6495        Ok(instance.unbind())
6496    }
6497
6498    fn python_method_was_called(py_actor: &Py<PyAny>, py: Python<'_>, method_name: &str) -> bool {
6499        py_actor
6500            .call_method1(py, "was_called", (method_name,))
6501            .and_then(|r| r.extract::<bool>(py))
6502            .unwrap_or(false)
6503    }
6504
6505    fn python_method_call_count(py_actor: &Py<PyAny>, py: Python<'_>, method_name: &str) -> i32 {
6506        py_actor
6507            .call_method1(py, "call_count", (method_name,))
6508            .and_then(|r| r.extract::<i32>(py))
6509            .unwrap_or(0)
6510    }
6511
6512    fn python_last_loaded_state(
6513        py_actor: &Py<PyAny>,
6514        py: Python<'_>,
6515    ) -> Option<HashMap<String, Vec<u8>>> {
6516        py_actor
6517            .call_method0(py, "last_loaded_state")
6518            .and_then(|result| result.extract::<Option<HashMap<String, Vec<u8>>>>(py))
6519            .unwrap_or(None)
6520    }
6521
6522    const TRACKING_INDICATOR_CODE: &std::ffi::CStr = c_str!(
6523        r#"
6524class TrackingIndicator:
6525    def __init__(self, events=None):
6526        self.initialized = False
6527        self.calls = []
6528        self.events = events
6529
6530    def handle_quote_tick(self, quote):
6531        self.calls.append("quote")
6532        if self.events is not None:
6533            self.events.append("indicator:quote")
6534
6535    def handle_trade_tick(self, trade):
6536        self.calls.append("trade")
6537        if self.events is not None:
6538            self.events.append("indicator:trade")
6539
6540    def handle_bar(self, bar):
6541        self.calls.append("bar")
6542        if self.events is not None:
6543            self.events.append("indicator:bar")
6544
6545    def call_count(self, name):
6546        return self.calls.count(name)
6547
6548class RaisingIndicator:
6549    initialized = True
6550
6551    def handle_quote_tick(self, quote):
6552        raise RuntimeError("indicator failed")
6553
6554class IndicatorEventActor:
6555    def __init__(self, events):
6556        self.events = events
6557
6558    def on_start(self):
6559        pass
6560
6561    def on_quote(self, quote):
6562        self.events.append("actor:quote")
6563
6564    def on_trade(self, trade):
6565        self.events.append("actor:trade")
6566
6567    def on_bar(self, bar):
6568        self.events.append("actor:bar")
6569"#
6570    );
6571
6572    fn create_tracking_python_indicator(py: Python<'_>) -> PyResult<Py<PyAny>> {
6573        py.run(TRACKING_INDICATOR_CODE, None, None)?;
6574        let indicator_class = py.eval(c_str!("TrackingIndicator"), None, None)?;
6575        Ok(indicator_class.call0()?.unbind())
6576    }
6577
6578    fn create_event_tracking_python_indicator(
6579        py: Python<'_>,
6580        events: &Bound<'_, PyList>,
6581    ) -> PyResult<Py<PyAny>> {
6582        py.run(TRACKING_INDICATOR_CODE, None, None)?;
6583        let indicator_class = py.eval(c_str!("TrackingIndicator"), None, None)?;
6584        Ok(indicator_class.call1((events,))?.unbind())
6585    }
6586
6587    fn create_raising_python_indicator(py: Python<'_>) -> PyResult<Py<PyAny>> {
6588        py.run(TRACKING_INDICATOR_CODE, None, None)?;
6589        let indicator_class = py.eval(c_str!("RaisingIndicator"), None, None)?;
6590        Ok(indicator_class.call0()?.unbind())
6591    }
6592
6593    fn create_indicator_event_actor(
6594        py: Python<'_>,
6595        events: &Bound<'_, PyList>,
6596    ) -> PyResult<Py<PyAny>> {
6597        py.run(TRACKING_INDICATOR_CODE, None, None)?;
6598        let actor_class = py.eval(c_str!("IndicatorEventActor"), None, None)?;
6599        Ok(actor_class.call1((events,))?.unbind())
6600    }
6601
6602    fn python_indicator_call_count(
6603        indicator: &Py<PyAny>,
6604        py: Python<'_>,
6605        method_name: &str,
6606    ) -> i32 {
6607        indicator
6608            .call_method1(py, "call_count", (method_name,))
6609            .and_then(|r| r.extract::<i32>(py))
6610            .unwrap_or(0)
6611    }
6612
6613    fn assert_python_dispatch<F>(
6614        py: Python<'_>,
6615        clock: Rc<RefCell<VirtualClock>>,
6616        cache: Rc<RefCell<Cache>>,
6617        trader_id: TraderId,
6618        method_name: &str,
6619        invoke: F,
6620    ) -> Py<PyAny>
6621    where
6622        F: FnOnce(&mut PyDataActor) -> anyhow::Result<()>,
6623    {
6624        let py_actor = create_tracking_python_actor(py).unwrap();
6625
6626        let mut rust_actor = PyDataActor::new(None);
6627        rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
6628        rust_actor.register(trader_id, clock, cache).unwrap();
6629
6630        let result = invoke(&mut rust_actor);
6631
6632        assert!(result.is_ok());
6633        assert!(python_method_was_called(&py_actor, py, method_name));
6634        assert_eq!(python_method_call_count(&py_actor, py, method_name), 1);
6635
6636        py_actor
6637    }
6638
6639    #[rstest]
6640    fn test_python_actor_callback_exception_preserves_traceback() {
6641        Python::initialize();
6642        Python::attach(|py| {
6643            let tracker = create_tracking_python_actor(py).unwrap();
6644            tracker.setattr(py, "raises", true).unwrap();
6645            let mut actor = PyDataActor::new(None);
6646            actor.set_python_instance(tracker.bind(py)).unwrap();
6647            let error = DataActor::on_bar(actor.inner_mut(), &sample_bar()).unwrap_err();
6648            let message = error.to_string();
6649
6650            assert_eq!(python_method_call_count(&tracker, py, "on_bar"), 1);
6651            assert!(message.contains("Python on_bar failed:"));
6652            assert!(message.contains("in _record"));
6653            assert!(message.contains("RuntimeError: actor callback failure"));
6654        });
6655    }
6656
6657    #[rstest]
6658    #[case("on_start")]
6659    #[case("on_stop")]
6660    #[case("on_resume")]
6661    #[case("on_reset")]
6662    #[case("on_dispose")]
6663    #[case("on_degrade")]
6664    #[case("on_fault")]
6665    fn test_python_dispatch_lifecycle_matrix(
6666        clock: Rc<RefCell<VirtualClock>>,
6667        cache: Rc<RefCell<Cache>>,
6668        trader_id: TraderId,
6669        #[case] method_name: &str,
6670    ) {
6671        pyo3::Python::initialize();
6672
6673        Python::attach(|py| {
6674            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
6675                match method_name {
6676                    "on_start" => DataActor::on_start(rust_actor.inner_mut()),
6677                    "on_stop" => DataActor::on_stop(rust_actor.inner_mut()),
6678                    "on_resume" => DataActor::on_resume(rust_actor.inner_mut()),
6679                    "on_reset" => DataActor::on_reset(rust_actor.inner_mut()),
6680                    "on_dispose" => DataActor::on_dispose(rust_actor.inner_mut()),
6681                    "on_degrade" => DataActor::on_degrade(rust_actor.inner_mut()),
6682                    "on_fault" => DataActor::on_fault(rust_actor.inner_mut()),
6683                    _ => unreachable!("unhandled lifecycle case: {method_name}"),
6684                }
6685            });
6686        });
6687    }
6688
6689    #[rstest]
6690    #[case("on_save")]
6691    #[case("on_load")]
6692    fn test_python_dispatch_persistence_matrix(
6693        clock: Rc<RefCell<VirtualClock>>,
6694        cache: Rc<RefCell<Cache>>,
6695        trader_id: TraderId,
6696        #[case] method_name: &str,
6697    ) {
6698        pyo3::Python::initialize();
6699
6700        Python::attach(|py| {
6701            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
6702                match method_name {
6703                    "on_save" => {
6704                        let state = DataActor::on_save(rust_actor.inner()).unwrap();
6705                        assert_eq!(
6706                            state.get("actor").map(Vec::as_slice),
6707                            Some(b"saved".as_slice())
6708                        );
6709                        Ok(())
6710                    }
6711                    "on_load" => {
6712                        let mut state = IndexMap::new();
6713                        state.insert("actor".to_string(), b"loaded".to_vec());
6714                        DataActor::on_load(rust_actor.inner_mut(), state)
6715                    }
6716                    _ => unreachable!("unhandled persistence case: {method_name}"),
6717                }
6718            });
6719        });
6720    }
6721
6722    #[rstest]
6723    fn test_python_persistence_methods_convert_state(
6724        clock: Rc<RefCell<VirtualClock>>,
6725        cache: Rc<RefCell<Cache>>,
6726        trader_id: TraderId,
6727    ) {
6728        pyo3::Python::initialize();
6729
6730        Python::attach(|py| {
6731            let py_actor = create_tracking_python_actor(py).unwrap();
6732
6733            let mut rust_actor = PyDataActor::new(None);
6734            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
6735            rust_actor.register(trader_id, clock, cache).unwrap();
6736
6737            let saved = rust_actor.py_save(py).unwrap();
6738            let saved_state = saved
6739                .bind(py)
6740                .extract::<HashMap<String, Vec<u8>>>()
6741                .unwrap();
6742            assert_eq!(
6743                saved_state.get("actor").map(Vec::as_slice),
6744                Some(&b"saved"[..])
6745            );
6746
6747            let load_state = PyDict::new(py);
6748            load_state
6749                .set_item("actor", PyBytes::new(py, b"loaded-from-python"))
6750                .unwrap();
6751
6752            rust_actor.py_load(&load_state).unwrap();
6753
6754            let loaded_state = python_last_loaded_state(&py_actor, py).unwrap();
6755            assert_eq!(
6756                loaded_state.get("actor").map(Vec::as_slice),
6757                Some(&b"loaded-from-python"[..])
6758            );
6759        });
6760    }
6761
6762    #[rstest]
6763    fn test_indicator_registration_exposes_readiness_and_registered_view(
6764        audusd_sim: CurrencyPair,
6765        bar_type: BarType,
6766    ) {
6767        pyo3::Python::initialize();
6768
6769        Python::attach(|py| {
6770            let mut rust_actor = PyDataActor::new(None);
6771            let indicator = create_tracking_python_indicator(py).unwrap();
6772
6773            assert_eq!(
6774                rust_actor
6775                    .py_registered_indicators(py)
6776                    .unwrap()
6777                    .bind(py)
6778                    .len()
6779                    .unwrap(),
6780                0
6781            );
6782            assert!(!rust_actor.py_indicators_initialized(py).unwrap());
6783
6784            rust_actor.py_register_indicator_for_quote_ticks(
6785                py,
6786                audusd_sim.id,
6787                indicator.clone_ref(py),
6788            );
6789            rust_actor.py_register_indicator_for_trade_ticks(
6790                py,
6791                audusd_sim.id,
6792                indicator.clone_ref(py),
6793            );
6794            rust_actor.py_register_indicator_for_bars(py, bar_type, indicator.clone_ref(py));
6795
6796            let registered = rust_actor.py_registered_indicators(py).unwrap();
6797            let registered = registered.bind(py);
6798
6799            assert_eq!(registered.len().unwrap(), 1);
6800            assert_eq!(
6801                registered.get_item(0).unwrap().as_ptr(),
6802                indicator.bind(py).as_ptr()
6803            );
6804            assert!(!rust_actor.py_indicators_initialized(py).unwrap());
6805
6806            indicator.bind(py).setattr("initialized", true).unwrap();
6807
6808            assert!(rust_actor.py_indicators_initialized(py).unwrap());
6809        });
6810    }
6811
6812    #[rstest]
6813    fn test_registered_indicators_receive_quote_trade_and_bar_before_actor_callbacks(
6814        clock: Rc<RefCell<VirtualClock>>,
6815        cache: Rc<RefCell<Cache>>,
6816        trader_id: TraderId,
6817    ) {
6818        pyo3::Python::initialize();
6819
6820        Python::attach(|py| {
6821            let events = PyList::empty(py);
6822            let py_actor = create_indicator_event_actor(py, &events).unwrap();
6823            let indicator = create_event_tracking_python_indicator(py, &events).unwrap();
6824
6825            let mut rust_actor = PyDataActor::new(None);
6826            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
6827            rust_actor.register(trader_id, clock, cache).unwrap();
6828            Component::start(rust_actor.inner_mut()).unwrap();
6829
6830            let quote = sample_quote();
6831            let trade = sample_trade();
6832            let bar = sample_bar();
6833            let external_bar_type = BarType::from_str(&format!(
6834                "{}-1-MINUTE-LAST-EXTERNAL",
6835                bar.bar_type.instrument_id()
6836            ))
6837            .unwrap();
6838
6839            rust_actor.py_register_indicator_for_quote_ticks(
6840                py,
6841                quote.instrument_id,
6842                indicator.clone_ref(py),
6843            );
6844            rust_actor.py_register_indicator_for_trade_ticks(
6845                py,
6846                trade.instrument_id,
6847                indicator.clone_ref(py),
6848            );
6849            rust_actor.py_register_indicator_for_bars(
6850                py,
6851                external_bar_type,
6852                indicator.clone_ref(py),
6853            );
6854
6855            DataActor::handle_quote(rust_actor.inner_mut(), &quote);
6856            DataActor::handle_trade(rust_actor.inner_mut(), &trade);
6857            DataActor::handle_bar(rust_actor.inner_mut(), &bar);
6858
6859            let events = events.extract::<Vec<String>>().unwrap();
6860
6861            assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
6862            assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
6863            assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
6864            assert_eq!(
6865                events,
6866                vec![
6867                    "indicator:quote",
6868                    "actor:quote",
6869                    "indicator:trade",
6870                    "actor:trade",
6871                    "indicator:bar",
6872                    "actor:bar",
6873                ]
6874            );
6875        });
6876    }
6877
6878    #[rstest]
6879    fn test_registered_indicators_receive_live_data_when_actor_not_running(
6880        clock: Rc<RefCell<VirtualClock>>,
6881        cache: Rc<RefCell<Cache>>,
6882        trader_id: TraderId,
6883    ) {
6884        pyo3::Python::initialize();
6885
6886        Python::attach(|py| {
6887            let events = PyList::empty(py);
6888            let py_actor = create_indicator_event_actor(py, &events).unwrap();
6889            let indicator = create_event_tracking_python_indicator(py, &events).unwrap();
6890
6891            let mut rust_actor = PyDataActor::new(None);
6892            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
6893            rust_actor.register(trader_id, clock, cache).unwrap();
6894
6895            let quote = sample_quote();
6896
6897            rust_actor.py_register_indicator_for_quote_ticks(
6898                py,
6899                quote.instrument_id,
6900                indicator.clone_ref(py),
6901            );
6902
6903            DataActor::handle_quote(rust_actor.inner_mut(), &quote);
6904
6905            let events = events.extract::<Vec<String>>().unwrap();
6906
6907            assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
6908            assert_eq!(events, vec!["indicator:quote"]);
6909        });
6910    }
6911
6912    #[rstest]
6913    fn test_registered_indicators_receive_historical_quote_trade_and_bar_batches() {
6914        pyo3::Python::initialize();
6915
6916        Python::attach(|py| {
6917            let mut rust_actor = PyDataActor::new(None);
6918            let indicator = create_tracking_python_indicator(py).unwrap();
6919            let quote = sample_quote();
6920            let trade = sample_trade();
6921            let bar = sample_bar();
6922            let quotes = vec![quote];
6923            let trades = vec![trade];
6924            let bars = vec![bar];
6925
6926            rust_actor.py_register_indicator_for_quote_ticks(
6927                py,
6928                quote.instrument_id,
6929                indicator.clone_ref(py),
6930            );
6931            rust_actor.py_register_indicator_for_trade_ticks(
6932                py,
6933                trade.instrument_id,
6934                indicator.clone_ref(py),
6935            );
6936            rust_actor.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
6937
6938            let client_id = ClientId::new("TEST");
6939            let quotes_response = QuotesResponse::new(
6940                UUID4::new(),
6941                client_id,
6942                quote.instrument_id,
6943                quotes,
6944                None,
6945                None,
6946                UnixNanos::default(),
6947                None,
6948            );
6949            let trades_response = TradesResponse::new(
6950                UUID4::new(),
6951                client_id,
6952                trade.instrument_id,
6953                trades,
6954                None,
6955                None,
6956                UnixNanos::default(),
6957                None,
6958            );
6959            let bars_response = BarsResponse::new(
6960                UUID4::new(),
6961                client_id,
6962                bar.bar_type,
6963                bars,
6964                None,
6965                None,
6966                UnixNanos::default(),
6967                None,
6968            );
6969
6970            DataActor::handle_quotes_response(rust_actor.inner_mut(), &quotes_response);
6971            DataActor::handle_trades_response(rust_actor.inner_mut(), &trades_response);
6972            DataActor::handle_bars_response(rust_actor.inner_mut(), &bars_response);
6973
6974            assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
6975            assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
6976            assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
6977        });
6978    }
6979
6980    #[rstest]
6981    fn test_indicators_initialized_requires_all_registered_indicators(audusd_sim: CurrencyPair) {
6982        pyo3::Python::initialize();
6983
6984        Python::attach(|py| {
6985            let mut rust_actor = PyDataActor::new(None);
6986            let first = create_tracking_python_indicator(py).unwrap();
6987            let second = create_tracking_python_indicator(py).unwrap();
6988
6989            rust_actor.py_register_indicator_for_quote_ticks(
6990                py,
6991                audusd_sim.id,
6992                first.clone_ref(py),
6993            );
6994            rust_actor.py_register_indicator_for_quote_ticks(
6995                py,
6996                audusd_sim.id,
6997                second.clone_ref(py),
6998            );
6999
7000            first.bind(py).setattr("initialized", true).unwrap();
7001
7002            assert!(!rust_actor.py_indicators_initialized(py).unwrap());
7003
7004            second.bind(py).setattr("initialized", true).unwrap();
7005
7006            assert!(rust_actor.py_indicators_initialized(py).unwrap());
7007        });
7008    }
7009
7010    #[rstest]
7011    fn test_duplicate_indicator_registration_does_not_duplicate_callbacks(
7012        clock: Rc<RefCell<VirtualClock>>,
7013        cache: Rc<RefCell<Cache>>,
7014        trader_id: TraderId,
7015    ) {
7016        pyo3::Python::initialize();
7017
7018        Python::attach(|py| {
7019            let mut rust_actor = PyDataActor::new(None);
7020            let indicator = create_tracking_python_indicator(py).unwrap();
7021            let quote = sample_quote();
7022            let trade = sample_trade();
7023            let bar = sample_bar();
7024            rust_actor.register(trader_id, clock, cache).unwrap();
7025            Component::start(rust_actor.inner_mut()).unwrap();
7026
7027            rust_actor.py_register_indicator_for_quote_ticks(
7028                py,
7029                quote.instrument_id,
7030                indicator.clone_ref(py),
7031            );
7032            rust_actor.py_register_indicator_for_quote_ticks(
7033                py,
7034                quote.instrument_id,
7035                indicator.clone_ref(py),
7036            );
7037            rust_actor.py_register_indicator_for_trade_ticks(
7038                py,
7039                trade.instrument_id,
7040                indicator.clone_ref(py),
7041            );
7042            rust_actor.py_register_indicator_for_trade_ticks(
7043                py,
7044                trade.instrument_id,
7045                indicator.clone_ref(py),
7046            );
7047            rust_actor.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
7048            rust_actor.py_register_indicator_for_bars(py, bar.bar_type, indicator.clone_ref(py));
7049
7050            DataActor::handle_quote(rust_actor.inner_mut(), &quote);
7051            DataActor::handle_trade(rust_actor.inner_mut(), &trade);
7052            DataActor::handle_bar(rust_actor.inner_mut(), &bar);
7053
7054            assert_eq!(
7055                rust_actor
7056                    .py_registered_indicators(py)
7057                    .unwrap()
7058                    .bind(py)
7059                    .len()
7060                    .unwrap(),
7061                1
7062            );
7063            assert_eq!(python_indicator_call_count(&indicator, py, "quote"), 1);
7064            assert_eq!(python_indicator_call_count(&indicator, py, "trade"), 1);
7065            assert_eq!(python_indicator_call_count(&indicator, py, "bar"), 1);
7066        });
7067    }
7068
7069    #[rstest]
7070    fn test_indicator_error_prevents_actor_callback(
7071        clock: Rc<RefCell<VirtualClock>>,
7072        cache: Rc<RefCell<Cache>>,
7073        trader_id: TraderId,
7074    ) {
7075        pyo3::Python::initialize();
7076
7077        Python::attach(|py| {
7078            let events = PyList::empty(py);
7079            let py_actor = create_indicator_event_actor(py, &events).unwrap();
7080            let indicator = create_raising_python_indicator(py).unwrap();
7081
7082            let mut rust_actor = PyDataActor::new(None);
7083            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
7084            rust_actor.register(trader_id, clock, cache).unwrap();
7085            Component::start(rust_actor.inner_mut()).unwrap();
7086
7087            let quote = sample_quote();
7088
7089            rust_actor.py_register_indicator_for_quote_ticks(py, quote.instrument_id, indicator);
7090
7091            DataActor::handle_quote(rust_actor.inner_mut(), &quote);
7092            let events = events.extract::<Vec<String>>().unwrap();
7093
7094            assert!(events.is_empty());
7095        });
7096    }
7097
7098    #[rstest]
7099    #[case("on_time_event")]
7100    #[case("on_data")]
7101    #[case("on_signal")]
7102    #[case("on_queue_state")]
7103    #[case("on_socket_state")]
7104    #[case("on_instrument")]
7105    #[case("on_quote")]
7106    #[case("on_trade")]
7107    #[case("on_bar")]
7108    #[case("on_book")]
7109    #[case("on_book_deltas")]
7110    #[case("on_book_depth")]
7111    #[case("on_mark_price")]
7112    #[case("on_index_price")]
7113    #[case("on_funding_rate")]
7114    #[case("on_instrument_status")]
7115    #[case("on_instrument_close")]
7116    #[case("on_option_greeks")]
7117    #[case("on_option_chain")]
7118    fn test_python_dispatch_typed_callback_matrix(
7119        clock: Rc<RefCell<VirtualClock>>,
7120        cache: Rc<RefCell<Cache>>,
7121        trader_id: TraderId,
7122        #[case] method_name: &str,
7123    ) {
7124        pyo3::Python::initialize();
7125
7126        Python::attach(|py| {
7127            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
7128                match method_name {
7129                    "on_time_event" => {
7130                        let event = sample_time_event();
7131                        rust_actor.inner_mut().on_time_event(&event)
7132                    }
7133                    "on_data" => {
7134                        let data = sample_data();
7135                        rust_actor.inner_mut().on_data(&data)
7136                    }
7137                    "on_signal" => {
7138                        let signal = sample_signal();
7139                        rust_actor.inner_mut().on_signal(&signal)
7140                    }
7141                    "on_queue_state" => {
7142                        let event = sample_queue_state_changed(QueueState::Triggered);
7143                        rust_actor.inner_mut().on_queue_state(&event)
7144                    }
7145                    "on_socket_state" => {
7146                        let event = sample_socket_state_changed(SocketState::Connected);
7147                        rust_actor.inner_mut().on_socket_state(&event)
7148                    }
7149                    "on_instrument" => {
7150                        let instrument = InstrumentAny::CurrencyPair(sample_instrument());
7151                        rust_actor.inner_mut().on_instrument(&instrument)
7152                    }
7153                    "on_quote" => {
7154                        let quote = sample_quote();
7155                        rust_actor.inner_mut().on_quote(&quote)
7156                    }
7157                    "on_trade" => {
7158                        let trade = sample_trade();
7159                        rust_actor.inner_mut().on_trade(&trade)
7160                    }
7161                    "on_bar" => {
7162                        let bar = sample_bar();
7163                        rust_actor.inner_mut().on_bar(&bar)
7164                    }
7165                    "on_book" => {
7166                        let book = sample_book();
7167                        rust_actor.inner_mut().on_book(&book)
7168                    }
7169                    "on_book_deltas" => {
7170                        let deltas = sample_book_deltas();
7171                        rust_actor.inner_mut().on_book_deltas(&deltas)
7172                    }
7173                    "on_book_depth" => {
7174                        let depth = sample_book_depth();
7175                        rust_actor.inner_mut().on_book_depth(&depth)
7176                    }
7177                    "on_mark_price" => {
7178                        let update = sample_mark_price();
7179                        rust_actor.inner_mut().on_mark_price(&update)
7180                    }
7181                    "on_index_price" => {
7182                        let update = sample_index_price();
7183                        rust_actor.inner_mut().on_index_price(&update)
7184                    }
7185                    "on_funding_rate" => {
7186                        let update = sample_funding_rate();
7187                        rust_actor.inner_mut().on_funding_rate(&update)
7188                    }
7189                    "on_instrument_status" => {
7190                        let status = sample_instrument_status();
7191                        rust_actor.inner_mut().on_instrument_status(&status)
7192                    }
7193                    "on_instrument_close" => {
7194                        let close = sample_instrument_close();
7195                        rust_actor.inner_mut().on_instrument_close(&close)
7196                    }
7197                    "on_option_greeks" => {
7198                        let greeks = sample_option_greeks();
7199                        rust_actor.inner_mut().on_option_greeks(&greeks)
7200                    }
7201                    "on_option_chain" => {
7202                        let chain = sample_option_chain();
7203                        rust_actor.inner_mut().on_option_chain(&chain)
7204                    }
7205                    _ => unreachable!("unhandled typed callback case: {method_name}"),
7206                }
7207            });
7208        });
7209    }
7210
7211    #[rstest]
7212    #[case("on_historical_data")]
7213    #[case("on_historical_book_deltas")]
7214    #[case("on_historical_book_depth")]
7215    #[case("on_historical_quotes")]
7216    #[case("on_historical_trades")]
7217    #[case("on_historical_funding_rates")]
7218    #[case("on_historical_bars")]
7219    #[case("on_historical_mark_prices")]
7220    #[case("on_historical_index_prices")]
7221    fn test_python_dispatch_historical_callback_matrix(
7222        clock: Rc<RefCell<VirtualClock>>,
7223        cache: Rc<RefCell<Cache>>,
7224        trader_id: TraderId,
7225        #[case] method_name: &str,
7226    ) {
7227        pyo3::Python::initialize();
7228
7229        Python::attach(|py| {
7230            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
7231                match method_name {
7232                    "on_historical_data" => {
7233                        let data = sample_data();
7234                        rust_actor.inner_mut().on_historical_data(&data)
7235                    }
7236                    "on_historical_book_deltas" => {
7237                        let deltas = sample_book_deltas().deltas;
7238                        rust_actor.inner_mut().on_historical_book_deltas(&deltas)
7239                    }
7240                    "on_historical_book_depth" => {
7241                        let depths = vec![sample_book_depth()];
7242                        rust_actor.inner_mut().on_historical_book_depth(&depths)
7243                    }
7244                    "on_historical_quotes" => {
7245                        let quotes = vec![sample_quote()];
7246                        rust_actor.inner_mut().on_historical_quotes(&quotes)
7247                    }
7248                    "on_historical_trades" => {
7249                        let trades = vec![sample_trade()];
7250                        rust_actor.inner_mut().on_historical_trades(&trades)
7251                    }
7252                    "on_historical_funding_rates" => {
7253                        let funding_rates = vec![sample_funding_rate()];
7254                        rust_actor
7255                            .inner_mut()
7256                            .on_historical_funding_rates(&funding_rates)
7257                    }
7258                    "on_historical_bars" => {
7259                        let bars = vec![sample_bar()];
7260                        rust_actor.inner_mut().on_historical_bars(&bars)
7261                    }
7262                    "on_historical_mark_prices" => {
7263                        let mark_prices = vec![sample_mark_price()];
7264                        rust_actor
7265                            .inner_mut()
7266                            .on_historical_mark_prices(&mark_prices)
7267                    }
7268                    "on_historical_index_prices" => {
7269                        let index_prices = vec![sample_index_price()];
7270                        rust_actor
7271                            .inner_mut()
7272                            .on_historical_index_prices(&index_prices)
7273                    }
7274                    _ => unreachable!("unhandled historical callback case: {method_name}"),
7275                }
7276            });
7277        });
7278    }
7279
7280    #[rstest]
7281    fn test_python_dispatch_historical_book_deltas_preserves_batch(
7282        clock: Rc<RefCell<VirtualClock>>,
7283        cache: Rc<RefCell<Cache>>,
7284        trader_id: TraderId,
7285    ) {
7286        pyo3::Python::initialize();
7287
7288        Python::attach(|py| {
7289            let expected = stub_deltas().deltas;
7290            let py_actor = assert_python_dispatch(
7291                py,
7292                clock,
7293                cache,
7294                trader_id,
7295                "on_historical_book_deltas",
7296                |rust_actor| rust_actor.inner_mut().on_historical_book_deltas(&expected),
7297            );
7298            let actual = py_actor
7299                .call_method1(py, "last_call_args", ("on_historical_book_deltas",))
7300                .unwrap()
7301                .bind(py)
7302                .get_item(0)
7303                .unwrap()
7304                .extract::<Vec<OrderBookDelta>>()
7305                .unwrap();
7306
7307            assert_eq!(actual, expected);
7308        });
7309    }
7310
7311    #[rstest]
7312    fn test_python_dispatch_historical_book_depth_preserves_batch(
7313        clock: Rc<RefCell<VirtualClock>>,
7314        cache: Rc<RefCell<Cache>>,
7315        trader_id: TraderId,
7316    ) {
7317        pyo3::Python::initialize();
7318
7319        Python::attach(|py| {
7320            let first = stub_depth10();
7321            let mut second = first.clone();
7322            second.sequence = 17;
7323            second.ts_event = UnixNanos::from(18);
7324            second.ts_init = UnixNanos::from(19);
7325            let expected = vec![first, second];
7326            let py_actor = assert_python_dispatch(
7327                py,
7328                clock,
7329                cache,
7330                trader_id,
7331                "on_historical_book_depth",
7332                |rust_actor| rust_actor.inner_mut().on_historical_book_depth(&expected),
7333            );
7334            let actual = py_actor
7335                .call_method1(py, "last_call_args", ("on_historical_book_depth",))
7336                .unwrap()
7337                .bind(py)
7338                .get_item(0)
7339                .unwrap()
7340                .extract::<Vec<OrderBookDepth>>()
7341                .unwrap();
7342
7343            assert_eq!(actual, expected);
7344        });
7345    }
7346
7347    #[cfg(feature = "defi")]
7348    #[rstest]
7349    #[case("on_block")]
7350    #[case("on_pool")]
7351    #[case("on_pool_swap")]
7352    #[case("on_pool_liquidity_update")]
7353    #[case("on_pool_fee_collect")]
7354    #[case("on_pool_flash")]
7355    fn test_python_dispatch_defi_callback_matrix(
7356        clock: Rc<RefCell<VirtualClock>>,
7357        cache: Rc<RefCell<Cache>>,
7358        trader_id: TraderId,
7359        #[case] method_name: &str,
7360    ) {
7361        pyo3::Python::initialize();
7362
7363        Python::attach(|py| {
7364            assert_python_dispatch(py, clock, cache, trader_id, method_name, |rust_actor| {
7365                match method_name {
7366                    "on_block" => {
7367                        let block = sample_block();
7368                        rust_actor.inner_mut().on_block(&block)
7369                    }
7370                    "on_pool" => {
7371                        let (_chain, _dex, pool) = sample_pool_components();
7372                        rust_actor.inner_mut().on_pool(&pool)
7373                    }
7374                    "on_pool_swap" => {
7375                        let swap = sample_pool_swap();
7376                        rust_actor.inner_mut().on_pool_swap(&swap)
7377                    }
7378                    "on_pool_liquidity_update" => {
7379                        let update = sample_pool_liquidity_update();
7380                        rust_actor.inner_mut().on_pool_liquidity_update(&update)
7381                    }
7382                    "on_pool_fee_collect" => {
7383                        let collect = sample_pool_fee_collect();
7384                        rust_actor.inner_mut().on_pool_fee_collect(&collect)
7385                    }
7386                    "on_pool_flash" => {
7387                        let flash = sample_pool_flash();
7388                        rust_actor.inner_mut().on_pool_flash(&flash)
7389                    }
7390                    _ => unreachable!("unhandled defi callback case: {method_name}"),
7391                }
7392            });
7393        });
7394    }
7395
7396    #[rstest]
7397    fn test_python_dispatch_multiple_calls_tracked(
7398        clock: Rc<RefCell<VirtualClock>>,
7399        cache: Rc<RefCell<Cache>>,
7400        trader_id: TraderId,
7401        audusd_sim: CurrencyPair,
7402    ) {
7403        pyo3::Python::initialize();
7404
7405        Python::attach(|py| {
7406            let py_actor = create_tracking_python_actor(py).unwrap();
7407
7408            let mut rust_actor = PyDataActor::new(None);
7409            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
7410            rust_actor.register(trader_id, clock, cache).unwrap();
7411
7412            let quote = QuoteTick::new(
7413                audusd_sim.id,
7414                Price::from("1.00000"),
7415                Price::from("1.00001"),
7416                Quantity::from(100_000),
7417                Quantity::from(100_000),
7418                UnixNanos::default(),
7419                UnixNanos::default(),
7420            );
7421
7422            rust_actor.inner_mut().on_quote(&quote).unwrap();
7423            rust_actor.inner_mut().on_quote(&quote).unwrap();
7424            rust_actor.inner_mut().on_quote(&quote).unwrap();
7425
7426            assert_eq!(python_method_call_count(&py_actor, py, "on_quote"), 3);
7427        });
7428    }
7429
7430    #[rstest]
7431    fn test_python_dispatch_historical_custom_data_preserves_payload_shape(
7432        clock: Rc<RefCell<VirtualClock>>,
7433        cache: Rc<RefCell<Cache>>,
7434        trader_id: TraderId,
7435        client_id: ClientId,
7436    ) {
7437        pyo3::Python::initialize();
7438
7439        Python::attach(|py| {
7440            let py_actor = create_tracking_python_actor(py).unwrap();
7441            let mut rust_actor = PyDataActor::new(None);
7442            rust_actor.set_python_instance(py_actor.bind(py)).unwrap();
7443            rust_actor.register(trader_id, clock, cache).unwrap();
7444
7445            let data = vec![
7446                stub_custom_data(1, 42, None, None),
7447                stub_custom_data(2, 84, None, None),
7448            ];
7449            let scalar = stub_custom_data(3, 126, None, None);
7450            let scalar_response = CustomDataResponse::new(
7451                UUID4::new(),
7452                client_id,
7453                None,
7454                scalar.data_type.clone(),
7455                scalar.clone(),
7456                None,
7457                None,
7458                UnixNanos::default(),
7459                None,
7460            );
7461
7462            DataActor::handle_data_response(rust_actor.inner_mut(), &scalar_response);
7463
7464            let actual_scalar = py_actor
7465                .call_method1(py, "last_call_args", ("on_historical_data",))
7466                .unwrap()
7467                .bind(py)
7468                .get_item(0)
7469                .unwrap()
7470                .extract::<CustomData>()
7471                .unwrap();
7472
7473            assert_eq!(
7474                python_method_call_count(&py_actor, py, "on_historical_data"),
7475                1
7476            );
7477            assert_eq!(actual_scalar, scalar);
7478
7479            let empty_response = CustomDataResponse::new(
7480                UUID4::new(),
7481                client_id,
7482                None,
7483                data[0].data_type.clone(),
7484                Vec::<CustomData>::new(),
7485                None,
7486                None,
7487                UnixNanos::default(),
7488                None,
7489            );
7490
7491            DataActor::handle_data_response(rust_actor.inner_mut(), &empty_response);
7492
7493            let empty = py_actor
7494                .call_method1(py, "last_call_args", ("on_historical_data",))
7495                .unwrap()
7496                .bind(py)
7497                .get_item(0)
7498                .unwrap()
7499                .extract::<Vec<CustomData>>()
7500                .unwrap();
7501
7502            assert_eq!(
7503                python_method_call_count(&py_actor, py, "on_historical_data"),
7504                2
7505            );
7506            assert!(empty.is_empty());
7507
7508            let response = CustomDataResponse::new(
7509                UUID4::new(),
7510                client_id,
7511                None,
7512                data[0].data_type.clone(),
7513                data.clone(),
7514                None,
7515                None,
7516                UnixNanos::default(),
7517                None,
7518            );
7519
7520            DataActor::handle_data_response(rust_actor.inner_mut(), &response);
7521
7522            let actual = py_actor
7523                .call_method1(py, "last_call_args", ("on_historical_data",))
7524                .unwrap()
7525                .bind(py)
7526                .get_item(0)
7527                .unwrap()
7528                .extract::<Vec<CustomData>>()
7529                .unwrap();
7530
7531            assert_eq!(
7532                python_method_call_count(&py_actor, py, "on_historical_data"),
7533                3
7534            );
7535            assert_eq!(actual, data);
7536        });
7537    }
7538
7539    #[rstest]
7540    fn test_python_dispatch_no_call_when_py_self_not_set(
7541        clock: Rc<RefCell<VirtualClock>>,
7542        cache: Rc<RefCell<Cache>>,
7543        trader_id: TraderId,
7544    ) {
7545        pyo3::Python::initialize();
7546
7547        Python::attach(|_py| {
7548            let mut rust_actor = PyDataActor::new(None);
7549            rust_actor.register(trader_id, clock, cache).unwrap();
7550
7551            // When py_self is None, the dispatch returns Ok(()) without calling Python
7552            let result = DataActor::on_start(rust_actor.inner_mut());
7553            assert!(result.is_ok());
7554        });
7555    }
7556
7557    #[rstest]
7558    fn test_python_on_historical_data_rejects_non_custom_data(
7559        clock: Rc<RefCell<VirtualClock>>,
7560        cache: Rc<RefCell<Cache>>,
7561        trader_id: TraderId,
7562    ) {
7563        pyo3::Python::initialize();
7564
7565        let mut rust_actor = PyDataActor::new(None);
7566        rust_actor.register(trader_id, clock, cache).unwrap();
7567
7568        let non_custom: String = "not CustomData".to_string();
7569        let result = rust_actor.inner_mut().on_historical_data(&non_custom);
7570
7571        assert!(result.is_err());
7572        assert!(result.unwrap_err().to_string().contains("unsupported type"));
7573    }
7574
7575    #[rstest]
7576    fn test_python_self_is_weak() {
7577        Python::initialize();
7578
7579        Python::attach(|py| {
7580            let instance = py
7581                .get_type::<PyDataActor>()
7582                .call0()
7583                .expect("DataActor should construct");
7584            let weakref =
7585                PyWeakrefReference::new(&instance).expect("DataActor should be weak-referenceable");
7586            assert!(weakref.upgrade().is_some());
7587
7588            drop(instance);
7589
7590            // A strong `py_self` would form an untraceable Rust-Python cycle and keep this alive
7591            assert!(
7592                weakref.upgrade().is_none(),
7593                "an unregistered DataActor must be collected once its last Python owner is dropped",
7594            );
7595        });
7596    }
7597
7598    #[rstest::rstest]
7599    #[case("on_start")]
7600    #[case("on_stop")]
7601    #[case("on_resume")]
7602    #[case("on_reset")]
7603    #[case("on_dispose")]
7604    #[case("on_degrade")]
7605    #[case("on_fault")]
7606    #[case("on_save")]
7607    #[case("on_load")]
7608    #[case("on_time_event")]
7609    #[case("on_data")]
7610    #[case("on_signal")]
7611    #[case("on_queue_state")]
7612    #[case("on_socket_state")]
7613    #[case("on_instrument")]
7614    #[case("on_quote")]
7615    #[case("on_trade")]
7616    #[case("on_bar")]
7617    #[case("on_book_deltas")]
7618    #[case("on_book_depth")]
7619    #[case("on_book")]
7620    #[case("on_mark_price")]
7621    #[case("on_index_price")]
7622    #[case("on_funding_rate")]
7623    #[case("on_instrument_status")]
7624    #[case("on_instrument_close")]
7625    #[case("on_option_greeks")]
7626    #[case("on_option_chain")]
7627    #[case("on_historical_data")]
7628    #[case("on_historical_book_deltas")]
7629    #[case("on_historical_book_depth")]
7630    #[case("on_historical_quotes")]
7631    #[case("on_historical_trades")]
7632    #[case("on_historical_funding_rates")]
7633    #[case("on_historical_bars")]
7634    #[case("on_historical_mark_prices")]
7635    #[case("on_historical_index_prices")]
7636    fn test_default_callback_during_exclusive_borrow(#[case] method: &str) {
7637        Python::initialize();
7638        Python::attach(|py| {
7639            let argument = match method {
7640                "on_start" | "on_stop" | "on_resume" | "on_reset" | "on_dispose" | "on_degrade"
7641                | "on_fault" | "on_save" => None,
7642                "on_load" => Some(PyDict::new(py).into_any().unbind()),
7643                "on_time_event" => Some(sample_time_event().into_py_any(py).unwrap()),
7644                "on_data" | "on_instrument" | "on_historical_data" => Some(py.None()),
7645                "on_signal" => Some(sample_signal().into_py_any(py).unwrap()),
7646                "on_queue_state" => Some(
7647                    sample_queue_state_changed(QueueState::Triggered)
7648                        .into_py_any(py)
7649                        .unwrap(),
7650                ),
7651                "on_socket_state" => Some(
7652                    sample_socket_state_changed(SocketState::Connected)
7653                        .into_py_any(py)
7654                        .unwrap(),
7655                ),
7656                "on_quote" => Some(sample_quote().into_py_any(py).unwrap()),
7657                "on_trade" => Some(sample_trade().into_py_any(py).unwrap()),
7658                "on_bar" => Some(sample_bar().into_py_any(py).unwrap()),
7659                "on_book_deltas" => Some(sample_book_deltas().into_py_any(py).unwrap()),
7660                "on_book_depth" => Some(sample_book_depth().into_py_any(py).unwrap()),
7661                "on_book" => Some(sample_book().into_py_any(py).unwrap()),
7662                "on_mark_price" => Some(sample_mark_price().into_py_any(py).unwrap()),
7663                "on_index_price" => Some(sample_index_price().into_py_any(py).unwrap()),
7664                "on_funding_rate" => Some(sample_funding_rate().into_py_any(py).unwrap()),
7665                "on_instrument_status" => Some(sample_instrument_status().into_py_any(py).unwrap()),
7666                "on_instrument_close" => Some(sample_instrument_close().into_py_any(py).unwrap()),
7667                "on_option_greeks" => Some(sample_option_greeks().into_py_any(py).unwrap()),
7668                "on_option_chain" => Some(sample_option_chain().into_py_any(py).unwrap()),
7669                "on_historical_book_deltas"
7670                | "on_historical_book_depth"
7671                | "on_historical_quotes"
7672                | "on_historical_trades"
7673                | "on_historical_funding_rates"
7674                | "on_historical_bars"
7675                | "on_historical_mark_prices"
7676                | "on_historical_index_prices" => Some(PyList::empty(py).into_any().unbind()),
7677                _ => unreachable!(),
7678            };
7679
7680            let instance = py.get_type::<PyDataActor>().call0().unwrap();
7681            let _borrow = instance
7682                .extract::<pyo3::PyRefMut<'_, PyDataActor>>()
7683                .unwrap();
7684
7685            let result = match argument {
7686                Some(argument) => instance.call_method1(method, (argument,)),
7687                None => instance.call_method0(method),
7688            }
7689            .unwrap();
7690
7691            if method == "on_save" {
7692                assert_eq!(result.len().unwrap(), 0);
7693            } else {
7694                assert!(result.is_none());
7695            }
7696        });
7697    }
7698
7699    #[cfg(feature = "defi")]
7700    #[rstest::rstest]
7701    #[case("on_block")]
7702    #[case("on_pool")]
7703    #[case("on_pool_swap")]
7704    #[case("on_pool_liquidity_update")]
7705    #[case("on_pool_fee_collect")]
7706    #[case("on_pool_flash")]
7707    fn test_default_defi_callback_during_exclusive_borrow(#[case] method: &str) {
7708        Python::initialize();
7709        Python::attach(|py| {
7710            let argument = match method {
7711                "on_block" => sample_block().into_py_any(py).unwrap(),
7712                "on_pool" => sample_pool_components().2.into_py_any(py).unwrap(),
7713                "on_pool_swap" => sample_pool_swap().into_py_any(py).unwrap(),
7714                "on_pool_liquidity_update" => {
7715                    sample_pool_liquidity_update().into_py_any(py).unwrap()
7716                }
7717                "on_pool_fee_collect" => sample_pool_fee_collect().into_py_any(py).unwrap(),
7718                "on_pool_flash" => sample_pool_flash().into_py_any(py).unwrap(),
7719                _ => unreachable!(),
7720            };
7721
7722            let instance = py.get_type::<PyDataActor>().call0().unwrap();
7723            let _borrow = instance
7724                .extract::<pyo3::PyRefMut<'_, PyDataActor>>()
7725                .unwrap();
7726            assert!(
7727                instance
7728                    .call_method1(method, (argument,))
7729                    .unwrap()
7730                    .is_none()
7731            );
7732        });
7733    }
7734
7735    #[rstest]
7736    fn test_python_instrument_lists_validate_before_dispatch(
7737        #[values(
7738            "instrument",
7739            "book_deltas",
7740            "book_depth",
7741            "book_at_interval",
7742            "mark_prices",
7743            "index_prices",
7744            "funding_rates",
7745            "instrument_status",
7746            "instrument_close",
7747            "option_greeks"
7748        )]
7749        family: &str,
7750        #[values(false, true)] subscribe: bool,
7751        #[values(false, true)] mixed_venues: bool,
7752        clock: Rc<RefCell<VirtualClock>>,
7753        cache: Rc<RefCell<Cache>>,
7754        trader_id: TraderId,
7755    ) {
7756        Python::initialize();
7757        Python::attach(|py| {
7758            let actor = Py::new(py, create_registered_actor(clock, cache, trader_id)).unwrap();
7759            let (handler, saver) = get_typed_into_message_saving_handler::<DataCommand>(None);
7760            msgbus::register_data_command_endpoint(
7761                MessagingSwitchboard::data_engine_queue_execute(),
7762                handler,
7763            );
7764
7765            let ids = if mixed_venues {
7766                vec![
7767                    InstrumentId::from("BTC/USD.SIM"),
7768                    InstrumentId::from("ETH/USD.OTHER"),
7769                ]
7770            } else {
7771                vec![]
7772            };
7773
7774            let argument = PyList::new(py, ids).unwrap();
7775            let kwargs = instrument_subscription_keywords(py, family, subscribe);
7776            let method = format!(
7777                "{}_{family}",
7778                if subscribe {
7779                    "subscribe"
7780                } else {
7781                    "unsubscribe"
7782                }
7783            );
7784            let error = actor
7785                .bind(py)
7786                .call_method(method.as_str(), (&argument,), Some(&kwargs))
7787                .unwrap_err();
7788            assert!(error.is_instance_of::<pyo3::exceptions::PyValueError>(py));
7789            assert_eq!(
7790                error.value(py).to_string(),
7791                if mixed_venues {
7792                    "multiple instrument venues require an explicit client ID"
7793                } else {
7794                    "instrument IDs cannot be empty"
7795                }
7796            );
7797            assert_eq!(saver.get_messages().len(), 0);
7798        });
7799    }
7800
7801    fn instrument_subscription_keywords<'py>(
7802        py: Python<'py>,
7803        family: &str,
7804        subscribe: bool,
7805    ) -> Bound<'py, PyDict> {
7806        let kwargs = PyDict::new(py);
7807        if family.starts_with("book_") && subscribe {
7808            kwargs.set_item("book_type", BookType::L2_MBP).unwrap();
7809        }
7810
7811        if family == "book_at_interval" {
7812            kwargs.set_item("interval_ms", 13).unwrap();
7813        }
7814
7815        kwargs
7816    }
7817}